// SQL numeric text, as CAST reads it (spec M4d2 §4.3).
///|
/// The value of `c` as a digit of `base`, if it is one.
fn digit_of(c : Char, base : Int32) -> Int32? {
let value = match c {
'0'..<='9' => c.to_int() - '0'.to_int()
'a'..<='f' => c.to_int() - 'a'.to_int() + 10
'A'..<='F' => c.to_int() - 'A'.to_int() + 10
_ => return None
}
if value < base {
Some(value)
} else {
None
}
}
///|
/// The digits of `base` from `start`, without the `_` separators, and the
/// index after them. A `_` counts only between two digits (SQL:2023).
fn digit_run(
chars : Array[Char],
start : Int32,
base : Int32,
) -> (String, Int32) {
let buf = StringBuilder()
let mut i = start
while i < chars.length() {
let c = chars[i]
if digit_of(c, base) is Some(_) {
buf.write_char(c)
i += 1
} else if c == '_' &&
i > start &&
i + 1 < chars.length() &&
digit_of(chars[i - 1], base) is Some(_) &&
digit_of(chars[i + 1], base) is Some(_) {
i += 1
} else {
break
}
}
(buf.to_string(), i)
}
///|
fn not_a_number(text : String) -> OpFailure {
TypeFailure("'\{text}' is not a number")
}
///|
/// A number from SQL text: leading and trailing spaces (U+0020 only) are
/// trimmed, then an optional sign and one of: `0x`/`0o`/`0b` digits (an
/// integer); digits with an optional fraction (an exact decimal); the same
/// with an exponent (also exact, as a PartiQL number literal); `nan`, `inf`
/// (any case; only `inf` takes a sign), the only approximate forms.
fn numeric_text(text : String) -> Num raise OpFailure {
let chars = text.trim(chars=" ").to_owned().to_array()
let n = chars.length()
let signed = n > 0 && (chars[0] == '-' || chars[0] == '+')
let negative = n > 0 && chars[0] == '-'
let i = if signed { 1 } else { 0 }
match String::from_array(chars[i:]).to_lower() {
"nan" if !signed => return Approx(@double.not_a_number)
"inf" =>
return Approx(
if negative {
@double.neg_infinity
} else {
@double.infinity
},
)
_ => ()
}
if n >= i + 2 && chars[i] == '0' {
let base = match chars[i + 1] {
'x' | 'X' => 16
'o' | 'O' => 8
'b' | 'B' => 2
_ => 0
}
if base > 0 {
let (digits, end) = digit_run(chars, i + 2, base)
if digits.is_empty() || end != n {
raise not_a_number(text)
}
let value = @bigint.BigInt::from_string(digits, radix=base)
return Whole(if negative { -value } else { value })
}
}
let (whole, after_whole) = digit_run(chars, i, 10)
let mut j = after_whole
let mut fraction = ""
if j < n && chars[j] == '.' {
let (digits, end) = digit_run(chars, j + 1, 10)
fraction = digits
j = end
}
if whole.is_empty() && fraction.is_empty() {
raise not_a_number(text)
}
if j < n && (chars[j] == 'e' || chars[j] == 'E') {
let mut k = j + 1
let exponent_sign = if k < n && (chars[k] == '-' || chars[k] == '+') {
k += 1
chars[k - 1].to_string()
} else {
""
}
let (exponent, end) = digit_run(chars, k, 10)
if exponent.is_empty() || end != n {
raise not_a_number(text)
}
// An exponent does not make the number approximate: PartiQL reads a
// number literal with an exponent as a DECIMAL, so the digits stay
// exact and each target rounds once (spec M4d2 §4.3).
let magnitude = @bigint.BigInt::from_string(exponent)
let shift = (if exponent_sign == "-" { -magnitude } else { magnitude }) -
@bigint.BigInt::from_int(fraction.length())
if shift.compare_int(2147483647) > 0 || shift.compare_int(-2147483648) < 0 {
raise DataFailure("the exponent of '\{text}' is out of range")
}
return Exact(signed_decimal(negative, whole + fraction, shift.to_int()))
}
if j != n {
raise not_a_number(text)
}
Exact(signed_decimal(negative, whole + fraction, -fraction.length()))
}
///|
/// The decimal `digits` × 10^`exponent`, negated when `negative`.
fn signed_decimal(
negative : Bool,
digits : String,
exponent : Int32,
) -> @ion_core.Decimal {
let coefficient = @bigint.BigInt::from_string(digits)
@ion_core.Decimal::Decimal(
if negative {
-coefficient
} else {
coefficient
},
exponent,
)
}