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 */
021package com.unboundid.util.json;
022
023
024
025import java.math.BigDecimal;
026
027import com.unboundid.util.Debug;
028import com.unboundid.util.NotMutable;
029import com.unboundid.util.StaticUtils;
030import com.unboundid.util.ThreadSafety;
031import com.unboundid.util.ThreadSafetyLevel;
032
033import 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)
105public 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}