dsa_rust/util/cmp.rs
1/// This module represents a weak partial order, meaning not all
2/// students are comparable, and some can be equivalent. This
3/// implementation illustrates a workaround to guarantee global
4/// reflexivity over the floating point "gotcha" by performing a
5/// memory address comparison. In this way, Students are reflexive,
6/// even if they have NaN grades, but not comparable if two students
7/// have NaN grades.
8pub mod partial_order {
9 use std::cmp::Ordering;
10
11 #[derive(Debug)]
12 pub struct Student {
13 pub name: &'static str,
14 pub grade: f32,
15 }
16
17 // The PartialEq trait is manually implemented instead of derived
18 // to override the default component-wise struct field comparison
19 // logic. The eq method specifies a partial equivalence relation
20 // in which two objects are only equal if their grades
21 // are strictly equal.
22 impl PartialEq for Student {
23 fn eq(&self, other: &Self) -> bool {
24 // Guarantees global reflexivity by only establishing
25 // equivalence for NANs if they represent the exact
26 // same memory address
27 if std::ptr::eq(self, other) {
28 return true;
29 }
30
31 self.grade.eq(&other.grade)
32 }
33 }
34
35 // The PartialOrd impl must be semantically consistent
36 // with the PartialEq impl so only grades are considered,
37 // and because not all floats are directly comparable, the
38 // comparison may return None.
39 impl PartialOrd for Student {
40 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
41 if std::ptr::eq(self, other) {
42 return Some(Ordering::Equal);
43 }
44
45 self.grade.partial_cmp(&other.grade)
46 }
47 }
48}
49
50/// This module represents a total preorder implementation
51pub mod total_preorder {
52 use std::cmp::Ordering;
53
54 #[derive(Debug)]
55 pub struct Student {
56 pub name: &'static str,
57 pub grade: f32,
58 }
59
60 /// An equivalence class to categorize grades based
61 /// on ranges of percentage point values.
62 /// Derives PartialEq to reduce boilerplate.
63 /// In Rust, the ordering of the discrimiants is important,
64 /// such that the first listed discrimiant carries a value
65 /// of 0, the next discrimiant a 1, and so on.
66 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
67 pub enum Grade {
68 INC, // 0
69 F, // 1
70 DMinus, // 2
71 D,
72 DPlus,
73 CMinus,
74 C,
75 CPlus,
76 BMinus,
77 B,
78 BPlus,
79 AMinus,
80 A,
81 APlus,
82 }
83 // Gets rid of the possibility of None for a total preorder,
84 // adds cases where NAN and _ == INC
85 impl Grade {
86 /// Maps a score into its corresponding equivalence class
87 pub fn eq_class(value: f32) -> Grade {
88 match value {
89 val if val.is_nan() => Grade::INC,
90 //..59.5 => Grade::F,
91 //59.5..63.0 => Grade::DMinus,
92 //63.0..66.5 => Grade::D,
93 //66.5..69.5 => Grade::DPlus,
94 //69.5..73.0 => Grade::CMinus,
95 //73.0..76.5 => Grade::C,
96 //76.5..79.5 => Grade::CPlus,
97 //79.5..83.0 => Grade::BMinus,
98 //83.0..86.5 => Grade::B,
99 //86.5..89.5 => Grade::BPlus,
100 //89.5..93.0 => Grade::AMinus,
101 //93.0..96.5 => Grade::A,
102 //96.5.. => Grade::APlus,
103 ..59.5 => Grade::F,
104
105 59.5..62.8 => Grade::DMinus,
106 62.8..66.1 => Grade::D,
107 66.1..69.5 => Grade::DPlus,
108
109 69.5..72.8 => Grade::CMinus,
110 72.8..76.1 => Grade::C,
111 76.1..79.5 => Grade::CPlus,
112
113 79.5..82.8 => Grade::BMinus,
114 82.8..86.1 => Grade::B,
115 86.1..89.5 => Grade::BPlus,
116
117 89.5..92.8 => Grade::AMinus,
118 92.8..96.1 => Grade::A,
119 96.1.. => Grade::APlus,
120
121 _ => Grade::INC, // catch-all because the compiler doesn't trust floats
122 }
123 }
124 }
125
126 // Implements float equality by overloading the == operator.
127 // PartialEq is derivable, but enforces that all struct fields
128 // are Eq, which may not be desired.
129 //
130 // Compares rounded scores to induce a preorder.
131 // NOTE: Must match the logic in Ord::cmp for consistency!!
132 impl PartialEq for Student {
133 fn eq(&self, other: &Self) -> bool {
134 //self.grade.round() as i32 == other.grade.round() as i32
135 Grade::eq_class(self.grade) == Grade::eq_class(other.grade)
136 }
137 }
138
139 // Implements Eq because reflexivity cannot be derived for floats
140 impl Eq for Student {}
141
142 // Imposes a generic comparison because the
143 // comparison is actually defined in cmp() for Ord.
144 impl PartialOrd for Student {
145 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
146 Some(Ord::cmp(self, other)) // Verbose expression
147 //Some(self.cmp(other)) // If you wanna be slick
148 }
149 }
150
151 // Actual comparison implementation for a preorder.
152 impl Ord for Student {
153 // Implements a total preorder
154 // Only the grade's equivalence class is compared
155 // such that 77.5 effectively equials 79.1
156 fn cmp(&self, other: &Self) -> Ordering {
157 // The actual f32 comparison logic, rounded to the nearest ingeter
158 //std::intrinsics::three_way_compare(self, other) // Unavailable to us plebs
159 //(self.grade.round() as i32).cmp(&(other.grade.round() as i32))
160 Grade::eq_class(self.grade).cmp(&Grade::eq_class(other.grade))
161 }
162 }
163}
164
165/// This module represents a strict total order implementation
166pub mod total_order {
167 use std::cmp::Ordering;
168
169 #[derive(Debug)]
170 pub struct Student {
171 pub name: &'static str,
172 pub grade: f32,
173 }
174
175 // Defines a full equivalence realtion for Student where two Students
176 // are only equal if their rounded grade and lexicographic names are equal.
177 // PartialEq can be derived, but would use structural equality,
178 // which may not match the intended equivalence relation.
179 // NOTE: Must remain consistent with Ord::cmp if both are implemented.
180 impl PartialEq for Student {
181 fn eq(&self, other: &Self) -> bool {
182 //self.cmp(other) == Ordering::Equal // Same as deriving the trait
183 self.grade.round().total_cmp(&other.grade.round()) == Ordering::Equal
184 && self.name.cmp(other.name) == Ordering::Equal
185 }
186 }
187
188 // Eq marks that equality is reflexive under the PartialEq impl.
189 // Cannot be derived because Eq is not implemented for floats.
190 impl Eq for Student {}
191
192 // Delegates ordering to Ord::cmp.
193 // NOTE: Though the code compiles, Rust does not like it when you derive
194 // PartialOrd when implementing Ord
195 impl PartialOrd for Student {
196 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
197 Some(self.cmp(other))
198 }
199 }
200
201 // First compares grades rounded to the nearest whole number,
202 // and then breaks ties by comparaing the names lexicographically.
203 impl Ord for Student {
204 fn cmp(&self, other: &Self) -> Ordering {
205 let grade_order = self.grade.round().total_cmp(&other.grade.round());
206 if grade_order == std::cmp::Ordering::Equal {
207 self.name.cmp(other.name)
208 } else {
209 grade_order
210 }
211 }
212 }
213}
214
215#[test]
216/// A partial order induces transitivity, reflexivity,
217/// and antisymmetry. In this non-strict partial order, only rounded
218/// grades get compared.
219fn partial_order() {
220 use partial_order::Student;
221 use std::cmp::Ordering;
222
223 // Partial order
224 let a = Student {
225 name: "Dichael",
226 grade: 78.8,
227 };
228 let b = Student {
229 name: "Ephraim",
230 grade: 78.8,
231 };
232 let c = Student {
233 name: "Froedrik",
234 grade: 78.8,
235 };
236 let d = Student {
237 name: "Dingus",
238 grade: 79.7,
239 };
240 let e = Student {
241 name: "Ephraim",
242 grade: 81.1,
243 };
244 let m = Student {
245 name: "Dingus",
246 grade: f32::NAN, // Incomplete/INC
247 };
248 let n = Student {
249 name: "Dangus",
250 grade: f32::NAN, // Incomplete/INC
251 };
252
253 // Transitivity
254 // if a <= b and b <= c, then a <= c
255 assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
256 assert_eq!(b.partial_cmp(&c), Some(Ordering::Equal));
257 assert_eq!(a.partial_cmp(&c), Some(Ordering::Equal));
258 // Mixed transitivity
259 // if a == b and b < c, then a < c
260 assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
261 assert_eq!(b.partial_cmp(&d), Some(Ordering::Less));
262 assert_eq!(a.partial_cmp(&d), Some(Ordering::Less));
263 // Directional chaining
264 assert_eq!(e.partial_cmp(&d), Some(Ordering::Greater));
265 assert_eq!(d.partial_cmp(&c), Some(Ordering::Greater));
266 assert_eq!(e.partial_cmp(&c), Some(Ordering::Greater));
267 assert_eq!(c.partial_cmp(&d), Some(Ordering::Less));
268 assert_eq!(d.partial_cmp(&e), Some(Ordering::Less));
269 assert_eq!(c.partial_cmp(&e), Some(Ordering::Less));
270
271 // Reflexivity
272 // a == a
273 assert_eq!(a.partial_cmp(&a), Some(Ordering::Equal));
274 assert_eq!(b.partial_cmp(&b), Some(Ordering::Equal));
275
276 assert!(a.eq(&a)); // a == a
277 assert!(a.eq(&b));
278 assert!(b.eq(&a));
279 assert!(n.eq(&n)); // NaN == NaN
280 assert!(m.eq(&m)); // NaN == NaN
281 assert!(!n.eq(&m)); // But it has to be the SAME NaN
282
283 // Equivalence
284 assert!(!a.eq(&m)); // 78.8 != NaN
285 // Indicates a lack of global reflexivity!
286 assert!(!n.eq(&m)); // NaN != NaN
287
288 // Antisymmetry
289 // if a <= b and b <= a, then a == b
290 assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
291 assert_eq!(b.partial_cmp(&a), Some(Ordering::Equal));
292 // Directionality check
293 assert_eq!(a.partial_cmp(&d), Some(Ordering::Less));
294 assert_eq!(d.partial_cmp(&a), Some(Ordering::Greater));
295
296 // Incomparability (lack of totality)
297 assert!(!a.eq(&m)); // Incomparability; 78.8 != NAN
298 assert!(!m.eq(&n)); // Incomparability; NAN != NAN
299 assert_eq!(a.partial_cmp(&m), None); // Incomparable!
300 assert_eq!(m.partial_cmp(&a), None); // Symmetry of incomparability
301 assert_eq!(m.partial_cmp(&n), None); // NAN != NAN
302}
303
304#[test]
305/// This tests a total preorder which induces transitivity,
306/// reflexivity, and comparability. In a total preorder, two elements
307/// can be roughly equivalent without strict equality. In this non-strict
308/// total preorder, two students can be equivalent if their scores fall
309/// into the same letter grade.
310fn tota_preorder() {
311 use std::cmp::Ordering;
312 use total_preorder::{Grade, Student};
313
314 let a = Student {
315 name: "Alonzo",
316 grade: 78.8, // C+
317 };
318 let b = Student {
319 name: "Brain",
320 grade: 79.4, // C+
321 };
322 let c = Student {
323 name: "Chamene",
324 grade: 79.1, // C+
325 };
326 let d = Student {
327 name: "Dichael",
328 grade: 81.8, // B-
329 };
330 let e = Student {
331 name: "Ephraim",
332 grade: 83.7, // B
333 };
334 let m = Student {
335 name: "Dingus",
336 grade: f32::NAN, // INC
337 };
338 let n = Student {
339 name: "Dangus",
340 grade: f32::NAN, // INC
341 };
342
343 // Preliminary equivalence class testing
344 assert_eq!(Grade::eq_class(a.grade), Grade::CPlus);
345 assert_eq!(Grade::eq_class(b.grade), Grade::CPlus);
346 assert_eq!(Grade::eq_class(c.grade), Grade::CPlus);
347 assert_eq!(Grade::eq_class(d.grade), Grade::BMinus);
348 assert_eq!(Grade::eq_class(e.grade), Grade::B);
349 assert_eq!(Grade::eq_class(m.grade), Grade::INC);
350 assert_eq!(Grade::eq_class(n.grade), Grade::INC);
351
352 // Illustrates discriminant values proving such
353 // assertions as Grade::CPlus < Grade::B
354 assert_eq!(Grade::INC as u8, 0);
355 assert_eq!(Grade::CPlus as u8, 7);
356 assert_eq!(Grade::A as u8, 12);
357 assert!(Grade::CPlus < Grade::B);
358
359 // Transitivity
360 // if a <= b and b <= c, then a <= c
361 //
362 // Because of the rounding logic, 78.8, 79.4, and 79.1 are all
363 // roughly equivalent, meaning that Alonzo, Brain, and Chamene
364 // all reqpresent roughly equivalent Students.
365 assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
366 assert_eq!(b.partial_cmp(&c), Some(Ordering::Equal));
367 assert_eq!(a.partial_cmp(&c), Some(Ordering::Equal));
368 // Mixed transitivity
369 // if a == b and b < c, then a < c
370 assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
371 assert_eq!(b.partial_cmp(&d), Some(Ordering::Less));
372 assert_eq!(a.partial_cmp(&d), Some(Ordering::Less));
373 // Directional chaining
374 assert_eq!(e.partial_cmp(&d), Some(Ordering::Greater));
375 assert_eq!(d.partial_cmp(&c), Some(Ordering::Greater));
376 assert_eq!(e.partial_cmp(&c), Some(Ordering::Greater));
377 assert_eq!(c.partial_cmp(&d), Some(Ordering::Less));
378 assert_eq!(d.partial_cmp(&e), Some(Ordering::Less));
379 assert_eq!(c.partial_cmp(&e), Some(Ordering::Less));
380
381 // Reflexivity
382 // a == a
383 assert_eq!(a.partial_cmp(&a), Some(Ordering::Equal));
384 assert_eq!(b.partial_cmp(&b), Some(Ordering::Equal));
385
386 // Totality/Comparability (with rough equivalence)
387 // For all elements a & b, a <=b || b <= a (even NaNs!)
388 assert!(a.eq(&b));
389 assert_eq!(a.cmp(&b), std::cmp::Ordering::Equal);
390 // Things that used to be incomparable, like NaN are now comparable
391 // because PartialEq explicitly casts NAN as i32
392 assert!(!a.eq(&m)); // 78.8 != NAN
393 assert!(m.eq(&n)); // NAN as i32 == NAN as i32
394 assert_eq!(a.partial_cmp(&m), Some(Ordering::Greater));
395 assert_eq!(m.partial_cmp(&a), Some(Ordering::Less));
396 assert_eq!(m.partial_cmp(&n), Some(Ordering::Equal)); // NaN is treated as equal to NaN
397
398 // In a total preorder, equivalence does NOT imply identity
399 assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
400 assert_ne!(a.name, b.name); // This proves it's a preorder, not a total order
401
402 // Approximate comparability testing by fuzzing "all pairs"
403 //use proptest::prelude::*;
404 //proptest! {
405 // #[test]
406 // fn all_pairs_are_comparable(a in any::<Student>(), b in any::<Student>()) {
407 // prop_assert!(a.partial_cmp(&b).is_some());
408 // prop_assert!(b.partial_cmp(&a).is_some());
409 // }
410 //}
411}
412
413#[test]
414/// A total order induces reflexivity, transitivity,
415/// antisymmetry, and comparability. In this non-strict total ordering,
416/// each student is first compared based on rounded score, and totality
417/// is induced via lexicographical name comparisons.
418///
419/// This test checks logic for both partial_cmp and cmp to illustrate that
420/// both PartialOrd and Ord traits match predicate implementations.
421fn total_order() {
422 use std::cmp::Ordering;
423 use total_order::Student;
424
425 let a = Student {
426 name: "Alonzo",
427 grade: 78.8,
428 };
429 let b = Student {
430 name: "Brain",
431 grade: 79.4,
432 };
433 let c = Student {
434 name: "Chamene",
435 grade: 79.1,
436 };
437 let d = Student {
438 name: "Dichael",
439 grade: 81.8,
440 };
441 let e = Student {
442 name: "Ephraim",
443 grade: 83.7,
444 };
445 let m = Student {
446 name: "Dingus",
447 grade: f32::NAN,
448 };
449 let n = Student {
450 name: "Dangus",
451 grade: f32::NAN,
452 };
453 let q = Student {
454 name: "Alonzo",
455 grade: 78.8,
456 };
457
458 // Transitivity
459 // if a <= b and b <= c, then a <= c
460 //
461 // Because of the rounding logic, 78.8, 79.4, and 79.1 are all
462 // roughly equivalent, but the totality adds a tie breaker
463 // by lexicographically comparing the name values.
464 assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
465 assert_eq!(b.partial_cmp(&c), Some(Ordering::Less));
466 assert_eq!(a.partial_cmp(&c), Some(Ordering::Less));
467
468 assert_eq!(a.cmp(&b), Ordering::Less);
469 assert_eq!(b.cmp(&c), Ordering::Less);
470 assert_eq!(a.cmp(&c), Ordering::Less);
471
472 // Mixed transitivity
473 // if a == b and b < c, then a < c
474 assert_eq!(a.partial_cmp(&q), Some(Ordering::Equal));
475 assert_eq!(q.partial_cmp(&d), Some(Ordering::Less));
476 assert_eq!(a.partial_cmp(&d), Some(Ordering::Less));
477
478 assert_eq!(a.cmp(&q), Ordering::Equal);
479 assert_eq!(q.cmp(&d), Ordering::Less);
480 assert_eq!(a.cmp(&d), Ordering::Less);
481
482 // Directional chaining
483 assert_eq!(e.partial_cmp(&d), Some(Ordering::Greater));
484 assert_eq!(d.partial_cmp(&c), Some(Ordering::Greater));
485 assert_eq!(e.partial_cmp(&c), Some(Ordering::Greater));
486 assert_eq!(c.partial_cmp(&d), Some(Ordering::Less));
487 assert_eq!(d.partial_cmp(&e), Some(Ordering::Less));
488 assert_eq!(c.partial_cmp(&e), Some(Ordering::Less));
489
490 assert_eq!(e.cmp(&d), Ordering::Greater);
491 assert_eq!(d.cmp(&c), Ordering::Greater);
492 assert_eq!(e.cmp(&c), Ordering::Greater);
493 assert_eq!(c.cmp(&d), Ordering::Less);
494 assert_eq!(d.cmp(&e), Ordering::Less);
495 assert_eq!(c.cmp(&e), Ordering::Less);
496
497 // Reflexivity
498 // a == a
499 assert_eq!(a.partial_cmp(&a), Some(Ordering::Equal));
500 assert_eq!(b.partial_cmp(&b), Some(Ordering::Equal));
501
502 assert_eq!(a.cmp(&a), Ordering::Equal);
503 assert_eq!(b.cmp(&b), Ordering::Equal);
504
505 // Antisymmetry
506 // if a <= q AND q <= a, then a MUST EQUAL q
507 // Antisymmetry is what separates this from a preorder
508 //let a_le_q = a.partial_cmp(&q).map_or(false, |o| o.is_eq() || o.is_le());
509 //let q_le_a = q.partial_cmp(&a).map_or(false, |o| o.is_eq() || o.is_le());
510 let a_q = a.partial_cmp(&q).is_some_and(|o| o.is_eq() || o.is_le()); // a <= q
511 let q_a = q.partial_cmp(&a).is_some_and(|o| o.is_eq() || o.is_le()); // q <= a
512
513 if a_q && q_a {
514 assert!(a.eq(&q));
515 assert_eq!(a.name, q.name);
516 assert_eq!(a.grade, q.grade);
517 }
518
519 // In a total order, equivalence implies identity
520 assert!(a.eq(&q));
521 assert_eq!(a.partial_cmp(&q), Some(Ordering::Equal));
522 assert_eq!(a.cmp(&q), Ordering::Equal);
523 assert_eq!(a.name, q.name);
524 assert_eq!(a.grade, q.grade);
525
526 // Totality/Comparability (with rough equivalence)
527 // For all elements a & b, a <=b || b <= a implying that there cannot
528 // be any None value for NaN comparisons
529 assert!(a.eq(&q));
530 assert_eq!(a.cmp(&b), std::cmp::Ordering::Less);
531 // Things that used to be incomparable, like NaN are now comparable
532 // because PartialEq explicitly casts NAN as i32, however the total order
533 // collapses the equivalence relation to an equality by lexicographically
534 // ordering otherwise equivalent Students by name
535 assert!(!m.eq(&n));
536 assert!(a.partial_cmp(&n).is_some()); // Even disparate types are comparable
537 assert_eq!(a.partial_cmp(&m), Some(Ordering::Less));
538 assert_eq!(m.partial_cmp(&a), Some(Ordering::Greater));
539 assert_eq!(m.partial_cmp(&n), Some(Ordering::Greater));
540
541 assert_eq!(a.cmp(&m), Ordering::Less);
542 assert_eq!(m.cmp(&a), Ordering::Greater);
543 assert_eq!(m.cmp(&n), Ordering::Greater);
544}
545
546// Requires total_cmp
547pub fn okie(a: f32, b: f32) -> std::cmp::Ordering {
548 // Illegal because NaN != NaN
549 //a.cmp(&b)
550
551 // Legal because total_cmp imposes a strict total order on floating point values
552 a.total_cmp(&b)
553}
554
555/// Compares values
556pub fn value_equality(a: &String, b: &String) -> bool {
557 a.cmp(b) == std::cmp::Ordering::Equal
558}
559/// Compares pointers
560pub fn ptr_equality(a: &String, b: &String) -> bool {
561 std::ptr::eq(a, b)
562}
563#[test]
564fn equal() {
565 let a = &String::from("Peter");
566 let b = &String::from("Peter");
567 //assert_eq!(equality(a, b), std::cmp::Ordering::Equal);
568 assert!(value_equality(a, b)); // Values are eqal
569
570 let a = &String::from("Peter");
571 let b = &String::from("Peter");
572 assert!(!ptr_equality(a, b)); // Pointers are NOT equal
573}
574
575pub fn sortt<T: Ord>(list: &mut [T]) {
576 list.sort()
577}
578#[test]
579fn sortable() {
580 let mut a = vec![2_i32, 3, 1];
581 sortt(&mut a);
582 assert_eq!(a, vec!(1, 2, 3));
583
584 let mut b = [3_u8, 2, 1];
585 sortt(&mut b);
586 assert_eq!(b, [1, 2, 3]);
587
588 let mut c = ["Peter", "Bobson", "Dangus"];
589 sortt(&mut c);
590 assert_eq!(c, ["Bobson", "Dangus", "Peter"]);
591}