Handling dates in JavaScript is notoriously error-prone. While ECMAScript’s native Date object has well-documented pitfalls—uncontrolled mutability, 0-indexed months, and automatic local-timezone conversions—there is an even larger blind spot in existing libraries like date-fns, dayjs, and luxon: non-Gregorian calendar systems and regional legal date semantics.
Global and regional enterprise applications (e.g., banking, fintech, tax compliance, healthcare, public sector, and international travel) frequently operate under official non-Gregorian legal rules:
- 🇹🇭 Thai Buddhist Era (
พ.ศ.= CE + 543) with official government numbering and Royal Gazette formatting presets. - 🇯🇵 Japanese Imperial Era (Reiwa 令和, Heisei 平成, Showa 昭和) with exact historical day-of-event rollover boundaries (e.g., May 1, 2019 Reiwa 1 Gannen).
- 🇹🇼 Taiwan Minguo (民國紀年) used across municipal and legal filings.
- 🇸🇦 Islamic Hijri (Astronomical Umm al-Qura, Islamic Civil, and Tabular systems).
- 🇮🇷 Persian / Solar Hijri (Jalali Khayyami 33-year astronomical leap cycle).
- 🇮🇳 Indian National Saka Calendar adopted as the official civil calendar of India.
To solve this without bloating runtime bundles, dragging in heavy astronomical dependencies, or resorting to loose string parsing and any, we engineered Chronera — an open-source, zero-dependency date and multi-calendar engine written in strict TypeScript.
In this deep dive, we’ll examine the architectural design decisions, mathematical foundations, and type-level techniques used to model complex multi-calendar domains safely.
1. The Architectural Dilemma: Monolithic Objects vs. Tagged Primitives
Most date libraries wrap a native timestamp inside a single monolithic object. The instant you create a date to represent someone’s birth date (e.g., 1995-05-15), the engine binds it to an hour, minute, second, and UTC timezone offset.
When that object is serialized to JSON or transferred across servers in different timezones, classic off-by-one errors happen:
// The classic JavaScript timezone trap:
const date = new Date("2026-09-05");
// On a machine in New York (UTC-4):
console.log(date.toISOString()); // "2026-09-05T00:00:00.000Z"
console.log(date.getDate()); // 4 <-- BUG: shifted to Sept 4!
Enter fullscreen mode Exit fullscreen mode
The Solution: Nominal Tagged Domain Primitives
In Chronera, date semantics are decomposed into distinct, immutable, branded primitives:
/**
* Date-only primitive: Pure calendar day (year, month, day).
* Completely immune to timezone shifts and daylight saving adjustments.
*/
export interface LocalDate {
readonly _brand: "LocalDate";
readonly year: number;
readonly month: number; // 1-indexed (1..12)
readonly day: number; // 1..31
}
/**
* Wall-clock timestamp primitive without timezone binding.
*/
export interface LocalDateTime {
readonly _brand: "LocalDateTime";
readonly date: LocalDate;
readonly time: LocalTime;
}
/**
* Exact point-in-time primitive anchored to the Unix epoch.
*/
export interface Instant {
readonly _brand: "Instant";
readonly epochMilliseconds: number;
}
Enter fullscreen mode Exit fullscreen mode
By separating LocalDate from Instant, a user’s date of birth or a legal tax deadline remains pure date data. Timezone offsets can only be applied when explicitly transitioning to an Instant or formatting for display.
2. Modeling Multi-Calendar Systems via Discriminated Unions
A calendar system isn’t just a different way to format a year string—each has unique leap year algorithms, differing month lengths, era transitions, and intercalary rules.
Instead of writing loose string parsers or subclassing mutable classes, we model calendar identities as a strict string literal union:
export type CalendarSystem =
| "gregory"
| "iso8601"
| "buddhist"
| "japanese"
| "roc"
| "persian"
| "islamic-civil"
| "islamic-tabular"
| "islamic-umalqura"
| "indian";
Enter fullscreen mode Exit fullscreen mode
A CalendarDate is typed with a generic system parameter:
export interface CalendarDate<TSystem extends CalendarSystem = CalendarSystem> {
readonly _brand: "CalendarDate";
readonly system: TSystem;
readonly year: number;
readonly month: number;
readonly day: number;
readonly era?: string;
readonly metadata?: Readonly<Record<string, unknown>>;
}
Enter fullscreen mode Exit fullscreen mode
This allows compile-time discrimination:
function printEraInfo(date: CalendarDate) {
if (date.system === "japanese") {
// TypeScript knows date can have Japanese imperial era metadata
console.log(`Reiwa/Heisei era: ${date.era}`);
}
}
Enter fullscreen mode Exit fullscreen mode
3. Mathematical Foundations: The AbsoluteDay Pivot (O(1) Conversion)
How do you convert a Persian date (1405-06-14) to a Thai Buddhist date (2569-09-05) without creating a combinatorial matrix of $N \times (N – 1)$ bespoke converters?
The Universal Scalar Day (AbsoluteDay)
Chronera establishes a universal integer pivot: AbsoluteDay (the continuous count of days elapsed since Gregorian epoch 0001-01-01, equivalent to Julian Day Number minus 1,721,424.5).
Calendar A ───(toAbsoluteDay)───► AbsoluteDay (Int) ───(fromAbsoluteDay)───► Calendar B
Enter fullscreen mode Exit fullscreen mode
Every calendar system implements an isolated, pure adapter adhering to the CalendarAdapter interface:
export interface CalendarAdapter {
readonly id: CalendarSystem;
toAbsoluteDay(date: CalendarDateInput): AbsoluteDay;
fromAbsoluteDay(day: AbsoluteDay): CalendarDate;
isValid(date: CalendarDateInput): boolean;
daysInMonth(year: number, month: number): number;
isLeapYear(year: number): boolean;
}
Enter fullscreen mode Exit fullscreen mode
With this architecture:
- Converting between any two calendars is always an $O(1)$ two-step arithmetic operation.
- Adding an 11th calendar requires writing only one adapter (translating to and from
AbsoluteDay), immediately enabling conversion with all other 10 existing systems.
Deep Dive: Cultural Adapter Implementations
1. 🇹🇭 Thai Buddhist Era (buddhist)
In Thailand, the official Buddhist Era (พุทธศักราช, B.E.) is legally fixed at CE + 543. However, before 1941 (B.E. 2484), the Thai new year began on April 1st. Chronera accounts for modern standardized alignment while preserving accurate leap year calculations following the Gregorian astronomical cycle.
2. 🇯🇵 Japanese Era (japanese)
The Japanese calendar system counts years within imperial eras (Gengō 元号). The transition does not happen on January 1st, but on the exact calendar day of imperial succession:
- Heisei (平成): 1989-01-08 to 2019-04-30
- Reiwa (令和): 2019-05-01 (Reiwa 1 Gannen 元年) to present
import {
convertCalendarDate,
createLocalDate,
} from "@intech-software/chronera";
const transitionDay = createLocalDate(2019, 5, 1);
const japaneseDate = convertCalendarDate(transitionDay, "japanese");
console.log(japaneseDate.era); // "reiwa"
console.log(japaneseDate.year); // 1 (Gannen)
Enter fullscreen mode Exit fullscreen mode
3. 🇮🇷 Persian Solar Hijri (persian)
Unlike the Gregorian 400-year leap rule, the Persian Jalali calendar uses a sophisticated 33-year cycle consisting of eight 4-year leap periods followed by a 5-year period. Chronera implements the Khayyami mathematical cycle, calculating exact vernal equinox alignments completely offline without external tables.
4. 🇸🇦 Islamic Hijri (islamic-civil & islamic-umalqura)
The Islamic lunar calendar contains 354 or 355 days across 12 synodic lunar months. Chronera provides both the algorithmic 30-year cyclic Civil system and the Saudi Umm al-Qura astronomical reference system.
4. Developer Ergonomics: Polymorphic Convenience Helpers (v0.1.1)
A common developer critique of pure functional date libraries is that simple tasks (like checking if date $A$ is before date $B$, or adding 5 days) become overly verbose.
In v0.1.1 (now live on npm), Chronera introduced high-frequency convenience helpers designed with polymorphic type union signatures:
export type DateOrCalendarDate = LocalDate | CalendarDate;
Enter fullscreen mode Exit fullscreen mode
Comparison & Range Checks
import {
createLocalDate,
isBefore,
isAfter,
isEqual,
isSameDay,
isBetween,
isToday,
} from "@intech-software/chronera";
const d1 = createLocalDate(2026, 9, 5);
const d2 = createLocalDate(2026, 9, 12);
isBefore(d1, d2); // true
isAfter(d1, d2); // false
isEqual(d1, d1); // true
// Range validation with boundary inclusivity controls:
// "[]" = inclusive, "()" = exclusive, "[)" = half-open
isBetween(d1, createLocalDate(2026, 9, 1), d2, "[]"); // true
Enter fullscreen mode Exit fullscreen mode
Date Arithmetic Shortcuts
All arithmetic helpers preserve immutability and return fresh branded objects:
import {
addDays,
subtractDays,
addMonths,
subtractMonths,
addYears,
subtractYears,
diffInDays,
} from "@intech-software/chronera";
const today = createLocalDate(2026, 9, 5);
const nextWeek = addDays(today, 7); // 2026-09-12
const nextMonth = addMonths(today, 1); // 2026-10-05
const nextYear = addYears(today, 1); // 2027-09-05
const diff = diffInDays(nextWeek, today); // 7
Enter fullscreen mode Exit fullscreen mode
Date Boundaries
import {
startOfMonth,
endOfMonth,
startOfYear,
endOfYear,
} from "@intech-software/chronera";
const current = createLocalDate(2026, 9, 5);
startOfMonth(current); // 2026-09-01
endOfMonth(current); // 2026-09-30
startOfYear(current); // 2026-01-01
endOfYear(current); // 2026-12-31
Enter fullscreen mode Exit fullscreen mode
5. Official Legal Presets & Culture Formatting
Formatting non-Gregorian dates for tax documents, court summons, or official receipts requires strict conformance with regional guidelines.
Chronera provides out-of-the-box legal presets:
import {
createLocalDate,
formatThaiOfficial,
formatJapaneseOfficial,
formatDate,
} from "@intech-software/chronera";
const date = createLocalDate(2026, 9, 5);
// 1. Thai Official Royal Gazette format (Thai digits + B.E. year)
console.log(formatThaiOfficial(date));
// "๕ กันยายน พ.ศ. ๒๕๖๙"
// 2. Japanese Imperial Official format
console.log(formatJapaneseOfficial(date));
// "令和8年9月5日"
// 3. Custom pattern with LDML tokens
console.log(formatDate(date, "EEEE, d MMMM GGGG yyyy", { locale: "th-TH" }));
// "วันเสาร์, 5 กันยายน พุทธศักราช 2569"
Enter fullscreen mode Exit fullscreen mode
6. Verification, Security & Clean Architecture
To maintain zero runtime bloat and ensure zero architectural erosion, Chronera enforces strict CI pipelines:
-
AST Architecture Boundary Guard: A custom TypeScript AST analyzer (
scripts/check-architecture.ts) scans every file to verify that internal domain layers never import from higher-level presentation layers. - Exhaustive Test Matrix: 59 test suites running 281 automated tests verifying every edge case (Gregorian leap years, Persian 33-year cycles, Islamic intercalary years, and Japanese era rollovers).
-
Cross-Package-Manager Consumer Verification: Every build artifact (
.tgz) is packed and tested against all 4 major package managers:-
npm(Node 22 LTS & Node 24) pnpm-
yarn(Classic & Berry) bun
-
- Security & Supply Chain: 0 external runtime dependencies, 0 CVEs, and 100% automated OpenSSF Scorecard auditing.
7. Interactive Live Playground
To let developers test multi-calendar conversions in real time, we built an interactive web playground:
👉 Experience the Live Playground
Features include:
- Real-time conversion across 7 calendar systems simultaneously.
- One-click historical presets (Songkran Festival, Reiwa 1 Imperial rollover, Hijri New Year).
- Dynamic TypeScript code generator that updates as you pick dates.
- World TimeZone converter and business days simulator.
8. Getting Started
Chronera is distributed as a lightweight, tree-shakeable dual ESM/CJS package:
# npm
npm install @intech-software/chronera
# pnpm
pnpm add @intech-software/chronera
# yarn
yarn add @intech-software/chronera
# bun
bun add @intech-software/chronera
Enter fullscreen mode Exit fullscreen mode
Quick Example
import {
createLocalDate,
convertCalendarDate,
addBusinessDays,
formatDate,
} from "@intech-software/chronera";
// Create a typed local date
const today = createLocalDate(2026, 9, 5);
// Convert to Thai Buddhist Era
const thaiDate = convertCalendarDate(today, "buddhist");
console.log(`Year: ${thaiDate.year}`); // 2569
// Convert to Japanese Imperial Era
const japanDate = convertCalendarDate(today, "japanese");
console.log(`${japanDate.era} Year ${japanDate.year}`); // reiwa Year 8
Enter fullscreen mode Exit fullscreen mode
9. Conclusion & Community Feedback
Modeling dates with discriminated unions, branding, and mathematical absolute days provides compile-time safety without sacrificing performance or bundle size.
Chronera is fully open-source under the MIT License:
- 🐙 GitHub Repository: INTECH-Software-House/chronera-js
- 📦 npm Package: @intech-software/chronera
We would love to hear your thoughts on our type contracts and API ergonomics! If this project helps solve calendar headaches in your applications, please consider starring ⭐ the repository on GitHub!