harper_core/
number.rs

1use std::fmt::Display;
2
3use is_macro::Is;
4use ordered_float::OrderedFloat;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd)]
8pub struct Number {
9    pub value: OrderedFloat<f64>,
10    pub suffix: Option<OrdinalSuffix>,
11    pub radix: u32,
12    pub precision: usize,
13}
14
15impl Display for Number {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        if self.radix == 16 {
18            write!(f, "0x{:X}", self.value.0 as u64)?;
19        } else {
20            write!(f, "{:.*}", self.precision, self.value.0)?;
21        }
22
23        if let Some(suffix) = self.suffix {
24            for c in suffix.to_chars() {
25                write!(f, "{}", c)?;
26            }
27        }
28
29        Ok(())
30    }
31}
32
33#[derive(
34    Debug, Serialize, Deserialize, Default, PartialEq, PartialOrd, Clone, Copy, Is, Hash, Eq,
35)]
36pub enum OrdinalSuffix {
37    #[default]
38    Th,
39    St,
40    Nd,
41    Rd,
42}
43
44impl OrdinalSuffix {
45    pub fn correct_suffix_for(number: impl Into<f64>) -> Option<Self> {
46        let number = number.into();
47
48        if number < 0.0 || number - number.floor() > f64::EPSILON || number > u64::MAX as f64 {
49            return None;
50        }
51
52        let integer = number as u64;
53
54        if let 11..=13 = integer % 100 {
55            return Some(Self::Th);
56        };
57
58        match integer % 10 {
59            0 => Some(Self::Th),
60            1 => Some(Self::St),
61            2 => Some(Self::Nd),
62            3 => Some(Self::Rd),
63            4 => Some(Self::Th),
64            5 => Some(Self::Th),
65            6 => Some(Self::Th),
66            7 => Some(Self::Th),
67            8 => Some(Self::Th),
68            9 => Some(Self::Th),
69            _ => None,
70        }
71    }
72
73    pub fn to_chars(self) -> Vec<char> {
74        match self {
75            OrdinalSuffix::Th => vec!['t', 'h'],
76            OrdinalSuffix::St => vec!['s', 't'],
77            OrdinalSuffix::Nd => vec!['n', 'd'],
78            OrdinalSuffix::Rd => vec!['r', 'd'],
79        }
80    }
81
82    /// Check the first several characters in a buffer to see if it matches a
83    /// number suffix.
84    pub fn from_chars(chars: &[char]) -> Option<Self> {
85        if chars.len() != 2 {
86            return None;
87        }
88
89        match (chars[0], chars[1]) {
90            ('t', 'h') => Some(OrdinalSuffix::Th),
91            ('T', 'h') => Some(OrdinalSuffix::Th),
92            ('t', 'H') => Some(OrdinalSuffix::Th),
93            ('T', 'H') => Some(OrdinalSuffix::Th),
94            ('s', 't') => Some(OrdinalSuffix::St),
95            ('S', 't') => Some(OrdinalSuffix::St),
96            ('s', 'T') => Some(OrdinalSuffix::St),
97            ('S', 'T') => Some(OrdinalSuffix::St),
98            ('n', 'd') => Some(OrdinalSuffix::Nd),
99            ('N', 'd') => Some(OrdinalSuffix::Nd),
100            ('n', 'D') => Some(OrdinalSuffix::Nd),
101            ('N', 'D') => Some(OrdinalSuffix::Nd),
102            ('r', 'd') => Some(OrdinalSuffix::Rd),
103            ('R', 'd') => Some(OrdinalSuffix::Rd),
104            ('r', 'D') => Some(OrdinalSuffix::Rd),
105            ('R', 'D') => Some(OrdinalSuffix::Rd),
106            _ => None,
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use itertools::Itertools;
114    use ordered_float::OrderedFloat;
115
116    use crate::OrdinalSuffix;
117
118    use super::Number;
119
120    #[test]
121    fn hex_fifteen() {
122        assert_eq!(
123            Number {
124                value: OrderedFloat(15.0),
125                suffix: None,
126                radix: 16,
127                precision: 0
128            }
129            .to_string(),
130            "0xF"
131        )
132    }
133
134    #[test]
135    fn decimal_fifteen() {
136        assert_eq!(
137            Number {
138                value: OrderedFloat(15.0),
139                suffix: None,
140                radix: 10,
141                precision: 0
142            }
143            .to_string(),
144            "15"
145        )
146    }
147
148    #[test]
149    fn decimal_fifteen_suffix() {
150        assert_eq!(
151            Number {
152                value: OrderedFloat(15.0),
153                suffix: Some(OrdinalSuffix::Th),
154                radix: 10,
155                precision: 0
156            }
157            .to_string(),
158            "15th"
159        )
160    }
161
162    #[test]
163    fn decimal_fifteen_and_a_half() {
164        assert_eq!(
165            Number {
166                value: OrderedFloat(15.5),
167                suffix: None,
168                radix: 10,
169                precision: 2
170            }
171            .to_string(),
172            "15.50"
173        )
174    }
175
176    #[test]
177    fn issue_1051() {
178        let word = "story".chars().collect_vec();
179        assert_eq!(None, OrdinalSuffix::from_chars(&word));
180    }
181}