ExamplesBy LevelBy TopicLearning Paths
915 Intermediate

915-iterator-min-by-max-by — Custom Comparison min_by / max_by

Functional Programming

Tutorial

The Problem

Standard min() and max() require Ord — total ordering — which excludes f64 (NaN breaks total order) and types requiring custom comparison semantics. min_by(cmp) and max_by(cmp) accept an explicit Fn(&A, &A) -> Ordering comparator, enabling: comparing floats with partial_cmp().unwrap_or(Equal), multi-key comparison with .then_with(|| a.secondary.cmp(&b.secondary)), reverse ordering by swapping arguments, and domain-specific orderings like "closest to zero." This is the Rust equivalent of OCaml's List.min_elt ~compare and Python's min(key=...).

🎯 Learning Outcomes

  • • Use .min_by(cmp) and .max_by(cmp) with a custom Ordering comparator
  • • Handle f64 comparison using partial_cmp().unwrap_or(Ordering::Equal)
  • • Implement multi-key comparison using .then_with()
  • • Achieve reverse ordering by swapping comparator arguments
  • • Compare with OCaml's List.min_elt ~compare and sort comparators
  • Code Example

    #![allow(clippy::all)]
    //! 276. Custom comparison min_by() and max_by()
    //!
    //! `min_by(cmp)` and `max_by(cmp)` take a `Fn(&A, &A) -> Ordering` comparator.
    
    use std::cmp::Ordering;
    
    #[cfg(test)]
    mod tests {
        use std::cmp::Ordering;
    
        #[test]
        fn test_min_by_float() {
            let floats = [3.0f64, 1.0, 2.0];
            let min = floats
                .iter()
                .copied()
                .min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
            assert_eq!(min, Some(1.0));
        }
    
        #[test]
        fn test_max_by_reversed() {
            let nums = [1i32, 5, 3, 2, 4];
            let max = nums.iter().min_by(|a, b| b.cmp(a));
            assert_eq!(max, Some(&5));
        }
    
        #[test]
        fn test_min_by_multi_key() {
            let words = ["bb", "aa", "c"];
            let min = words
                .iter()
                .min_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)));
            assert_eq!(min, Some(&"c"));
        }
    }

    Key Differences

  • Typed Ordering: Rust Ordering is an enum (Less, Equal, Greater); OCaml's comparator returns int (negative, zero, positive).
  • f64 safety: Rust explicitly requires handling NaN via partial_cmp().unwrap_or(); OCaml's Float.compare defines NaN ordering (NaN < everything).
  • Composition: Rust .then_with() on Ordering makes multi-key comparison ergonomic; OCaml chains if c <> 0 then c else ....
  • Standard library: Both min_by and max_by are standard in Rust; OCaml's standard library requires Base or manual implementation.
  • OCaml Approach

    List.min_elt ~compare: ('a -> 'a -> int) -> 'a list -> 'a option (Base library). Standard OCaml requires manual fold_left: List.fold_left (fun acc x -> if compare x acc <= 0 then x else acc). Multi-key: let cmp a b = let c = String.length a - String.length b in if c <> 0 then c else String.compare a b. Float comparison: Float.compare handles NaN consistently. OCaml's compare returns int, Rust's Ordering is a typed enum — both provide equivalent expressiveness.

    Full Source

    #![allow(clippy::all)]
    //! 276. Custom comparison min_by() and max_by()
    //!
    //! `min_by(cmp)` and `max_by(cmp)` take a `Fn(&A, &A) -> Ordering` comparator.
    
    use std::cmp::Ordering;
    
    #[cfg(test)]
    mod tests {
        use std::cmp::Ordering;
    
        #[test]
        fn test_min_by_float() {
            let floats = [3.0f64, 1.0, 2.0];
            let min = floats
                .iter()
                .copied()
                .min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
            assert_eq!(min, Some(1.0));
        }
    
        #[test]
        fn test_max_by_reversed() {
            let nums = [1i32, 5, 3, 2, 4];
            let max = nums.iter().min_by(|a, b| b.cmp(a));
            assert_eq!(max, Some(&5));
        }
    
        #[test]
        fn test_min_by_multi_key() {
            let words = ["bb", "aa", "c"];
            let min = words
                .iter()
                .min_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)));
            assert_eq!(min, Some(&"c"));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use std::cmp::Ordering;
    
        #[test]
        fn test_min_by_float() {
            let floats = [3.0f64, 1.0, 2.0];
            let min = floats
                .iter()
                .copied()
                .min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
            assert_eq!(min, Some(1.0));
        }
    
        #[test]
        fn test_max_by_reversed() {
            let nums = [1i32, 5, 3, 2, 4];
            let max = nums.iter().min_by(|a, b| b.cmp(a));
            assert_eq!(max, Some(&5));
        }
    
        #[test]
        fn test_min_by_multi_key() {
            let words = ["bb", "aa", "c"];
            let min = words
                .iter()
                .min_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)));
            assert_eq!(min, Some(&"c"));
        }
    }

    Exercises

  • Find the string that comes last lexicographically (max by reverse alphabetical order) using max_by(|a, b| b.cmp(a)).
  • Implement closest_to_zero(data: &[f64]) -> Option<f64> using min_by(|a, b| a.abs().partial_cmp(&b.abs()).unwrap()).
  • Write a comparator for struct Event { timestamp: u64, priority: u8 } that sorts by priority descending, then timestamp ascending, using .then_with().
  • Open Source Repos