001/* 002 * Copyright 2015-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2015-2024 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-2024 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 * Retrieves the value of this JSON number as an {@code Integer}, but only if 428 * the value can be losslessly represented as an integer. 429 * 430 * @return The {@code Integer} value for this JSON number, or {@code null} if 431 * the value has a fractional component or is outside the range of a 432 * Java integer. 433 */ 434 @Nullable() 435 public Integer getValueAsInteger() 436 { 437 try 438 { 439 return value.intValueExact(); 440 } 441 catch (final Exception e) 442 { 443 Debug.debugException(e); 444 return null; 445 } 446 } 447 448 449 450 /** 451 * Retrieves the value of this JSON number as a {@code Long}, but only if 452 * the value can be losslessly represented as a long. 453 * 454 * @return The {@code Long} value for this JSON number, or {@code null} if 455 * the value has a fractional component or is outside the range of a 456 * Java long. 457 */ 458 @Nullable() 459 public Long getValueAsLong() 460 { 461 try 462 { 463 return value.longValueExact(); 464 } 465 catch (final Exception e) 466 { 467 Debug.debugException(e); 468 return null; 469 } 470 } 471 472 473 474 /** 475 * Retrieves the value of this JSON number as a {@code double}. Note that if 476 * the {@code BigDecimal} value is outside the range that can be represented 477 * by a {@code double}, then the value returned may be converted to positive 478 * or negative infinity. Further, the {@code double} value that is returned 479 * could potentially have less precision than the associated {@code BigDouble} 480 * value. 481 * 482 * @return The {@code double} value for this JSON number. 483 */ 484 public double getValueAsDouble() 485 { 486 return value.doubleValue(); 487 } 488 489 490 491 /** 492 * {@inheritDoc} 493 */ 494 @Override() 495 public int hashCode() 496 { 497 return normalizedValue.hashCode(); 498 } 499 500 501 502 /** 503 * {@inheritDoc} 504 */ 505 @Override() 506 public boolean equals(@Nullable final Object o) 507 { 508 if (o == this) 509 { 510 return true; 511 } 512 513 if (o instanceof JSONNumber) 514 { 515 // NOTE: BigDecimal.equals probably doesn't do what you want, nor what 516 // anyone would normally expect. If you want to determine if two 517 // BigDecimal values are the same, then use compareTo. 518 final JSONNumber n = (JSONNumber) o; 519 return (value.compareTo(n.value) == 0); 520 } 521 522 return false; 523 } 524 525 526 527 /** 528 * {@inheritDoc} 529 */ 530 @Override() 531 public boolean equals(@NotNull final JSONValue v, 532 final boolean ignoreFieldNameCase, 533 final boolean ignoreValueCase, 534 final boolean ignoreArrayOrder) 535 { 536 return ((v instanceof JSONNumber) && 537 (value.compareTo(((JSONNumber) v).value) == 0)); 538 } 539 540 541 542 /** 543 * Retrieves a string representation of this number as it should appear in a 544 * JSON object. If the object containing this number was decoded from a 545 * string, then this method will use the same string representation as in that 546 * original object. Otherwise, the string representation will be constructed. 547 * 548 * @return A string representation of this number as it should appear in a 549 * JSON object. 550 */ 551 @Override() 552 @NotNull() 553 public String toString() 554 { 555 return stringRepresentation; 556 } 557 558 559 560 /** 561 * Appends a string representation of this number as it should appear in a 562 * JSON object to the provided buffer. If the object containing this number 563 * was decoded from a string, then this method will use the same string 564 * representation as in that original object. Otherwise, the string 565 * representation will be constructed. 566 * 567 * @param buffer The buffer to which the information should be appended. 568 */ 569 @Override() 570 public void toString(@NotNull final StringBuilder buffer) 571 { 572 buffer.append(stringRepresentation); 573 } 574 575 576 577 /** 578 * Retrieves a single-line string representation of this number as it should 579 * appear in a JSON object. If the object containing this number was decoded 580 * from a string, then this method will use the same string representation as 581 * in that original object. Otherwise, the string representation will be 582 * constructed. 583 * 584 * @return A single-line string representation of this number as it should 585 * appear in a JSON object. 586 */ 587 @Override() 588 @NotNull() 589 public String toSingleLineString() 590 { 591 return stringRepresentation; 592 } 593 594 595 596 /** 597 * Appends a single-line string representation of this number as it should 598 * appear in a JSON object to the provided buffer. If the object containing 599 * this number was decoded from a string, then this method will use the same 600 * string representation as in that original object. Otherwise, the string 601 * representation will be constructed. 602 * 603 * @param buffer The buffer to which the information should be appended. 604 */ 605 @Override() 606 public void toSingleLineString(@NotNull final StringBuilder buffer) 607 { 608 buffer.append(stringRepresentation); 609 } 610 611 612 613 /** 614 * Retrieves a normalized string representation of this number as it should 615 * appear in a JSON object. The normalized representation will not use 616 * exponentiation, will not include a decimal point if the value can be 617 * represented as an integer, and will not include any unnecessary trailing 618 * zeroes if it can only be represented as a floating-point value. 619 * 620 * @return A normalized string representation of this number as it should 621 * appear in a JSON object. 622 */ 623 @Override() 624 @NotNull() 625 public String toNormalizedString() 626 { 627 return normalizedValue.toPlainString(); 628 } 629 630 631 632 /** 633 * Appends a normalized string representation of this number as it should 634 * appear in a JSON object to the provided buffer. The normalized 635 * representation will not use exponentiation, will not include a decimal 636 * point if the value can be represented as an integer, and will not include 637 * any unnecessary trailing zeroes if it can only be represented as a 638 * floating-point value. 639 * 640 * @param buffer The buffer to which the information should be appended. 641 */ 642 @Override() 643 public void toNormalizedString(@NotNull final StringBuilder buffer) 644 { 645 buffer.append(normalizedValue.toPlainString()); 646 } 647 648 649 650 /** 651 * Retrieves a normalized string representation of this number as it should 652 * appear in a JSON object. The normalized representation will not use 653 * exponentiation, will not include a decimal point if the value can be 654 * represented as an integer, and will not include any unnecessary trailing 655 * zeroes if it can only be represented as a floating-point value. 656 * 657 * @param ignoreFieldNameCase Indicates whether field names should be 658 * treated in a case-sensitive (if {@code false}) 659 * or case-insensitive (if {@code true}) manner. 660 * @param ignoreValueCase Indicates whether string field values should 661 * be treated in a case-sensitive (if 662 * {@code false}) or case-insensitive (if 663 * {@code true}) manner. 664 * @param ignoreArrayOrder Indicates whether the order of elements in an 665 * array should be considered significant (if 666 * {@code false}) or insignificant (if 667 * {@code true}). 668 * 669 * @return A normalized string representation of this number as it should 670 * appear in a JSON object. 671 */ 672 @Override() 673 @NotNull() 674 public String toNormalizedString(final boolean ignoreFieldNameCase, 675 final boolean ignoreValueCase, 676 final boolean ignoreArrayOrder) 677 { 678 return normalizedValue.toPlainString(); 679 } 680 681 682 683 /** 684 * Appends a normalized string representation of this number as it should 685 * appear in a JSON object to the provided buffer. The normalized 686 * representation will not use exponentiation, will not include a decimal 687 * point if the value can be represented as an integer, and will not include 688 * any unnecessary trailing zeroes if it can only be represented as a 689 * floating-point value. 690 * 691 * @param buffer The buffer to which the information should be 692 * appended. 693 * @param ignoreFieldNameCase Indicates whether field names should be 694 * treated in a case-sensitive (if {@code false}) 695 * or case-insensitive (if {@code true}) manner. 696 * @param ignoreValueCase Indicates whether string field values should 697 * be treated in a case-sensitive (if 698 * {@code false}) or case-insensitive (if 699 * {@code true}) manner. 700 * @param ignoreArrayOrder Indicates whether the order of elements in an 701 * array should be considered significant (if 702 * {@code false}) or insignificant (if 703 * {@code true}). 704 */ 705 @Override() 706 public void toNormalizedString(@NotNull final StringBuilder buffer, 707 final boolean ignoreFieldNameCase, 708 final boolean ignoreValueCase, 709 final boolean ignoreArrayOrder) 710 { 711 buffer.append(normalizedValue.toPlainString()); 712 } 713 714 715 716 /** 717 * {@inheritDoc} 718 */ 719 @Override() 720 @NotNull() 721 public JSONNumber toNormalizedValue(final boolean ignoreFieldNameCase, 722 final boolean ignoreValueCase, 723 final boolean ignoreArrayOrder) 724 { 725 return new JSONNumber(normalizedValue); 726 } 727 728 729 730 /** 731 * {@inheritDoc} 732 */ 733 @Override() 734 public void appendToJSONBuffer(@NotNull final JSONBuffer buffer) 735 { 736 buffer.appendNumber(stringRepresentation); 737 } 738 739 740 741 /** 742 * {@inheritDoc} 743 */ 744 @Override() 745 public void appendToJSONBuffer(@NotNull final String fieldName, 746 @NotNull final JSONBuffer buffer) 747 { 748 buffer.appendNumber(fieldName, stringRepresentation); 749 } 750}