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}