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