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