001 /*
002 * Copyright 2015-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2015-2016 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 * {@inheritDoc}
460 */
461 @Override()
462 public String toString()
463 {
464 return stringRepresentation;
465 }
466
467
468
469 /**
470 * {@inheritDoc}
471 */
472 @Override()
473 public void toString(final StringBuilder buffer)
474 {
475 buffer.append(stringRepresentation);
476 }
477
478
479
480 /**
481 * {@inheritDoc}
482 */
483 @Override()
484 public String toSingleLineString()
485 {
486 return stringRepresentation;
487 }
488
489
490
491 /**
492 * {@inheritDoc}
493 */
494 @Override()
495 public void toSingleLineString(final StringBuilder buffer)
496 {
497 buffer.append(stringRepresentation);
498 }
499
500
501
502 /**
503 * {@inheritDoc}
504 */
505 @Override()
506 public String toNormalizedString()
507 {
508 final StringBuilder buffer = new StringBuilder();
509 toNormalizedString(buffer);
510 return buffer.toString();
511 }
512
513
514
515 /**
516 * {@inheritDoc}
517 */
518 @Override()
519 public void toNormalizedString(final StringBuilder buffer)
520 {
521 buffer.append(normalizedValue.toPlainString());
522 }
523
524
525
526 /**
527 * {@inheritDoc}
528 */
529 @Override()
530 public void appendToJSONBuffer(final JSONBuffer buffer)
531 {
532 buffer.appendNumber(stringRepresentation);
533 }
534
535
536
537 /**
538 * {@inheritDoc}
539 */
540 @Override()
541 public void appendToJSONBuffer(final String fieldName,
542 final JSONBuffer buffer)
543 {
544 buffer.appendNumber(fieldName, stringRepresentation);
545 }
546 }