1use std::fmt::Display;
2
3use is_macro::Is;
4use itertools::Itertools;
5use ordered_float::OrderedFloat;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd)]
10pub struct Number {
11 pub value: OrderedFloat<f64>,
13 pub suffix: Option<OrdinalSuffix>,
15 pub radix: u32,
17 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 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}