001/*
002 * Copyright 2015-2023 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2015-2023 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-2023 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   * {@inheritDoc}
428   */
429  @Override()
430  public int hashCode()
431  {
432    return normalizedValue.hashCode();
433  }
434
435
436
437  /**
438   * {@inheritDoc}
439   */
440  @Override()
441  public boolean equals(@Nullable final Object o)
442  {
443    if (o == this)
444    {
445      return true;
446    }
447
448    if (o instanceof JSONNumber)
449    {
450      // NOTE:  BigDecimal.equals probably doesn't do what you want, nor what
451      // anyone would normally expect.  If you want to determine if two
452      // BigDecimal values are the same, then use compareTo.
453      final JSONNumber n = (JSONNumber) o;
454      return (value.compareTo(n.value) == 0);
455    }
456
457    return false;
458  }
459
460
461
462  /**
463   * {@inheritDoc}
464   */
465  @Override()
466  public boolean equals(@NotNull final JSONValue v,
467                        final boolean ignoreFieldNameCase,
468                        final boolean ignoreValueCase,
469                        final boolean ignoreArrayOrder)
470  {
471    return ((v instanceof JSONNumber) &&
472         (value.compareTo(((JSONNumber) v).value) == 0));
473  }
474
475
476
477  /**
478   * Retrieves a string representation of this number as it should appear in a
479   * JSON object.  If the object containing this number was decoded from a
480   * string, then this method will use the same string representation as in that
481   * original object.  Otherwise, the string representation will be constructed.
482   *
483   * @return  A string representation of this number as it should appear in a
484   *          JSON object.
485   */
486  @Override()
487  @NotNull()
488  public String toString()
489  {
490    return stringRepresentation;
491  }
492
493
494
495  /**
496   * Appends a string representation of this number as it should appear in a
497   * JSON object to the provided buffer.  If the object containing this number
498   * was decoded from a string, then this method will use the same string
499   * representation as in that original object.  Otherwise, the string
500   * representation will be constructed.
501   *
502   * @param  buffer  The buffer to which the information should be appended.
503   */
504  @Override()
505  public void toString(@NotNull final StringBuilder buffer)
506  {
507    buffer.append(stringRepresentation);
508  }
509
510
511
512  /**
513   * Retrieves a single-line string representation of this number as it should
514   * appear in a JSON object.  If the object containing this number was decoded
515   * from a string, then this method will use the same string representation as
516   * in that original object.  Otherwise, the string representation will be
517   * constructed.
518   *
519   * @return  A single-line string representation of this number as it should
520   *          appear in a JSON object.
521   */
522  @Override()
523  @NotNull()
524  public String toSingleLineString()
525  {
526    return stringRepresentation;
527  }
528
529
530
531  /**
532   * Appends a single-line string representation of this number as it should
533   * appear in a JSON object to the provided buffer.  If the object containing
534   * this number was decoded from a string, then this method will use the same
535   * string representation as in that original object.  Otherwise, the string
536   * representation will be constructed.
537   *
538   * @param  buffer  The buffer to which the information should be appended.
539   */
540  @Override()
541  public void toSingleLineString(@NotNull final StringBuilder buffer)
542  {
543    buffer.append(stringRepresentation);
544  }
545
546
547
548  /**
549   * Retrieves a normalized string representation of this number as it should
550   * appear in a JSON object.  The normalized representation will not use
551   * exponentiation, will not include a decimal point if the value can be
552   * represented as an integer, and will not include any unnecessary trailing
553   * zeroes if it can only be represented as a floating-point value.
554   *
555   * @return  A normalized string representation of this number as it should
556   *          appear in a JSON object.
557   */
558  @Override()
559  @NotNull()
560  public String toNormalizedString()
561  {
562    return normalizedValue.toPlainString();
563  }
564
565
566
567  /**
568   * Appends a normalized string representation of this number as it should
569   * appear in a JSON object to the provided buffer.  The normalized
570   * representation will not use exponentiation, will not include a decimal
571   * point if the value can be represented as an integer, and will not include
572   * any unnecessary trailing zeroes if it can only be represented as a
573   * floating-point value.
574   *
575   * @param  buffer  The buffer to which the information should be appended.
576   */
577  @Override()
578  public void toNormalizedString(@NotNull final StringBuilder buffer)
579  {
580    buffer.append(normalizedValue.toPlainString());
581  }
582
583
584
585  /**
586   * Retrieves a normalized string representation of this number as it should
587   * appear in a JSON object.  The normalized representation will not use
588   * exponentiation, will not include a decimal point if the value can be
589   * represented as an integer, and will not include any unnecessary trailing
590   * zeroes if it can only be represented as a floating-point value.
591   *
592   * @param  ignoreFieldNameCase  Indicates whether field names should be
593   *                              treated in a case-sensitive (if {@code false})
594   *                              or case-insensitive (if {@code true}) manner.
595   * @param  ignoreValueCase      Indicates whether string field values should
596   *                              be treated in a case-sensitive (if
597   *                              {@code false}) or case-insensitive (if
598   *                              {@code true}) manner.
599   * @param  ignoreArrayOrder     Indicates whether the order of elements in an
600   *                              array should be considered significant (if
601   *                              {@code false}) or insignificant (if
602   *                              {@code true}).
603   *
604   * @return  A normalized string representation of this number as it should
605   *          appear in a JSON object.
606   */
607  @Override()
608  @NotNull()
609  public String toNormalizedString(final boolean ignoreFieldNameCase,
610                                   final boolean ignoreValueCase,
611                                   final boolean ignoreArrayOrder)
612  {
613    return normalizedValue.toPlainString();
614  }
615
616
617
618  /**
619   * Appends a normalized string representation of this number as it should
620   * appear in a JSON object to the provided buffer.  The normalized
621   * representation will not use exponentiation, will not include a decimal
622   * point if the value can be represented as an integer, and will not include
623   * any unnecessary trailing zeroes if it can only be represented as a
624   * floating-point value.
625   *
626   * @param  buffer               The buffer to which the information should be
627   *                              appended.
628   * @param  ignoreFieldNameCase  Indicates whether field names should be
629   *                              treated in a case-sensitive (if {@code false})
630   *                              or case-insensitive (if {@code true}) manner.
631   * @param  ignoreValueCase      Indicates whether string field values should
632   *                              be treated in a case-sensitive (if
633   *                              {@code false}) or case-insensitive (if
634   *                              {@code true}) manner.
635   * @param  ignoreArrayOrder     Indicates whether the order of elements in an
636   *                              array should be considered significant (if
637   *                              {@code false}) or insignificant (if
638   *                              {@code true}).
639   */
640  @Override()
641  public void toNormalizedString(@NotNull final StringBuilder buffer,
642                                 final boolean ignoreFieldNameCase,
643                                 final boolean ignoreValueCase,
644                                 final boolean ignoreArrayOrder)
645  {
646    buffer.append(normalizedValue.toPlainString());
647  }
648
649
650
651  /**
652   * {@inheritDoc}
653   */
654  @Override()
655  public void appendToJSONBuffer(@NotNull final JSONBuffer buffer)
656  {
657    buffer.appendNumber(stringRepresentation);
658  }
659
660
661
662  /**
663   * {@inheritDoc}
664   */
665  @Override()
666  public void appendToJSONBuffer(@NotNull final String fieldName,
667                                 @NotNull final JSONBuffer buffer)
668  {
669    buffer.appendNumber(fieldName, stringRepresentation);
670  }
671}