tiger_lib/
lowercase.rs

1//! Type-safety wrapper for strings that must be lowercase
2
3use std::borrow::{Borrow, Cow};
4use std::fmt::{Display, Error, Formatter};
5#[cfg(any(feature = "vic3", feature = "imperator"))]
6use std::slice::SliceIndex;
7#[cfg(any(feature = "vic3", feature = "imperator"))]
8use std::str::RMatchIndices;
9
10/// Wraps a string (either owned or `&str`) and guarantees that it's lowercase.
11///
12/// This allows interfaces that expect lowercase strings to declare this expectation in their
13/// argument type, so that the caller can choose how to fulfill it.
14///
15/// Only ASCII characters are lowercased. This is faster than full unicode casemapping, and it
16/// matches what the game engine does.
17///
18/// The lowercase string is a [`Cow<str>`] internally, so it can be either owned or borrowed.
19#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
20pub struct Lowercase<'a>(Cow<'a, str>);
21
22impl<'a> Lowercase<'a> {
23    /// Take a string and return the lowercased version.
24    pub fn new(s: &'a str) -> Self {
25        // Avoid allocating if it's not necessary
26        if s.chars().any(|c| c.is_ascii_uppercase()) {
27            Lowercase(Cow::Owned(s.to_ascii_lowercase()))
28        } else {
29            Lowercase(Cow::Borrowed(s))
30        }
31    }
32
33    /// Take a string that is known to already be lowercase and return a `Lowercase` wrapper for it.
34    ///
35    /// This operation is free.
36    pub fn new_unchecked(s: &'a str) -> Self {
37        Lowercase(Cow::Borrowed(s))
38    }
39
40    /// Take an owned `String` that is known to already be lowercase and return a `Lowercase` wrapper
41    pub fn from_string_unchecked(s: String) -> Self {
42        Lowercase(Cow::Owned(s))
43    }
44
45    pub fn empty() -> &'static Self {
46        const EMPTY_LOWERCASE: Lowercase = Lowercase(Cow::Borrowed(""));
47        &EMPTY_LOWERCASE
48    }
49
50    pub fn as_str(&'a self) -> &'a str {
51        &self.0
52    }
53
54    pub fn into_cow(self) -> Cow<'a, str> {
55        self.0
56    }
57
58    pub fn to_uppercase(&self) -> String {
59        self.as_str().to_ascii_uppercase()
60    }
61
62    /// Like [`str::strip_prefix`]. Takes a prefix that is known to be already lowercase.
63    pub fn strip_prefix_unchecked<S: Borrow<str>>(&'a self, prefix: S) -> Option<Lowercase<'a>> {
64        self.0.strip_prefix(prefix.borrow()).map(|s| Self(Cow::Borrowed(s)))
65    }
66
67    /// Like [`str::strip_suffix`]. Takes a suffix that is known to be already lowercase.
68    pub fn strip_suffix_unchecked<S: Borrow<str>>(&'a self, suffix: S) -> Option<Lowercase<'a>> {
69        self.0.strip_suffix(suffix.borrow()).map(|s| Self(Cow::Borrowed(s)))
70    }
71
72    #[allow(dead_code)]
73    pub fn contains_unchecked<S: Borrow<str>>(&self, infix: S) -> bool {
74        self.0.contains(infix.borrow())
75    }
76
77    #[cfg(any(feature = "vic3", feature = "imperator"))]
78    pub fn rmatch_indices_unchecked(&self, separator: char) -> RMatchIndices<'_, char> {
79        self.0.rmatch_indices(separator)
80    }
81
82    #[cfg(any(feature = "vic3", feature = "imperator"))]
83    pub fn slice<R: 'a + SliceIndex<str, Output = str>>(&'a self, range: R) -> Self {
84        Lowercase::new_unchecked(&self.0[range])
85    }
86}
87
88impl Display for Lowercase<'_> {
89    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
90        write!(f, "{}", self.0)
91    }
92}
93
94impl<'a> Borrow<Cow<'a, str>> for Lowercase<'a> {
95    fn borrow(&self) -> &Cow<'a, str> {
96        &self.0
97    }
98}
99
100impl Borrow<str> for Lowercase<'_> {
101    fn borrow(&self) -> &str {
102        &self.0
103    }
104}
105
106impl Default for Lowercase<'static> {
107    fn default() -> Lowercase<'static> {
108        Lowercase::new_unchecked("")
109    }
110}
111
112impl PartialEq<str> for Lowercase<'_> {
113    fn eq(&self, s: &str) -> bool {
114        self.as_str() == s
115    }
116}
117
118impl PartialEq<&str> for Lowercase<'_> {
119    fn eq(&self, s: &&str) -> bool {
120        self.as_str() == *s
121    }
122}
123
124impl PartialEq<String> for &Lowercase<'_> {
125    fn eq(&self, s: &String) -> bool {
126        self.as_str() == s
127    }
128}