icydb_core/value/ops/
text.rs1use 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]
28pub(crate) fn upper_text(input: &str) -> String {
29 if input.is_ascii() {
30 return input.to_ascii_uppercase();
31 }
32
33 input.to_uppercase()
34}
35
36fn lowercase_text(input: &str) -> String {
37 if input.is_ascii() {
38 return input.to_ascii_lowercase();
39 }
40
41 input.to_lowercase()
42}
43
44fn text_with_mode(s: &'_ str, mode: TextMode) -> Cow<'_, str> {
45 match mode {
46 TextMode::Cs => Cow::Borrowed(s),
47 TextMode::Ci => Cow::Owned(casefold_text(s)),
48 }
49}
50
51fn text_op(
52 left: &Value,
53 right: &Value,
54 mode: TextMode,
55 f: impl Fn(&str, &str) -> bool,
56) -> Option<bool> {
57 let (a, b) = (left.as_text()?, right.as_text()?);
58 let a = text_with_mode(a, mode);
59 let b = text_with_mode(b, mode);
60 Some(f(&a, &b))
61}
62
63fn ci_key(value: &Value) -> Option<String> {
64 match value {
65 Value::Text(s) => Some(casefold_text(s)),
66 Value::Ulid(u) => Some(u.to_string().to_ascii_lowercase()),
67 Value::Principal(p) => Some(p.to_string().to_ascii_lowercase()),
68 Value::Account(a) => Some(a.to_string().to_ascii_lowercase()),
69 _ => None,
70 }
71}
72
73pub(super) fn eq_ci(left: &Value, right: &Value) -> bool {
74 if let (Some(left_key), Some(right_key)) = (ci_key(left), ci_key(right)) {
75 return left_key == right_key;
76 }
77
78 left == right
79}
80
81#[must_use]
83fn text_eq(left: &Value, right: &Value, mode: TextMode) -> Option<bool> {
84 text_op(left, right, mode, |a, b| a == b)
85}
86
87#[must_use]
89fn text_contains(value: &Value, needle: &Value, mode: TextMode) -> Option<bool> {
90 text_op(value, needle, mode, |a, b| a.contains(b))
91}
92
93#[must_use]
95fn text_starts_with(value: &Value, needle: &Value, mode: TextMode) -> Option<bool> {
96 text_op(value, needle, mode, |a, b| a.starts_with(b))
97}
98
99#[must_use]
101fn text_ends_with(value: &Value, needle: &Value, mode: TextMode) -> Option<bool> {
102 text_op(value, needle, mode, |a, b| a.ends_with(b))
103}
104
105impl Value {
106 #[must_use]
108 pub fn text_eq(&self, other: &Self, mode: TextMode) -> Option<bool> {
109 text_eq(self, other, mode)
110 }
111
112 #[must_use]
114 pub fn text_contains(&self, needle: &Self, mode: TextMode) -> Option<bool> {
115 text_contains(self, needle, mode)
116 }
117
118 #[must_use]
120 pub fn text_starts_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
121 text_starts_with(self, needle, mode)
122 }
123
124 #[must_use]
126 pub fn text_ends_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
127 text_ends_with(self, needle, mode)
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::{casefold_text, lower_text, upper_text};
134
135 #[test]
136 fn canonical_text_transforms_preserve_current_ascii_and_unicode_semantics() {
137 assert_eq!(casefold_text("IcYDB"), "icydb");
138 assert_eq!(lower_text("IcYDB"), "icydb");
139 assert_eq!(upper_text("IcYDB"), "ICYDB");
140
141 assert_eq!(casefold_text("Straße"), "straße");
142 assert_eq!(lower_text("Straße"), "straße");
143 assert_eq!(upper_text("Straße"), "STRASSE");
144 }
145}