All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.unboundid.util.json.JSONNumber Maven / Gradle / Ivy

Go to download

The UnboundID LDAP SDK for Java is a fast, comprehensive, and easy-to-use Java API for communicating with LDAP directory servers and performing related tasks like reading and writing LDIF, encoding and decoding data using base64 and ASN.1 BER, and performing secure communication. This package contains the Standard Edition of the LDAP SDK, which is a complete, general-purpose library for communicating with LDAPv3 directory servers.

There is a newer version: 7.0.1
Show newest version
/*
 * Copyright 2015-2018 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright (C) 2015-2018 Ping Identity Corporation
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPLv2 only)
 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see .
 */
package com.unboundid.util.json;



import java.math.BigDecimal;

import com.unboundid.util.Debug;
import com.unboundid.util.NotMutable;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;

import static com.unboundid.util.json.JSONMessages.*;



/**
 * This class provides an implementation of a JSON value that represents a
 * base-ten numeric value of arbitrary size.  It may or may not be a
 * floating-point value (including a decimal point with numbers to the right of
 * it), and it may or may not be expressed using scientific notation.  The
 * numeric value will be represented internally as a {@code BigDecimal}.
 * 

* The string representation of a JSON number consists of the following * elements, in the following order: *
    *
  1. * An optional minus sign to indicate that the value is negative. If this * is absent, then the number will be positive. Positive numbers must not * be prefixed with a plus sign. *
  2. *
  3. * One or more numeric digits to specify the whole number portion of the * value. There must not be any unnecessary leading zeroes, so the first * digit may be zero only if it is the only digit in the whole number * portion of the value. *
  4. *
  5. * An optional decimal point followed by at least one numeric digit to * indicate the fractional portion of the value. Trailing zeroes are * allowed in the fractional component. *
  6. *
  7. * An optional 'e' or 'E' character, followed by an optional '+' or '-' * character and at least one numeric digit to indicate that the value is * expressed in scientific notation and the number before the uppercase or * lowercase E should be multiplied by the specified positive or negative * power of ten. *
  8. *
* It is possible for the same number to have multiple equivalent string * representations. For example, all of the following valid string * representations of JSON numbers represent the same numeric value: *
    *
  • 12345
  • *
  • 12345.0
  • *
  • 1.2345e4
  • *
  • 1.2345e+4
  • *
* JSON numbers must not be enclosed in quotation marks. *

* If a JSON number is created from its string representation, then that * string representation will be returned from the {@link #toString()} method * (or appended to the provided buffer for the {@link #toString(StringBuilder)} * method). If a JSON number is created from a {@code long} or {@code double} * value, then the Java string representation of that value (as obtained from * the {@code String.valueOf} method) will be used as the string representation * for the number. If a JSON number is created from a {@code BigDecimal} value, * then the Java string representation will be obtained via that value's * {@code toPlainString} method. *

* The normalized representation of a JSON number is a canonical string * representation for that number. That is, all equivalent JSON number values * will have the same normalized representation. The normalized representation * will never use scientific notation, will never have trailing zeroes in the * fractional component, and will never have a fractional component if that * fractional component would be zero. For example, for the * logically-equivalent values "12345", "12345.0", "1.2345e4", and "1.2345e+4", * the normalized representation will be "12345". For the logically-equivalent * values "9876.5", "9876.50", and "9.8765e3", the normalized representation * will be "9876.5". */ @NotMutable() @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) public final class JSONNumber extends JSONValue { /** * The serial version UID for this serializable class. */ private static final long serialVersionUID = -9194944952299318254L; // The numeric value for this object. private final BigDecimal value; // The normalized representation of the value. private final BigDecimal normalizedValue; // The string representation for this object. private final String stringRepresentation; /** * Creates a new JSON number with the provided value. * * @param value The value for this JSON number. */ public JSONNumber(final long value) { this.value = new BigDecimal(value); normalizedValue = this.value; stringRepresentation = String.valueOf(value); } /** * Creates a new JSON number with the provided value. * * @param value The value for this JSON number. */ public JSONNumber(final double value) { this.value = new BigDecimal(value); normalizedValue = this.value; stringRepresentation = String.valueOf(value); } /** * Creates a new JSON number with the provided value. * * @param value The value for this JSON number. It must not be * {@code null}. */ public JSONNumber(final BigDecimal value) { this.value = value; stringRepresentation = value.toPlainString(); // There isn't a simple way to get a good normalized value from a // BigDecimal. If it represents an integer but has a decimal point followed // by some zeroes, then the only way we can strip them off is to convert it // from a BigDecimal to a BigInteger and back. If it represents a // floating-point value that has unnecessary zeros then we have to call the // stripTrailingZeroes method. BigDecimal minimalValue; try { minimalValue = new BigDecimal(value.toBigIntegerExact()); } catch (final Exception e) { // This is fine -- it just means that the value does not represent an // integer. minimalValue = value.stripTrailingZeros(); } normalizedValue = minimalValue; } /** * Creates a new JSON number from the provided string representation. * * @param stringRepresentation The string representation to parse as a JSON * number. It must not be {@code null}. * * @throws JSONException If the provided string cannot be parsed as a valid * JSON number. */ public JSONNumber(final String stringRepresentation) throws JSONException { this.stringRepresentation = stringRepresentation; // Make sure that the provided string represents a valid JSON number. This // is a little more strict than what BigDecimal accepts. First, make sure // it's not an empty string. final char[] chars = stringRepresentation.toCharArray(); if (chars.length == 0) { throw new JSONException(ERR_NUMBER_EMPTY_STRING.get()); } // Make sure that the last character is a digit. All valid string // representations of JSON numbers must end with a digit, and validating // that now allows us to do less error handling in subsequent checks. if (! isDigit(chars[chars.length-1])) { throw new JSONException(ERR_NUMBER_LAST_CHAR_NOT_DIGIT.get( stringRepresentation)); } // If the value starts with a minus sign, then skip over it. int pos = 0; if (chars[0] == '-') { pos++; } // Make sure that the first character (after the potential minus sign) is a // digit. If it's a zero, then make sure it's not followed by another // digit. if (! isDigit(chars[pos])) { throw new JSONException(ERR_NUMBER_ILLEGAL_CHAR.get(stringRepresentation, pos)); } if (chars[pos++] == '0') { if ((chars.length > pos) && isDigit(chars[pos])) { throw new JSONException(ERR_NUMBER_ILLEGAL_LEADING_ZERO.get( stringRepresentation)); } } // Parse the rest of the string. Make sure that it satisfies all of the // following constraints: // - There can be at most one decimal point. If there is a decimal point, // it must be followed by at least one digit. // - There can be at most one uppercase or lowercase 'E'. If there is an // 'E', then it must be followed by at least one digit, or it must be // followed by a plus or minus sign and at least one digit. // - If there are both a decimal point and an 'E', then the decimal point // must come before the 'E'. // - The only other characters allowed are digits. boolean decimalFound = false; boolean eFound = false; for ( ; pos < chars.length; pos++) { final char c = chars[pos]; if (c == '.') { if (decimalFound) { throw new JSONException(ERR_NUMBER_MULTIPLE_DECIMAL_POINTS.get( stringRepresentation)); } else { decimalFound = true; } if (eFound) { throw new JSONException(ERR_NUMBER_DECIMAL_IN_EXPONENT.get( stringRepresentation)); } if (! isDigit(chars[pos+1])) { throw new JSONException(ERR_NUMBER_DECIMAL_NOT_FOLLOWED_BY_DIGIT.get( stringRepresentation)); } } else if ((c == 'e') || (c == 'E')) { if (eFound) { throw new JSONException(ERR_NUMBER_MULTIPLE_EXPONENTS.get( stringRepresentation)); } else { eFound = true; } if ((chars[pos+1] == '-') || (chars[pos+1] == '+')) { if (! isDigit(chars[pos+2])) { throw new JSONException( ERR_NUMBER_EXPONENT_NOT_FOLLOWED_BY_DIGIT.get( stringRepresentation)); } // Increment the counter to skip over the sign. pos++; } else if (! isDigit(chars[pos+1])) { throw new JSONException(ERR_NUMBER_EXPONENT_NOT_FOLLOWED_BY_DIGIT.get( stringRepresentation)); } } else if (! isDigit(chars[pos])) { throw new JSONException(ERR_NUMBER_ILLEGAL_CHAR.get( stringRepresentation, pos)); } } // If we've gotten here, then we know the string represents a valid JSON // number. BigDecimal should be able to parse all valid JSON numbers. try { value = new BigDecimal(stringRepresentation); } catch (final Exception e) { Debug.debugException(e); // This should never happen if all of the validation above is correct, but // handle it just in case. throw new JSONException( ERR_NUMBER_CANNOT_PARSE.get(stringRepresentation, StaticUtils.getExceptionMessage(e)), e); } // There isn't a simple way to get a good normalized value from a // BigDecimal. If it represents an integer but has a decimal point followed // by some zeroes, then the only way we can strip them off is to convert it // from a BigDecimal to a BigInteger and back. If it represents a // floating-point value that has unnecessary zeros then we have to call the // stripTrailingZeroes method. BigDecimal minimalValue; try { minimalValue = new BigDecimal(value.toBigIntegerExact()); } catch (final Exception e) { // This is fine -- it just means that the value does not represent an // integer. minimalValue = value.stripTrailingZeros(); } normalizedValue = minimalValue; } /** * Indicates whether the specified character represents a digit. * * @param c The character for which to make the determination. * * @return {@code true} if the specified character represents a digit, or * {@code false} if not. */ private static boolean isDigit(final char c) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return true; default: return false; } } /** * Retrieves the value of this JSON number as a {@code BigDecimal}. * * @return The value of this JSON number as a {@code BigDecimal}. */ public BigDecimal getValue() { return value; } /** * {@inheritDoc} */ @Override() public int hashCode() { return normalizedValue.hashCode(); } /** * {@inheritDoc} */ @Override() public boolean equals(final Object o) { if (o == this) { return true; } if (o instanceof JSONNumber) { // NOTE: BigDecimal.equals probably doesn't do what you want, nor what // anyone would normally expect. If you want to determine if two // BigDecimal values are the same, then use compareTo. final JSONNumber n = (JSONNumber) o; return (value.compareTo(n.value) == 0); } return false; } /** * {@inheritDoc} */ @Override() public boolean equals(final JSONValue v, final boolean ignoreFieldNameCase, final boolean ignoreValueCase, final boolean ignoreArrayOrder) { return ((v instanceof JSONNumber) && (value.compareTo(((JSONNumber) v).value) == 0)); } /** * Retrieves a string representation of this number as it should appear in a * JSON object. If the object containing this number was decoded from a * string, then this method will use the same string representation as in that * original object. Otherwise, the string representation will be constructed. * * @return A string representation of this number as it should appear in a * JSON object. */ @Override() public String toString() { return stringRepresentation; } /** * Appends a string representation of this number as it should appear in a * JSON object to the provided buffer. If the object containing this number * was decoded from a string, then this method will use the same string * representation as in that original object. Otherwise, the string * representation will be constructed. * * @param buffer The buffer to which the information should be appended. */ @Override() public void toString(final StringBuilder buffer) { buffer.append(stringRepresentation); } /** * Retrieves a single-line string representation of this number as it should * appear in a JSON object. If the object containing this number was decoded * from a string, then this method will use the same string representation as * in that original object. Otherwise, the string representation will be * constructed. * * @return A single-line string representation of this number as it should * appear in a JSON object. */ @Override() public String toSingleLineString() { return stringRepresentation; } /** * Appends a single-line string representation of this number as it should * appear in a JSON object to the provided buffer. If the object containing * this number was decoded from a string, then this method will use the same * string representation as in that original object. Otherwise, the string * representation will be constructed. * * @param buffer The buffer to which the information should be appended. */ @Override() public void toSingleLineString(final StringBuilder buffer) { buffer.append(stringRepresentation); } /** * Retrieves a normalized string representation of this number as it should * appear in a JSON object. The normalized representation will not use * exponentiation, will not include a decimal point if the value can be * represented as an integer, and will not include any unnecessary trailing * zeroes if it can only be represented as a floating-point value. * * @return A normalized string representation of this number as it should * appear in a JSON object. */ @Override() public String toNormalizedString() { return normalizedValue.toPlainString(); } /** * Appends a normalized string representation of this number as it should * appear in a JSON object to the provided buffer. The normalized * representation will not use exponentiation, will not include a decimal * point if the value can be represented as an integer, and will not include * any unnecessary trailing zeroes if it can only be represented as a * floating-point value. * * @param buffer The buffer to which the information should be appended. */ @Override() public void toNormalizedString(final StringBuilder buffer) { buffer.append(normalizedValue.toPlainString()); } /** * Retrieves a normalized string representation of this number as it should * appear in a JSON object. The normalized representation will not use * exponentiation, will not include a decimal point if the value can be * represented as an integer, and will not include any unnecessary trailing * zeroes if it can only be represented as a floating-point value. * * @param ignoreFieldNameCase Indicates whether field names should be * treated in a case-sensitive (if {@code false}) * or case-insensitive (if {@code true}) manner. * @param ignoreValueCase Indicates whether string field values should * be treated in a case-sensitive (if * {@code false}) or case-insensitive (if * {@code true}) manner. * @param ignoreArrayOrder Indicates whether the order of elements in an * array should be considered significant (if * {@code false}) or insignificant (if * {@code true}). * * @return A normalized string representation of this number as it should * appear in a JSON object. */ @Override() public String toNormalizedString(final boolean ignoreFieldNameCase, final boolean ignoreValueCase, final boolean ignoreArrayOrder) { return normalizedValue.toPlainString(); } /** * Appends a normalized string representation of this number as it should * appear in a JSON object to the provided buffer. The normalized * representation will not use exponentiation, will not include a decimal * point if the value can be represented as an integer, and will not include * any unnecessary trailing zeroes if it can only be represented as a * floating-point value. * * @param buffer The buffer to which the information should be * appended. * @param ignoreFieldNameCase Indicates whether field names should be * treated in a case-sensitive (if {@code false}) * or case-insensitive (if {@code true}) manner. * @param ignoreValueCase Indicates whether string field values should * be treated in a case-sensitive (if * {@code false}) or case-insensitive (if * {@code true}) manner. * @param ignoreArrayOrder Indicates whether the order of elements in an * array should be considered significant (if * {@code false}) or insignificant (if * {@code true}). */ @Override() public void toNormalizedString(final StringBuilder buffer, final boolean ignoreFieldNameCase, final boolean ignoreValueCase, final boolean ignoreArrayOrder) { buffer.append(normalizedValue.toPlainString()); } /** * {@inheritDoc} */ @Override() public void appendToJSONBuffer(final JSONBuffer buffer) { buffer.appendNumber(stringRepresentation); } /** * {@inheritDoc} */ @Override() public void appendToJSONBuffer(final String fieldName, final JSONBuffer buffer) { buffer.appendNumber(fieldName, stringRepresentation); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy