is_thirteen/
lib.rs

1#![doc = include_str!("../README.md")]
2
3/// Contains all thirteen strings.
4pub mod thirteen_strings;
5
6use fnv::FnvHashSet as HashSet;
7use num_traits::FromPrimitive;
8use once_cell::sync::OnceCell;
9use std::fmt::Debug;
10use std::ops::Rem;
11use thirteen_strings::THIRTEEN_STRINGS;
12
13/// A type that can be compared to thirteen. This trait is implemented for all primitive types and
14/// `&str`.
15pub trait IsThirteen {
16    /// Returns `true` if self is thirteen.
17    fn thirteen(&self) -> bool;
18}
19
20macro_rules! impl_for_integer {
21    ($type:ty) => {
22        impl IsThirteen for $type {
23            /// Returns `true` if `self == 13`.
24            fn thirteen(&self) -> bool {
25                *self == 13
26            }
27        }
28    };
29}
30
31impl_for_integer!(i8);
32impl_for_integer!(i16);
33impl_for_integer!(i32);
34impl_for_integer!(i64);
35impl_for_integer!(i128);
36impl_for_integer!(isize);
37impl_for_integer!(u8);
38impl_for_integer!(u16);
39impl_for_integer!(u32);
40impl_for_integer!(u64);
41impl_for_integer!(u128);
42impl_for_integer!(usize);
43
44macro_rules! impl_for_float {
45    ($type:ty) => {
46        impl IsThirteen for $type {
47            /// Returns `true` if `self` is approximately `13`.
48            fn thirteen(&self) -> bool {
49                (self - 13.0).abs() < <$type>::EPSILON
50            }
51        }
52    };
53}
54
55impl_for_float!(f64);
56impl_for_float!(f32);
57
58impl IsThirteen for &str {
59    /// Returns `true` if:
60    /// - `self` equals `"13"` or `"B"`
61    /// - `self` is 13 characters long and all characters are equal to each other
62    /// - The lowercase version of `self` is included in [`thirteen_strings::THIRTEEN_STRINGS`]
63    fn thirteen(&self) -> bool {
64        matches!(*self, "13" | "B")
65            || (self.len() == 13 && self.bytes().all(|b| matches!(b, b'I' | b'l' | b'1')))
66            || is_thirteen_equal_chars(self)
67            // The next line could be non-allocating if there is an ascii-only IsThirteen
68            || THIRTEEN_STRINGS.contains(self.to_lowercase().as_str())
69    }
70}
71
72fn is_thirteen_equal_chars(s: &str) -> bool {
73    if let Some(first_char) = s.chars().next() {
74        if s.chars().count() == 13 {
75            s.chars().all(|c| c == first_char)
76        } else {
77            false
78        }
79    } else {
80        false
81    }
82}
83
84impl IsThirteen for String {
85    fn thirteen(&self) -> bool {
86        self.as_str().thirteen()
87    }
88}
89
90impl IsThirteen for char {
91    /// Returns `true` if self matches a thirteen character.
92    fn thirteen(&self) -> bool {
93        matches!(*self, 'B' | 'ß' | 'β' | '阝')
94    }
95}
96
97macro_rules! impl_always_false {
98    ($type:ty) => {
99        impl IsThirteen for $type {
100            /// Returns `false`.
101            fn thirteen(&self) -> bool {
102                false
103            }
104        }
105    };
106}
107
108impl_always_false!(bool);
109impl_always_false!(());
110
111/// `Roughly` is thirteen if it is in [12.5, 13.5).
112#[derive(Debug, Copy, Clone)]
113pub struct Roughly(pub f64);
114
115impl IsThirteen for Roughly {
116    fn thirteen(&self) -> bool {
117        (12.5..13.5).contains(&self.0)
118    }
119}
120
121/// `Returns` calls its closure and compares the returned value to thirteen.
122#[derive(Debug, Clone)]
123pub struct Returns<T>(pub T);
124
125impl<F, R> IsThirteen for Returns<F>
126where
127    F: Fn() -> R,
128    R: IsThirteen,
129{
130    fn thirteen(&self) -> bool {
131        self.0().thirteen()
132    }
133}
134
135/// `DivisibleBy` is thirteen if it is a divisor of 13.
136#[derive(Debug, Copy, Clone)]
137pub struct DivisibleBy<T>(pub T);
138
139impl<T, RemOutput> IsThirteen for DivisibleBy<T>
140where
141    T: Rem<Output = RemOutput> + FromPrimitive + Copy,
142    RemOutput: PartialEq + FromPrimitive,
143{
144    fn thirteen(&self) -> bool {
145        self.0 % FromPrimitive::from_u64(13).unwrap() == FromPrimitive::from_u64(0).unwrap()
146    }
147}
148
149/// `GreaterThan` returns `true` if it is greater than 13.
150#[derive(Debug, Copy, Clone)]
151pub struct GreaterThan<T>(pub T);
152
153impl<T> IsThirteen for GreaterThan<T>
154where
155    T: PartialOrd + FromPrimitive,
156{
157    fn thirteen(&self) -> bool {
158        self.0 > FromPrimitive::from_u64(13).unwrap()
159    }
160}
161
162/// `LessThan` returns `true` if it is greater than 13.
163#[derive(Debug, Copy, Clone)]
164pub struct LessThan<T>(pub T);
165
166impl<T> IsThirteen for LessThan<T>
167where
168    T: PartialOrd + FromPrimitive,
169{
170    fn thirteen(&self) -> bool {
171        self.0 < FromPrimitive::from_u64(13).unwrap()
172    }
173}
174
175/// `Within` has a custom tolerance for equalling thirteen.
176#[derive(Debug, Copy, Clone)]
177pub struct Within {
178    value: f64,
179    radius: f64,
180}
181
182impl Within {
183    /// `radius` is how far `value` can be from 13 to equal 13. That makes sense, right?
184    pub fn new(value: f64, radius: f64) -> Self {
185        Self { value, radius }
186    }
187}
188
189impl IsThirteen for Within {
190    fn thirteen(&self) -> bool {
191        (self.value - 13.0).abs() <= self.radius
192    }
193}
194
195/// `CanSpell` is thirteen if its set of characters is a superset of those in "thirteen."
196#[derive(Debug, Clone)]
197pub struct CanSpell {
198    letters: HashSet<u8>,
199}
200
201impl CanSpell {
202    pub fn new(s: &str) -> Self {
203        Self {
204            letters: s.bytes().map(|b| b.to_ascii_lowercase()).collect(),
205        }
206    }
207}
208
209impl IsThirteen for CanSpell {
210    fn thirteen(&self) -> bool {
211        [b't', b'h', b'i', b'r', b't', b'e', b'e', b'n']
212            .iter()
213            .all(|b| self.letters.contains(b))
214    }
215}
216
217/// `AnagramOf` is thirteen if it is an [anagram](https://en.wikipedia.org/wiki/Anagram) of
218/// "thirteen."
219#[derive(Debug, Clone)]
220pub struct AnagramOf {
221    bytes: HashSet<u8>,
222}
223
224impl AnagramOf {
225    pub fn new(s: &str) -> Self {
226        Self {
227            bytes: s.bytes().map(|b| b.to_ascii_lowercase()).collect(),
228        }
229    }
230}
231
232const THIRTEEN_STR: &str = "thirteen";
233static THIRTEEN_LETTERS: OnceCell<HashSet<u8>> = OnceCell::new();
234
235impl IsThirteen for AnagramOf {
236    fn thirteen(&self) -> bool {
237        self.bytes == *THIRTEEN_LETTERS.get_or_init(|| THIRTEEN_STR.bytes().collect())
238    }
239}
240
241/// `Backwards` is thirteen if its lowercase version equals `"neetriht"` (reverse spelling of
242/// "thirteen"). This is different from the original JS version as the original is case-sensitive.
243#[derive(Debug, Clone)]
244pub struct Backwards<'s>(pub &'s str);
245
246impl IsThirteen for Backwards<'_> {
247    fn thirteen(&self) -> bool {
248        self.0.eq_ignore_ascii_case("neetriht")
249    }
250}
251
252/// `AtomicNumber` is thirteen if the string equals `"aluminum"`.
253#[derive(Debug, Clone)]
254pub struct AtomicNumber<'s>(pub &'s str);
255
256impl IsThirteen for AtomicNumber<'_> {
257    fn thirteen(&self) -> bool {
258        self.0.eq_ignore_ascii_case("aluminum")
259    }
260}
261
262#[cfg(test)]
263mod lib_test;