1use bigdecimal::{BigDecimal, RoundingMode, Signed};
8use std::fmt;
9use std::num::NonZeroU64;
10use std::str::FromStr;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub struct RexxValue {
16 data: String,
17}
18
19impl RexxValue {
20 pub fn new(s: impl Into<String>) -> Self {
21 Self { data: s.into() }
22 }
23
24 pub fn as_str(&self) -> &str {
25 &self.data
26 }
27
28 pub fn into_string(self) -> String {
29 self.data
30 }
31
32 pub fn len(&self) -> usize {
33 self.data.len()
34 }
35
36 pub fn is_empty(&self) -> bool {
37 self.data.is_empty()
38 }
39
40 pub fn to_decimal(&self) -> Option<BigDecimal> {
44 let trimmed = self.data.trim();
45 if trimmed.is_empty() {
46 return None;
47 }
48 BigDecimal::from_str(trimmed).ok()
49 }
50
51 pub fn is_number(&self) -> bool {
53 self.to_decimal().is_some()
54 }
55
56 pub fn is_whole_number(&self, digits: u32) -> bool {
59 match self.to_decimal() {
60 Some(d) => {
61 let rounded = d.round(0);
62 let diff = (&d - &rounded).abs();
63 diff < BigDecimal::from_str(&format!("1E-{digits}")).unwrap_or(BigDecimal::from(0))
64 }
65 None => false,
66 }
67 }
68
69 pub fn from_decimal(d: &BigDecimal, digits: u32, form: NumericForm) -> Self {
72 let formatted = format_rexx_number(d, digits, form);
73 Self::new(formatted)
74 }
75}
76
77impl fmt::Display for RexxValue {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 write!(f, "{}", self.data)
80 }
81}
82
83impl From<&str> for RexxValue {
84 fn from(s: &str) -> Self {
85 Self::new(s)
86 }
87}
88
89impl From<String> for RexxValue {
90 fn from(s: String) -> Self {
91 Self::new(s)
92 }
93}
94
95impl From<i64> for RexxValue {
96 fn from(n: i64) -> Self {
97 Self::new(n.to_string())
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
103pub enum NumericForm {
104 #[default]
106 Scientific,
107 Engineering,
109}
110
111#[derive(Debug, Clone)]
113pub struct NumericSettings {
114 pub digits: u32,
116 pub form: NumericForm,
118 pub fuzz: u32,
120}
121
122impl Default for NumericSettings {
123 fn default() -> Self {
124 Self {
125 digits: 9,
126 form: NumericForm::Scientific,
127 fuzz: 0,
128 }
129 }
130}
131
132#[allow(clippy::cast_possible_wrap)]
136fn format_rexx_number(d: &BigDecimal, digits: u32, form: NumericForm) -> String {
137 use bigdecimal::num_bigint::Sign;
138
139 if d.sign() == Sign::NoSign {
140 return "0".to_string();
141 }
142
143 let prec = NonZeroU64::new(u64::from(digits)).unwrap_or(NonZeroU64::MIN);
144 let rounded = d.with_precision_round(prec, RoundingMode::HalfUp);
145 let normed = rounded.normalized();
146 let (coeff, scale) = normed.as_bigint_and_exponent();
147 let is_negative = coeff.sign() == Sign::Minus;
148 let coeff_str = coeff.abs().to_string();
149 let n = coeff_str.len() as i64;
151
152 let adj_exp = n - 1 - scale;
155
156 let use_plain = if adj_exp >= 0 {
160 adj_exp < i64::from(digits) * 2
161 } else {
162 adj_exp >= -i64::from(digits)
163 };
164
165 let sign = if is_negative { "-" } else { "" };
166
167 if use_plain {
168 format!("{sign}{}", format_plain(&coeff_str, adj_exp))
169 } else {
170 format!("{sign}{}", format_exp(&coeff_str, adj_exp, form))
171 }
172}
173
174#[allow(
176 clippy::cast_possible_wrap,
177 clippy::cast_possible_truncation,
178 clippy::cast_sign_loss
179)]
180fn format_plain(coeff: &str, adj_exp: i64) -> String {
181 let n = coeff.len() as i64;
182 if adj_exp >= n - 1 {
183 let trailing = (adj_exp - n + 1) as usize;
185 format!("{coeff}{}", "0".repeat(trailing))
186 } else if adj_exp >= 0 {
187 let split = (adj_exp + 1) as usize;
189 format!("{}.{}", &coeff[..split], &coeff[split..])
190 } else {
191 let leading = (-adj_exp - 1) as usize;
193 format!("0.{}{coeff}", "0".repeat(leading))
194 }
195}
196
197#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
199fn format_exp(coeff: &str, adj_exp: i64, form: NumericForm) -> String {
200 let (digits_before, exp) = match form {
201 NumericForm::Scientific => (1usize, adj_exp),
202 NumericForm::Engineering => {
203 let e = adj_exp - adj_exp.rem_euclid(3);
204 ((adj_exp - e + 1) as usize, e)
205 }
206 };
207
208 let n = coeff.len();
209 let mantissa = if digits_before >= n {
210 let padding = digits_before - n;
211 format!("{coeff}{}", "0".repeat(padding))
212 } else {
213 format!("{}.{}", &coeff[..digits_before], &coeff[digits_before..])
214 };
215
216 if exp >= 0 {
217 format!("{mantissa}E+{exp}")
218 } else {
219 format!("{mantissa}E{exp}")
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn string_value() {
229 let v = RexxValue::new("hello");
230 assert_eq!(v.as_str(), "hello");
231 assert!(!v.is_number());
232 }
233
234 #[test]
235 fn numeric_value() {
236 let v = RexxValue::new("42");
237 assert!(v.is_number());
238 assert_eq!(v.to_decimal().unwrap(), BigDecimal::from(42));
239 }
240
241 #[test]
242 fn numeric_with_spaces() {
243 let v = RexxValue::new(" 3.14 ");
244 assert!(v.is_number());
245 }
246
247 #[test]
248 fn non_numeric() {
249 let v = RexxValue::new("hello");
250 assert!(!v.is_number());
251 assert!(v.to_decimal().is_none());
252 }
253
254 #[test]
255 fn from_integer() {
256 let v = RexxValue::from(42i64);
257 assert_eq!(v.as_str(), "42");
258 }
259
260 #[test]
261 fn default_numeric_settings() {
262 let settings = NumericSettings::default();
263 assert_eq!(settings.digits, 9);
264 assert_eq!(settings.fuzz, 0);
265 assert_eq!(settings.form, NumericForm::Scientific);
266 }
267
268 #[test]
271 fn format_integer() {
272 let d = BigDecimal::from(42);
273 assert_eq!(format_rexx_number(&d, 9, NumericForm::Scientific), "42");
274 }
275
276 #[test]
277 fn format_decimal() {
278 let d = BigDecimal::from_str("3.14").unwrap();
279 assert_eq!(format_rexx_number(&d, 9, NumericForm::Scientific), "3.14");
280 }
281
282 #[test]
283 fn format_zero() {
284 let d = BigDecimal::from(0);
285 assert_eq!(format_rexx_number(&d, 9, NumericForm::Scientific), "0");
286 }
287
288 #[test]
289 fn format_negative() {
290 let d = BigDecimal::from(-42);
291 assert_eq!(format_rexx_number(&d, 9, NumericForm::Scientific), "-42");
292 }
293
294 #[test]
295 fn format_significant_digit_rounding() {
296 let d = BigDecimal::from_str("123456789.5").unwrap();
298 assert_eq!(
299 format_rexx_number(&d, 9, NumericForm::Scientific),
300 "123456790"
301 );
302 }
303
304 #[test]
305 fn format_large_plain() {
306 let d = BigDecimal::from_str("1E17").unwrap();
308 assert_eq!(
309 format_rexx_number(&d, 9, NumericForm::Scientific),
310 "100000000000000000"
311 );
312 }
313
314 #[test]
315 fn format_large_exponential() {
316 let d = BigDecimal::from_str("1E18").unwrap();
318 assert_eq!(format_rexx_number(&d, 9, NumericForm::Scientific), "1E+18");
319 }
320
321 #[test]
322 fn format_small_plain() {
323 let d = BigDecimal::from_str("1E-9").unwrap();
325 assert_eq!(
326 format_rexx_number(&d, 9, NumericForm::Scientific),
327 "0.000000001"
328 );
329 }
330
331 #[test]
332 fn format_small_exponential() {
333 let d = BigDecimal::from_str("1.23E-10").unwrap();
335 assert_eq!(
336 format_rexx_number(&d, 9, NumericForm::Scientific),
337 "1.23E-10"
338 );
339 }
340
341 #[test]
342 fn format_engineering_form() {
343 let d = BigDecimal::from_str("1.23E20").unwrap();
344 assert_eq!(
345 format_rexx_number(&d, 9, NumericForm::Engineering),
346 "123E+18"
347 );
348 }
349
350 #[test]
351 fn format_trailing_zeros_stripped() {
352 let d = BigDecimal::from_str("5.00").unwrap();
353 assert_eq!(format_rexx_number(&d, 9, NumericForm::Scientific), "5");
354 }
355
356 #[test]
357 fn format_negative_exponential() {
358 let d = BigDecimal::from_str("-1.5E20").unwrap();
359 assert_eq!(
360 format_rexx_number(&d, 9, NumericForm::Scientific),
361 "-1.5E+20"
362 );
363 }
364}