Card 03/ 08

ExampleDifficulty: Intermediate5 min

Sorting Orders by Date, Then by Customer

Six orders sorted twice: a stable second sort keeps each customer's orders by date, and a composite key does it in one pass.

Sorting by two keys with two sorts works only when the second sort is stable, meaning items with equal keys keep the order they went in. This example shows that idea on six shop orders, which a report must list by customer and, within each customer, by date.

The orders are 1001 from Chen on 2 March, 1002 from Abara on 1 March, 1003 from Chen on 1 March, 1004 from Bose on 2 March, 1005 from Abara on 2 March and 1006 from Bose on 1 March. Sorting them by customer alone groups them, but leaves each group in whatever order the orders arrived: a run printed Bose's 2 March order before the 1 March one. The fix is to sort by the minor key, the date, first, and then by the major key, the customer, with a stable sort.

The six orders after each sort; the second sort is stable, so each customer's orders stay in date order
PositionAs placedAfter sorting by dateAfter then sorting by customer
11001 Chen, 2 Mar1002 Abara, 1 Mar1002 Abara, 1 Mar
21002 Abara, 1 Mar1003 Chen, 1 Mar1005 Abara, 2 Mar
31003 Chen, 1 Mar1006 Bose, 1 Mar1006 Bose, 1 Mar
41004 Bose, 2 Mar1001 Chen, 2 Mar1004 Bose, 2 Mar
51005 Abara, 2 Mar1004 Bose, 2 Mar1003 Chen, 1 Mar
61006 Bose, 1 Mar1005 Abara, 2 Mar1001 Chen, 2 Mar

The second sort compares customers only, so Bose's two orders tie, and a stable sort keeps them in the order the first sort left them: 1006 on 1 March, then 1004 on 2 March. An unstable sort could put them either way round, and the date order the first sort built would be lost for that customer.

The listings below do the same in each language with the built-in sort, which is stable for objects in all three, and then get the same result in a single sort with a composite key: a key made of the customer and then the date, compared customer first.

Sorting six orders by date, then stably by customer, and by a composite key

orders_code.pypy
from operator import attrgetter
from typing import NamedTuple


class Order(NamedTuple):
    number: int
    customer: str
    date: str


def show(orders: list[Order]) -> str:
    return " ".join(f"{o.number}:{o.customer}:{o.date[-2:]}" for o in orders)


orders = [
    Order(1001, "Chen", "2026-03-02"),
    Order(1002, "Abara", "2026-03-01"),
    Order(1003, "Chen", "2026-03-01"),
    Order(1004, "Bose", "2026-03-02"),
    Order(1005, "Abara", "2026-03-02"),
    Order(1006, "Bose", "2026-03-01"),
]
print("customer only:", show(sorted(orders, key=attrgetter("customer"))))
by_date = sorted(orders, key=attrgetter("date"))
print("by date:", show(by_date))
print("then by customer:", show(sorted(by_date, key=attrgetter("customer"))))
print("composite key:", show(sorted(orders, key=lambda o: (o.customer, o.date))))
OutputRan with python:3.14.8-slim-trixie (Python 3.14.8)
customer only: 1002:Abara:01 1005:Abara:02 1004:Bose:02 1006:Bose:01 1001:Chen:02 1003:Chen:01
by date: 1002:Abara:01 1003:Chen:01 1006:Bose:01 1001:Chen:02 1004:Bose:02 1005:Abara:02
then by customer: 1002:Abara:01 1005:Abara:02 1006:Bose:01 1004:Bose:02 1003:Chen:01 1001:Chen:02
composite key: 1002:Abara:01 1005:Abara:02 1006:Bose:01 1004:Bose:02 1003:Chen:01 1001:Chen:02
OrdersCode.javajava
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class OrdersCode {
    record Order(int number, String customer, String date) {}

    static String show(List<Order> orders) {
        return orders.stream()
                .map(o -> o.number() + ":" + o.customer() + ":" + o.date().substring(8))
                .collect(Collectors.joining(" "));
    }

    public static void main(String[] args) {
        List<Order> orders = List.of(
                new Order(1001, "Chen", "2026-03-02"),
                new Order(1002, "Abara", "2026-03-01"),
                new Order(1003, "Chen", "2026-03-01"),
                new Order(1004, "Bose", "2026-03-02"),
                new Order(1005, "Abara", "2026-03-02"),
                new Order(1006, "Bose", "2026-03-01"));
        List<Order> customerOnly = new ArrayList<>(orders);
        customerOnly.sort(Comparator.comparing(Order::customer));
        System.out.println("customer only: " + show(customerOnly));
        List<Order> sorted = new ArrayList<>(orders);
        sorted.sort(Comparator.comparing(Order::date));
        System.out.println("by date: " + show(sorted));
        sorted.sort(Comparator.comparing(Order::customer));
        System.out.println("then by customer: " + show(sorted));
        List<Order> composite = new ArrayList<>(orders);
        composite.sort(Comparator.comparing(Order::customer).thenComparing(Order::date));
        System.out.println("composite key: " + show(composite));
    }
}
OutputRan with eclipse-temurin:25.0.4.1_1-jdk-noble (Java 25.0.4.1)
customer only: 1002:Abara:01 1005:Abara:02 1004:Bose:02 1006:Bose:01 1001:Chen:02 1003:Chen:01
by date: 1002:Abara:01 1003:Chen:01 1006:Bose:01 1001:Chen:02 1004:Bose:02 1005:Abara:02
then by customer: 1002:Abara:01 1005:Abara:02 1006:Bose:01 1004:Bose:02 1003:Chen:01 1001:Chen:02
composite key: 1002:Abara:01 1005:Abara:02 1006:Bose:01 1004:Bose:02 1003:Chen:01 1001:Chen:02
orders_code.tsts
interface Order {
  number: number;
  customer: string;
  date: string;
}

const byField =
  (field: "customer" | "date") =>
  (a: Order, b: Order): number =>
    a[field] < b[field] ? -1 : a[field] > b[field] ? 1 : 0;

function show(orders: Order[]): string {
  return orders.map((o) => `${o.number}:${o.customer}:${o.date.slice(-2)}`).join(" ");
}

const orders: Order[] = [
  { number: 1001, customer: "Chen", date: "2026-03-02" },
  { number: 1002, customer: "Abara", date: "2026-03-01" },
  { number: 1003, customer: "Chen", date: "2026-03-01" },
  { number: 1004, customer: "Bose", date: "2026-03-02" },
  { number: 1005, customer: "Abara", date: "2026-03-02" },
  { number: 1006, customer: "Bose", date: "2026-03-01" },
];
console.log(`customer only: ${show([...orders].sort(byField("customer")))}`);
const byDate = [...orders].sort(byField("date"));
console.log(`by date: ${show(byDate)}`);
console.log(`then by customer: ${show([...byDate].sort(byField("customer")))}`);
console.log(`composite key: ${show([...orders].sort((a, b) => byField("customer")(a, b) || byField("date")(a, b)))}`);
OutputRan with node:24.22.0-trixie-slim (Node 24.22.0), TypeScript 5.9.3
customer only: 1002:Abara:01 1005:Abara:02 1004:Bose:02 1006:Bose:01 1001:Chen:02 1003:Chen:01
by date: 1002:Abara:01 1003:Chen:01 1006:Bose:01 1001:Chen:02 1004:Bose:02 1005:Abara:02
then by customer: 1002:Abara:01 1005:Abara:02 1006:Bose:01 1004:Bose:02 1003:Chen:01 1001:Chen:02
composite key: 1002:Abara:01 1005:Abara:02 1006:Bose:01 1004:Bose:02 1003:Chen:01 1001:Chen:02

All three printed the same four lines. The highlighted lines are the date sort, the customer sort that follows it, and the composite key. In Python the composite key is a tuple, which Python compares item by item; in Java it is Comparator.comparing(...).thenComparing(...); in TypeScript it is a comparator that falls through to the date when the customers compare equal, because 0 || x is x.

Prefer the composite key when you write both sorts at once, since it sorts once and does not depend on stability. Sort twice when the data arrives already sorted by the minor key, as a log arrives sorted by time, or when the user picks the major key at run time by clicking a column; then the second sort must be stable, and heapsort or quicksort would not do.