Quick Links:

bal | bbl | bdl | bsl

Namespaces

Component bdljsn_jsonnumber
[Package bdljsn]

Provide a value-semantic type representing a JSON number. More...

Namespaces

namespace  bdljsn

Detailed Description

Outline
Purpose:
Provide a value-semantic type representing a JSON number.
Classes:
bdljsn::JsonNumber value-semantic type representing a JSON number
Description:
This component provides a single value-semantic class, bdljsn::JsonNumber, that represents a JSON number. The value of a bdljsn::JsonNumber object is set at construction using a string representation of the JSON number (see JSON Textual Specification) or from one of several C++ arithmetic types (see Supported Conversions).
Arithmetic operations are not defined for for bdljsn::JsonNumber objects. For such operations, the value of a bdljsn::JsonNumber object can be converted to any of those supported types, though the conversion may not be exact.
The bdlsn::JsonNumber equality operation returns true if the string representation of the number (returned by the value accessor method) is the same, even where the two strings represent the same number (e.g., "10" and "1e1"). This definition of equality reflects the fact that the JSON textual representation for the two JsonNumber objects will be different. The function isEqual is provided for (a more expensive) numeric equality comparison.
JSON Textual Specification:
JSON numbers are defined by strings that match the grammar given at https://www.rfc-editor.org/rfc/rfc8259#section-6. The equivalent regular expression is:
  /^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][-+]?[0-9]+)?\z/
Note that "\z" matches end-of-string but not a preceding \n.
For example:
   1
   2.1
  -3
   4e1
   5.1e+2
   6.12e-3
   7e+04
  -8.1e+005
Notice that:
  • Leading zeros are not allowed for the mantissa but are allowed for the exponent.
  • A decimal point must be followed by at least one digit.
  • The grammar does not specify any limit on the number of digits in the mantissa or the exponent.
  • One can validly represent JSON numbers that are too large or too small for conversion to any of the supported arithmetic types.
The value of bdljsn::JsonNumber object is determined by the input string and that is not canonical. Thus, unequal strings lead to unequal bdljsn::JsonNumber objects even if their numeric values (e.g., asInt) are equal:
  assert(bdljsn::JsonNumber("1")         != bdljsn::JsonNumber("1.0");
  assert(bdljsn::JsonNumber("1").asInt() == bdljsn::JsonNumber("1.0")
                                                                    .asInt();
Supported Conversions:
The value of a bdljsn::JsonNumber object can be converted to an assortment of useful types:
In addition to named conversion functions (like asInt and asDouble) This component provides explicit conversion operations for floating point types (float, double, Decimal64) on platforms where explicit conversions are supported.
Handling Inexact Conversions: Floating Point Vs Integral Types:
Converting a bdlsjn::JsonNumber to another representation may result in a value that is not the same as the original bdljsn::JsonNumber. Either the bdljsn::JsonNumber may represent a numeric value outside of the representable range of the requested type (i.e., it is too large, or too small), or the value may not be representable exactly. bdljsn::JsonNumber conversions will return the closest approximation of the bdljsn::JsonNumber, even when a non-zero status is returned indicating an inexact conversion.
All the provided conversions to integral types have signatures that require a return status, whereas conversion functions are provided for floating point types that do not return a status. This is because:
  1. Floating point representations have specific values to indicate a bdljsn::JsonNumber is outside of the representable range (-INF, +INF).
  2. Truncating the fractional part of a number to coerce a value to an integer is typically an error (the data being processed did not meet the programmer's expectation), whereas returning the closest floating point approximation to a bdljsn::JsonNumber is very often not an error.
Exact Decima64 Representations:
For users requiring precise conversions to bdldfp::Decimal64, the function asDecimal64Exact returns additional status indicating whether the conversion is exact. An exact conversion for a Decimal64 is one that preserves all the significant digits resulting in a decimal representation having the same numerical value as the original JSON text. Note that asDecimal64Exact has very similar performance to asDecimal64 (i.e., there is not a notable performance penalty to determining this property).
Known Issues With asDecima64Exact:
Currently asDecimal64Exact will return bdljsn::JsonNumber::k_NOT_EXACT if the input is 0 with an exponent outside of the range ([-398, 369]). For example, 0e-400. This reflects the behavior of the underlying 3rd party implementation. Please contact BDE if this is a concern.
Usage:
This section illustrates intended use of this component.
Example 1: Creating JSON Number Object from User Input:
The specification of values for JSON numbers often starts with user input textual representations of those values. As the specifications for valid representation are complicated and not always intuitive it is prudent to validate that input using the bdljsn::JsonNumber::isValidNumber function; otherwise, one might try to create a bdljsn::JsonNumber object from an invalid specification and that leads to undefined behavior.
First, as a expedient for this example, we organize in an array input that might well be entered by some user:
  struct {
      const char *d_text_p;
      const char *d_description_p;
      bool        d_expected;
  } USER_INPUT[] = {

  //  VALUE                   DESCRIPTION                             EXP
  //  ----------------------  --------------------------------------  ---

    // Invalid Input (that is valid in other contexts).

    { "1.",                   "Not uncommon way to write '1'."      , 0  }
  , { "1,000",                "No commas allowed"                   , 0  }
  , { "01",                   "Leading '0',  disallowed by JSON."   , 0  }
  , { "",                     "0 per 'atoi', disallowed by JSON."   , 0  }
  , { "Hello, world!",        "0 per 'atoi', disallowed by JSON."   , 0  }
  , { "NaN",                  "invalid number"                      , 0  }
  , { "INF",                  "invalid number"                      , 0  }
  , { "-INF",                 "invalid number"                      , 0  }
  , { "+INF",                 "invalid number"                      , 0  }

    // Valid input (some surprising)

  , { "1234567890",           "Integral value"                      , 1  }
  , { "1234567890.123456",    "Non-integral value"                  , 1  }
  , { "1234567890.1234567",   "Beyond Decimal64 precision"          , 1  }
  , { "-9223372036854775809", "INT64_MIN, underflow, but valid JSON", 1  }
  , { "1.5e27",               "INT64_MAX,  overflow, but valid JSON", 1  }
  , { "999999999999999999999999999999999999999999999999999999999999"
      "e"
      "999999999999999999999999999999999999999999999999999999999999",
                              "astronomic value"                    , 1 }
  };

  const bsl::size_t NUM_USER_INPUT = sizeof USER_INPUT / sizeof *USER_INPUT;
Now, if and only if the input is valid, we use the input to construct a bdljsn::JsonNumber object and add that object to a vector for later processing.
  bsl::vector<bdljsn::JsonNumber> userInput; // when valid input

  for (bsl::size_t ti = 0; ti < NUM_USER_INPUT; ++ti) {
      const char *TEXT = USER_INPUT[ti].d_text_p;
      const char *DESC = USER_INPUT[ti].d_description_p; (void) DESC;
      const bool  EXP  = USER_INPUT[ti].d_expected;

      const bool isValid  = bdljsn::JsonNumber::isValidNumber(TEXT);
      assert(EXP == isValid);

      if (isValid) {
          userInput.push_back(bdljsn::JsonNumber(TEXT));
      }
  }
Finally, we confirm that the vector has the expected number of elements:
  assert(6 == userInput.size());
Example 2: Using bdljsn::JsonNumber Objects:
We saw in Example 1 that bdljsn::JsonNumber objects can validly hold values numeric values that cannot be converted to any of the supported types (e.g., the "astronomic value") for arithmetic operations. Applications that accept arbitrary bdljsn::JsonNumber objects should be prepared to categorize the contained value and adapt their handling accordingly. In practice, applications may have some assurances of the contents of received bdljsn::JsonNumber objects. Here, we intentionally avoid such assumptions to explore the wide range of variations that can arise.
Legend, in the output below:
  • "OK":

    • Means "OKay to use". In some cases, the numeric value of the arithmetic type is an approximation of JSON number and the application may have to allow for that difference.

  • "NG":

    • Means "No Good" (do not use). The JSON number is outside of the valid range of the arithmetic type.

First, we set up a framework (in this case, a for loop) for examining our input, the same userInput vector created in Example 1:
  for (bsl::size_t i = 0; i < userInput.size(); ++i) {
      const bdljsn::JsonNumber obj = userInput[i];
Then, we categorize the value as integral or not:
      if (obj.isIntegral()) {
          bsl::cout << "Integral: ";
If integral, we check if the value is a usable range. Let us assume that bslsl::Type::Int64 is as large a number as we can accept.
Then, we convert the JSON number to that type and check for overflow and underflow:
          bsls::Types::Int64 value;
          int                rc = obj.asInt64(&value);
          switch (rc) {
              case 0: {
                  bsl::cout << value      << " : OK to USE" << bsl::endl;
              } break;
              case bdljsn::JsonNumber::k_OVERFLOW: {
                  bsl::cout << obj.value() << ": NG too large" << bsl::endl;
              } break;
              case bdljsn::JsonNumber::k_UNDERFLOW: {
                  bsl::cout << obj.value() << ": NG too small" << bsl::endl;
              } break;
              case bdljsn::JsonNumber::k_NOT_INTEGRAL: {
                assert(!"reached");
              } break;
          }
Next, if the value is not integral, we try to handle it as a floating point value -- a bdldfp::Decimal64 in this example -- and further categorize it as exact/inexact, too large/small.
      } else {
          bsl::cout << "Not-Integral: ";

          bdldfp::Decimal64 value;
          int               rc = obj.asDecimal64Exact(&value);
          switch (rc) {
              case 0: {
                  bsl::cout << value << " :  exact: OK to USE";
              } break;
              case bdljsn::JsonNumber::k_INEXACT: {
                  bsl::cout << value << ": inexact: USE approximation";
              } break;
              case bdljsn::JsonNumber::k_NOT_INTEGRAL: {
                assert(!"reached");
              } break;
          }

          const bdldfp::Decimal64 INF =
                          bsl::numeric_limits<bdldfp::Decimal64>::infinity();

          if        ( INF == value) {
              bsl::cout << ": NG too large" << bsl::endl;
          } else if (-INF == value) {
              bsl::cout << ": NG too small" << bsl::endl;
          } else {
              bsl::cout << bsl::endl;
          }
      }
  }
Finally, we observe for particular input:
  Integral: 1234567890 : OK to USE
  Not-Integral: 1234567890.123456 :  exact: OK to USE
  Not-Integral: 1234567890.123457: inexact: USE approximation
  Integral: -9223372036854775809: NG too small
  Integral: 1.5e27: NG too large
  Integral: 999999999999999999999999999999999999999999999999999999999999e9999
  99999999999999999999999999999999999999999999999999999999: NG too large