Skip to main content

pickems/datatypes/
name.rs

1use std::{cmp::Ordering, hash::Hash};
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5/// Team name used in TOML input and CLI pick arguments.
6///
7/// Names are trimmed, limited to 30 characters, and compared,
8/// ordered, and hashed case-insensitively.
9#[derive(Debug, Clone)]
10pub struct Name(String);
11
12impl Name {
13    pub fn try_new(name: impl AsRef<str>) -> anyhow::Result<Self> {
14        let name = name.as_ref().trim();
15
16        if name.is_empty() {
17            anyhow::bail!("invalid name: cannot be empty");
18        }
19
20        if name.chars().count() > 30 {
21            anyhow::bail!("invalid name: cannot be longer than 30 characters");
22        }
23
24        Ok(Self(name.to_string()))
25    }
26
27    pub fn new(name: impl AsRef<str>) -> Self {
28        Self::try_new(name).unwrap()
29    }
30
31    /// # Safety
32    /// Must ensure that `name` is less than 30 characters and has whitespace trimmed.
33    #[inline]
34    #[must_use]
35    pub const unsafe fn new_unchecked(name: String) -> Self {
36        Self(name)
37    }
38}
39
40impl std::fmt::Display for Name {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        self.0.fmt(f)
43    }
44}
45
46impl Serialize for Name {
47    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
48        serializer.serialize_str(&self.0)
49    }
50}
51
52impl<'de> Deserialize<'de> for Name {
53    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
54        Self::try_new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
55    }
56}
57
58/// Case-insensitive name comparison.
59impl PartialEq for Name {
60    fn eq(&self, other: &Self) -> bool {
61        self.0.eq_ignore_ascii_case(&other.0)
62    }
63}
64
65impl Eq for Name {}
66
67/// Case-insensitive ordering.
68impl Ord for Name {
69    fn cmp(&self, other: &Self) -> Ordering {
70        self.0
71            .chars()
72            .map(|c| c.to_ascii_lowercase())
73            .cmp(other.0.chars().map(|c| c.to_ascii_lowercase()))
74    }
75}
76
77impl PartialOrd for Name {
78    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
79        Some(self.cmp(other))
80    }
81}
82
83/// Case-insensitive hashing.
84impl Hash for Name {
85    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
86        for c in self.0.chars() {
87            c.to_ascii_lowercase().hash(state);
88        }
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn validates_and_trims_names() {
98        assert_eq!(Name::new("  Vitality  ").to_string(), "Vitality");
99        assert!(Name::try_new("   ").is_err());
100        assert!(Name::try_new("1234567890123456789012345678901").is_err());
101    }
102
103    #[test]
104    fn compares_and_hashes_case_insensitively() {
105        use std::collections::{BTreeSet, HashSet};
106
107        let name = Name::new("NAVI");
108        let lowercase = Name::new("navi");
109
110        assert_eq!(name, lowercase);
111
112        let mut hash_set = HashSet::new();
113        hash_set.insert(name.clone());
114        assert!(hash_set.contains(&lowercase));
115
116        let mut tree_set = BTreeSet::new();
117        tree_set.insert(name);
118        tree_set.insert(lowercase);
119        assert_eq!(tree_set.len(), 1);
120    }
121}