///|
/// The number classes of the JMESPath runtime of the Smithy reference. A
/// number token without `.`, `e` or `E` is a `Long`, or a `BigInteger` when
/// 64 bits do not hold it. A decimal token is a `Double` when a double holds
/// its exact value, and a `BigDecimal` otherwise.
priv enum NumberClass {
Long
BigInteger
Double
BigDecimal
}
///|
/// An exact decimal value: `coefficient` times ten to the power `exponent`.
/// The coefficient has no trailing zeros, and `digits` is its number of
/// decimal digits (zero for the value zero).
priv struct Decimal {
coefficient : BigInt
exponent : BigInt
digits : Int32
}
///|
/// Parse a JSON number token, or the text of `Double::to_string`, into its
/// exact decimal value.
fn Decimal::parse(text : String) -> Decimal? {
let digits = StringBuilder()
let mut negative = false
let mut fraction = 0
let mut in_fraction = false
let mut exponent = BigInt::from_int(0)
let mut seen_digit = false
let mut i = 0
if i < text.length() &&
(text.code_unit_at(i) == '-' || text.code_unit_at(i) == '+') {
negative = text.code_unit_at(i) == '-'
i += 1
}
while i < text.length() {
let c = text.code_unit_at(i)
if c >= '0' && c <= '9' {
seen_digit = true
digits.write_char(c.to_int().unsafe_to_char())
if in_fraction {
fraction += 1
}
} else if c == '.' && !in_fraction {
in_fraction = true
} else if c == 'e' || c == 'E' {
let rest = text[i + 1:].to_owned()
let rest = if rest.has_prefix("+") { rest[1:].to_owned() } else { rest }
if rest.is_empty() || rest == "-" {
return None
}
for d in (if rest.has_prefix("-") { rest[1:] } else { rest[:] }) {
if d < '0' || d > '9' {
return None
}
}
exponent = BigInt::from_string(rest)
break
} else {
return None
}
i += 1
}
guard seen_digit else { return None }
let mut text = digits.to_string()
let mut start = 0
while start < text.length() && text.code_unit_at(start) == '0' {
start += 1
}
text = text[start:].to_owned()
let mut end = text.length()
while end > 0 && text.code_unit_at(end - 1) == '0' {
end -= 1
}
let trailing = text.length() - end
text = text[:end].to_owned()
if text.is_empty() {
return Some({ coefficient: 0N, exponent: 0N, digits: 0, })
}
let coefficient = BigInt::from_string(text)
Some({
coefficient: if negative {
-coefficient
} else {
coefficient
},
exponent: exponent - BigInt::from_int(fraction) + BigInt::from_int(trailing),
digits: text.length(),
})
}
///|
/// Compare two exact decimal values.
fn Decimal::compare(self : Decimal, other : Decimal) -> Int32 {
let sign = self.coefficient.compare_int(0)
let other_sign = other.coefficient.compare_int(0)
if sign != other_sign || sign == 0 {
return sign.compare(other_sign)
}
// The position of the most significant digit decides when it differs, so
// a large exponent never needs a large power of ten.
let magnitude = self.exponent + BigInt::from_int(self.digits)
let other_magnitude = other.exponent + BigInt::from_int(other.digits)
let order = magnitude.compare(other_magnitude)
if order != 0 {
return if sign > 0 { order } else { -order }
}
// The same magnitude bounds the exponent difference by the digit counts.
let shift = (self.exponent - other.exponent).to_int()
let ten = BigInt::from_int(10)
if shift >= 0 {
(self.coefficient * ten.pow(BigInt::from_int(shift))).compare(
other.coefficient,
)
} else {
self.coefficient.compare(
other.coefficient * ten.pow(BigInt::from_int(-shift)),
)
}
}
///|
let long_min : BigInt = BigInt::from_string("-9223372036854775808")
///|
let long_max : BigInt = BigInt::from_string("9223372036854775807")
///|
fn is_decimal_token(text : String) -> Bool {
text.contains(".") || text.contains("e") || text.contains("E")
}
///|
/// The class of a number. A number with its source token takes the class of
/// the token. A number without its token (a computed number, or a JSON value
/// that a double holds exactly) is a `Long` when it is a whole number in the
/// 64-bit range, and a `Double` otherwise.
fn number_class(value : Double, repr : String?) -> NumberClass {
match repr {
Some(text) if is_decimal_token(text) => {
if value.is_nan() || value.is_inf() {
return BigDecimal
}
match (Decimal::parse(text), Decimal::parse(value.to_string())) {
(Some(exact), Some(held)) if exact.compare(held) == 0 => Double
_ => BigDecimal
}
}
Some(text) => {
let digits = if text.has_prefix("-") || text.has_prefix("+") {
text[1:].to_owned()
} else {
text
}
guard !digits.is_empty() && digits.iter().all(c => c >= '0' && c <= '9') else {
return BigInteger
}
let whole = BigInt::from_string(digits)
let whole = if text.has_prefix("-") { -whole } else { whole }
if whole >= long_min && whole <= long_max {
Long
} else {
BigInteger
}
}
None =>
if value.is_nan() ||
value.is_inf() ||
value != value.floor() ||
// A 64-bit integer has no negative zero.
(value == 0.0 && value.reinterpret_as_int64() != 0L) ||
value < -9223372036854775808.0 ||
value >= 9223372036854775808.0 {
Double
} else {
Long
}
}
}
///|
/// The exact value of a number: its source token, or the shortest decimal of
/// its double, as Java's `BigDecimal.valueOf(double)` gives.
fn exact_value(value : Double, repr : String?) -> Decimal? {
match repr {
Some(text) => Decimal::parse(text)
None =>
if value.is_nan() || value.is_inf() {
None
} else {
Decimal::parse(value.to_string())
}
}
}
///|
/// Order two doubles as Java's `Double.compare`: -0.0 is less than 0.0, and
/// NaN is greater than every other value.
fn double_order(a : Double, b : Double) -> Int32 {
if a < b {
return -1
}
if a > b {
return 1
}
let bits = fn(d : Double) {
if d.is_nan() {
0x7ff8000000000000L
} else {
d.reinterpret_as_int64()
}
}
bits(a).compare(bits(b))
}
///|
/// Order two JSON numbers as the Smithy reference does. When one side is a
/// `BigInteger` or a `BigDecimal`, or both are `Long`, the exact values
/// decide. When one side is a `Double`, the doubles decide.
fn number_order(
a : Double,
a_repr : String?,
b : Double,
b_repr : String?,
) -> Int32 {
// Without tokens, both sides are a 64-bit integer or a double, and the
// shortest decimal of a double orders as the double does, so the doubles
// decide. This keeps sort and max free of allocation.
if a_repr is None && b_repr is None {
return double_order(a, b)
}
let a_class = number_class(a, a_repr)
let b_class = number_class(b, b_repr)
let big = a_class is (BigInteger | BigDecimal) ||
b_class is (BigInteger | BigDecimal)
if !big && (a_class is Double || b_class is Double) {
return double_order(a, b)
}
match (exact_value(a, a_repr), exact_value(b, b_repr)) {
(Some(x), Some(y)) => x.compare(y)
_ => double_order(a, b)
}
}
///|
/// Tell whether two JSON values are equal as JMESPath values: numbers by
/// `number_order`, arrays by element, objects by key. The walk keeps its own
/// stack, so deep values do not recurse.
fn json_equal(a : Json, b : Json) -> Bool {
let pending = [(a, b)]
while pending.pop() is Some((a, b)) {
match (a, b) {
(Number(x, repr=x_repr), Number(y, repr=y_repr)) =>
if number_order(x, x_repr, y, y_repr) != 0 {
return false
}
(Array(xs), Array(ys)) => {
if xs.length() != ys.length() {
return false
}
for i, x in xs {
pending.push((x, ys[i]))
}
}
(Object(xs), Object(ys)) => {
if xs.length() != ys.length() {
return false
}
for key, x in xs {
guard ys.get(key) is Some(y) else { return false }
pending.push((x, y))
}
}
_ => if a != b { return false }
}
}
true
}