Dates

Month grids, ISO day strings, selection state, locale formatting and locale parsing, with no time zones and no DOM.

Reach for them for any calendar, scheduler or date grid, and for storing or comparing days without time zones. The helpers are pure: no DOM at import time or in the call path, so they run on the server and in tests. Each result below is computed when the page is built, by running the call shown.

Import

import {
	buildCalendarMonth,
	getWeekStartsOn,
	addMonths,
	getVisibleMonthViews,
	isoToDate,
	dateToIso,
	isIsoInRange,
	compareIso,
	normalizeRange,
	parseIsoRange,
	serializeIsoRange,
	parseMultipleIsos,
	serializeMultipleIsos,
	toggleMultipleIso,
	advanceRangeSelection,
	getActiveRangeBounds,
	getDaySelectionAppearance,
	isIsoInSpan,
	formatDisplayDate,
	formatDateRange,
	formatMonthYear,
	formatTodayLabel,
	getWeekdayLabels,
	getDateTimeFormat,
	parseLocaleDateString,
	getDatePartOrder,
	getDateSeparators,
} from '@ecopages/radiant-ui/helpers';

Month grids

buildCalendarMonth returns at most six weeks of day cells in the locale's week order, with isToday, isSelected, isDisabled and range flags. addMonths and getVisibleMonthViews move and list months; months are zero-based.

const weeks = buildCalendarMonth(2026, 10, {
	locale: 'en-US',
	value: '2026-11-14',
	min: '2026-11-10',
	today: new Date(2026, 10, 20),
});
CallResult
weeks[0][0].iso"2026-11-01"
weeks.flat().find((cell) => cell.iso === '2026-11-14')?.isSelectedtrue
weeks.flat().find((cell) => cell.iso === '2026-11-09')?.isDisabledtrue
getWeekStartsOn('en-US')0
getWeekStartsOn('en-GB')1
addMonths(2026, 11, 1){"year":2027,"month":0}
getVisibleMonthViews(2026, 10, 2)[{"year":2026,"month":10,"offset":0},{"year":2026,"month":11,"offset":1}]

ISO dates

YYYY-MM-DD strings to and from local dates, and a bounds check. An impossible date is null.

CallResult
isoToDate('2026-11-14')Date(2026-11-14)
isoToDate('2026-02-30')null
dateToIso(new Date(2026, 10, 14))"2026-11-14"
isIsoInRange('2026-11-14', '2026-11-01', '2026-11-30')true
compareIso('2026-11-14', '2026-11-20')-1

Selection

Single, multiple and range selection as pure state transitions on ISO strings. A range is { start, end } and is serialized as start/end.

CallResult
normalizeRange('2026-11-20', '2026-11-14'){"start":"2026-11-14","end":"2026-11-20"}
parseIsoRange('2026-11-20/2026-11-14'){"start":"2026-11-14","end":"2026-11-20"}
serializeIsoRange({ start: '2026-11-14', end: '2026-11-20' })"2026-11-14/2026-11-20"
parseMultipleIsos('2026-11-20, 2026-11-14')["2026-11-14","2026-11-20"]
toggleMultipleIso(['2026-11-14'], '2026-11-20')["2026-11-14","2026-11-20"]
advanceRangeSelection(null, { anchor: null, hover: null }, '2026-11-14'){"committed":null,"draft":{"anchor":"2026-11-14","hover":"2026-11-14"}}
getDaySelectionAppearance('2026-11-15', 'range', { range: { start: '2026-11-14', end: '2026-11-16' } }){"isSelected":true,"isRangeStart":false,"isRangeEnd":false,"isRangeMiddle":true}

Formatting

Localized labels, with a cached Intl.DateTimeFormat behind getDateTimeFormat.

CallResult
formatDisplayDate(new Date(2026, 10, 14), 'en-US')"Nov 14, 2026"
formatMonthYear(new Date(2026, 10, 14), 'en-US')"November 2026"
formatDateRange(new Date(2026, 10, 14), new Date(2026, 10, 20), 'en-US')"Nov 14 – 20, 2026"
getWeekdayLabels('en-US', 0)["S","M","T","W","T","F","S"]
formatTodayLabel('en-US')"today"

Parsing

Typed dates in locale order: ISO, masked, locale-ordered numbers or month names.

CallResult
getDatePartOrder('en-GB')["day","month","year"]
getDateSeparators('en-US')["/","/"]
parseLocaleDateString('21/08/2002', 'en-GB')Date(2002-08-21)
parseLocaleDateString('Aug 21, 2002', 'en-US')Date(2002-08-21)
parseLocaleDateString('not a date', 'en-US')null