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}