1use crate::value::{TextMode, Value};
8use std::borrow::Cow;
9
10#[must_use]
16pub(crate) fn casefold_text(input: &str) -> String {
17 lowercase_text(input)
18}
19
20#[must_use]
22pub(crate) fn lower_text(input: &str) -> String {
23 lowercase_text(input)
24}
25
26#[must_use]
30pub(crate) fn lower_text_construction_allowance(input_len: usize) -> (u64, u64) {
31 let len = input_len as u64;
32 if len <= 1 {
33 return (len, len.saturating_mul(3));
36 }
37 let backing = len.saturating_add(len.saturating_mul(2).max(8));
41 (backing, len.saturating_mul(6).saturating_add(backing))
45}
46
47#[must_use]
49pub(crate) fn upper_text(input: &str) -> String {
50 if input.is_ascii() {
51 return input.to_ascii_uppercase();
52 }
53
54 input.to_uppercase()
55}
56
57fn lowercase_text(input: &str) -> String {
58 if input.is_ascii() {
59 return input.to_ascii_lowercase();
60 }
61
62 input.to_lowercase()
63}
64
65fn text_with_mode(s: &'_ str, mode: TextMode) -> Cow<'_, str> {
66 match mode {
67 TextMode::Cs => Cow::Borrowed(s),
68 TextMode::Ci => Cow::Owned(casefold_text(s)),
69 }
70}
71
72fn text_op(
73 left: &Value,
74 right: &Value,
75 mode: TextMode,
76 f: impl Fn(&str, &str) -> bool,
77) -> Option<bool> {
78 let (a, b) = (left.as_text()?, right.as_text()?);
79 let a = text_with_mode(a, mode);
80 let b = text_with_mode(b, mode);
81 Some(f(&a, &b))
82}
83
84fn ci_key(value: &Value) -> Option<String> {
85 match value {
86 Value::Text(s) => Some(casefold_text(s)),
87 Value::Ulid(u) => Some(u.to_string().to_ascii_lowercase()),
88 Value::Principal(p) => Some(p.to_string().to_ascii_lowercase()),
89 Value::Account(a) => Some(a.to_string().to_ascii_lowercase()),
90 _ => None,
91 }
92}
93
94pub(super) fn eq_ci(left: &Value, right: &Value) -> bool {
95 if let (Some(left_key), Some(right_key)) = (ci_key(left), ci_key(right)) {
96 return left_key == right_key;
97 }
98
99 left == right
100}
101
102#[must_use]
104fn text_eq(left: &Value, right: &Value, mode: TextMode) -> Option<bool> {
105 text_op(left, right, mode, |a, b| a == b)
106}
107
108#[must_use]
110fn text_contains(value: &Value, needle: &Value, mode: TextMode) -> Option<bool> {
111 text_op(value, needle, mode, |a, b| a.contains(b))
112}
113
114#[must_use]
116fn text_starts_with(value: &Value, needle: &Value, mode: TextMode) -> Option<bool> {
117 text_op(value, needle, mode, |a, b| a.starts_with(b))
118}
119
120#[must_use]
122fn text_ends_with(value: &Value, needle: &Value, mode: TextMode) -> Option<bool> {
123 text_op(value, needle, mode, |a, b| a.ends_with(b))
124}
125
126impl Value {
127 #[must_use]
129 pub fn text_eq(&self, other: &Self, mode: TextMode) -> Option<bool> {
130 text_eq(self, other, mode)
131 }
132
133 #[must_use]
135 pub fn text_contains(&self, needle: &Self, mode: TextMode) -> Option<bool> {
136 text_contains(self, needle, mode)
137 }
138
139 #[must_use]
141 pub fn text_starts_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
142 text_starts_with(self, needle, mode)
143 }
144
145 #[must_use]
147 pub fn text_ends_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
148 text_ends_with(self, needle, mode)
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::{casefold_text, lower_text, lower_text_construction_allowance, upper_text};
155
156 #[test]
157 fn lowercase_allowance_covers_unicode_expansion_and_output_growth() {
158 for scalar in (0..=u32::from(char::MAX)).filter_map(char::from_u32) {
161 let output_bytes: usize = scalar.to_lowercase().map(char::len_utf8).sum();
162 assert!(output_bytes <= 2 * scalar.len_utf8(), "{scalar:?}");
163 }
164 for text in ["", "A", "İ", "Aİ", "İΣ", "ΟΣ\u{301}", "ΣΑ", "ASCII"] {
165 for repeats in [1, 2, 16, 1024] {
166 let input = text.repeat(repeats);
167 let output = lower_text(&input);
168 let (backing, _) = lower_text_construction_allowance(input.len());
169 let requested = if output.capacity() > input.len() {
170 input.len() + output.capacity()
171 } else {
172 input.len()
173 };
174 assert!(requested as u64 <= backing);
175 assert_eq!(output, input.to_lowercase());
176 }
177 }
178 }
179
180 #[test]
181 fn canonical_text_transforms_preserve_current_ascii_and_unicode_semantics() {
182 assert_eq!(casefold_text("IcYDB"), "icydb");
183 assert_eq!(lower_text("IcYDB"), "icydb");
184 assert_eq!(upper_text("IcYDB"), "ICYDB");
185
186 assert_eq!(casefold_text("Straße"), "straße");
187 assert_eq!(lower_text("Straße"), "straße");
188 assert_eq!(upper_text("Straße"), "STRASSE");
189 }
190}