Skip to main content

latexml_core/common/
number.rs

1use std::fmt;
2
3use crate::{
4  Object,
5  common::{error::*, numeric_ops::NumericOps},
6  definition::register::{RegisterType, RegisterValue},
7  digested::Digested,
8  mouth,
9  token::Catcode,
10  tokens::{TeXString, Tokens},
11};
12
13#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
14pub struct Number(pub i64);
15impl Object for Number {
16  fn revert(&self) -> Result<Tokens> { Ok(Tokens::new(ExplodeText!(&self.0.to_string()))) }
17  // Perl: beDigested returns $self (duck typing). In Rust, wrap as RegisterValue.
18  fn be_digested(self) -> Result<Digested> { Ok(Digested::from(RegisterValue::Number(self))) }
19}
20impl NumericOps for Number {
21  fn new(number: i64) -> Self { Number(number) }
22  fn new_f64(number: f64) -> Self { Number(number.trunc() as i64) }
23  fn value_of(self) -> i64 { self.0 }
24  fn register_type(&self) -> RegisterType { RegisterType::Number }
25}
26
27impl Number {
28  pub fn to_attribute(&self) -> String { self.0.to_string() }
29}
30
31impl From<Number> for Tokens {
32  fn from(v: Number) -> Tokens { mouth::tokenize_internal(TeXString::assembled(v.0.to_string())) }
33}
34
35impl From<Number> for Option<Tokens> {
36  fn from(v: Number) -> Option<Tokens> { Some(v.into()) }
37}
38
39#[macro_export]
40macro_rules! Number {
41  ($number:expr_2021) => {{ ::latexml_core::common::number::Number::new($number as i64) }};
42}
43
44impl From<String> for Number {
45  /// Parse a string into a Number. Non-numeric input silently becomes 0
46  /// to match Perl's `Number(ToString($x))` coercion — Perl's implicit
47  /// numeric context treats "abc" / undef as 0 without panicking.
48  fn from(s: String) -> Number { Number(s.trim().parse::<i64>().unwrap_or(0)) }
49}
50
51impl fmt::Display for Number {
52  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) }
53}
54
55impl From<Catcode> for Number {
56  fn from(c: Catcode) -> Number { Number::new(u8::from(c) as i64) }
57}
58
59#[cfg(test)]
60mod tests {
61  use super::*;
62
63  #[test]
64  fn number_new_and_value_of() {
65    assert_eq!(Number::new(42).value_of(), 42);
66    assert_eq!(Number::new(-1).value_of(), -1);
67    assert_eq!(Number::new(0).value_of(), 0);
68  }
69
70  #[test]
71  fn number_new_f64_truncates() {
72    // new_f64 truncates (not rounds).
73    assert_eq!(Number::new_f64(3.7).value_of(), 3);
74    assert_eq!(Number::new_f64(-3.7).value_of(), -3);
75    assert_eq!(Number::new_f64(0.0).value_of(), 0);
76  }
77
78  #[test]
79  fn number_from_string_numeric() {
80    assert_eq!(Number::from(String::from("42")).value_of(), 42);
81    assert_eq!(Number::from(String::from("-7")).value_of(), -7);
82    assert_eq!(Number::from(String::from("  100  ")).value_of(), 100);
83  }
84
85  #[test]
86  fn number_from_string_nonnumeric_is_zero() {
87    // Perl-parity: non-numeric → 0 silently.
88    assert_eq!(Number::from(String::from("abc")).value_of(), 0);
89    assert_eq!(Number::from(String::from("")).value_of(), 0);
90    assert_eq!(
91      Number::from(String::from("3.14")).value_of(),
92      0,
93      "float string doesn't parse as integer — coerces to 0"
94    );
95  }
96
97  #[test]
98  fn number_display() {
99    assert_eq!(format!("{}", Number::new(42)), "42");
100    assert_eq!(format!("{}", Number::new(-7)), "-7");
101    assert_eq!(format!("{}", Number::new(0)), "0");
102  }
103
104  #[test]
105  fn number_to_attribute() {
106    assert_eq!(Number::new(42).to_attribute(), "42");
107    assert_eq!(Number::new(-7).to_attribute(), "-7");
108  }
109
110  #[test]
111  fn number_default_is_zero() {
112    assert_eq!(Number::default().value_of(), 0);
113  }
114
115  #[test]
116  fn number_equality() {
117    assert_eq!(Number::new(42), Number::new(42));
118    assert_ne!(Number::new(42), Number::new(43));
119  }
120
121  #[test]
122  fn number_from_catcode_matches_u8() {
123    // From<Catcode> coerces to the catcode's u8 value.
124    assert_eq!(
125      Number::from(Catcode::ESCAPE).value_of(),
126      u8::from(Catcode::ESCAPE) as i64
127    );
128    assert_eq!(
129      Number::from(Catcode::LETTER).value_of(),
130      u8::from(Catcode::LETTER) as i64
131    );
132  }
133
134  // Note: the Number!() macro references `::latexml_core::...` and
135  // so can only be tested from external integration tests. Not
136  // covered here.
137}