#![forbid(unsafe_code)]
use crate::Result;
pub(crate) const MAX_DIGITS: usize = 42;
pub(crate) enum DecodedNumberStack {
Sentinel {
text: &'static str,
is_integer: bool,
},
Parts {
digit_len: usize,
is_negative: bool,
decimal_point_index: i16,
is_integer: bool,
coefficient: Option<i128>,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OracleNumber {
Inline {
coefficient_le: [u8; 16],
scale: i16,
is_integer: bool,
},
Text { text: Box<str>, is_integer: bool },
}
impl OracleNumber {
fn inline(coefficient: i128, scale: i16, is_integer: bool) -> Self {
OracleNumber::Inline {
coefficient_le: coefficient.to_le_bytes(),
scale,
is_integer,
}
}
pub fn coefficient(&self) -> Option<i128> {
match self {
OracleNumber::Inline { coefficient_le, .. } => {
Some(i128::from_le_bytes(*coefficient_le))
}
OracleNumber::Text { .. } => None,
}
}
pub fn scale(&self) -> Option<i16> {
match self {
OracleNumber::Inline { scale, .. } => Some(*scale),
OracleNumber::Text { .. } => None,
}
}
pub fn from_wire(bytes: &[u8]) -> Result<Self> {
let mut digit_buf = [0u8; MAX_DIGITS];
match super::codecs::decode_number_parts_stack(bytes, &mut digit_buf)? {
DecodedNumberStack::Sentinel { text, is_integer } => Ok(OracleNumber::Text {
text: text.into(),
is_integer,
}),
DecodedNumberStack::Parts {
digit_len,
is_negative,
decimal_point_index,
is_integer,
coefficient,
} => {
let digits = &digit_buf[..digit_len];
match coefficient {
Some(coefficient) => {
let len = i32::try_from(digits.len()).unwrap_or(i32::MAX);
let scale_i32 = len - i32::from(decimal_point_index);
match i16::try_from(scale_i32) {
Ok(scale) => Ok(OracleNumber::inline(coefficient, scale, is_integer)),
Err(_) => Ok(Self::spill_text(
digits,
is_negative,
decimal_point_index,
is_integer,
)),
}
}
None => Ok(Self::spill_text(
digits,
is_negative,
decimal_point_index,
is_integer,
)),
}
}
}
}
fn spill_text(
digits: &[u8],
is_negative: bool,
decimal_point_index: i16,
is_integer: bool,
) -> Self {
let mut text = String::new();
super::codecs::format_number_digits(digits, is_negative, decimal_point_index, &mut text);
OracleNumber::Text {
text: text.into_boxed_str(),
is_integer,
}
}
pub fn from_canonical_text(text: &str) -> Self {
Self::from_canonical_text_with_flag(text, !text.contains('.'))
}
pub fn from_canonical_text_with_flag(text: &str, is_integer: bool) -> Self {
match parse_canonical_inline(text) {
Some((coefficient, scale)) => OracleNumber::inline(coefficient, scale, is_integer),
None => OracleNumber::Text {
text: text.into(),
is_integer,
},
}
}
pub fn as_borrowed_text(&self) -> Option<&str> {
match self {
OracleNumber::Text { text, .. } => Some(text),
OracleNumber::Inline { .. } => None,
}
}
pub fn is_integer(&self) -> bool {
match self {
OracleNumber::Inline { is_integer, .. } | OracleNumber::Text { is_integer, .. } => {
*is_integer
}
}
}
pub fn fmt_into(&self, out: &mut String) {
match self {
OracleNumber::Text { text, .. } => out.push_str(text),
OracleNumber::Inline {
coefficient_le,
scale,
..
} => fmt_inline_into(i128::from_le_bytes(*coefficient_le), *scale, out),
}
}
pub fn to_canonical_string(&self) -> String {
let mut out = String::new();
self.fmt_into(&mut out);
out
}
pub fn to_canonical_cow(&self) -> std::borrow::Cow<'_, str> {
match self {
OracleNumber::Text { text, .. } => std::borrow::Cow::Borrowed(text),
OracleNumber::Inline { .. } => std::borrow::Cow::Owned(self.to_canonical_string()),
}
}
pub fn to_i64(&self) -> Option<i64> {
match self {
OracleNumber::Inline {
coefficient_le,
scale,
..
} => inline_to_i128(i128::from_le_bytes(*coefficient_le), *scale)
.and_then(|v| i64::try_from(v).ok()),
OracleNumber::Text { text, .. } => text.parse::<i64>().ok(),
}
}
pub fn to_i128(&self) -> Option<i128> {
match self {
OracleNumber::Inline {
coefficient_le,
scale,
..
} => inline_to_i128(i128::from_le_bytes(*coefficient_le), *scale),
OracleNumber::Text { text, .. } => text.parse::<i128>().ok(),
}
}
}
pub(crate) enum DecodedNumber {
Text { is_integer: bool },
Parts {
is_negative: bool,
decimal_point_index: i16,
is_integer: bool,
},
}
#[cfg(test)]
fn digits_to_i128(digits: &[u8], is_negative: bool) -> Option<i128> {
let mut acc: i128 = 0;
for &d in digits {
acc = acc.checked_mul(10)?.checked_add(i128::from(d))?;
}
if is_negative {
Some(-acc)
} else {
Some(acc)
}
}
fn inline_to_i128(coefficient: i128, scale: i16) -> Option<i128> {
match scale.cmp(&0) {
std::cmp::Ordering::Equal => Some(coefficient),
std::cmp::Ordering::Less => {
let mut v = coefficient;
for _ in 0..(-(i32::from(scale))) {
v = v.checked_mul(10)?;
}
Some(v)
}
std::cmp::Ordering::Greater => {
let mut divisor: i128 = 1;
for _ in 0..i32::from(scale) {
divisor = divisor.checked_mul(10)?;
}
if coefficient % divisor == 0 {
Some(coefficient / divisor)
} else {
None
}
}
}
}
fn fmt_inline_into(coefficient: i128, scale: i16, out: &mut String) {
if coefficient == 0 {
out.push('0');
return;
}
let is_negative = coefficient < 0;
let mut buf = [0u8; 40];
let mut mag = coefficient.unsigned_abs();
let mut idx = buf.len();
while mag > 0 {
idx -= 1;
buf[idx] = b'0' + (mag % 10) as u8;
mag /= 10;
}
let digits = &buf[idx..];
let digit_count = digits.len() as i32;
let decimal_point_index = digit_count - i32::from(scale);
if is_negative {
out.push('-');
}
if decimal_point_index <= 0 {
out.push_str("0.");
for _ in decimal_point_index..0 {
out.push('0');
}
for &d in digits {
out.push(d as char);
}
return;
}
for (i, &d) in digits.iter().enumerate() {
if i as i32 == decimal_point_index {
out.push('.');
}
out.push(d as char);
}
if decimal_point_index > digit_count {
for _ in digit_count..decimal_point_index {
out.push('0');
}
}
}
fn parse_canonical_inline(text: &str) -> Option<(i128, i16)> {
let (is_negative, rest) = match text.strip_prefix('-') {
Some(r) => (true, r),
None => (false, text),
};
if rest.is_empty() {
return None;
}
let (int_part, frac_part) = match rest.split_once('.') {
Some((i, f)) => (i, f),
None => (rest, ""),
};
if !int_part.bytes().all(|b| b.is_ascii_digit())
|| !frac_part.bytes().all(|b| b.is_ascii_digit())
{
return None;
}
let mut acc: i128 = 0;
for b in int_part.bytes().chain(frac_part.bytes()) {
acc = acc.checked_mul(10)?.checked_add(i128::from(b - b'0'))?;
}
let mut coefficient = if is_negative { acc.checked_neg()? } else { acc };
let mut scale = i16::try_from(frac_part.len()).ok()?;
if coefficient == 0 && scale == 0 {
return None;
}
if scale == 0 {
while coefficient % 10 == 0 {
coefficient /= 10;
scale = scale.checked_sub(1)?;
}
}
Some((coefficient, scale))
}
impl std::fmt::Display for OracleNumber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = String::new();
self.fmt_into(&mut s);
f.write_str(&s)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::thin::codecs::{decode_number_parts_stack, encode_number_text};
fn assert_fused_matches_reference(wire: &[u8], label: &str) {
let mut digit_buf = [0u8; MAX_DIGITS];
let parts = decode_number_parts_stack(wire, &mut digit_buf).expect("decode valid wire");
if let DecodedNumberStack::Parts {
digit_len,
is_negative,
coefficient,
..
} = parts
{
let reference = digits_to_i128(&digit_buf[..digit_len], is_negative);
assert_eq!(
coefficient, reference,
"{label}: fused coefficient {coefficient:?} != reference walk {reference:?} \
(wire={wire:02x?})"
);
}
}
#[test]
fn fused_coefficient_matches_reference_walk_corpus() {
let corpus: &[&str] = &[
"0",
"1",
"-1",
"9",
"-9",
"10",
"99",
"-99",
"100",
"12345",
"-12345",
"0.5",
"-0.5",
"3.14159",
"100.001",
"0.0001",
"1000000000000000000",
"12345678901234567890",
"123456789012345678901234567890",
"12345678901234567890123456789012345678",
"-12345678901234567890123456789012345678",
"0.12345678901234567890123456789012345678",
"123456789012345678901234567890123456789",
"9999999999999999999999999999999999999999", "1e125",
"-1e125",
"1e-120",
];
for text in corpus {
let wire = encode_number_text(text).unwrap_or_else(|e| panic!("encode {text}: {e:?}"));
assert_fused_matches_reference(&wire, text);
}
}
#[test]
fn inline_form_fits_the_size_budget() {
assert!(core::mem::size_of::<OracleNumber>() <= 24);
assert_eq!(core::mem::align_of::<OracleNumber>(), 8);
}
#[test]
fn formatter_matches_known_canonical_text() {
let cases: &[(i128, i16, bool, &str)] = &[
(0, 0, true, "0"),
(1, 0, true, "1"),
(-1, 0, true, "-1"),
(5, 1, false, "0.5"),
(-5, 1, false, "-0.5"),
(314159, 5, false, "3.14159"),
(1, -2, true, "100"), (12, 0, true, "12"),
(100001, 3, false, "100.001"),
(15, 1, false, "1.5"),
];
for &(coeff, scale, is_int, expect) in cases {
let n = OracleNumber::inline(coeff, scale, is_int);
assert_eq!(
n.to_canonical_string(),
expect,
"coeff={coeff} scale={scale}"
);
assert_eq!(n.is_integer(), is_int);
}
}
#[test]
fn from_canonical_text_round_trips() {
for text in [
"0",
"1",
"-1",
"0.5",
"100",
"0.001",
"12345678901234567890",
] {
let n = OracleNumber::from_canonical_text(text);
assert_eq!(n.to_canonical_string(), text);
}
}
#[test]
fn from_canonical_text_matches_wire_decoder_for_trailing_zero_integers() {
for text in ["10", "100", "-1000", "1000000000000000000"] {
let wire = encode_number_text(text).expect("encode trailing-zero integer");
let from_wire = OracleNumber::from_wire(&wire).expect("decode trailing-zero integer");
let from_text = OracleNumber::from_canonical_text(text);
assert_eq!(
from_text, from_wire,
"canonical text materialization should match wire decode for {text}"
);
assert_eq!(from_text.to_canonical_string(), text);
}
}
#[test]
fn owned_and_borrowed_number_decode_agree_for_large_integers() {
for text in [
"1",
"100",
"1000000000000000000", "99999999999999999999999999999999999999", "100000000000000000000000000000000000000", "1000000000000000000000000000000000000000", "10000000000000000000000000000000000000000", ] {
let wire = match encode_number_text(text) {
Ok(wire) => wire,
Err(err) => panic!("encode {text}: {err:?}"),
};
let owned = OracleNumber::from_wire(&wire).expect("from_wire");
let mut digits = Vec::new();
let mut canon = String::new();
let is_int =
crate::thin::codecs::decode_number_text_into(&wire, &mut digits, &mut canon)
.expect("decode_number_text_into");
let borrowed = OracleNumber::from_canonical_text_with_flag(&canon, is_int);
assert_eq!(
owned.to_canonical_string(),
borrowed.to_canonical_string(),
"canon {text}"
);
assert_eq!(owned.to_i128(), borrowed.to_i128(), "i128 {text}");
assert_eq!(owned.to_i64(), borrowed.to_i64(), "i64 {text}");
assert_eq!(owned.is_integer(), borrowed.is_integer(), "is_int {text}");
}
}
#[test]
fn overflow_value_falls_back_to_text_losslessly() {
let big = "1234567890123456789012345678901234567890"; let n = OracleNumber::from_canonical_text(big);
assert!(
matches!(n, OracleNumber::Text { .. }),
"40-digit -> text fallback"
);
assert_eq!(n.to_canonical_string(), big);
}
}