How to format ordinal numbers like 1st, 2nd, 3rd
Use JavaScript to display ordinal numbers with locale-appropriate suffixes and formatting
Introduction
Ordinal numbers indicate position or rank in a sequence. In English, you write 1st, 2nd, 3rd, 4th to describe finishing positions in a race or items in a list. These suffixes help distinguish ordinal numbers from regular counting numbers.
Different languages use completely different conventions for ordinals. English adds suffixes like st, nd, rd, and th. French uses superscript letters like 1er and 2e. German adds a period after the number like 1. and 2. Japanese prefixes numbers with the character 第. When you hardcode English ordinal suffixes, you assume all users follow the same convention.
JavaScript provides the Intl.PluralRules API with an ordinal type to handle these differences automatically. This lesson explains what ordinal numbers are, why their formatting varies across languages, and how to format them correctly for any locale.
What ordinal numbers are
Ordinal numbers express position, rank, or order in a sequence. They answer the question "which one" rather than "how many". The numbers 1st, 2nd, 3rd describe positions in a race. First, second, third describe items in a list.
Cardinal numbers express quantity or amount. They answer the question "how many". The numbers 1, 2, 3 describe counts of objects. One, two, three describe amounts.
The same numeric value serves both purposes depending on context. The number 5 is cardinal in "5 apples" but ordinal in "5th place". The distinction matters because many languages format ordinals differently from cardinals.
In English, ordinals under 10 have unique word forms. First, second, third, fourth, fifth are distinct words. Above 10, English forms ordinals by adding suffixes. Eleventh, twelfth, twentieth, twenty-first follow patterns but require suffixes.
When writing ordinals as numerals rather than words, English adds suffixes st, nd, rd, or th. These suffixes follow specific rules based on the final digits of the number.
Why ordinal formatting varies by locale
Different languages developed different systems for expressing ordinal numbers. These conventions reflect grammar rules, writing systems, and cultural practices unique to each language.
In English, ordinals use four different suffixes. Numbers ending in 1 use st, numbers ending in 2 use nd, numbers ending in 3 use rd, and all other numbers use th. However, numbers ending in 11, 12, or 13 all use th. This creates 1st, 2nd, 3rd, 4th, 11th, 12th, 13th, 21st, 22nd, 23rd.
In French, ordinals use superscript abbreviations. The first item uses 1er for masculine or 1re for feminine. All other ordinals use e in superscript, like 2e, 3e, 4e. The formatting includes superscript typography, not just suffix letters.
In German, ordinals use a period after the number. The notation 1., 2., 3. represents first, second, third. This period signals that the reader should mentally add the appropriate grammatical ending when reading aloud.
In Spanish, ordinals use gendered superscript indicators. Masculine ordinals use 1.º, 2.º, 3.º while feminine ordinals use 1.ª, 2.ª, 3.ª. The period separates the number from the indicator.
In Japanese, ordinals add the prefix 第 before the number. First, second, third appear as 第一, 第二, 第三. This prefix changes the meaning from cardinal to ordinal.
When you build ordinal strings by concatenating numbers with hardcoded suffixes, you force all users to interpret English conventions. This makes your application harder to use for people who expect different formats.
Understanding Intl.PluralRules with ordinal type
The Intl.PluralRules API determines which plural category a number belongs to for a given locale. While this API is commonly used for choosing between singular and plural word forms, it also handles ordinal numbers.
The constructor accepts a locale identifier and an options object. Set the type option to "ordinal" to work with ordinal numbers instead of cardinal numbers.
const rules = new Intl.PluralRules('en-US', { type: 'ordinal' });
This creates a rules object that understands English ordinal patterns. The select() method returns a category name for any number you pass to it.
const rules = new Intl.PluralRules('en-US', { type: 'ordinal' });
console.log(rules.select(1));
// Output: "one"
console.log(rules.select(2));
// Output: "two"
console.log(rules.select(3));
// Output: "few"
console.log(rules.select(4));
// Output: "other"
The categories returned are linguistic terms, not the actual suffixes. The category "one" corresponds to numbers that take the st suffix in English. The category "two" corresponds to nd suffixes. The category "few" corresponds to rd suffixes. The category "other" corresponds to th suffixes.
You map these category names to the appropriate suffixes for your locale. The API handles the complex rules for determining which category each number belongs to.
Building an ordinal formatter function
To format ordinal numbers, you combine Intl.PluralRules with a mapping from plural categories to suffixes. Create a formatter function that takes a number and returns the formatted string.
function formatOrdinal(number, locale) {
const rules = new Intl.PluralRules(locale, { type: 'ordinal' });
const category = rules.select(number);
const suffixes = {
one: 'st',
two: 'nd',
few: 'rd',
other: 'th'
};
const suffix = suffixes[category];
return `${number}${suffix}`;
}
console.log(formatOrdinal(1, 'en-US'));
// Output: "1st"
console.log(formatOrdinal(2, 'en-US'));
// Output: "2nd"
console.log(formatOrdinal(3, 'en-US'));
// Output: "3rd"
console.log(formatOrdinal(4, 'en-US'));
// Output: "4th"
This function creates a new PluralRules instance each time it runs. For better performance, create the rules object once and reuse it for multiple numbers.
const rules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const suffixes = {
one: 'st',
two: 'nd',
few: 'rd',
other: 'th'
};
function formatOrdinal(number) {
const category = rules.select(number);
const suffix = suffixes[category];
return `${number}${suffix}`;
}
console.log(formatOrdinal(1));
// Output: "1st"
console.log(formatOrdinal(21));
// Output: "21st"
console.log(formatOrdinal(22));
// Output: "22nd"
console.log(formatOrdinal(23));
// Output: "23rd"
The API correctly handles numbers like 11, 12, and 13, which all use th despite their final digits.
console.log(formatOrdinal(11));
// Output: "11th"
console.log(formatOrdinal(12));
// Output: "12th"
console.log(formatOrdinal(13));
// Output: "13th"
The plural rules encode all the special cases and exceptions for the locale. You do not need to write conditional logic to handle these edge cases.
Formatting ordinal numbers for different locales
The plural categories and their meanings change across locales. Some languages have fewer categories than English. Others have different rules for which numbers fall into each category.
Welsh uses a different categorization system. The rules identify more categories, each corresponding to different ordinal forms in Welsh.
const enRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const cyRules = new Intl.PluralRules('cy', { type: 'ordinal' });
console.log(enRules.select(1));
// Output: "one"
console.log(cyRules.select(1));
// Output: "one"
console.log(enRules.select(2));
// Output: "two"
console.log(cyRules.select(2));
// Output: "two"
console.log(enRules.select(5));
// Output: "other"
console.log(cyRules.select(5));
// Output: "many"
To support multiple locales, you need different suffix mappings for each locale. The categories remain the same, but the suffixes change.
const ordinalSuffixes = {
'en-US': {
one: 'st',
two: 'nd',
few: 'rd',
other: 'th'
},
'fr-FR': {
one: 'er',
other: 'e'
}
};
function formatOrdinal(number, locale) {
const rules = new Intl.PluralRules(locale, { type: 'ordinal' });
const category = rules.select(number);
const suffixes = ordinalSuffixes[locale];
const suffix = suffixes[category] || suffixes.other;
return `${number}${suffix}`;
}
console.log(formatOrdinal(1, 'en-US'));
// Output: "1st"
console.log(formatOrdinal(1, 'fr-FR'));
// Output: "1er"
console.log(formatOrdinal(2, 'en-US'));
// Output: "2nd"
console.log(formatOrdinal(2, 'fr-FR'));
// Output: "2e"
This approach works well when you control the suffix strings for each locale. However, it requires maintaining suffix data for every locale you support.
Understanding ordinal plural categories
The Intl.PluralRules API uses six possible category names. Different locales use different subsets of these categories.
The categories are zero, one, two, few, many, and other. Not all languages distinguish between all six categories. English ordinals use only four: one, two, few, and other.
The category names do not directly correspond to the numeric values. The category "one" includes 1, 21, 31, 41, and all numbers ending in 1 except 11. The category "two" includes 2, 22, 32, 42, and all numbers ending in 2 except 12.
You can check which categories a locale uses by calling the resolvedOptions() method and examining the pluralCategories property.
const rules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const options = rules.resolvedOptions();
console.log(options.pluralCategories);
// Output: ["one", "two", "few", "other"]
This shows that English ordinals use four categories. Other locales use different sets of categories.
const rules = new Intl.PluralRules('fr-FR', { type: 'ordinal' });
const options = rules.resolvedOptions();
console.log(options.pluralCategories);
// Output: ["one", "other"]
French ordinals only distinguish between one and other. This simpler categorization reflects the simpler suffix rules in French.
Formatting ordinal numbers for the user's locale
Instead of hardcoding a specific locale, you can use the user's preferred language from the browser. The navigator.language property returns the user's top language preference.
const userLocale = navigator.language;
const rules = new Intl.PluralRules(userLocale, { type: 'ordinal' });
const suffixes = {
one: 'st',
two: 'nd',
few: 'rd',
other: 'th'
};
function formatOrdinal(number) {
const category = rules.select(number);
const suffix = suffixes[category] || suffixes.other;
return `${number}${suffix}`;
}
console.log(formatOrdinal(1));
// Output varies by user's locale
This approach automatically adapts to the user's language preference. However, you still need to provide appropriate suffix mappings for each locale your application supports.
For locales without specific suffix mappings, you can fall back to a default behavior or display the number without a suffix.
function formatOrdinal(number, locale = navigator.language) {
const rules = new Intl.PluralRules(locale, { type: 'ordinal' });
const category = rules.select(number);
const localeMapping = ordinalSuffixes[locale];
if (!localeMapping) {
return String(number);
}
const suffix = localeMapping[category] || localeMapping.other || '';
return `${number}${suffix}`;
}
This function returns just the number when no suffix mapping exists for the locale.
Common use cases for ordinal numbers
Ordinal numbers appear in several common contexts in user interfaces. Understanding these use cases helps you decide when to format numbers as ordinals.
Rankings and leaderboards show user positions. A gaming application displays "1st place", "2nd place", "3rd place" instead of "place 1", "place 2", "place 3".
function formatRanking(position) {
const rules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const category = rules.select(position);
const suffixes = {
one: 'st',
two: 'nd',
few: 'rd',
other: 'th'
};
const suffix = suffixes[category];
return `${position}${suffix} place`;
}
console.log(formatRanking(1));
// Output: "1st place"
console.log(formatRanking(42));
// Output: "42nd place"
Date formatting sometimes uses ordinals for the day of the month. Some locales write "January 1st" instead of "January 1".
Step-by-step instructions use ordinals to number each step. A tutorial shows "1st step: Install the software", "2nd step: Configure settings", "3rd step: Start the application".
List items in long sequences benefit from ordinal formatting when emphasizing position matters more than just enumeration.
Reusing rules objects for performance
Creating a new Intl.PluralRules instance involves loading locale data and processing options. When you format multiple ordinal numbers with the same locale, create the rules object once and reuse it.
const rules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const suffixes = {
one: 'st',
two: 'nd',
few: 'rd',
other: 'th'
};
function formatOrdinal(number) {
const category = rules.select(number);
const suffix = suffixes[category];
return `${number}${suffix}`;
}
const positions = [1, 2, 3, 4, 5];
positions.forEach(position => {
console.log(formatOrdinal(position));
});
// Output:
// "1st"
// "2nd"
// "3rd"
// "4th"
// "5th"
This approach is more efficient than creating a new rules object for each number. The performance difference becomes significant when formatting arrays with hundreds or thousands of values.
You can also create a formatter factory that returns a function configured for a specific locale.
function createOrdinalFormatter(locale, suffixMapping) {
const rules = new Intl.PluralRules(locale, { type: 'ordinal' });
return function(number) {
const category = rules.select(number);
const suffix = suffixMapping[category] || suffixMapping.other || '';
return `${number}${suffix}`;
};
}
const formatEnglishOrdinal = createOrdinalFormatter('en-US', {
one: 'st',
two: 'nd',
few: 'rd',
other: 'th'
});
console.log(formatEnglishOrdinal(1));
// Output: "1st"
console.log(formatEnglishOrdinal(2));
// Output: "2nd"
This pattern encapsulates the rules object and suffix mapping together, making the formatter easy to reuse across your application.