harper_core/
number.rs

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