Skip to main content

ordinal/
lib.rs

1// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
2// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
3
4//! # Ordinal formatting
5//!
6//! Format numbers as ordinals efficiently.
7//! You can get the ordinal suffix e.g., "st", "nd", "rd", or "th" without allocations.
8//!
9//! ## Examples
10//!
11//! Get an ordinal suffix without allocating.
12//!
13//! ```
14//! use ordinal::ToOrdinal as _;
15//! assert_eq!(12.suffix(), "th");
16//! ```
17//!
18#![cfg_attr(
19    feature = "alloc",
20    doc = r##"
21Format a number as an ordinal, allocating a new `String`:
22
23```
24use ordinal::ToOrdinal as _;
25assert_eq!(12.to_ordinal_string(), "12th");
26```
27
28Get a number representing an ordinal you can use with comparisons and formatting.
29
30```
31use ordinal::ToOrdinal as _;
32let n = 12.to_ordinal();
33assert_eq!(*n, 12);
34assert_eq!(format!("{n}"), "12th");
35```
36"##
37)]
38#![cfg_attr(not(feature = "std"), no_std)]
39
40#[cfg(feature = "alloc")]
41extern crate alloc;
42#[cfg(feature = "std")]
43extern crate std;
44
45#[allow(unused_imports)]
46#[cfg(feature = "alloc")]
47use alloc::{
48    format,
49    string::{String, ToString as _},
50};
51use core::fmt;
52
53#[cfg(feature = "alloc")]
54mod number {
55    use super::*;
56    use core::ops::Deref;
57
58    /// Represent numbers as ordinals when displayed.
59    ///
60    /// # Examples
61    ///
62    /// Get a `Number` from an integer that implements [`ToOrdinal`].
63    ///
64    /// ```
65    /// use ordinal::ToOrdinal as _;
66    /// let n = 12.to_ordinal();
67    /// assert_eq!(*n, 12);
68    /// assert_eq!(format!("{n}"), "12th");
69    /// ```
70    ///
71    /// You can also create a `Number` in a `const` expression.
72    ///
73    /// ```
74    /// use ordinal::Ordinal;
75    /// const TWELVE: Ordinal<i32> = Ordinal(12);
76    /// ```
77    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
78    pub struct Ordinal<T: ToOrdinal>(pub T);
79
80    impl<T: ToOrdinal> Ordinal<T> {
81        /// Gets the suffix for the number.
82        ///
83        /// # Examples
84        ///
85        /// ```
86        /// use ordinal::Ordinal;
87        /// assert_eq!(Ordinal(12).suffix(), "th");
88        /// ```
89        pub fn suffix(&self) -> &'static str {
90            self.0.suffix()
91        }
92    }
93
94    impl<T: ToOrdinal> Deref for Ordinal<T> {
95        type Target = T;
96
97        fn deref(&self) -> &Self::Target {
98            &self.0
99        }
100    }
101
102    impl<T: ToOrdinal> fmt::Display for Ordinal<T> {
103        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104            write!(f, "{}{}", self.0, self.0.suffix())
105        }
106    }
107
108    impl<T: ToOrdinal> From<T> for Ordinal<T> {
109        fn from(value: T) -> Self {
110            Ordinal(value)
111        }
112    }
113
114    #[test]
115    fn test_number() {
116        const TWO: Ordinal<i32> = Ordinal(2);
117        let twelve = Ordinal::from(12);
118
119        assert!(TWO < twelve);
120        assert_eq!(*twelve, 12);
121        assert_eq!(twelve.to_string(), String::from("12th"));
122    }
123}
124
125#[cfg(feature = "alloc")]
126pub use number::Ordinal;
127
128/// Format numbers as ordinals e.g., 1st, 12th, 21st, etc.
129pub trait ToOrdinal: fmt::Display + Copy {
130    /// Get a [`Ordinal`] to format as an ordinal string.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use ordinal::ToOrdinal as _;
136    /// let n = 12.to_ordinal();
137    /// assert_eq!(12, 12);
138    /// assert_eq!(format!("{n}"), "12th");
139    /// ```
140    #[cfg(feature = "alloc")]
141    fn to_ordinal(self) -> Ordinal<Self> {
142        Ordinal(self)
143    }
144
145    /// Format a number as an ordinal. Implementations should not allocate.
146    ///
147    /// # Examples
148    ///
149    /// ```
150    /// use ordinal::ToOrdinal as _;
151    /// assert_eq!(12.to_ordinal_string(), "12th");
152    /// ```
153    #[cfg(feature = "alloc")]
154    fn to_ordinal_string(self) -> String {
155        format!("{}{}", self, self.suffix())
156    }
157
158    /// Gets the suffix for the number.
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use ordinal::ToOrdinal as _;
164    /// assert_eq!(12.suffix(), "th");
165    /// ```
166    fn suffix(self) -> &'static str;
167}
168
169macro_rules! impl_ordinal {
170    ($($t:ty)*) => { $(
171        impl $crate::ToOrdinal for $t {
172            fn suffix(self) -> &'static str {
173                let n = Abs::abs(self);
174                let n = (n % 20) as u8;
175                if (11..=13).contains(&n) {
176                    return "th";
177                }
178
179                match (n % 10) {
180                    1 => "st",
181                    2 => "nd",
182                    3 => "rd",
183                    _ => "th",
184                }
185            }
186        }
187    )* }
188}
189
190impl_ordinal!(u8 u16 u32 u64 u128 usize);
191impl_ordinal!(i8 i16 i32 i64 i128 isize);
192
193trait Abs<T> {
194    fn abs(self) -> T;
195}
196
197macro_rules! impl_abs {
198    (signed $($t:ty)*) => { $(
199        impl $crate::Abs<$t> for $t {
200            fn abs(self) -> $t {
201                self.abs()
202            }
203        }
204    )* };
205
206    (unsigned $($t:ty)*) => { $(
207        impl $crate::Abs<$t> for $t {
208            fn abs(self) -> $t {
209                self
210            }
211        }
212    )* };
213}
214
215impl_abs!(unsigned u8 u16 u32 u64 u128 usize);
216impl_abs!(signed i8 i16 i32 i64 i128 isize);
217
218#[cfg(feature = "alloc")]
219#[test]
220fn test_fmt() {
221    assert_eq!(0u8.to_ordinal_string(), "0th");
222    assert_eq!(1u16.to_ordinal_string(), "1st");
223    assert_eq!(2u32.to_ordinal_string(), "2nd");
224    assert_eq!(3u64.to_ordinal_string(), "3rd");
225    assert_eq!(4u128.to_ordinal_string(), "4th");
226    assert_eq!(5usize.to_ordinal_string(), "5th");
227    assert_eq!(6i8.to_ordinal_string(), "6th");
228    assert_eq!(7i16.to_ordinal_string(), "7th");
229    assert_eq!(8i32.to_ordinal_string(), "8th");
230    assert_eq!(9i64.to_ordinal_string(), "9th");
231    assert_eq!(10i128.to_ordinal_string(), "10th");
232    assert_eq!(11isize.to_ordinal_string(), "11th");
233
234    assert_eq!((-0i8).to_ordinal_string(), "0th");
235    assert_eq!((-1i16).to_ordinal_string(), "-1st");
236    assert_eq!((-2i32).to_ordinal_string(), "-2nd");
237    assert_eq!((-3i64).to_ordinal_string(), "-3rd");
238    assert_eq!((-4i128).to_ordinal_string(), "-4th");
239    assert_eq!((-5isize).to_ordinal_string(), "-5th");
240    assert_eq!((-6i8).to_ordinal_string(), "-6th");
241    assert_eq!((-7i16).to_ordinal_string(), "-7th");
242    assert_eq!((-8i32).to_ordinal_string(), "-8th");
243    assert_eq!((-9i64).to_ordinal_string(), "-9th");
244    assert_eq!((-10i128).to_ordinal_string(), "-10th");
245    assert_eq!((-11isize).to_ordinal_string(), "-11th");
246
247    assert_eq!(19u8.to_ordinal_string(), "19th");
248    assert_eq!(20u8.to_ordinal_string(), "20th");
249    assert_eq!(21u8.to_ordinal_string(), "21st");
250    assert_eq!(22u8.to_ordinal_string(), "22nd");
251    assert_eq!(23u8.to_ordinal_string(), "23rd");
252    assert_eq!(24u8.to_ordinal_string(), "24th");
253
254    assert_eq!(100u8.to_ordinal_string(), "100th");
255    assert_eq!(101u8.to_ordinal_string(), "101st");
256
257    assert_eq!(111u8.to_ordinal_string(), "111th");
258    assert_eq!(112u8.to_ordinal_string(), "112th");
259
260    assert_eq!(1001u32.to_ordinal_string(), "1001st");
261    assert_eq!(1002u32.to_ordinal_string(), "1002nd");
262    assert_eq!(1003u32.to_ordinal_string(), "1003rd");
263    assert_eq!(1004u32.to_ordinal_string(), "1004th");
264
265    assert_eq!(10001001u128.to_ordinal_string(), "10001001st");
266    assert_eq!(10001002u128.to_ordinal_string(), "10001002nd");
267    assert_eq!(10001003u128.to_ordinal_string(), "10001003rd");
268    assert_eq!(10001004u128.to_ordinal_string(), "10001004th");
269
270    assert_eq!(10001111u128.to_ordinal_string(), "10001111th");
271    assert_eq!(10001111u128.to_ordinal_string(), "10001111th");
272    assert_eq!(10001111u128.to_ordinal_string(), "10001111th");
273}
274
275#[test]
276fn test_suffix() {
277    assert_eq!(0u8.suffix(), "th");
278    assert_eq!(1u16.suffix(), "st");
279    assert_eq!(2u32.suffix(), "nd");
280    assert_eq!(3u64.suffix(), "rd");
281    assert_eq!(4u128.suffix(), "th");
282    assert_eq!(5usize.suffix(), "th");
283    assert_eq!(6i8.suffix(), "th");
284    assert_eq!(7i16.suffix(), "th");
285    assert_eq!(8i32.suffix(), "th");
286    assert_eq!(9i64.suffix(), "th");
287    assert_eq!(10i128.suffix(), "th");
288    assert_eq!(11isize.suffix(), "th");
289
290    assert_eq!((-0i8).suffix(), "th");
291    assert_eq!((-1i16).suffix(), "st");
292    assert_eq!((-2i32).suffix(), "nd");
293    assert_eq!((-3i64).suffix(), "rd");
294    assert_eq!((-4i128).suffix(), "th");
295    assert_eq!((-5isize).suffix(), "th");
296    assert_eq!((-6i8).suffix(), "th");
297    assert_eq!((-7i16).suffix(), "th");
298    assert_eq!((-8i32).suffix(), "th");
299    assert_eq!((-9i64).suffix(), "th");
300    assert_eq!((-10i128).suffix(), "th");
301    assert_eq!((-11isize).suffix(), "th");
302
303    assert_eq!(19u8.suffix(), "th");
304    assert_eq!(20u8.suffix(), "th");
305    assert_eq!(21u8.suffix(), "st");
306    assert_eq!(22u8.suffix(), "nd");
307    assert_eq!(23u8.suffix(), "rd");
308    assert_eq!(24u8.suffix(), "th");
309
310    assert_eq!(100u8.suffix(), "th");
311    assert_eq!(101u8.suffix(), "st");
312
313    assert_eq!(111u8.suffix(), "th");
314    assert_eq!(112u8.suffix(), "th");
315
316    assert_eq!(1001u32.suffix(), "st");
317    assert_eq!(1002u32.suffix(), "nd");
318    assert_eq!(1003u32.suffix(), "rd");
319    assert_eq!(1004u32.suffix(), "th");
320
321    assert_eq!(10001001u128.suffix(), "st");
322    assert_eq!(10001002u128.suffix(), "nd");
323    assert_eq!(10001003u128.suffix(), "rd");
324    assert_eq!(10001004u128.suffix(), "th");
325
326    assert_eq!(10001111u128.suffix(), "th");
327    assert_eq!(10001111u128.suffix(), "th");
328    assert_eq!(10001111u128.suffix(), "th");
329}
330
331#[cfg(all(doctest, feature = "std"))]
332#[doc = include_str!("../README.md")]
333struct ReadMe;