1use crate::value::{CoercionFamily, TextMode, Value};
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)]
74pub enum CoercionRuleFamily {
75 Any,
76 Family(CoercionFamily),
77}
78
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
90pub struct CoercionRule {
91 pub left: CoercionRuleFamily,
92 pub right: CoercionRuleFamily,
93 pub id: CoercionId,
94}
95
96pub const COERCION_TABLE: &[CoercionRule] = &[
100 CoercionRule {
101 left: CoercionRuleFamily::Any,
102 right: CoercionRuleFamily::Any,
103 id: CoercionId::Strict,
104 },
105 CoercionRule {
106 left: CoercionRuleFamily::Family(CoercionFamily::Numeric),
107 right: CoercionRuleFamily::Family(CoercionFamily::Numeric),
108 id: CoercionId::NumericWiden,
109 },
110 CoercionRule {
111 left: CoercionRuleFamily::Family(CoercionFamily::Textual),
112 right: CoercionRuleFamily::Family(CoercionFamily::Textual),
113 id: CoercionId::TextCasefold,
114 },
115 CoercionRule {
116 left: CoercionRuleFamily::Any,
117 right: CoercionRuleFamily::Any,
118 id: CoercionId::CollectionElement,
119 },
120];
121
122#[must_use]
124pub fn supports_coercion(left: CoercionFamily, right: CoercionFamily, id: CoercionId) -> bool {
125 COERCION_TABLE.iter().any(|rule| {
126 rule.id == id && family_matches(rule.left, left) && family_matches(rule.right, right)
127 })
128}
129
130fn family_matches(rule: CoercionRuleFamily, value: CoercionFamily) -> bool {
131 match rule {
132 CoercionRuleFamily::Any => true,
133 CoercionRuleFamily::Family(expected) => expected == value,
134 }
135}
136
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
142pub enum TextOp {
143 Eq,
144 Contains,
145 StartsWith,
146 EndsWith,
147}
148
149#[must_use]
154pub fn compare_eq(left: &Value, right: &Value, coercion: &CoercionSpec) -> Option<bool> {
155 match coercion.id {
156 CoercionId::Strict | CoercionId::CollectionElement => {
157 same_variant(left, right).then_some(left == right)
158 }
159 CoercionId::NumericWiden => {
160 if !left.supports_numeric_coercion() || !right.supports_numeric_coercion() {
161 return None;
162 }
163
164 left.cmp_numeric(right).map(|ord| ord == Ordering::Equal)
165 }
166 CoercionId::TextCasefold => compare_casefold(left, right),
167 }
168}
169
170#[must_use]
175pub fn compare_order(left: &Value, right: &Value, coercion: &CoercionSpec) -> Option<Ordering> {
176 match coercion.id {
177 CoercionId::Strict | CoercionId::CollectionElement => {
178 if !same_variant(left, right) {
179 return None;
180 }
181 strict_ordering(left, right)
182 }
183 CoercionId::NumericWiden => {
184 if !left.supports_numeric_coercion() || !right.supports_numeric_coercion() {
185 return None;
186 }
187
188 left.cmp_numeric(right)
189 }
190 CoercionId::TextCasefold => {
191 let left = casefold_value(left)?;
192 let right = casefold_value(right)?;
193 Some(left.cmp(&right))
194 }
195 }
196}
197
198#[must_use]
205pub(crate) fn canonical_cmp(left: &Value, right: &Value) -> Ordering {
206 if let Some(ordering) = strict_ordering(left, right) {
207 return ordering;
208 }
209
210 canonical_rank(left).cmp(&canonical_rank(right))
211}
212
213const fn canonical_rank(value: &Value) -> u8 {
214 match value {
215 Value::Account(_) => 0,
216 Value::Blob(_) => 1,
217 Value::Bool(_) => 2,
218 Value::Date(_) => 3,
219 Value::Decimal(_) => 4,
220 Value::Duration(_) => 5,
221 Value::Enum(_) => 6,
222 Value::E8s(_) => 7,
223 Value::E18s(_) => 8,
224 Value::Float32(_) => 9,
225 Value::Float64(_) => 10,
226 Value::Int(_) => 11,
227 Value::Int128(_) => 12,
228 Value::IntBig(_) => 13,
229 Value::List(_) => 14,
230 Value::Map(_) => 15,
231 Value::None => 16,
232 Value::Principal(_) => 17,
233 Value::Subaccount(_) => 18,
234 Value::Text(_) => 19,
235 Value::Timestamp(_) => 20,
236 Value::Uint(_) => 21,
237 Value::Uint128(_) => 22,
238 Value::UintBig(_) => 23,
239 Value::Ulid(_) => 24,
240 Value::Unit => 25,
241 Value::Unsupported => 26,
242 }
243}
244
245#[must_use]
250pub fn compare_text(
251 left: &Value,
252 right: &Value,
253 coercion: &CoercionSpec,
254 op: TextOp,
255) -> Option<bool> {
256 if !matches!(left, Value::Text(_)) || !matches!(right, Value::Text(_)) {
257 return None;
259 }
260
261 let mode = match coercion.id {
262 CoercionId::Strict => TextMode::Cs,
263 CoercionId::TextCasefold => TextMode::Ci,
264 _ => return None,
265 };
266
267 match op {
268 TextOp::Eq => left.text_eq(right, mode),
269 TextOp::Contains => left.text_contains(right, mode),
270 TextOp::StartsWith => left.text_starts_with(right, mode),
271 TextOp::EndsWith => left.text_ends_with(right, mode),
272 }
273}
274
275fn same_variant(left: &Value, right: &Value) -> bool {
276 discriminant(left) == discriminant(right)
277}
278
279fn strict_ordering(left: &Value, right: &Value) -> Option<Ordering> {
284 match (left, right) {
285 (Value::Account(a), Value::Account(b)) => Some(a.cmp(b)),
286 (Value::Bool(a), Value::Bool(b)) => a.partial_cmp(b),
287 (Value::Date(a), Value::Date(b)) => a.partial_cmp(b),
288 (Value::Decimal(a), Value::Decimal(b)) => a.partial_cmp(b),
289 (Value::Duration(a), Value::Duration(b)) => a.partial_cmp(b),
290 (Value::E8s(a), Value::E8s(b)) => a.partial_cmp(b),
291 (Value::E18s(a), Value::E18s(b)) => a.partial_cmp(b),
292 (Value::Enum(a), Value::Enum(b)) => a.partial_cmp(b),
293 (Value::Float32(a), Value::Float32(b)) => a.partial_cmp(b),
294 (Value::Float64(a), Value::Float64(b)) => a.partial_cmp(b),
295 (Value::Int(a), Value::Int(b)) => a.partial_cmp(b),
296 (Value::Int128(a), Value::Int128(b)) => a.partial_cmp(b),
297 (Value::IntBig(a), Value::IntBig(b)) => a.partial_cmp(b),
298 (Value::Map(a), Value::Map(b)) => map_ordering(a.as_slice(), b.as_slice()),
299 (Value::Principal(a), Value::Principal(b)) => a.partial_cmp(b),
300 (Value::Subaccount(a), Value::Subaccount(b)) => a.partial_cmp(b),
301 (Value::Text(a), Value::Text(b)) => a.partial_cmp(b),
302 (Value::Timestamp(a), Value::Timestamp(b)) => a.partial_cmp(b),
303 (Value::Uint(a), Value::Uint(b)) => a.partial_cmp(b),
304 (Value::Uint128(a), Value::Uint128(b)) => a.partial_cmp(b),
305 (Value::UintBig(a), Value::UintBig(b)) => a.partial_cmp(b),
306 (Value::Ulid(a), Value::Ulid(b)) => a.partial_cmp(b),
307 (Value::Unit, Value::Unit) => Some(Ordering::Equal),
308 _ => {
309 None
311 }
312 }
313}
314
315fn map_ordering(left: &[(Value, Value)], right: &[(Value, Value)]) -> Option<Ordering> {
316 let limit = left.len().min(right.len());
317 for ((left_key, left_value), (right_key, right_value)) in
318 left.iter().zip(right.iter()).take(limit)
319 {
320 let key_cmp = Value::canonical_cmp_key(left_key, right_key);
321 if key_cmp != Ordering::Equal {
322 return Some(key_cmp);
323 }
324
325 let value_cmp = strict_ordering(left_value, right_value)?;
326 if value_cmp != Ordering::Equal {
327 return Some(value_cmp);
328 }
329 }
330
331 left.len().partial_cmp(&right.len())
332}
333
334fn compare_casefold(left: &Value, right: &Value) -> Option<bool> {
335 let left = casefold_value(left)?;
336 let right = casefold_value(right)?;
337 Some(left == right)
338}
339
340fn casefold_value(value: &Value) -> Option<String> {
343 match value {
344 Value::Text(text) => Some(casefold(text)),
345 _ => {
347 None
349 }
350 }
351}
352
353fn casefold(input: &str) -> String {
354 if input.is_ascii() {
355 return input.to_ascii_lowercase();
356 }
357
358 input.to_lowercase()
360}
361
362#[cfg(test)]
363mod tests {
364 use super::canonical_cmp;
365 use crate::{types::Account, value::Value};
366 use std::cmp::Ordering;
367
368 #[test]
369 fn canonical_cmp_orders_accounts() {
370 let left = Value::Account(Account::dummy(1));
371 let right = Value::Account(Account::dummy(2));
372
373 assert_eq!(canonical_cmp(&left, &right), Ordering::Less);
374 assert_eq!(canonical_cmp(&right, &left), Ordering::Greater);
375 }
376
377 #[test]
378 fn canonical_cmp_is_total_for_mixed_variants() {
379 let left = Value::Account(Account::dummy(1));
380 let right = Value::Text("x".to_string());
381
382 assert_ne!(canonical_cmp(&left, &right), Ordering::Equal);
383 assert_eq!(
384 canonical_cmp(&left, &right),
385 canonical_cmp(&right, &left).reverse()
386 );
387 }
388}