Loading learning content...
Library sort functions provide sensible defaults—numbers sort numerically, strings sort lexicographically. But real-world applications demand far more sophisticated ordering:
These requirements are where custom comparison functions (also called comparators, compare functions, or ordering predicates) become essential. They transform generic sort algorithms into precisely tailored ordering systems.
By the end of this page, you will master comparison function design across multiple languages. You'll understand the mathematical properties that make comparisons valid, avoid subtle bugs that break sort stability and correctness, and implement complex multi-criteria sorting with confidence.
Every library sort function assumes your comparison function obeys specific mathematical rules. Violating these rules doesn't just produce wrong results—it can cause infinite loops, crashes, or unpredictable behavior.
The Three Essential Properties:
A valid comparison function must be a strict weak ordering, which requires three properties:
compare(a, a) must indicate equality (return 0, or return false for less-than predicates). No element comes before itself.compare(a, b) < 0, then compare(b, a) > 0. Ordering is one-way.compare(a, b) < 0 and compare(b, c) < 0, then compare(a, c) < 0. Ordering chains must be consistent.A fourth property for equivalence:
Transitivity of Equivalence — If a is equivalent to b, and b is equivalent to c, then a must be equivalent to c. This ensures that "equal" elements form consistent groups.
Why These Rules Matter:
Sorting algorithms rely on these properties to make decisions about where to place elements. If compare(a, b) < 0 but compare(b, a) < 0 (violating asymmetry), the algorithm might endlessly swap them. If compare(a, b) < 0 and compare(b, c) < 0 but compare(a, c) >= 0 (violating transitivity), the final ordering is undefined.
The most common comparison bug is returning 0 (equal) for unequal elements when it shouldn't, or using non-deterministic comparisons (e.g., based on random values or mutable state). These violate transitivity and can cause subtle, intermittent bugs that are extremely hard to debug.
1234567891011121314151617181920212223242526272829303132333435363738
// ✅ VALID: Simple numeric comparisonconst valid1 = (a, b) => a - b; // Obeys all three properties // ✅ VALID: String comparisonconst valid2 = (a, b) => a.localeCompare(b); // Built-in is correct // ❌ INVALID: Non-transitive comparison// This compares by the first digit onlyconst invalid1 = (a, b) => { const firstA = String(a)[0]; const firstB = String(b)[0]; return firstA.localeCompare(firstB);};// Problem: 19 and 12 are "equal" (both start with 1)// 12 and 25 are "equal"? No, 1 < 2, so 12 < 25// But 19 and 25? 1 < 2, so 19 < 25// This seems okay... let's try another example:// 15, 19, 12 — by this logic 15=19=12 (all start with 1)// But we could end up with [19, 12, 15] or any permutation// Actually this breaks because it's not a strict ordering at all // ❌ INVALID: Random comparison (non-deterministic)const invalid2 = (a, b) => Math.random() - 0.5; // DON'T DO THIS!// Violates asymmetry: same pair might return different results on each call // ❌ INVALID: Asymmetry violationconst invalid3 = (a, b) => a.priority - b.priority || -1;// When priorities are equal, always returns -1 (a < b)// But then compare(b, a) also returns -1 (b < a)!// Violates asymmetry: both a < b and b < a can't be true // ✅ VALID: Correct multi-field comparisonconst valid3 = (a, b) => { if (a.priority !== b.priority) { return a.priority - b.priority; } return a.name.localeCompare(b.name);};Different languages use different conventions for comparison functions. Understanding these conventions is crucial for writing correct comparators.
| Language | Signature | Return Values | Notes |
|---|---|---|---|
| Python | key=func or cmp_to_key | Key: any comparable value Cmp: -1, 0, +1 | Modern Python prefers key functions; cmp_to_key for legacy |
| JavaScript | (a, b) => number | Negative: a first 0: equal Positive: b first | Return actual difference for numbers, not just -1/0/1 |
| Java | Comparator<T> interface | -1, 0, +1 (by convention) Any negative/positive works | Use Comparator.comparing() for modern approach |
| C++ | bool(a, b) predicate | true: a < b false: a >= b | Strict less-than semantics; NOT a three-way comparison |
| Go | func(i, j int) bool | true: element[i] < element[j] false: otherwise | Operates on indices, not values directly |
| Rust | Fn(&T, &T) -> Ordering | Less, Equal, Greater | Enum provides type safety; can also use key functions |
C++'s comparison function returns bool, not int. It answers "is a strictly less than b?" not "how does a compare to b?". Returning true when a equals b violates strict weak ordering and causes undefined behavior. Always use a < b, never a <= b.
Python takes a unique approach among mainstream languages: instead of comparison functions, it primarily uses key functions. A key function transforms each element into a value that Python's default comparison can handle.
Why Key Functions?
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
from functools import cmp_to_keyfrom operator import attrgetter, itemgetter # Example 1: Sort strings by lengthwords = ["apple", "pie", "banana", "strawberry"]sorted_by_length = sorted(words, key=len)print(sorted_by_length) # ['pie', 'apple', 'banana', 'strawberry'] # Example 2: Sort case-insensitivelywords = ["Banana", "apple", "Cherry"]sorted_case_insensitive = sorted(words, key=str.lower)print(sorted_case_insensitive) # ['apple', 'Banana', 'Cherry'] # Example 3: Sort by absolute valuenumbers = [-5, 2, -1, 7, -3]sorted_by_abs = sorted(numbers, key=abs)print(sorted_by_abs) # [-1, 2, -3, -5, 7] # Example 4: Sort dictionaries by a specific keyproducts = [ {"name": "Widget", "price": 25.99, "stock": 100}, {"name": "Gadget", "price": 15.99, "stock": 50}, {"name": "Doohickey", "price": 25.99, "stock": 75},] # By priceby_price = sorted(products, key=lambda x: x["price"])print([p["name"] for p in by_price]) # ['Gadget', 'Widget', 'Doohickey'] # By price, then by stock (descending) — use tuples!by_price_then_stock = sorted(products, key=lambda x: (x["price"], -x["stock"]))print([p["name"] for p in by_price_then_stock]) # ['Gadget', 'Doohickey', 'Widget']# Doohickey comes before Widget: same price, but 75 > 50 (negated comparison) # Example 5: Sort objects by attributeclass Person: def __init__(self, name, age, salary): self.name = name self.age = age self.salary = salary def __repr__(self): return f"{self.name}({self.age})" people = [ Person("Alice", 30, 70000), Person("Bob", 25, 50000), Person("Charlie", 30, 60000),] # Using lambdaby_age = sorted(people, key=lambda p: p.age)print(by_age) # [Bob(25), Alice(30), Charlie(30)] # Using attrgetter (faster for frequently-used sorts)by_age_v2 = sorted(people, key=attrgetter("age"))print(by_age_v2) # [Bob(25), Alice(30), Charlie(30)] # Multiple attributesby_age_then_salary = sorted(people, key=attrgetter("age", "salary"))print(by_age_then_salary) # [Bob(25), Charlie(30), Alice(30)]# Charlie before Alice: same age, but 60000 < 70000 # Example 6: Descending orderby_age_desc = sorted(people, key=lambda p: p.age, reverse=True)print(by_age_desc) # [Alice(30), Charlie(30), Bob(25)] # Example 7: Complex key with tuple (ascending age, descending salary)by_age_asc_salary_desc = sorted(people, key=lambda p: (p.age, -p.salary))print(by_age_asc_salary_desc) # [Bob(25), Alice(30), Charlie(30)]# Alice before Charlie: same age, but 70000 > 60000 (negated gives Alice priority) # Example 8: itemgetter for list-of-lists or list-of-tuplesdata = [("apple", 3, 150), ("banana", 1, 100), ("cherry", 2, 200)]by_second = sorted(data, key=itemgetter(1))print(by_second) # [('banana', 1, 100), ('cherry', 2, 200), ('apple', 3, 150)] # Example 9: Converting legacy comparison functiondef legacy_compare(a, b): """Old-style comparison function""" if a.age != b.age: return a.age - b.age return a.salary - b.salary by_legacy = sorted(people, key=cmp_to_key(legacy_compare))print(by_legacy) # Works, but prefer key functions for new code # Example 10: Sorting with None valuesitems = [3, None, 1, None, 2]# Put None values at the endsorted_none_last = sorted(items, key=lambda x: (x is None, x if x is not None else 0))print(sorted_none_last) # [1, 2, 3, None, None]Python compares tuples lexicographically: first by the first element, then by second if first elements are equal, and so on. This means key=lambda x: (x.age, x.name) sorts by age first, then by name. Use -value within tuples to reverse individual criteria (works for numbers only).
Performance Consideration: Key Caching
Python's sort implementation caches key values, computing each key only once. This means:
# This key function is called O(n) times, not O(n log n) times
sorted(data, key=expensive_computation)
For expensive key computations (database lookups, complex calculations), Python's key-based approach is more efficient than comparison-based approaches in other languages, where the comparison function is called O(n log n) times.
Java provides the Comparator<T> interface for custom orderings. Modern Java (8+) offers powerful static factory methods that make comparator creation fluent and readable.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
import java.util.*;import java.util.Comparator;import static java.util.Comparator.*; public class ComparatorExamples { public static void main(String[] args) { List<Person> people = Arrays.asList( new Person("Alice", 30, 70000.0), new Person("Bob", 25, 50000.0), new Person("Charlie", 30, 60000.0), new Person("Diana", 25, 55000.0) ); // ===== TRADITIONAL APPROACH (Pre-Java 8) ===== // Anonymous class comparator Comparator<Person> byAgeOld = new Comparator<Person>() { @Override public int compare(Person a, Person b) { return Integer.compare(a.getAge(), b.getAge()); } }; // ===== MODERN APPROACH (Java 8+) ===== // Example 1: Lambda expression Comparator<Person> byAgeLambda = (a, b) -> Integer.compare(a.getAge(), b.getAge()); // Example 2: Method reference with Comparator.comparing (PREFERRED) Comparator<Person> byAge = comparing(Person::getAge); // Example 3: Chained comparators (multi-field sorting) Comparator<Person> byAgeThenSalary = comparing(Person::getAge) .thenComparing(Person::getSalary); // Example 4: Reversed ordering Comparator<Person> byAgeDescending = comparing(Person::getAge).reversed(); // Example 5: Mixed ascending/descending Comparator<Person> byAgeDescSalaryAsc = comparing(Person::getAge).reversed() .thenComparing(Person::getSalary); // Example 6: Comparing with Comparator for the extracted key // Sort by name length, then by name itself Comparator<Person> byNameLength = comparing( Person::getName, comparingInt(String::length) ); // Example 7: Handling nulls List<Person> peopleWithNulls = new ArrayList<>(people); peopleWithNulls.add(null); // Add a null person Comparator<Person> nullsFirst = nullsFirst(comparing(Person::getAge)); Comparator<Person> nullsLast = nullsLast(comparing(Person::getAge)); // Example 8: Handling null fields // If salary could be null Comparator<Person> bySalaryNullsFirst = comparing( Person::getSalary, nullsFirst(naturalOrder()) ); // Example 9: Using it with sort List<Person> sorted = new ArrayList<>(people); sorted.sort(comparing(Person::getAge).thenComparing(Person::getName)); // Example 10: Case-insensitive string comparison List<String> words = Arrays.asList("Banana", "apple", "Cherry"); words.sort(String.CASE_INSENSITIVE_ORDER); // Result: [apple, Banana, Cherry] // Example 11: Complex multi-criteria (demonstration) // Sort by: in-stock first, then by price ascending, then by name Comparator<Product> productSort = comparing(Product::isInStock).reversed() .thenComparing(Product::getPrice) .thenComparing(Product::getName); // Example 12: Building comparator conditionally boolean sortDescending = true; Comparator<Person> dynamicSort = comparing(Person::getAge); if (sortDescending) { dynamicSort = dynamicSort.reversed(); } // Example 13: Using comparingInt/Long/Double for primitives (avoids boxing) Comparator<Person> byAgeOptimized = comparingInt(Person::getAge); Comparator<Person> bySalaryOptimized = comparingDouble(Person::getSalary); System.out.println("Original: " + people); people.sort(byAgeThenSalary); System.out.println("Sorted: " + people); }} class Person { private String name; private int age; private double salary; Person(String name, int age, double salary) { this.name = name; this.age = age; this.salary = salary; } String getName() { return name; } int getAge() { return age; } double getSalary() { return salary; } @Override public String toString() { return name + "(" + age + ")"; }} class Product { private String name; private double price; private boolean inStock; String getName() { return name; } double getPrice() { return price; } boolean isInStock() { return inStock; }}For primitive fields, use comparingInt(), comparingLong(), or comparingDouble() instead of comparing(). These avoid the overhead of auto-boxing primitives into wrapper objects, which matters for performance-critical sorting of large collections.
The Comparable Interface vs Comparator:
| Aspect | Comparable<T> | Comparator<T> |
|---|---|---|
| Method | compareTo(T other) | compare(T a, T b) |
| Location | Inside the class being compared | External to the class |
| Use case | Natural ordering | Custom/alternative orderings |
| Example | String.compareTo() | String.CASE_INSENSITIVE_ORDER |
| Flexibility | One ordering per class | Unlimited comparators |
Best Practice: Implement Comparable for the natural/default ordering of a class. Create Comparator instances for alternative orderings.
C++ uses a fundamentally different comparison model: instead of returning -1/0/+1, comparators return a boolean indicating whether the first argument should come before the second.
The Strict Weak Ordering Requirement:
Your predicate must implement strict less-than semantics:
comp(a, a) must return false (irreflexivity)comp(a, b) is true, then comp(b, a) must be false (asymmetry)Critical: Never use <= in a comparison predicate. The standard requires strict inequality (<). Using <= violates irreflexivity since comp(a, a) would return true.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
#include <algorithm>#include <vector>#include <string>#include <functional>#include <iostream> struct Person { std::string name; int age; double salary;}; int main() { std::vector<Person> people = { {"Alice", 30, 70000.0}, {"Bob", 25, 50000.0}, {"Charlie", 30, 60000.0}, {"Diana", 25, 55000.0} }; // ===== LAMBDA COMPARATORS ===== // Example 1: Sort by age (ascending) std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) { return a.age < b.age; // Note: <, not <= }); // Example 2: Sort by age descending std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) { return a.age > b.age; // Flip the comparison for descending }); // Example 3: Multi-field sorting std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) { if (a.age != b.age) return a.age < b.age; if (a.salary != b.salary) return a.salary < b.salary; return a.name < b.name; }); // Example 4: Using std::tie for cleaner multi-field comparison std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) { return std::tie(a.age, a.salary, a.name) < std::tie(b.age, b.salary, b.name); }); // Example 5: Mixed ascending/descending with tie // Age ascending, salary descending std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) { if (a.age != b.age) return a.age < b.age; return a.salary > b.salary; // Note: > for descending }); // ===== FUNCTOR COMPARATORS ===== // Example 6: Reusable functor class struct CompareByAge { bool operator()(const Person& a, const Person& b) const { return a.age < b.age; } }; std::sort(people.begin(), people.end(), CompareByAge{}); // Example 7: Configurable functor struct CompareByField { enum class Field { AGE, SALARY, NAME }; Field field; bool descending; CompareByField(Field f, bool desc = false) : field(f), descending(desc) {} bool operator()(const Person& a, const Person& b) const { bool result; switch (field) { case Field::AGE: result = a.age < b.age; break; case Field::SALARY: result = a.salary < b.salary; break; case Field::NAME: result = a.name < b.name; break; } return descending ? !result && !(a.age == b.age) : result; // Note: This descending logic is simplified; proper implementation // needs more careful handling for equality cases } }; // ===== STANDARD LIBRARY UTILITIES ===== // Example 8: std::greater for descending order std::vector<int> numbers = {3, 1, 4, 1, 5, 9, 2, 6}; std::sort(numbers.begin(), numbers.end(), std::greater<int>()); // Result: {9, 6, 5, 4, 3, 2, 1, 1} // Example 9: Using std::greater<> (transparent comparator, C++14) std::sort(numbers.begin(), numbers.end(), std::greater<>()); // Example 10: Stable sort with custom comparator std::stable_sort(people.begin(), people.end(), [](const Person& a, const Person& b) { return a.age < b.age; }); // ===== C++20 THREE-WAY COMPARISON ===== // In C++20, the spaceship operator <=> can simplify comparisons // Person would define: auto operator<=>(const Person&) const = default; // Example 11: Partial sort (get k smallest elements sorted) std::partial_sort(people.begin(), people.begin() + 2, people.end(), [](const Person& a, const Person& b) { return a.salary < b.salary; }); // First 2 elements are the lowest-salary people, sorted // Example 12: nth_element (find median without full sort) std::nth_element(people.begin(), people.begin() + people.size()/2, people.end(), [](const Person& a, const Person& b) { return a.age < b.age; }); // Element at middle position is the median by age return 0;}Using <= instead of < causes undefined behavior. The sort algorithm may infinite loop, crash, or corrupt memory. This is because comp(a, a) returning true violates the strict weak ordering contract that the algorithm depends on.
C++20 Three-Way Comparison (Spaceship Operator):
C++20 introduced <=> (the "spaceship operator") which returns a comparison category:
auto result = a <=> b;
// result is std::strong_ordering::less, equal, or greater
// or std::weak_ordering, std::partial_ordering
This enables compiler-generated comparison operators:
struct Person {
std::string name;
int age;
auto operator<=>(const Person&) const = default;
// Automatically generates <, <=, >, >=, ==, !=
};
The default comparison compares fields in declaration order (name first, then age).
JavaScript's comparison functions return a number indicating the relative ordering. While the specification only requires negative/zero/positive, in practice you'll see patterns that return -1/0/+1 or actual differences.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
// ===== BASIC PATTERNS ===== // Numeric ascendingconst ascNum = (a, b) => a - b; // Numeric descendingconst descNum = (a, b) => b - a; // String ascending (locale-aware)const ascStr = (a, b) => a.localeCompare(b); // String descendingconst descStr = (a, b) => b.localeCompare(a); // Boolean: true first, then falseconst boolTrueFirst = (a, b) => b - a; // true (1) - false (0) = positive, so false comes "after" // Boolean: false first, then true const boolFalseFirst = (a, b) => a - b; // ===== OBJECT PROPERTY SORTING ===== const products = [ { name: "Widget", price: 25, inStock: true, rating: 4.5 }, { name: "Gadget", price: 15, inStock: false, rating: 4.8 }, { name: "Doohickey", price: 25, inStock: true, rating: 3.9 }, { name: "Thingamajig", price: 10, inStock: false, rating: 4.5 }]; // Sort by single propertyconst byPrice = [...products].sort((a, b) => a.price - b.price); // Sort by property descendingconst byPriceDesc = [...products].sort((a, b) => b.price - a.price); // Multi-property: price ascending, then rating descendingconst byPriceThenRating = [...products].sort((a, b) => { if (a.price !== b.price) return a.price - b.price; return b.rating - a.rating; // Descending for rating}); // Multi-property: in-stock first, then price, then nameconst byStockPriceName = [...products].sort((a, b) => { // in-stock first (true < false in our desired order) if (a.inStock !== b.inStock) return a.inStock ? -1 : 1; if (a.price !== b.price) return a.price - b.price; return a.name.localeCompare(b.name);}); // ===== REUSABLE COMPARATOR BUILDERS ===== // Factory for property-based comparisonconst compareBy = (key, descending = false) => (a, b) => { const aVal = typeof key === 'function' ? key(a) : a[key]; const bVal = typeof key === 'function' ? key(b) : b[key]; let result; if (typeof aVal === 'string') { result = aVal.localeCompare(bVal); } else { result = aVal - bVal; } return descending ? -result : result;}; // Usage:const byRating = [...products].sort(compareBy('rating'));const byRatingDesc = [...products].sort(compareBy('rating', true));const byNameLength = [...products].sort(compareBy(p => p.name.length)); // Chain multiple comparatorsconst chainComparators = (...comparators) => (a, b) => { for (const compare of comparators) { const result = compare(a, b); if (result !== 0) return result; } return 0;}; // Usage:const complexSort = chainComparators( compareBy('price'), compareBy('rating', true), compareBy('name'));const sorted = [...products].sort(complexSort); // ===== HANDLING SPECIAL VALUES ===== // Handling nulls/undefined (nulls last)const compareWithNulls = (a, b) => { if (a == null && b == null) return 0; if (a == null) return 1; // null goes to end if (b == null) return -1; return a - b;}; // Handling NaN in numeric comparisonsconst compareNumbers = (a, b) => { const aIsNaN = Number.isNaN(a); const bIsNaN = Number.isNaN(b); if (aIsNaN && bIsNaN) return 0; if (aIsNaN) return 1; // NaN goes to end if (bIsNaN) return -1; return a - b;}; // ===== DATE SORTING ===== const events = [ { name: "Conference", date: new Date("2024-06-15") }, { name: "Meeting", date: new Date("2024-03-01") }, { name: "Workshop", date: new Date("2024-06-15") },]; // Sort by date (chronological)const byDate = [...events].sort((a, b) => a.date - b.date);// Dates can be subtracted to get milliseconds difference // Sort by date, then by name for same datesconst byDateThenName = [...events].sort((a, b) => { const dateCompare = a.date - b.date; if (dateCompare !== 0) return dateCompare; return a.name.localeCompare(b.name);}); // ===== COMPLEX SORTING: PRIORITY QUEUE STYLE ===== const tasks = [ { title: "Bug fix", priority: "high", dueDate: new Date("2024-03-15"), completed: false }, { title: "Feature", priority: "medium", dueDate: new Date("2024-03-01"), completed: false }, { title: "Docs", priority: "low", dueDate: new Date("2024-03-10"), completed: true }, { title: "Review", priority: "high", dueDate: new Date("2024-03-05"), completed: false },]; const priorityOrder = { high: 0, medium: 1, low: 2 }; // Sort: incomplete first, then by priority, then by due dateconst taskSort = [...tasks].sort((a, b) => { // Completed tasks go to the end if (a.completed !== b.completed) return a.completed ? 1 : -1; // Among incomplete: sort by priority (high first) const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority]; if (priorityDiff !== 0) return priorityDiff; // Same priority: earlier due date first return a.dueDate - b.dueDate;});Your comparison function must be consistent: if you compare the same pair of elements multiple times during a single sort, it must always return the same result. Avoid comparisons that depend on external mutable state, random values, or the current time.
Comparison functions are deceptively easy to get wrong. Here are the most common pitfalls and how to avoid them:
a - b for comparison can overflow for large integers. For example, INT_MIN - 1 wraps around. Use explicit comparisons (a < b ? -1 : a > b ? 1 : 0) for safety.a.x - b.x might produce NaN. Handle missing values explicitly..toLowerCase() or .localeCompare() for expected results..localeCompare() with explicit locale for consistency.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
// ❌ BUG: Integer overflow (in languages with fixed-size integers)const badCompare = (a, b) => a - b;// In JavaScript: Number.MAX_SAFE_INTEGER - (-Number.MAX_SAFE_INTEGER) = Infinity// In Java/C++: INT_MAX - INT_MIN = overflow! // ✅ FIX: Use explicit comparisonsconst safeCompare = (a, b) => { if (a < b) return -1; if (a > b) return 1; return 0;}; // ❌ BUG: NaN breaks transitivityconst data = [3, NaN, 1, NaN, 2];data.sort((a, b) => a - b); // Result is unpredictable because NaN comparisons are always false // ✅ FIX: Handle NaN explicitlyconst nanSafeCompare = (a, b) => { const aIsNaN = Number.isNaN(a); const bIsNaN = Number.isNaN(b); if (aIsNaN && bIsNaN) return 0; if (aIsNaN) return 1; // NaN goes to end if (bIsNaN) return -1; return a - b;}; // ❌ BUG: Missing property causes NaNconst items = [ { name: "A", price: 10 }, { name: "B" }, // No price! { name: "C", price: 5 }];items.sort((a, b) => a.price - b.price);// undefined - 10 = NaN, breaks comparison // ✅ FIX: Handle missing propertiesconst safePropertyCompare = (a, b) => { const aPrice = a.price ?? Infinity; // Missing = expensive = end const bPrice = b.price ?? Infinity; return aPrice - bPrice;}; // ❌ BUG: Case-sensitivity surpriseconst words = ['banana', 'Apple', 'cherry'];words.sort(); // ['Apple', 'banana', 'cherry']// 'A' (65) comes before 'b' (98) in ASCII // ✅ FIX: Case-insensitive comparisonwords.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));// ['Apple', 'banana', 'cherry'] — but respecting original case // ❌ BUG: Returning non-consistent valueslet counter = 0;const badCompareStateful = (a, b) => { counter++; if (counter % 2 === 0) return a - b; return b - a; // Flip every other comparison!}; // ✅ FIX: Always be deterministicconst goodCompare = (a, b) => a - b; // Same result for same inputs, always // ❌ BUG: Forgetting to handle equality in multi-fieldconst badMulti = (a, b) => { if (a.priority < b.priority) return -1; if (a.priority > b.priority) return 1; // Forgot to compare by next field when priorities are equal!}; // ✅ FIX: Chain all comparison fieldsconst goodMulti = (a, b) => { if (a.priority !== b.priority) return a.priority - b.priority; if (a.date !== b.date) return a.date - b.date; return a.name.localeCompare(b.name);};When debugging comparison issues, temporarily add logging to see what's being compared: console.log(a, b, result). Look for pairs where the result seems wrong, then trace backward to find the logic error. Also verify your function handles self-comparison correctly: compare(x, x) should return 0.
Custom comparison functions unlock the full power of library sorting. Let's consolidate the key principles:
compareBy() and chainComparators() make complex sorting maintainable.| Language | a Comes First | Equal | b Comes First |
|---|---|---|---|
| Python (cmp) | -1 (or negative) | 0 | +1 (or positive) |
| JavaScript | Negative number | 0 | Positive number |
| Java | -1 (or negative) | 0 | +1 (or positive) |
| C++ | true | false | false |
| Go | true | false | false |
| Rust | Ordering::Less | Ordering::Equal | Ordering::Greater |
You now understand how to write correct, efficient custom comparison functions across multiple programming languages. In the next page, we'll explore stability guarantees in library implementations—what they mean, when they matter, and how they affect real-world sorting scenarios.