latexml_core/common/
float.rs1use std::fmt;
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5
6use crate::{
7 common::{error::Result, numeric_ops::NumericOps, object::Object},
8 definition::register::RegisterType,
9 mouth,
10 tokens::{TeXString, Tokens},
11};
12
13static TRAILING_ZEROS: Lazy<Regex> = Lazy::new(|| Regex::new(r"0+$").unwrap());
14
15#[derive(Debug, Copy, Clone, PartialEq)]
19pub struct Float(pub f64);
20
21impl Default for Float {
22 fn default() -> Self { Float(0.0) }
23}
24
25impl Object for Float {
26 fn revert(&self) -> Result<Tokens> { Ok(Tokens::new(ExplodeText!(&self.to_string()))) }
27 fn stringify(&self) -> String { s!("Float[{}]", self.0) }
28 fn be_digested(self) -> Result<crate::Digested> {
29 let s = self.to_string();
31 Ok(
32 crate::Tbox::new(
33 crate::common::arena::pin(&s),
36 None,
37 None,
38 Tokens::new(ExplodeText!(&s)),
39 crate::common::arena::SymHashMap::default(),
40 )
41 .into(),
42 )
43 }
44}
45
46impl NumericOps for Float {
47 fn new(number: i64) -> Self { Float(number as f64) }
48 fn new_f64(number: f64) -> Self { Float(number) }
49 fn value_of(self) -> i64 { self.0 as i64 }
50 fn value_f64(self) -> f64 { self.0 }
51 fn negate(self) -> Self { Float(-self.0) }
52 fn register_type(&self) -> RegisterType { RegisterType::Number }
53 fn add<T: NumericOps>(self, other: T) -> Self { Float::new_f64(self.0 + other.value_f64()) }
54 fn subtract<T: NumericOps>(self, other: T) -> Self { Float::new_f64(self.0 - other.value_f64()) }
55 fn multiply<T: NumericOps>(self, other: T) -> Self { Float::new_f64(self.0 * other.value_f64()) }
56 fn divide<T: NumericOps>(self, other: T) -> Self { Float::new_f64(self.0 / other.value_f64()) }
57}
58
59impl From<Float> for Tokens {
60 fn from(v: Float) -> Tokens { mouth::tokenize_internal(TeXString::assembled(v.to_string())) }
61}
62
63impl From<Float> for Option<Tokens> {
64 fn from(v: Float) -> Option<Tokens> { Some(v.into()) }
65}
66
67impl fmt::Display for Float {
68 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", floatformat(self.0)) }
69}
70
71impl Float {
72 pub fn to_tight_string(&self) -> String { custom_float_format(self.0, true) }
75}
76
77pub fn floatformat(n: f64) -> String { custom_float_format(n, false) }
79pub fn custom_float_format(n: f64, tight: bool) -> String {
80 let mut s = format!("{:.5}", n);
81 if s.contains('.') {
82 s = TRAILING_ZEROS.replace(&s, "").to_string();
83 }
84 if s.ends_with('.') {
85 if tight {
86 s.pop();
88 } else {
89 s.push('0'); }
91 }
92 s
93}
94
95impl From<&str> for Float {
96 fn from(spec: &str) -> Self { Float(spec.trim().parse::<f64>().unwrap_or(0.0)) }
99}
100impl From<String> for Float {
101 fn from(spec: String) -> Self { Float(spec.trim().parse::<f64>().unwrap_or(0.0)) }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn floatformat_integer_gets_dot_zero() {
113 assert_eq!(floatformat(1.0), "1.0");
114 assert_eq!(floatformat(0.0), "0.0");
115 assert_eq!(floatformat(-5.0), "-5.0");
116 }
117
118 #[test]
119 fn floatformat_trims_trailing_zeros() {
120 assert_eq!(floatformat(1.5), "1.5");
121 assert_eq!(floatformat(1.25), "1.25");
122 assert_eq!(floatformat(0.10000), "0.1");
123 }
124
125 #[test]
126 fn tight_format_drops_dot_for_integers() {
127 assert_eq!(Float(1.0).to_tight_string(), "1");
128 assert_eq!(Float(0.0).to_tight_string(), "0");
129 assert_eq!(Float(1.5).to_tight_string(), "1.5");
130 }
131
132 #[test]
133 fn custom_float_format_precision() {
134 let out = custom_float_format(0.123456789, false);
135 assert!(
136 out.starts_with("0.12346") || out.starts_with("0.12345"),
137 "got {out:?}"
138 );
139 }
140
141 #[test]
142 fn from_str_nonnumeric_is_zero() {
143 assert_eq!(Float::from("abc").0, 0.0);
144 assert_eq!(Float::from(" xyz ").0, 0.0);
145 assert_eq!(Float::from("").0, 0.0);
146 }
147
148 #[test]
149 fn from_str_numeric_parses() {
150 assert_eq!(Float::from("1.5").0, 1.5);
151 assert_eq!(Float::from(" -3.125 ").0, -3.125);
152 assert_eq!(Float::from("42").0, 42.0);
153 }
154
155 #[test]
156 fn from_string_matches_from_str() {
157 for s in &["1", "1.5", "", "abc", "-0.0001"] {
158 let a = Float::from(*s).0;
159 let b = Float::from(s.to_string()).0;
160 assert_eq!(a, b, "divergence on {s:?}: {a} vs {b}");
161 }
162 }
163
164 #[test]
165 fn float_arithmetic_roundtrip() {
166 let a = Float::new_f64(1.5);
167 let b = Float::new_f64(2.5);
168 assert_eq!(a.add(b).value_f64(), 4.0);
169 assert_eq!(b.subtract(a).value_f64(), 1.0);
170 assert_eq!(a.multiply(b).value_f64(), 3.75);
171 assert_eq!(b.divide(a).value_f64(), 2.5 / 1.5);
172 }
173
174 #[test]
175 fn float_negate() {
176 assert_eq!(Float::new_f64(1.5).negate().value_f64(), -1.5);
177 assert_eq!(Float::new_f64(0.0).negate().value_f64(), 0.0);
178 }
179}