← thecodex.expert · The Codex Family of Knowledge
Recipe · Iteration & data

Group and count

Turn a flat list of records into counts per key, then rank the tallies — the same working program in 10 languages.

10 languages ~30 lines each 9 compiled & run

The problem

A list of records comes in -- page views, log lines, orders, survey answers -- and the question is always some version of “how many of each”. It is the first real step in turning raw events into something a person can read: a flat list becomes a handful of totals, and the totals become a ranking.

The job has two parts that are easy to blur together: building the tally (a key seen once more needs its count bumped, a key seen for the first time needs to start at one) and then ordering the result (by count, with ties broken somehow so the output is deterministic). Every language below keeps that split, and the interesting part is what each one gives you to do the first half with.

The shared design

Every version below implements exactly the same thing, so that the differences you see are differences between the languages and not between ten people's ideas of the problem.

  • Source: a fixed list of 16 page-view events, each just a page name (home, docs, blog or pricing).
  • Tally: count how many times each page name appears.
  • Rank: sort the tallies by count, highest first; when two counts are equal, break the tie by the key, alphabetically.
  • The point: the input is built so two pairs tie (docs/home at 5, blog/pricing at 3) — a single-key sort would leave their order undefined, so the tie-break is load-bearing, not decoration.

What every version prints

Byte-for-byte identical across all nine languages that could be compiled and run here. docs and home both occur 5 times but docs prints first; blog and pricing both occur 3 times but blog prints first. That ordering only happens if the tie-break actually ran.

docs: 5
home: 5
blog: 3
pricing: 3

The ten implementations

Filter to the languages you care about, or read straight down to watch the same idea change shape.

Python Counter compiled & run

Counter(events) does the whole first half in one call. The ranking is the only code left to write: sorted with a key that returns (-count, key) so one comparison does both the descending count and the ascending tie-break.

group_count.py
"""Group and count: tally how often each key appears, then rank the tallies."""
from collections import Counter

EVENTS = [
    "home", "docs", "home", "blog", "pricing", "docs", "home", "blog",
    "docs", "home", "pricing", "docs", "blog", "pricing", "home", "docs",
]


def ranked_counts(events: list[str]) -> list[tuple[str, int]]:
    """Highest count first; equal counts broken by key, ascending."""
    counts = Counter(events)
    return sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))


def main() -> None:
    for key, count in ranked_counts(EVENTS):
        print(f"{key}: {count}")


if __name__ == "__main__":
    main()
python3 group_count.py
JavaScript Map compiled & run

A Map preserves insertion order but has no tally helper, so counts.get(key) || 0 plus one is the increment. The sort comparator chains the two criteria with ||: the count difference first, the string comparison only when that is zero.

group_count.js
"use strict";
/* Group and count: tally how often each key appears, then rank the tallies. */

const EVENTS = [
  "home", "docs", "home", "blog", "pricing", "docs", "home", "blog",
  "docs", "home", "pricing", "docs", "blog", "pricing", "home", "docs",
];

function rankedCounts(events) {
  const counts = new Map();
  for (const key of events) {
    counts.set(key, (counts.get(key) || 0) + 1);
  }
  // Highest count first; equal counts broken by key, ascending.
  return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
}

function main() {
  for (const [key, count] of rankedCounts(EVENTS)) {
    console.log(`${key}: ${count}`);
  }
}

main();
node group_count.js
TypeScript Map compiled & run

Identical to JavaScript with Map<string, number> naming what is already there. ?? instead of || because a key could in principle map to 0, which || would treat as falsy -- the type checker does not force this, habit and strictness do.

group_count.ts
/* Group and count: tally how often each key appears, then rank the tallies. */

const EVENTS: string[] = [
  "home", "docs", "home", "blog", "pricing", "docs", "home", "blog",
  "docs", "home", "pricing", "docs", "blog", "pricing", "home", "docs",
];

function rankedCounts(events: string[]): [string, number][] {
  const counts = new Map<string, number>();
  for (const key of events) {
    counts.set(key, (counts.get(key) ?? 0) + 1);
  }
  // Highest count first; equal counts broken by key, ascending.
  return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
}

function main(): void {
  for (const [key, count] of rankedCounts(EVENTS)) {
    console.log(`${key}: ${count}`);
  }
}

main();
tsc group_count.ts --strict --target ES2022 --lib ES2022,DOM
Kotlin groupingBy compiled & run

groupingBy { it }.eachCount() is the grouping and the counting as one expression -- closer to Python's Counter than to the hand-rolled maps either side of it. The sort chains two comparator builders, compareByDescending then .thenBy, instead of one hand-written function.

GroupCount.kt
// Group and count: tally how often each key appears, then rank the tallies.

val EVENTS = listOf(
    "home", "docs", "home", "blog", "pricing", "docs", "home", "blog",
    "docs", "home", "pricing", "docs", "blog", "pricing", "home", "docs"
)

fun rankedCounts(events: List<String>): List<Pair<String, Int>> {
    val counts = events.groupingBy { it }.eachCount()
    // Highest count first; equal counts broken by key, ascending.
    return counts.entries
        .sortedWith(compareByDescending<Map.Entry<String, Int>> { it.value }.thenBy { it.key })
        .map { it.key to it.value }
}

fun main() {
    for ((key, count) in rankedCounts(EVENTS)) {
        println("$key: $count")
    }
}
kotlinc GroupCount.kt -include-runtime -d group_count.jar
Go Map compiled & run

A plain map[string]int, and counts[key]++ works on a missing key because Go's maps return the zero value rather than an error -- the increment needs no existence check at all. There is no comparator-chaining shorthand, so the tie-break is an explicit if inside sort.Slice.

group_count.go
// Group and count: tally how often each key appears, then rank the tallies.
package main

import (
	"fmt"
	"sort"
)

var events = []string{
	"home", "docs", "home", "blog", "pricing", "docs", "home", "blog",
	"docs", "home", "pricing", "docs", "blog", "pricing", "home", "docs",
}

type keyCount struct {
	key   string
	count int
}

func rankedCounts(events []string) []keyCount {
	counts := make(map[string]int)
	for _, key := range events {
		counts[key]++
	}
	ranked := make([]keyCount, 0, len(counts))
	for key, count := range counts {
		ranked = append(ranked, keyCount{key, count})
	}
	// Highest count first; equal counts broken by key, ascending.
	sort.Slice(ranked, func(i, j int) bool {
		if ranked[i].count != ranked[j].count {
			return ranked[i].count > ranked[j].count
		}
		return ranked[i].key < ranked[j].key
	})
	return ranked
}

func main() {
	for _, kc := range rankedCounts(events) {
		fmt.Printf("%s: %d\n", kc.key, kc.count)
	}
}
go run group_count.go
Rust HashMap compiled & run

counts.entry(key).or_insert(0) is the explicit version of what Go's zero value does silently: ask for the entry, supplying what to put there if it is not found yet, then deref and add. Ordering::then_with chains the two comparisons lazily -- the key comparison only runs when the counts tie.

group_count.rs
// Group and count: tally how often each key appears, then rank the tallies.
use std::collections::HashMap;

const EVENTS: [&str; 16] = [
    "home", "docs", "home", "blog", "pricing", "docs", "home", "blog",
    "docs", "home", "pricing", "docs", "blog", "pricing", "home", "docs",
];

fn ranked_counts(events: &[&str]) -> Vec<(String, usize)> {
    let mut counts: HashMap<&str, usize> = HashMap::new();
    for &key in events {
        *counts.entry(key).or_insert(0) += 1;
    }
    let mut ranked: Vec<(String, usize)> =
        counts.into_iter().map(|(k, v)| (k.to_string(), v)).collect();
    // Highest count first; equal counts broken by key, ascending.
    ranked.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
    ranked
}

fn main() {
    for (key, count) in ranked_counts(&EVENTS) {
        println!("{}: {}", key, count);
    }
}
rustc -O group_count.rs
Java HashMap compiled & run

counts.merge(key, 1, Integer::sum) is one call that inserts 1 for a new key or adds 1 to an existing one, avoiding the separate contains-check Java's older map API would need. The comparator is written as an int arithmetic difference first, falling back to compareTo only when that difference is zero.

GroupCount.java
// Group and count: tally how often each key appears, then rank the tallies.
import java.util.*;

public class GroupCount {
    static final List<String> EVENTS = List.of(
        "home", "docs", "home", "blog", "pricing", "docs", "home", "blog",
        "docs", "home", "pricing", "docs", "blog", "pricing", "home", "docs"
    );

    static List<Map.Entry<String, Integer>> rankedCounts(List<String> events) {
        Map<String, Integer> counts = new HashMap<>();
        for (String key : events) {
            counts.merge(key, 1, Integer::sum);
        }
        List<Map.Entry<String, Integer>> ranked = new ArrayList<>(counts.entrySet());
        // Highest count first; equal counts broken by key, ascending.
        ranked.sort((a, b) -> {
            int byCount = b.getValue() - a.getValue();
            return byCount != 0 ? byCount : a.getKey().compareTo(b.getKey());
        });
        return ranked;
    }

    public static void main(String[] args) {
        for (Map.Entry<String, Integer> e : rankedCounts(EVENTS)) {
            System.out.println(e.getKey() + ": " + e.getValue());
        }
    }
}
javac GroupCount.java && java GroupCount
C++ HashMap compiled & run

unordered_map's operator[] default-constructs a missing key to 0 before the ++ runs, the same zero-value trick as Go. Structured bindings (auto& [key, count]) unpack the pairs in both the sort comparator and the final loop without naming .first/.second.

group_count.cpp
// Group and count: tally how often each key appears, then rank the tallies.
#include <algorithm>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

const std::vector<std::string> EVENTS = {
    "home", "docs", "home", "blog", "pricing", "docs", "home", "blog",
    "docs", "home", "pricing", "docs", "blog", "pricing", "home", "docs",
};

std::vector<std::pair<std::string, int>> ranked_counts(const std::vector<std::string>& events) {
    std::unordered_map<std::string, int> counts;
    for (const auto& key : events) {
        counts[key]++;
    }
    std::vector<std::pair<std::string, int>> ranked(counts.begin(), counts.end());
    // Highest count first; equal counts broken by key, ascending.
    std::sort(ranked.begin(), ranked.end(), [](const auto& a, const auto& b) {
        if (a.second != b.second) return a.second > b.second;
        return a.first < b.first;
    });
    return ranked;
}

int main() {
    for (const auto& [key, count] : ranked_counts(EVENTS)) {
        std::cout << key << ": " << count << "\n";
    }
    return 0;
}
g++ -std=c++20 -Wall -Wextra -O2 group_count.cpp
C By hand compiled & run

No hash map in the standard library, so the table is a fixed-size array of structs and find_or_add is a linear scan comparing every existing key with strcmp before adding a new one. It is exactly what the hash maps above are doing per bucket, just without the hashing step that makes it fast at scale. qsort takes a comparator in the oldest C idiom here: a function pointer and two void*.

group_count.c
/* Group and count: tally how often each key appears, then rank the tallies.
 * No hash map in the standard library, so counting is a linear scan over a
 * small fixed array of (key, count) pairs -- the thing every map above does
 * underneath, just without the amortised O(1) lookup. */
#include <stdio.h>
#include <string.h>

static const char *EVENTS[] = {
    "home", "docs", "home", "blog", "pricing", "docs", "home", "blog",
    "docs", "home", "pricing", "docs", "blog", "pricing", "home", "docs",
};
#define N_EVENTS (sizeof(EVENTS) / sizeof(EVENTS[0]))

typedef struct { const char *key; int count; } KeyCount;

static int find_or_add(KeyCount *table, int *n, const char *key) {
    for (int i = 0; i < *n; i++) {
        if (strcmp(table[i].key, key) == 0) return i;
    }
    table[*n].key = key;
    table[*n].count = 0;
    return (*n)++;
}

static int cmp(const void *pa, const void *pb) {
    const KeyCount *a = pa, *b = pb;
    if (a->count != b->count) return b->count - a->count;
    return strcmp(a->key, b->key);
}

int main(void) {
    KeyCount table[8];
    int n = 0;
    for (size_t i = 0; i < N_EVENTS; i++) {
        int idx = find_or_add(table, &n, EVENTS[i]);
        table[idx].count++;
    }
    qsort(table, n, sizeof(KeyCount), cmp);
    for (int i = 0; i < n; i++) {
        printf("%s: %d\n", table[i].key, table[i].count);
    }
    return 0;
}
gcc -Wall -Wextra -O2 group_count.c
Swift Dictionary not machine-verified

counts[key, default: 0] += 1 folds the missing-key case into the subscript itself, reading closest to Python's one-liner of any manually-incremented map here. sorted(by:) takes a single closure, so the two-part comparison is one ?: expression rather than a chained call.

group_count.swift
// Group and count: tally how often each key appears, then rank the tallies.

let events = [
    "home", "docs", "home", "blog", "pricing", "docs", "home", "blog",
    "docs", "home", "pricing", "docs", "blog", "pricing", "home", "docs",
]

func rankedCounts(_ events: [String]) -> [(key: String, count: Int)] {
    var counts: [String: Int] = [:]
    for key in events {
        counts[key, default: 0] += 1
    }
    // Highest count first; equal counts broken by key, ascending.
    return counts.sorted { a, b in
        a.value != b.value ? a.value > b.value : a.key < b.key
    }.map { (key: $0.key, count: $0.value) }
}

for (key, count) in rankedCounts(events) {
    print("\(key): \(count)")
}
swiftc group_count.swift

What the comparison actually shows

One question with three different answers: does the language give you a tool built for counting, or just a map you drive by hand?

Built for exactly this

Python's Counter and Kotlin's groupingBy { }.eachCount() name the operation directly. Grouping and counting are one call, and the rest of the program is only the ranking.

A general map, driven by hand

JavaScript, Go, Rust, Java, C++ and Swift all reach for an ordinary hash map and write the increment themselves. The differences are small but real: Go and C++ default a missing key to zero silently, Rust and Swift make the default explicit, and Java folds both cases into one merge call.

No map at all

C has nothing to reach for. The table is an array, finding a key is a loop comparing strings, and that loop is what every hash map above is doing per bucket -- hashing is just the trick that keeps it fast once there are more than a handful of keys.

The tie-break is the second comparison worth watching: five different ways to say “sort by count descending, then by key ascending” -- a tuple key in Python, chained comparator builders in Kotlin, || / ?? short-circuiting in JS/TS, then_with in Rust, and a plain two-line if in Go.

The Swift version is the only one here that was not compiled and run: its toolchain cannot be obtained in the environment this site is built in. It is written against the language reference and marked accordingly rather than quietly presented as equal to the rest.