001 /*
002 * Copyright 2015-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2015-2017 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021 package com.unboundid.util.json;
022
023
024
025 import java.math.BigDecimal;
026
027 import com.unboundid.util.Debug;
028 import com.unboundid.util.NotMutable;
029 import com.unboundid.util.StaticUtils;
030 import com.unboundid.util.ThreadSafety;
031 import com.unboundid.util.ThreadSafetyLevel;
032
033 import static com.unboundid.util.json.JSONMessages.*;
034
035
036
037 /**
038 * This class provides an implementation of a JSON value that represents a
039 * base-ten numeric value of arbitrary size. It may or may not be a
040 * floating-point value (including a decimal point with numbers to the right of
041 * it), and it may or may not be expressed using scientific notation. The
042 * numeric value will be represented internally as a {@code BigDecimal}.
043 * <BR><BR>
044 * The string representation of a JSON number consists of the following
045 * elements, in the following order:
046 * <OL>
047 * <LI>
048 * An optional minus sign to indicate that the value is negative. If this
049 * is absent, then the number will be positive. Positive numbers must not
050 * be prefixed with a plus sign.
051 * </LI>
052 * <LI>
053 * One or more numeric digits to specify the whole number portion of the
054 * value. There must not be any unnecessary leading zeroes, so the first
055 * digit may be zero only if it is the only digit in the whole number
056 * portion of the value.
057 * </LI>
058 * <LI>
059 * An optional decimal point followed by at least one numeric digit to
060 * indicate the fractional portion of the value. Trailing zeroes are
061 * allowed in the fractional component.
062 * </LI>
063 * <LI>
064 * An optional 'e' or 'E' character, followed by an optional '+' or '-'
065 * character and at least one numeric digit to indicate that the value is
066 * expressed in scientific notation and the number before the uppercase or
067 * lowercase E should be multiplied by the specified positive or negative
068 * power of ten.
069 * </LI>
070 * </OL>
071 * It is possible for the same number to have multiple equivalent string
072 * representations. For example, all of the following valid string
073 * representations of JSON numbers represent the same numeric value:
074 * <UL>
075 * <LI>12345</LI>
076 * <LI>12345.0</LI>
077 * <LI>1.2345e4</LI>
078 * <LI>1.2345e+4</LI>
079 * </UL>
080 * JSON numbers must not be enclosed in quotation marks.
081 * <BR><BR>
082 * If a JSON number is created from its string representation, then that
083 * string representation will be returned from the {@link #toString()} method
084 * (or appended to the provided buffer for the {@link #toString(StringBuilder)}
085 * method). If a JSON number is created from a {@code long} or {@code double}
086 * value, then the Java string representation of that value (as obtained from
087 * the {@code String.valueOf} method) will be used as the string representation
088 * for the number. If a JSON number is created from a {@code BigDecimal} value,
089 * then the Java string representation will be obtained via that value's
090 * {@code toPlainString} method.
091 * <BR><BR>
092 * The normalized representation of a JSON number is a canonical string
093 * representation for that number. That is, all equivalent JSON number values
094 * will have the same normalized representation. The normalized representation
095 * will never use scientific notation, will never have trailing zeroes in the
096 * fractional component, and will never have a fractional component if that
097 * fractional component would be zero. For example, for the
098 * logically-equivalent values "12345", "12345.0", "1.2345e4", and "1.2345e+4",
099 * the normalized representation will be "12345". For the logically-equivalent
100 * values "9876.5", "9876.50", and "9.8765e3", the normalized representation
101 * will be "9876.5".
102 */
103 @NotMutable()
104 @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
105 public final class JSONNumber
106 extends JSONValue
107 {
108 /**
109 * The serial version UID for this serializable class.
110 */
111 private static final long serialVersionUID = -9194944952299318254L;
112
113
114
115 // The numeric value for this object.
116 private final BigDecimal value;
117
118 // The normalized representation of the value.
119 private final BigDecimal normalizedValue;
120
121 // The string representation for this object.
122 private final String stringRepresentation;
123
124
125
126 /**
127 * Creates a new JSON number with the provided value.
128 *
129 * @param value The value for this JSON number.
130 */
131 public JSONNumber(final long value)
132 {
133 this.value = new BigDecimal(value);
134 normalizedValue = this.value;
135 stringRepresentation = String.valueOf(value);
136 }
137
138
139
140 /**
141 * Creates a new JSON number with the provided value.
142 *
143 * @param value The value for this JSON number.
144 */
145 public JSONNumber(final double value)
146 {
147 this.value = new BigDecimal(value);
148 normalizedValue = this.value;
149 stringRepresentation = String.valueOf(value);
150 }
151
152
153
154 /**
155 * Creates a new JSON number with the provided value.
156 *
157 * @param value The value for this JSON number. It must not be
158 * {@code null}.
159 */
160 public JSONNumber(final BigDecimal value)
161 {
162 this.value = value;
163 stringRepresentation = value.toPlainString();
164
165 // There isn't a simple way to get a good normalized value from a
166 // BigDecimal. If it represents an integer but has a decimal point followed
167 // by some zeroes, then the only way we can strip them off is to convert it
168 // from a BigDecimal to a BigInteger and back. If it represents a
169 // floating-point value that has unnecessary zeros then we have to call the
170 // stripTrailingZeroes method.
171 BigDecimal minimalValue;
172 try
173 {
174 minimalValue = new BigDecimal(value.toBigIntegerExact());
175 }
176 catch (final Exception e)
177 {
178 // This is fine -- it just means that the value does not represent an
179 // integer.
180 minimalValue = value.stripTrailingZeros();
181 }
182 normalizedValue = minimalValue;
183 }
184
185
186
187 /**
188 * Creates a new JSON number from the provided string representation.
189 *
190 * @param stringRepresentation The string representation to parse as a JSON
191 * number. It must not be {@code null}.
192 *
193 * @throws JSONException If the provided string cannot be parsed as a valid
194 * JSON number.
195 */
196 public JSONNumber(final String stringRepresentation)
197 throws JSONException
198 {
199 this.stringRepresentation = stringRepresentation;
200
201
202 // Make sure that the provided string represents a valid JSON number. This
203 // is a little more strict than what BigDecimal accepts. First, make sure
204 // it's not an empty string.
205 final char[] chars = stringRepresentation.toCharArray();
206 if (chars.length == 0)
207 {
208 throw new JSONException(ERR_NUMBER_EMPTY_STRING.get());
209 }
210
211
212 // Make sure that the last character is a digit. All valid string
213 // representations of JSON numbers must end with a digit, and validating
214 // that now allows us to do less error handling in subsequent checks.
215 if (! isDigit(chars[chars.length-1]))
216 {
217 throw new JSONException(ERR_NUMBER_LAST_CHAR_NOT_DIGIT.get(
218 stringRepresentation));
219 }
220
221
222 // If the value starts with a minus sign, then skip over it.
223 int pos = 0;
224 if (chars[0] == '-')
225 {
226 pos++;
227 }
228
229
230 // Make sure that the first character (after the potential minus sign) is a
231 // digit. If it's a zero, then make sure it's not followed by another
232 // digit.
233 if (! isDigit(chars[pos]))
234 {
235 throw new JSONException(ERR_NUMBER_ILLEGAL_CHAR.get(stringRepresentation,
236 pos));
237 }
238
239 if (chars[pos++] == '0')
240 {
241 if ((chars.length > pos) && isDigit(chars[pos]))
242 {
243 throw new JSONException(ERR_NUMBER_ILLEGAL_LEADING_ZERO.get(
244 stringRepresentation));
245 }
246 }
247
248
249 // Parse the rest of the string. Make sure that it satisfies all of the
250 // following constraints:
251 // - There can be at most one decimal point. If there is a decimal point,
252 // it must be followed by at least one digit.
253 // - There can be at most one uppercase or lowercase 'E'. If there is an
254 // 'E', then it must be followed by at least one digit, or it must be
255 // followed by a plus or minus sign and at least one digit.
256 // - If there are both a decimal point and an 'E', then the decimal point
257 // must come before the 'E'.
258 // - The only other characters allowed are digits.
259 boolean decimalFound = false;
260 boolean eFound = false;
261 for ( ; pos < chars.length; pos++)
262 {
263 final char c = chars[pos];
264 if (c == '.')
265 {
266 if (decimalFound)
267 {
268 throw new JSONException(ERR_NUMBER_MULTIPLE_DECIMAL_POINTS.get(
269 stringRepresentation));
270 }
271 else
272 {
273 decimalFound = true;
274 }
275
276 if (eFound)
277 {
278 throw new JSONException(ERR_NUMBER_DECIMAL_IN_EXPONENT.get(
279 stringRepresentation));
280 }
281
282 if (! isDigit(chars[pos+1]))
283 {
284 throw new JSONException(ERR_NUMBER_DECIMAL_NOT_FOLLWED_BY_DIGIT.get(
285 stringRepresentation));
286 }
287 }
288 else if ((c == 'e') || (c == 'E'))
289 {
290 if (eFound)
291 {
292 throw new JSONException(ERR_NUMBER_MULTIPLE_EXPONENTS.get(
293 stringRepresentation));
294 }
295 else
296 {
297 eFound = true;
298 }
299
300 if ((chars[pos+1] == '-') || (chars[pos+1] == '+'))
301 {
302 if (! isDigit(chars[pos+2]))
303 {
304 throw new JSONException(
305 ERR_NUMBER_EXPONENT_NOT_FOLLOWED_BY_DIGIT.get(
306 stringRepresentation));
307 }
308
309 // Increment the counter to skip over the sign.
310 pos++;
311 }
312 else if (! isDigit(chars[pos+1]))
313 {
314 throw new JSONException(ERR_NUMBER_EXPONENT_NOT_FOLLOWED_BY_DIGIT.get(
315 stringRepresentation));
316 }
317 }
318 else if (! isDigit(chars[pos]))
319 {
320 throw new JSONException(ERR_NUMBER_ILLEGAL_CHAR.get(
321 stringRepresentation, pos));
322 }
323 }
324
325
326 // If we've gotten here, then we know the string represents a valid JSON
327 // number. BigDecimal should be able to parse all valid JSON numbers.
328 try
329 {
330 value = new BigDecimal(stringRepresentation);
331 }
332 catch (final Exception e)
333 {
334 Debug.debugException(e);
335
336 // This should never happen if all of the validation above is correct, but
337 // handle it just in case.
338 throw new JSONException(
339 ERR_NUMBER_CANNOT_PARSE.get(stringRepresentation,
340 StaticUtils.getExceptionMessage(e)),
341 e);
342 }
343
344 // There isn't a simple way to get a good normalized value from a
345 // BigDecimal. If it represents an integer but has a decimal point followed
346 // by some zeroes, then the only way we can strip them off is to convert it
347 // from a BigDecimal to a BigInteger and back. If it represents a
348 // floating-point value that has unnecessary zeros then we have to call the
349 // stripTrailingZeroes method.
350 BigDecimal minimalValue;
351 try
352 {
353 minimalValue = new BigDecimal(value.toBigIntegerExact());
354 }
355 catch (final Exception e)
356 {
357 // This is fine -- it just means that the value does not represent an
358 // integer.
359 minimalValue = value.stripTrailingZeros();
360 }
361 normalizedValue = minimalValue;
362 }
363
364
365
366 /**
367 * Indicates whether the specified character represents a digit.
368 *
369 * @param c The character for which to make the determination.
370 *
371 * @return {@code true} if the specified character represents a digit, or
372 * {@code false} if not.
373 */
374 private static boolean isDigit(final char c)
375 {
376 switch (c)
377 {
378 case '0':
379 case '1':
380 case '2':
381 case '3':
382 case '4':
383 case '5':
384 case '6':
385 case '7':
386 case '8':
387 case '9':
388 return true;
389 default:
390 return false;
391 }
392 }
393
394
395
396 /**
397 * Retrieves the value of this JSON number as a {@code BigDecimal}.
398 *
399 * @return The value of this JSON number as a {@code BigDecimal}.
400 */
401 public BigDecimal getValue()
402 {
403 return value;
404 }
405
406
407
408 /**
409 * {@inheritDoc}
410 */
411 @Override()
412 public int hashCode()
413 {
414 return normalizedValue.hashCode();
415 }
416
417
418
419 /**
420 * {@inheritDoc}
421 */
422 @Override()
423 public boolean equals(final Object o)
424 {
425 if (o == this)
426 {
427 return true;
428 }
429
430 if (o instanceof JSONNumber)
431 {
432 // NOTE: BigDecimal.equals probably doesn't do what you want, nor what
433 // anyone would normally expect. If you want to determine if two
434 // BigDecimal values are the same, then use compareTo.
435 final JSONNumber n = (JSONNumber) o;
436 return (value.compareTo(n.value) == 0);
437 }
438
439 return false;
440 }
441
442
443
444 /**
445 * {@inheritDoc}
446 */
447 @Override()
448 public boolean equals(final JSONValue v, final boolean ignoreFieldNameCase,
449 final boolean ignoreValueCase,
450 final boolean ignoreArrayOrder)
451 {
452 return ((v instanceof JSONNumber) &&
453 (value.compareTo(((JSONNumber) v).value) == 0));
454 }
455
456
457
458 /**
459 * Retrieves a string representation of this number as it should appear in a
460 * JSON object. If the object containing this number was decoded from a
461 * string, then this method will use the same string representation as in that
462 * original object. Otherwise, the string representation will be constructed.
463 *
464 * @return A string representation of this number as it should appear in a
465 * JSON object.
466 */
467 @Override()
468 public String toString()
469 {
470 return stringRepresentation;
471 }
472
473
474
475 /**
476 * Appends a string representation of this number as it should appear in a
477 * JSON object to the provided buffer. If the object containing this number
478 * was decoded from a string, then this method will use the same string
479 * representation as in that original object. Otherwise, the string
480 * representation will be constructed.
481 *
482 * @param buffer The buffer to which the information should be appended.
483 */
484 @Override()
485 public void toString(final StringBuilder buffer)
486 {
487 buffer.append(stringRepresentation);
488 }
489
490
491
492 /**
493 * Retrieves a single-line string representation of this number as it should
494 * appear in a JSON object. If the object containing this number was decoded
495 * from a string, then this method will use the same string representation as
496 * in that original object. Otherwise, the string representation will be
497 * constructed.
498 *
499 * @return A single-line string representation of this number as it should
500 * appear in a JSON object.
501 */
502 @Override()
503 public String toSingleLineString()
504 {
505 return stringRepresentation;
506 }
507
508
509
510 /**
511 * Appends a single-line string representation of this number as it should
512 * appear in a JSON object to the provided buffer. If the object containing
513 * this number was decoded from a string, then this method will use the same
514 * string representation as in that original object. Otherwise, the string
515 * representation will be constructed.
516 *
517 * @param buffer The buffer to which the information should be appended.
518 */
519 @Override()
520 public void toSingleLineString(final StringBuilder buffer)
521 {
522 buffer.append(stringRepresentation);
523 }
524
525
526
527 /**
528 * Retrieves a normalized string representation of this number as it should
529 * appear in a JSON object. The normalized representation will not use
530 * exponentiation, will not include a decimal point if the value can be
531 * represented as an integer, and will not include any unnecessary trailing
532 * zeroes if it can only be represented as a floating-point value.
533 *
534 * @return A normalized string representation of this number as it should
535 * appear in a JSON object.
536 */
537 @Override()
538 public String toNormalizedString()
539 {
540 final StringBuilder buffer = new StringBuilder();
541 toNormalizedString(buffer);
542 return buffer.toString();
543 }
544
545
546
547 /**
548 * Appends a normalized string representation of this number as it should
549 * appear in a JSON object to the provided buffer. The normalized
550 * representation will not use exponentiation, will not include a decimal
551 * point if the value can be represented as an integer, and will not include
552 * any unnecessary trailing zeroes if it can only be represented as a
553 * floating-point value.
554 *
555 * @param buffer The buffer to which the information should be appended.
556 */
557 @Override()
558 public void toNormalizedString(final StringBuilder buffer)
559 {
560 buffer.append(normalizedValue.toPlainString());
561 }
562
563
564
565 /**
566 * {@inheritDoc}
567 */
568 @Override()
569 public void appendToJSONBuffer(final JSONBuffer buffer)
570 {
571 buffer.appendNumber(stringRepresentation);
572 }
573
574
575
576 /**
577 * {@inheritDoc}
578 */
579 @Override()
580 public void appendToJSONBuffer(final String fieldName,
581 final JSONBuffer buffer)
582 {
583 buffer.appendNumber(fieldName, stringRepresentation);
584 }
585 }