1use serde_json::Value;
5
6use super::canonical::number_text;
7use super::whitespace::trim;
8
9fn is_numeric_text(text: &str) -> bool {
12 let digits = |part: &str| !part.is_empty() && part.bytes().all(|b| b.is_ascii_digit());
13 let unsigned = text.strip_prefix('-').unwrap_or(text);
14 let (mantissa, exponent) = match unsigned.split_once(['e', 'E']) {
15 Some((mantissa, exponent)) => (mantissa, Some(exponent)),
16 None => (unsigned, None),
17 };
18 let mantissa_ok = match mantissa.split_once('.') {
19 Some((integer, fraction)) => (integer.is_empty() || digits(integer)) && digits(fraction),
20 None => digits(mantissa),
21 };
22 let exponent_ok = exponent
23 .is_none_or(|exponent| digits(exponent.strip_prefix(['-', '+']).unwrap_or(exponent)));
24 mantissa_ok && exponent_ok
25}
26
27pub(crate) fn numeric_text_value(text: &str) -> Option<f64> {
30 if !is_numeric_text(text) {
31 return None;
32 }
33 let value = text.parse::<f64>().expect("numeric text parses as f64");
35 value.is_finite().then_some(value)
36}
37
38pub(crate) fn numeric_value(value: &Value) -> Option<f64> {
41 match value {
42 Value::Number(number) => number.as_f64().filter(|value| value.is_finite()),
43 Value::String(text) => numeric_text_value(trim(text)),
44 _ => None,
45 }
46}
47
48pub fn number_range(parameter: &Value) -> Option<(f64, f64)> {
51 match parameter.as_array().map(Vec::as_slice) {
52 Some([minimum, maximum]) => match (finite(minimum), finite(maximum)) {
53 (Some(minimum), Some(maximum)) if minimum <= maximum => Some((minimum, maximum)),
54 _ => None,
55 },
56 _ => None,
57 }
58}
59
60pub fn step(parameter: &Value) -> Option<f64> {
62 finite(parameter).filter(|step| *step > 0.0)
63}
64
65fn finite(value: &Value) -> Option<f64> {
67 value.as_f64().filter(|number| number.is_finite())
68}
69
70fn decimal(value: f64) -> (u64, i32) {
73 let text = number_text(value.abs());
74 let (mantissa, exponent) = match text.split_once('e') {
75 Some((mantissa, exponent)) => (
76 mantissa,
77 exponent
78 .parse::<i32>()
79 .expect("canonical text writes a decimal exponent"),
80 ),
81 None => (text.as_str(), 0),
82 };
83 let (integer, fraction) = mantissa.split_once('.').unwrap_or((mantissa, ""));
84 let digits = format!("{integer}{fraction}");
85 let significant = digits.trim_end_matches('0');
88 if significant.is_empty() {
89 return (0, 0);
90 }
91 let significand = significant
92 .parse::<u64>()
93 .expect("canonical text has at most 17 significant digits");
94 let zeros = (digits.len() - significant.len()) as i32;
95 (significand, exponent - fraction.len() as i32 + zeros)
96}
97
98fn mul_mod(a: u64, b: u64, m: u64) -> u64 {
100 ((a as u128 * b as u128) % m as u128) as u64
101}
102
103fn pow10_mod(mut exponent: u32, m: u64) -> u64 {
105 let mut result = 1 % m;
106 let mut base = 10 % m;
107 while exponent > 0 {
108 if exponent & 1 == 1 {
109 result = mul_mod(result, base, m);
110 }
111 base = mul_mod(base, base, m);
112 exponent >>= 1;
113 }
114 result
115}
116
117pub fn is_multiple(value: f64, step: f64) -> bool {
120 let (a, p) = decimal(value);
121 let (b, q) = decimal(step);
122 if a == 0 {
123 return true;
124 }
125 if p >= q {
126 mul_mod(a % b, pow10_mod((p - q) as u32, b), b) == 0
128 } else {
129 let shift = (q - p) as u32;
130 match 10u64.checked_pow(shift) {
132 Some(power) if a % power == 0 => (a / power) % b == 0,
133 _ => false,
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use serde_json::json;
142
143 #[test]
144 fn numeric_text_grammar() {
145 for text in [
146 "0", "12", "-12", "0.5", ".5", "-.5", "1e5", "1E-3", "-.5e+2", "007", "1e999999",
147 ] {
148 assert!(is_numeric_text(text), "{text}");
149 }
150 for text in [
151 "",
152 "-",
153 ".",
154 "+1",
155 "1.",
156 "-.",
157 "1.e5",
158 "e5",
159 "1e",
160 "1e+",
161 "--1",
162 "0x10",
163 "Infinity",
164 "NaN",
165 "1_000",
166 "1,5",
167 "1 2",
168 " 1",
169 "\u{661}\u{662}",
170 "1.2.3",
171 "1e5.0",
172 ] {
173 assert!(!is_numeric_text(text), "{text}");
174 }
175 }
176
177 #[test]
178 fn numeric_values() {
179 assert_eq!(numeric_value(&json!(" 12\t")), Some(12.0));
180 assert_eq!(numeric_value(&json!("1e-400")), Some(0.0));
181 assert_eq!(numeric_value(&json!("1e999")), None);
182 assert_eq!(numeric_value(&json!(1.5)), Some(1.5));
183 for value in [json!(true), json!(null), json!(["1"]), json!({})] {
184 assert_eq!(numeric_value(&value), None, "{value}");
185 }
186 }
187
188 #[test]
189 fn decimals_follow_the_canonical_text() {
190 assert_eq!(decimal(0.0), (0, 0));
191 assert_eq!(decimal(1.5), (15, -1));
192 assert_eq!(decimal(100.0), (1, 2));
193 assert_eq!(decimal(0.000001), (1, -6));
194 assert_eq!(decimal(-2e-7), (2, -7));
195 assert_eq!(decimal(1e21), (1, 21));
196 assert_eq!(decimal(1.05e21), (105, 19));
197 assert_eq!(decimal(123456789012345680000.0), (12345678901234568, 4));
198 }
199
200 #[test]
201 fn multiples_are_exact() {
202 for (value, step, expected) in [
203 (0.0, 0.1, true),
204 (0.3, 0.1, true),
205 (0.1 + 0.2, 0.1, false),
206 (2e-7, 0.1, false),
207 (4e-7, 2e-7, true),
208 (3e-7, 2e-7, false),
209 (-15.0, 5.0, true),
210 (1e21, 1e20, true),
211 (1.05e21, 1e20, false),
212 (1.0000000001, 0.1, false),
213 (1e300, 3.0, false),
214 (1e300, 7.0, false),
215 (1.2e300, 3.0, true),
216 (5e-324, 5e-324, true),
217 (1e-300, 1e300, false),
218 (0.75, 0.25, true),
219 (
220 123456789012345680000.0,
221 7.0,
222 123456789012345680000u128.is_multiple_of(7),
223 ),
224 (123456789012345680000.0, 8.0, true),
225 (98765432109876540.0, 0.001, true),
226 ] {
227 assert_eq!(is_multiple(value, step), expected, "{value} / {step}");
228 }
229 }
230}