latexml_core/common/
number.rs1use 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 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 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 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 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 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 }