Skip to main content

harper_core/
number.rs

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