1use crate::value::{TextMode, Value, ValueFamily};
2use std::{cmp::Ordering, collections::BTreeMap, mem::discriminant};
3
4#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
28pub enum CoercionId {
29 Strict,
30 NumericWiden,
31 TextCasefold,
32 CollectionElement,
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct CoercionSpec {
47 pub id: CoercionId,
48 pub params: BTreeMap<String, String>,
49}
50
51impl CoercionSpec {
52 #[must_use]
53 pub const fn new(id: CoercionId) -> Self {
54 Self {
55 id,
56 params: BTreeMap::new(),
57 }
58 }
59}
60
61impl Default for CoercionSpec {
62 fn default() -> Self {
63 Self::new(CoercionId::Strict)
64 }
65}
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum CoercionFamily {
73 Any,
74 Family(ValueFamily),
75}
76
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88pub struct CoercionRule {
89 pub left: CoercionFamily,
90 pub right: CoercionFamily,
91 pub id: CoercionId,
92}
93
94pub const COERCION_TABLE: &[CoercionRule] = &[
95 CoercionRule {
96 left: CoercionFamily::Any,
97 right: CoercionFamily::Any,
98 id: CoercionId::Strict,
99 },
100 CoercionRule {
101 left: CoercionFamily::Family(ValueFamily::Numeric),
102 right: CoercionFamily::Family(ValueFamily::Numeric),
103 id: CoercionId::NumericWiden,
104 },
105 CoercionRule {
106 left: CoercionFamily::Family(ValueFamily::Textual),
107 right: CoercionFamily::Family(ValueFamily::Textual),
108 id: CoercionId::TextCasefold,
109 },
110 CoercionRule {
111 left: CoercionFamily::Any,
112 right: CoercionFamily::Any,
113 id: CoercionId::CollectionElement,
114 },
115];
116
117#[must_use]
118pub fn supports_coercion(left: ValueFamily, right: ValueFamily, id: CoercionId) -> bool {
119 COERCION_TABLE.iter().any(|rule| {
120 rule.id == id && family_matches(rule.left, left) && family_matches(rule.right, right)
121 })
122}
123
124fn family_matches(rule: CoercionFamily, value: ValueFamily) -> bool {
125 match rule {
126 CoercionFamily::Any => true,
127 CoercionFamily::Family(expected) => expected == value,
128 }
129}
130
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136pub enum TextOp {
137 Eq,
138 Contains,
139 StartsWith,
140 EndsWith,
141}
142
143#[must_use]
148pub fn compare_eq(left: &Value, right: &Value, coercion: &CoercionSpec) -> Option<bool> {
149 match coercion.id {
150 CoercionId::Strict | CoercionId::CollectionElement => {
151 same_variant(left, right).then_some(left == right)
152 }
153 CoercionId::NumericWiden => left.cmp_numeric(right).map(|ord| ord == Ordering::Equal),
154 CoercionId::TextCasefold => compare_casefold(left, right),
155 }
156}
157
158#[must_use]
163pub fn compare_order(left: &Value, right: &Value, coercion: &CoercionSpec) -> Option<Ordering> {
164 match coercion.id {
165 CoercionId::Strict | CoercionId::CollectionElement => {
166 if !same_variant(left, right) {
167 return None;
168 }
169 strict_ordering(left, right)
170 }
171 CoercionId::NumericWiden => left.cmp_numeric(right),
172 CoercionId::TextCasefold => {
173 let left = casefold_value(left)?;
174 let right = casefold_value(right)?;
175 Some(left.cmp(&right))
176 }
177 }
178}
179
180#[must_use]
187pub(crate) fn canonical_cmp(left: &Value, right: &Value) -> Ordering {
188 if let Some(ordering) = strict_ordering(left, right) {
189 return ordering;
190 }
191
192 canonical_rank(left).cmp(&canonical_rank(right))
193}
194
195const fn canonical_rank(value: &Value) -> u8 {
196 match value {
197 Value::Account(_) => 0,
198 Value::Blob(_) => 1,
199 Value::Bool(_) => 2,
200 Value::Date(_) => 3,
201 Value::Decimal(_) => 4,
202 Value::Duration(_) => 5,
203 Value::Enum(_) => 6,
204 Value::E8s(_) => 7,
205 Value::E18s(_) => 8,
206 Value::Float32(_) => 9,
207 Value::Float64(_) => 10,
208 Value::Int(_) => 11,
209 Value::Int128(_) => 12,
210 Value::IntBig(_) => 13,
211 Value::List(_) => 14,
212 Value::None => 15,
213 Value::Principal(_) => 16,
214 Value::Subaccount(_) => 17,
215 Value::Text(_) => 18,
216 Value::Timestamp(_) => 19,
217 Value::Uint(_) => 20,
218 Value::Uint128(_) => 21,
219 Value::UintBig(_) => 22,
220 Value::Ulid(_) => 23,
221 Value::Unit => 24,
222 Value::Unsupported => 25,
223 }
224}
225
226#[must_use]
231pub fn compare_text(
232 left: &Value,
233 right: &Value,
234 coercion: &CoercionSpec,
235 op: TextOp,
236) -> Option<bool> {
237 if !matches!(left, Value::Text(_)) || !matches!(right, Value::Text(_)) {
238 return None;
240 }
241
242 let mode = match coercion.id {
243 CoercionId::Strict => TextMode::Cs,
244 CoercionId::TextCasefold => TextMode::Ci,
245 _ => return None,
246 };
247
248 match op {
249 TextOp::Eq => left.text_eq(right, mode),
250 TextOp::Contains => left.text_contains(right, mode),
251 TextOp::StartsWith => left.text_starts_with(right, mode),
252 TextOp::EndsWith => left.text_ends_with(right, mode),
253 }
254}
255
256fn same_variant(left: &Value, right: &Value) -> bool {
257 discriminant(left) == discriminant(right)
258}
259
260fn strict_ordering(left: &Value, right: &Value) -> Option<Ordering> {
265 match (left, right) {
266 (Value::Account(a), Value::Account(b)) => Some(a.cmp(b)),
267 (Value::Bool(a), Value::Bool(b)) => a.partial_cmp(b),
268 (Value::Date(a), Value::Date(b)) => a.partial_cmp(b),
269 (Value::Decimal(a), Value::Decimal(b)) => a.partial_cmp(b),
270 (Value::Duration(a), Value::Duration(b)) => a.partial_cmp(b),
271 (Value::E8s(a), Value::E8s(b)) => a.partial_cmp(b),
272 (Value::E18s(a), Value::E18s(b)) => a.partial_cmp(b),
273 (Value::Enum(a), Value::Enum(b)) => a.partial_cmp(b),
274 (Value::Float32(a), Value::Float32(b)) => a.partial_cmp(b),
275 (Value::Float64(a), Value::Float64(b)) => a.partial_cmp(b),
276 (Value::Int(a), Value::Int(b)) => a.partial_cmp(b),
277 (Value::Int128(a), Value::Int128(b)) => a.partial_cmp(b),
278 (Value::IntBig(a), Value::IntBig(b)) => a.partial_cmp(b),
279 (Value::Principal(a), Value::Principal(b)) => a.partial_cmp(b),
280 (Value::Subaccount(a), Value::Subaccount(b)) => a.partial_cmp(b),
281 (Value::Text(a), Value::Text(b)) => a.partial_cmp(b),
282 (Value::Timestamp(a), Value::Timestamp(b)) => a.partial_cmp(b),
283 (Value::Uint(a), Value::Uint(b)) => a.partial_cmp(b),
284 (Value::Uint128(a), Value::Uint128(b)) => a.partial_cmp(b),
285 (Value::UintBig(a), Value::UintBig(b)) => a.partial_cmp(b),
286 (Value::Ulid(a), Value::Ulid(b)) => a.partial_cmp(b),
287 (Value::Unit, Value::Unit) => Some(Ordering::Equal),
288 _ => {
289 None
291 }
292 }
293}
294
295fn compare_casefold(left: &Value, right: &Value) -> Option<bool> {
296 let left = casefold_value(left)?;
297 let right = casefold_value(right)?;
298 Some(left == right)
299}
300
301fn casefold_value(value: &Value) -> Option<String> {
304 match value {
305 Value::Text(text) => Some(casefold(text)),
306 _ => {
308 None
310 }
311 }
312}
313
314fn casefold(input: &str) -> String {
315 if input.is_ascii() {
316 return input.to_ascii_lowercase();
317 }
318
319 input.to_lowercase()
321}
322
323#[cfg(test)]
324mod tests {
325 use super::canonical_cmp;
326 use crate::{types::Account, value::Value};
327 use std::cmp::Ordering;
328
329 #[test]
330 fn canonical_cmp_orders_accounts() {
331 let left = Value::Account(Account::dummy(1));
332 let right = Value::Account(Account::dummy(2));
333
334 assert_eq!(canonical_cmp(&left, &right), Ordering::Less);
335 assert_eq!(canonical_cmp(&right, &left), Ordering::Greater);
336 }
337
338 #[test]
339 fn canonical_cmp_is_total_for_mixed_variants() {
340 let left = Value::Account(Account::dummy(1));
341 let right = Value::Text("x".to_string());
342
343 assert_ne!(canonical_cmp(&left, &right), Ordering::Equal);
344 assert_eq!(
345 canonical_cmp(&left, &right),
346 canonical_cmp(&right, &left).reverse()
347 );
348 }
349}