type_bridge_contract/
decimal.rs1use std::cmp::Ordering;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct CanonicalDecimal<'a> {
12 negative: bool,
13 whole: &'a str,
14 fraction: &'a str,
15}
16
17impl CanonicalDecimal<'_> {
18 pub fn compare(&self, other: &Self) -> Ordering {
20 if self.negative != other.negative {
21 return if self.negative {
22 Ordering::Less
23 } else {
24 Ordering::Greater
25 };
26 }
27 let width = self.fraction.len().max(other.fraction.len());
28 let magnitude = self
29 .whole
30 .len()
31 .cmp(&other.whole.len())
32 .then_with(|| self.whole.cmp(other.whole))
33 .then_with(|| {
34 self.fraction
35 .bytes()
36 .chain(std::iter::repeat_n(b'0', width - self.fraction.len()))
37 .cmp(
38 other
39 .fraction
40 .bytes()
41 .chain(std::iter::repeat_n(b'0', width - other.fraction.len())),
42 )
43 });
44 if self.negative {
45 magnitude.reverse()
46 } else {
47 magnitude
48 }
49 }
50
51 pub fn canonical_string(&self) -> String {
53 let mut value = String::with_capacity(self.whole.len() + self.fraction.len() + 2);
54 if self.negative {
55 value.push('-');
56 }
57 value.push_str(self.whole);
58 if !self.fraction.is_empty() {
59 value.push('.');
60 value.push_str(self.fraction);
61 }
62 value
63 }
64}
65
66pub fn parse_decimal(value: &str) -> Option<CanonicalDecimal<'_>> {
68 let value = value.strip_suffix("dec").unwrap_or(value);
69 let (negative, unsigned) = if let Some(value) = value.strip_prefix('-') {
70 (true, value)
71 } else {
72 (false, value.strip_prefix('+').unwrap_or(value))
73 };
74 let (raw_whole, raw_fraction) = match unsigned.split_once('.') {
75 Some((whole, fraction)) => (whole, Some(fraction)),
76 None => (unsigned, None),
77 };
78 if raw_whole.is_empty()
79 || !raw_whole.bytes().all(|byte| byte.is_ascii_digit())
80 || raw_fraction.is_some_and(|fraction| {
81 fraction.is_empty()
82 || fraction.len() > 19
83 || !fraction.bytes().all(|byte| byte.is_ascii_digit())
84 })
85 {
86 return None;
87 }
88 let whole = raw_whole.trim_start_matches('0');
89 let whole = if whole.is_empty() { "0" } else { whole };
90 let fraction = raw_fraction.unwrap_or_default().trim_end_matches('0');
91 let limit = if negative {
92 "9223372036854775808"
93 } else {
94 "9223372036854775807"
95 };
96 match whole.len().cmp(&limit.len()).then_with(|| whole.cmp(limit)) {
97 Ordering::Greater => return None,
98 Ordering::Equal if negative && !fraction.is_empty() => return None,
99 Ordering::Less | Ordering::Equal => {}
100 }
101 Some(CanonicalDecimal {
102 negative: negative && !(whole == "0" && fraction.is_empty()),
103 whole,
104 fraction,
105 })
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn accepts_driver_suffix_and_compares_canonical_values() {
114 let driver = parse_decimal("001234.5600dec").unwrap();
115 let literal = parse_decimal("+1234.56").unwrap();
116 assert_eq!(driver, literal);
117 assert_eq!(driver.compare(&literal), Ordering::Equal);
118 assert_eq!(driver.canonical_string(), "1234.56");
119 assert_eq!(parse_decimal("-0.000dec"), parse_decimal("0"));
120 }
121
122 #[test]
123 fn enforces_fraction_width_and_decimal_range() {
124 for valid in [
125 "-9223372036854775808",
126 "-9223372036854775808.0000000000000000000",
127 "9223372036854775807.9999999999999999999dec",
128 ] {
129 assert!(
130 parse_decimal(valid).is_some(),
131 "expected {valid:?} to parse"
132 );
133 }
134 for invalid in [
135 "",
136 "dec",
137 "1.",
138 ".1",
139 "1.00000000000000000000",
140 "1.0DEC",
141 "1.0decdec",
142 "9223372036854775808",
143 "-9223372036854775808.0000000000000000001",
144 "-9223372036854775809",
145 ] {
146 assert!(
147 parse_decimal(invalid).is_none(),
148 "expected {invalid:?} to fail"
149 );
150 }
151 }
152}