Skip to content

Month-Day Values

  • The core functions, which every industry uses.

A month-day is a month and a day with no year. Use it for a birthday, an anniversary or a recurring holiday, where the year would only drift. gmt writes it as --MM-DD, the ISO 8601 truncated form that RFC 3339 and RFC 6350 also use.

import { isValidMonthDay } from "@northguild/gmt";
isValidMonthDay("--12-25"); // true
isValidMonthDay("12-25"); // true — the leading -- is optional on input
isValidMonthDay("--02-29"); // true — 29 February exists, in leap years
isValidMonthDay("--1225"); // false — the basic form is refused

gmt reads --MM-DD and MM-DD, and always writes --MM-DD. The basic form --MMDD is refused, as isValidDate refuses 20241225.

import { isValidMonthDay, parseMonthDay } from "@northguild/gmt";
isValidMonthDay("--02-30"); // false — February has no 30th
isValidMonthDay("--12-25[u-ca=gregory]"); // false — without a year, only the ISO calendar is read
parseMonthDay("12-25"); // "--12-25"
parseMonthDay("--12-25[u-ca=iso8601]"); // "--12-25" — the annotation is dropped
parseMonthDay("--12-25[u-ca=gregory]"); // ""
parseMonthDay("--02-30"); // ""

parseMonthDay returns "" for anything isValidMonthDay refuses, so the two always agree.

import { parseMonthDayFromDate, parseMonthDayFromDateTime } from "@northguild/gmt";
parseMonthDayFromDate("2024-12-25"); // "--12-25"
parseMonthDayFromDate("2024-02-29"); // "--02-29" — the leap day is kept
parseMonthDayFromDate("2024-12-25T10:00:00"); // "" — a date-time is not a date
parseMonthDayFromDateTime("2024-02-29T23:59:59.999999999"); // "--02-29" — the time never moves the day
parseMonthDayFromDateTime("2024-12-25"); // "" — a date has no time

parseMonthDayWithPattern reads a month-day a producer wrote in its own layout. The locale matters for month names:

import { parseMonthDayWithPattern } from "@northguild/gmt";
parseMonthDayWithPattern("December 25", "MMMM d", "en-US"); // "--12-25"
parseMonthDayWithPattern("25 декабря", "d MMMM", "ru-RU"); // "--12-25"
parseMonthDayWithPattern("12月25日", "M月d日", "ja-JP"); // "--12-25"
parseMonthDayWithPattern("25 décembre 2024", "d MMMM yyyy", "fr-FR"); // "" — a year token is refused

Some languages inflect a month name when a day comes with it (25 декабря) and write the bare name another way (декабрь). The MMMM token reads either form, and so does its stand-alone twin LLLL. A pattern with a year token is refused: use parseDateWithPattern for those.

import { formatMonthDay } from "@northguild/gmt";
formatMonthDay("--12-25", "en-US"); // "12/25"
formatMonthDay("--12-25", "en-US", { month: "long", day: "numeric" }); // "December 25"
formatMonthDay("--12-25", "en-US", { dateStyle: "medium" }); // "Dec 25"
formatMonthDay("--12-25", "de-DE"); // "25.12."
formatMonthDay("--12-25", "de-DE", { month: "long", day: "numeric" }); // "25. Dezember"
formatMonthDay("--12-25", "de-DE", { dateStyle: "medium" }); // "25.12."
formatMonthDay("--12-25", "ja-JP"); // "12/25"
formatMonthDay("--12-25", "ja-JP", { month: "long", day: "numeric" }); // "12月25日"
formatMonthDay("--12-25", "ja-JP", { dateStyle: "long" }); // "12/25"

A dateStyle takes the month and day widths from the locale’s own date pattern, as the specification says and as native Temporal.PlainMonthDay#toLocaleString does. So de-DE medium is 25.12., and ja-JP writes the month as a number in every style. formatMonthDay formats through the Gregorian calendar, whatever calendar the locale tag names. A year or weekday option is dropped, and a timeStyle gives "".

Order is by month, then by day. 29 February sorts between 28 February and 1 March.

import { compareMonthDays, areMonthDaysEqual } from "@northguild/gmt";
compareMonthDays("--01-01", "--12-31"); // -1 — month first
compareMonthDays("--02-29", "--03-01"); // -1
compareMonthDays("12-25", "--12-25"); // 0 — both input forms
compareMonthDays("--02-30", "--03-01"); // null — an invalid month-day
areMonthDaysEqual("12-25", "--12-25"); // true
areMonthDaysEqual("--02-29", "--02-28"); // false — 29 February is not constrained
["--12-25", "--02-29", "--01-01", "--03-01", "--02-28"].sort((a, b) => compareMonthDays(a, b) ?? 0);
// ["--01-01", "--02-28", "--02-29", "--03-01", "--12-25"]

compareMonthDays returns null for an invalid value, so a sort callback maps it to 0.

monthDayToDate puts a month-day in a year:

import { monthDayToDate } from "@northguild/gmt";
monthDayToDate("--12-25", 2024); // "2024-12-25"
monthDayToDate("--02-29", 2024); // "2024-02-29" — a leap year
monthDayToDate("--02-29", 2023); // "2023-02-28" — not a leap year
monthDayToDate("--02-29", 1900); // "1900-02-28" — a century that is not a leap year
monthDayToDate("--02-29", 2000); // "2000-02-29" — divisible by 400

In a year with no 29 February, the date is moved back to the last day of February. Some rules and contracts use 1 March instead. That is your policy to apply: see a 29 February birthday in a common year.

Code examples: MIT-0. Packages: MIT. Third-party data and components: their own licences, credited where they appear. Everything else:© 2026 NorthGuild, all rights reserved.