001    /*
002     * Copyright 2015-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2015-2016 UnboundID Corp.
007     *
008     * This program is free software; you can redistribute it and/or modify
009     * it under the terms of the GNU General Public License (GPLv2 only)
010     * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011     * as published by the Free Software Foundation.
012     *
013     * This program is distributed in the hope that it will be useful,
014     * but WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program; if not, see <http://www.gnu.org/licenses>.
020     */
021    package com.unboundid.util.json;
022    
023    
024    
025    import java.math.BigDecimal;
026    
027    import com.unboundid.util.Debug;
028    import com.unboundid.util.NotMutable;
029    import com.unboundid.util.StaticUtils;
030    import com.unboundid.util.ThreadSafety;
031    import com.unboundid.util.ThreadSafetyLevel;
032    
033    import static com.unboundid.util.json.JSONMessages.*;
034    
035    
036    
037    /**
038     * This class provides an implementation of a JSON value that represents a
039     * base-ten numeric value of arbitrary size.  It may or may not be a
040     * floating-point value (including a decimal point with numbers to the right of
041     * it), and it may or may not be expressed using scientific notation.  The
042     * numeric value will be represented internally as a {@code BigDecimal}.
043     * <BR><BR>
044     * The string representation of a JSON number consists of the following
045     * elements, in the following order:
046     * <OL>
047     *   <LI>
048     *     An optional minus sign to indicate that the value is negative.  If this
049     *     is absent, then the number will be positive.  Positive numbers must not
050     *     be prefixed with a plus sign.
051     *   </LI>
052     *   <LI>
053     *     One or more numeric digits to specify the whole number portion of the
054     *     value.  There must not be any unnecessary leading zeroes, so the first
055     *     digit may be zero only if it is the only digit in the whole number
056     *     portion of the value.
057     *   </LI>
058     *   <LI>
059     *     An optional decimal point followed by at least one numeric digit to
060     *     indicate the fractional portion of the value.  Trailing zeroes are
061     *     allowed in the fractional component.
062     *   </LI>
063     *   <LI>
064     *     An optional 'e' or 'E' character, followed by an optional '+' or '-'
065     *     character and at least one numeric digit to indicate that the value is
066     *     expressed in scientific notation and the number before the uppercase or
067     *     lowercase E should be multiplied by the specified positive or negative
068     *     power of ten.
069     *   </LI>
070     * </OL>
071     * It is possible for the same number to have multiple equivalent string
072     * representations.  For example, all of the following valid string
073     * representations of JSON numbers represent the same numeric value:
074     * <UL>
075     *   <LI>12345</LI>
076     *   <LI>12345.0</LI>
077     *   <LI>1.2345e4</LI>
078     *   <LI>1.2345e+4</LI>
079     * </UL>
080     * JSON numbers must not be enclosed in quotation marks.
081     * <BR><BR>
082     * If a JSON number is created from its string representation, then that
083     * string representation will be returned from the {@link #toString()} method
084     * (or appended to the provided buffer for the {@link #toString(StringBuilder)}
085     * method).  If a JSON number is created from a {@code long} or {@code double}
086     * value, then the Java string representation of that value (as obtained from
087     * the {@code String.valueOf} method) will be used as the string representation
088     * for the number.  If a JSON number is created from a {@code BigDecimal} value,
089     * then the Java string representation will be obtained via that value's
090     * {@code toPlainString} method.
091     * <BR><BR>
092     * The normalized representation of a JSON number is a canonical string
093     * representation for that number.  That is, all equivalent JSON number values
094     * will have the same normalized representation.  The normalized representation
095     * will never use scientific notation, will never have trailing zeroes in the
096     * fractional component, and will never have a fractional component if that
097     * fractional component would be zero.  For example, for the
098     * logically-equivalent values "12345", "12345.0", "1.2345e4", and "1.2345e+4",
099     * the normalized representation will be "12345".  For the logically-equivalent
100     * values "9876.5", "9876.50", and "9.8765e3", the normalized representation
101     * will be "9876.5".
102     */
103    @NotMutable()
104    @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
105    public final class JSONNumber
106           extends JSONValue
107    {
108      /**
109       * The serial version UID for this serializable class.
110       */
111      private static final long serialVersionUID = -9194944952299318254L;
112    
113    
114    
115      // The numeric value for this object.
116      private final BigDecimal value;
117    
118      // The normalized representation of the value.
119      private final BigDecimal normalizedValue;
120    
121      // The string representation for this object.
122      private final String stringRepresentation;
123    
124    
125    
126      /**
127       * Creates a new JSON number with the provided value.
128       *
129       * @param  value  The value for this JSON number.
130       */
131      public JSONNumber(final long value)
132      {
133        this.value = new BigDecimal(value);
134        normalizedValue = this.value;
135        stringRepresentation = String.valueOf(value);
136      }
137    
138    
139    
140      /**
141       * Creates a new JSON number with the provided value.
142       *
143       * @param  value  The value for this JSON number.
144       */
145      public JSONNumber(final double value)
146      {
147        this.value = new BigDecimal(value);
148        normalizedValue = this.value;
149        stringRepresentation = String.valueOf(value);
150      }
151    
152    
153    
154      /**
155       * Creates a new JSON number with the provided value.
156       *
157       * @param  value  The value for this JSON number.  It must not be
158       *                {@code null}.
159       */
160      public JSONNumber(final BigDecimal value)
161      {
162        this.value = value;
163        stringRepresentation = value.toPlainString();
164    
165        // There isn't a simple way to get a good normalized value from a
166        // BigDecimal.  If it represents an integer but has a decimal point followed
167        // by some zeroes, then the only way we can strip them off is to convert it
168        // from a BigDecimal to a BigInteger and back.  If it represents a
169        // floating-point value that has unnecessary zeros then we have to call the
170        // stripTrailingZeroes method.
171        BigDecimal minimalValue;
172        try
173        {
174          minimalValue = new BigDecimal(value.toBigIntegerExact());
175        }
176        catch (final Exception e)
177        {
178          // This is fine -- it just means that the value does not represent an
179          // integer.
180          minimalValue = value.stripTrailingZeros();
181        }
182        normalizedValue = minimalValue;
183      }
184    
185    
186    
187      /**
188       * Creates a new JSON number from the provided string representation.
189       *
190       * @param  stringRepresentation  The string representation to parse as a JSON
191       *                               number.  It must not be {@code null}.
192       *
193       * @throws  JSONException  If the provided string cannot be parsed as a valid
194       *                         JSON number.
195       */
196      public JSONNumber(final String stringRepresentation)
197             throws JSONException
198      {
199        this.stringRepresentation = stringRepresentation;
200    
201    
202        // Make sure that the provided string represents a valid JSON number.  This
203        // is a little more strict than what BigDecimal accepts.  First, make sure
204        // it's not an empty string.
205        final char[] chars = stringRepresentation.toCharArray();
206        if (chars.length == 0)
207        {
208          throw new JSONException(ERR_NUMBER_EMPTY_STRING.get());
209        }
210    
211    
212        // Make sure that the last character is a digit.  All valid string
213        // representations of JSON numbers must end with a digit, and validating
214        // that now allows us to do less error handling in subsequent checks.
215        if (! isDigit(chars[chars.length-1]))
216        {
217          throw new JSONException(ERR_NUMBER_LAST_CHAR_NOT_DIGIT.get(
218               stringRepresentation));
219        }
220    
221    
222        // If the value starts with a minus sign, then skip over it.
223        int pos = 0;
224        if (chars[0] == '-')
225        {
226          pos++;
227        }
228    
229    
230        // Make sure that the first character (after the potential minus sign) is a
231        // digit.  If it's a zero, then make sure it's not followed by another
232        // digit.
233        if (! isDigit(chars[pos]))
234        {
235          throw new JSONException(ERR_NUMBER_ILLEGAL_CHAR.get(stringRepresentation,
236               pos));
237        }
238    
239        if (chars[pos++] == '0')
240        {
241          if ((chars.length > pos) && isDigit(chars[pos]))
242          {
243            throw new JSONException(ERR_NUMBER_ILLEGAL_LEADING_ZERO.get(
244                 stringRepresentation));
245          }
246        }
247    
248    
249        // Parse the rest of the string.  Make sure that it satisfies all of the
250        // following constraints:
251        // - There can be at most one decimal point.  If there is a decimal point,
252        //   it must be followed by at least one digit.
253        // - There can be at most one uppercase or lowercase 'E'.  If there is an
254        //   'E', then it must be followed by at least one digit, or it must be
255        //   followed by a plus or minus sign and at least one digit.
256        // - If there are both a decimal point and an 'E', then the decimal point
257        //   must come before the 'E'.
258        // - The only other characters allowed are digits.
259        boolean decimalFound = false;
260        boolean eFound = false;
261        for ( ; pos < chars.length; pos++)
262        {
263          final char c = chars[pos];
264          if (c == '.')
265          {
266            if (decimalFound)
267            {
268              throw new JSONException(ERR_NUMBER_MULTIPLE_DECIMAL_POINTS.get(
269                   stringRepresentation));
270            }
271            else
272            {
273              decimalFound = true;
274            }
275    
276            if (eFound)
277            {
278              throw new JSONException(ERR_NUMBER_DECIMAL_IN_EXPONENT.get(
279                   stringRepresentation));
280            }
281    
282            if (! isDigit(chars[pos+1]))
283            {
284              throw new JSONException(ERR_NUMBER_DECIMAL_NOT_FOLLWED_BY_DIGIT.get(
285                   stringRepresentation));
286            }
287          }
288          else if ((c == 'e') || (c == 'E'))
289          {
290            if (eFound)
291            {
292              throw new JSONException(ERR_NUMBER_MULTIPLE_EXPONENTS.get(
293                   stringRepresentation));
294            }
295            else
296            {
297              eFound = true;
298            }
299    
300            if ((chars[pos+1] == '-') || (chars[pos+1] == '+'))
301            {
302              if (! isDigit(chars[pos+2]))
303              {
304                throw new JSONException(
305                     ERR_NUMBER_EXPONENT_NOT_FOLLOWED_BY_DIGIT.get(
306                          stringRepresentation));
307              }
308    
309              // Increment the counter to skip over the sign.
310              pos++;
311            }
312            else if (! isDigit(chars[pos+1]))
313            {
314              throw new JSONException(ERR_NUMBER_EXPONENT_NOT_FOLLOWED_BY_DIGIT.get(
315                   stringRepresentation));
316            }
317          }
318          else if (! isDigit(chars[pos]))
319          {
320            throw new JSONException(ERR_NUMBER_ILLEGAL_CHAR.get(
321                 stringRepresentation, pos));
322          }
323        }
324    
325    
326        // If we've gotten here, then we know the string represents a valid JSON
327        // number.  BigDecimal should be able to parse all valid JSON numbers.
328        try
329        {
330          value = new BigDecimal(stringRepresentation);
331        }
332        catch (final Exception e)
333        {
334          Debug.debugException(e);
335    
336          // This should never happen if all of the validation above is correct, but
337          // handle it just in case.
338          throw new JSONException(
339               ERR_NUMBER_CANNOT_PARSE.get(stringRepresentation,
340                    StaticUtils.getExceptionMessage(e)),
341               e);
342        }
343    
344        // There isn't a simple way to get a good normalized value from a
345        // BigDecimal.  If it represents an integer but has a decimal point followed
346        // by some zeroes, then the only way we can strip them off is to convert it
347        // from a BigDecimal to a BigInteger and back.  If it represents a
348        // floating-point value that has unnecessary zeros then we have to call the
349        // stripTrailingZeroes method.
350        BigDecimal minimalValue;
351        try
352        {
353          minimalValue = new BigDecimal(value.toBigIntegerExact());
354        }
355        catch (final Exception e)
356        {
357          // This is fine -- it just means that the value does not represent an
358          // integer.
359          minimalValue = value.stripTrailingZeros();
360        }
361        normalizedValue = minimalValue;
362      }
363    
364    
365    
366      /**
367       * Indicates whether the specified character represents a digit.
368       *
369       * @param  c  The character for which to make the determination.
370       *
371       * @return  {@code true} if the specified character represents a digit, or
372       *          {@code false} if not.
373       */
374      private static boolean isDigit(final char c)
375      {
376        switch (c)
377        {
378          case '0':
379          case '1':
380          case '2':
381          case '3':
382          case '4':
383          case '5':
384          case '6':
385          case '7':
386          case '8':
387          case '9':
388            return true;
389          default:
390            return false;
391        }
392      }
393    
394    
395    
396      /**
397       * Retrieves the value of this JSON number as a {@code BigDecimal}.
398       *
399       * @return  The value of this JSON number as a {@code BigDecimal}.
400       */
401      public BigDecimal getValue()
402      {
403        return value;
404      }
405    
406    
407    
408      /**
409       * {@inheritDoc}
410       */
411      @Override()
412      public int hashCode()
413      {
414        return normalizedValue.hashCode();
415      }
416    
417    
418    
419      /**
420       * {@inheritDoc}
421       */
422      @Override()
423      public boolean equals(final Object o)
424      {
425        if (o == this)
426        {
427          return true;
428        }
429    
430        if (o instanceof JSONNumber)
431        {
432          // NOTE:  BigDecimal.equals probably doesn't do what you want, nor what
433          // anyone would normally expect.  If you want to determine if two
434          // BigDecimal values are the same, then use compareTo.
435          final JSONNumber n = (JSONNumber) o;
436          return (value.compareTo(n.value) == 0);
437        }
438    
439        return false;
440      }
441    
442    
443    
444      /**
445       * {@inheritDoc}
446       */
447      @Override()
448      public boolean equals(final JSONValue v, final boolean ignoreFieldNameCase,
449                            final boolean ignoreValueCase,
450                            final boolean ignoreArrayOrder)
451      {
452        return ((v instanceof JSONNumber) &&
453             (value.compareTo(((JSONNumber) v).value) == 0));
454      }
455    
456    
457    
458      /**
459       * {@inheritDoc}
460       */
461      @Override()
462      public String toString()
463      {
464        return stringRepresentation;
465      }
466    
467    
468    
469      /**
470       * {@inheritDoc}
471       */
472      @Override()
473      public void toString(final StringBuilder buffer)
474      {
475        buffer.append(stringRepresentation);
476      }
477    
478    
479    
480      /**
481       * {@inheritDoc}
482       */
483      @Override()
484      public String toSingleLineString()
485      {
486        return stringRepresentation;
487      }
488    
489    
490    
491      /**
492       * {@inheritDoc}
493       */
494      @Override()
495      public void toSingleLineString(final StringBuilder buffer)
496      {
497        buffer.append(stringRepresentation);
498      }
499    
500    
501    
502      /**
503       * {@inheritDoc}
504       */
505      @Override()
506      public String toNormalizedString()
507      {
508        final StringBuilder buffer = new StringBuilder();
509        toNormalizedString(buffer);
510        return buffer.toString();
511      }
512    
513    
514    
515      /**
516       * {@inheritDoc}
517       */
518      @Override()
519      public void toNormalizedString(final StringBuilder buffer)
520      {
521        buffer.append(normalizedValue.toPlainString());
522      }
523    
524    
525    
526      /**
527       * {@inheritDoc}
528       */
529      @Override()
530      public void appendToJSONBuffer(final JSONBuffer buffer)
531      {
532        buffer.appendNumber(stringRepresentation);
533      }
534    
535    
536    
537      /**
538       * {@inheritDoc}
539       */
540      @Override()
541      public void appendToJSONBuffer(final String fieldName,
542                                     final JSONBuffer buffer)
543      {
544        buffer.appendNumber(fieldName, stringRepresentation);
545      }
546    }