Ordinals in JavaScript

If you search for a way to format numbers into their ordinal representation (e.g. 1 becomes 1st, 2 becomes 2nd), you might come across hand-minified algorithms that inspire, but there's a widely available API that you almost certainly should be using instead.

The recent Intl.PluralRules API makes it painless to define rules to format numbers into human-readable strings. Unfortunately, it doesn’t make it painless finding the rules in the first place, but I’ve had a decades of practice to get us up and running.

The code below includes some type annotations, so it’s not really JavaScript anymore, but the lines are so blurred between Microsoft’s JavaScript and the version we used with jQuery in the early naughties that I beg the reader’s forgiveness.

const pr = new Intl.PluralRules("en-GB", { type: "ordinal" });

const suffixes = new Map([
  ["one", "st"],
  ["two", "nd"],
  ["few", "rd"],
  ["other", "th"],
]);

export function formatOrdinal(n: number): string {
  const rule = pr.select(n);
  const suffix = suffixes.get(rule);
  return `${n}${suffix}`;
}

I also wrote some clumsy tests with vitest because I want to make sure things work without getting deep into the vitest API (which seems a little anemic).

import { describe, expect, test } from "vitest";
import { formatOrdinal } from "@/lib/number";

describe("formatOrdinal", () => {
  [
    { number: 0, string: "0th" },
    { number: 1, string: "1st" },
    { number: 2, string: "2nd" },
    { number: 3, string: "3rd" },
    { number: 4, string: "4th" },
    { number: 10, string: "10th" },
    { number: 11, string: "11th" },
    { number: 12, string: "12th" },
    { number: 13, string: "13th" },
    { number: 14, string: "14th" },
    { number: 100, string: "100th" },
    { number: 101, string: "101st" },
    { number: 102, string: "102nd" },
    { number: 103, string: "103rd" },
    { number: 104, string: "104th" },
  ].forEach(({ number, string }) => {
    test(`with ${number}`, () => {
      expect(formatOrdinal(number)).toBe(string);
    });
  });
});