Skip to content

← Shape Your Data: Structs, Enums, Pattern Matching step 20 of 26

Medium Primitives

PartialOrd and Ord: derived ordering follows declaration order

Two functions, both graded.

pub fn sort_versions(versions: Vec<String>) -> Vec<String>
pub fn rank_priorities(items: Vec<(String, String)>) -> Vec<String>

sort_versions sorts semver-ish strings correctly: ["1.10.0", "1.9.0"] becomes ["1.9.0", "1.10.0"]. Missing or unparseable components count as 0, so "2", "2.0" and "2.0.0" all mean the same version — order equal versions by the original string so the result is deterministic.

rank_priorities takes (name, priority_label) pairs and returns the names ordered most urgent first: critical, high, normal, then anything else as low. Ties break by name.

You should not write a single comparison function. Both orderings come for free from #[derive(Ord)].

Derived ordering is lexicographic by declaration order

For a struct, the derived Ord compares the first field first; only if that ties does it look at the second, and so on:

#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Version { major: u32, minor: u32, patch: u32 }

That is semver, exactly, with no code. And it defeats the classic bug: string sorting puts "1.10.0" before "1.9.0" because '1' < '9'. Comparing (1, 10, 0) against (1, 9, 0) as numbers does the right thing.

For an enum, the derived Ord compares variant declaration order: earlier variants are less than later ones. So declaring

enum Priority { Critical, High, Normal, Low }

makes Critical < High < Normal < Low, and sorting ascending gives you the triage list. Payloads are compared only within the same variant.

Tuples derive the same way, which is why Vec<(Priority, String)>::sort() ranks by priority and then breaks ties by name with no work from you.

The landmine

Reordering fields or variants changes your program’s ordering, and nothing warns you. Move patch above major because it reads better, and every comparison in your codebase silently changes meaning. No compiler error, no clippy lint, no test failure unless you happened to have one.

This is the clearest small example of the principle from the derive item: derive is generated code, and you own what it generates. If the ordering matters, say so in a comment next to the declaration, or write the impl by hand.

The deny-by-default lint

derive_ord_xor_partial_ord is a correctness lint denied by default. It fires when you derive one of the pair and hand-write the other, in either direction:

error: you are deriving `Ord` but have implemented `PartialOrd` explicitly

The reason is that a.partial_cmp(b) must agree with Some(a.cmp(b)) — the standard library, sort, BTreeMap and everything else assumes it. Two independently written implementations will eventually disagree. The starter ships this exact mistake, with a partial_cmp that only compares major, so you can see both the lint and the wrong answers it predicts.

Its companion, non_canonical_partial_ord_impl, says the same thing more concretely: if a type is Ord, its partial_cmp should be exactly Some(self.cmp(other)) and nothing else.

Deriving Ord also requires Eq and PartialOrd. #[derive(PartialEq, Ord)] on its own gives you E0277: the trait bound Version: Eq is not satisfied.

An honest note about broken comparators

Since Rust 1.81 the sort implementations may detect that a comparator does not define a total order and panic with “user-provided comparison function does not correctly implement a total order”. The docs say may, and they mean it. Verified on 1.95: a comparator that always returns Less does not panic at n = 20, 40, 100, 1 000 or 10 000 — it returns quietly with garbage order — while a pseudo-random comparator does panic from around n = 40 upward. Detection is best-effort.

The docs also warn that “even if the function exits normally, the resulting order of elements in the slice is unspecified.” So: never write a test that depends on the panic firing, and never assume a quiet sort means a correct comparator. When the panic does fire it is an ordinary unwinding panic, which catch_unwind will catch — it is not an abort.

Remember the grade is compile + tests + clippy -D warnings.