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
57#[must_use]
64pub(crate) fn upper_text_construction_allowance(input_len: usize) -> (u64, u64) {
65 let len = input_len as u64;
66 if len <= 1 {
67 return (len, len.saturating_mul(3));
68 }
69 let first_growth = len.saturating_mul(2).max(8);
70 let backing = len
71 .saturating_add(first_growth)
72 .saturating_add(first_growth.saturating_mul(2));
73 (backing, len.saturating_mul(4).saturating_add(backing))
74}
75
76fn lowercase_text(input: &str) -> String {
77 if input.is_ascii() {
78 return input.to_ascii_lowercase();
79 }
80
81 input.to_lowercase()
82}
83
84fn text_with_mode(s: &'_ str, mode: TextMode) -> Cow<'_, str> {
85 match mode {
86 TextMode::Cs => Cow::Borrowed(s),
87 TextMode::Ci => Cow::Owned(casefold_text(s)),
88 }
89}
90
91fn text_op(
92 left: &Value,
93 right: &Value,
94 mode: TextMode,
95 f: impl Fn(&str, &str) -> bool,
96) -> Option<bool> {
97 let (a, b) = (left.as_text()?, right.as_text()?);
98 let a = text_with_mode(a, mode);
99 let b = text_with_mode(b, mode);
100 Some(f(&a, &b))
101}
102
103fn ci_key(value: &Value) -> Option<String> {
104 match value {
105 Value::Text(s) => Some(casefold_text(s)),
106 Value::Ulid(u) => Some(u.to_string().to_ascii_lowercase()),
107 Value::Principal(p) => Some(p.to_string().to_ascii_lowercase()),
108 Value::Account(a) => Some(a.to_string().to_ascii_lowercase()),
109 _ => None,
110 }
111}
112
113pub(super) fn eq_ci(left: &Value, right: &Value) -> bool {
114 if let (Some(left_key), Some(right_key)) = (ci_key(left), ci_key(right)) {
115 return left_key == right_key;
116 }
117
118 left == right
119}
120
121#[must_use]
123fn text_eq(left: &Value, right: &Value, mode: TextMode) -> Option<bool> {
124 text_op(left, right, mode, |a, b| a == b)
125}
126
127#[must_use]
129fn text_contains(value: &Value, needle: &Value, mode: TextMode) -> Option<bool> {
130 text_op(value, needle, mode, |a, b| a.contains(b))
131}
132
133#[must_use]
135fn text_starts_with(value: &Value, needle: &Value, mode: TextMode) -> Option<bool> {
136 text_op(value, needle, mode, |a, b| a.starts_with(b))
137}
138
139#[must_use]
141fn text_ends_with(value: &Value, needle: &Value, mode: TextMode) -> Option<bool> {
142 text_op(value, needle, mode, |a, b| a.ends_with(b))
143}
144
145impl Value {
146 #[must_use]
148 pub fn text_eq(&self, other: &Self, mode: TextMode) -> Option<bool> {
149 text_eq(self, other, mode)
150 }
151
152 #[must_use]
154 pub fn text_contains(&self, needle: &Self, mode: TextMode) -> Option<bool> {
155 text_contains(self, needle, mode)
156 }
157
158 #[must_use]
160 pub fn text_starts_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
161 text_starts_with(self, needle, mode)
162 }
163
164 #[must_use]
166 pub fn text_ends_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
167 text_ends_with(self, needle, mode)
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::{
174 casefold_text, lower_text, lower_text_construction_allowance, upper_text,
175 upper_text_construction_allowance,
176 };
177
178 #[test]
179 fn uppercase_allowance_covers_unicode_expansion_and_cumulative_growth() {
180 for scalar in (0..=u32::from(char::MAX)).filter_map(char::from_u32) {
181 let output_bytes: usize = scalar.to_uppercase().map(char::len_utf8).sum();
182 assert!(output_bytes <= 3 * scalar.len_utf8(), "{scalar:?}");
183 }
184 for text in ["", "a", "ß", "ΐ", "aΐ", "ffi", "é🍋", "ASCII"] {
185 for repeats in [1, 2, 16, 1024] {
186 let input = text.repeat(repeats);
187 let output = upper_text(&input);
188 let initial = input.len();
189 let first_growth = (2 * initial).max(8);
190 let requested = if output.capacity() > first_growth {
191 initial + first_growth + output.capacity()
192 } else if output.capacity() > initial {
193 initial + output.capacity()
194 } else {
195 initial
196 };
197 let (backing, work) = upper_text_construction_allowance(input.len());
198 assert!(requested as u64 <= backing, "{input:?}");
199 assert!(output.len() as u64 <= work);
200 assert_eq!(output, input.to_uppercase());
201 }
202 }
203 assert_eq!(upper_text_construction_allowance(0), (0, 0));
204 assert!(upper_text_construction_allowance(usize::MAX).0 >= usize::MAX as u64);
205 }
206
207 #[test]
208 fn lowercase_allowance_covers_unicode_expansion_and_output_growth() {
209 for scalar in (0..=u32::from(char::MAX)).filter_map(char::from_u32) {
212 let output_bytes: usize = scalar.to_lowercase().map(char::len_utf8).sum();
213 assert!(output_bytes <= 2 * scalar.len_utf8(), "{scalar:?}");
214 }
215 for text in ["", "A", "İ", "Aİ", "İΣ", "ΟΣ\u{301}", "ΣΑ", "ASCII"] {
216 for repeats in [1, 2, 16, 1024] {
217 let input = text.repeat(repeats);
218 let output = lower_text(&input);
219 let (backing, _) = lower_text_construction_allowance(input.len());
220 let requested = if output.capacity() > input.len() {
221 input.len() + output.capacity()
222 } else {
223 input.len()
224 };
225 assert!(requested as u64 <= backing);
226 assert_eq!(output, input.to_lowercase());
227 }
228 }
229 }
230
231 #[test]
232 fn canonical_text_transforms_preserve_current_ascii_and_unicode_semantics() {
233 assert_eq!(casefold_text("IcYDB"), "icydb");
234 assert_eq!(lower_text("IcYDB"), "icydb");
235 assert_eq!(upper_text("IcYDB"), "ICYDB");
236
237 assert_eq!(casefold_text("Straße"), "straße");
238 assert_eq!(lower_text("Straße"), "straße");
239 assert_eq!(upper_text("Straße"), "STRASSE");
240 }
241}