--- Modern civil calendars not covered by "Calendrical Calculations". -- -- This module collects widely used civil calendars that are **not** part of -- Dershowitz & Reingold's *Calendrical Calculations* (and therefore not ported -- from CALENDRICA 4.0). Each calendar's algorithm and source (with reference -- links) is documented with the relevant conversion functions below. All are -- built on the book's Gregorian or Julian routines. -- -- Calendars provided: -- -- * **Indian National calendar** (reformed Śaka) — India's official civil -- calendar, defined by the Calendar Reform Committee (chaired by Meghnad Saha), -- adopted 1957-03-22. Matches Unicode CLDR/ICU `indian`. -- -- * **Minguo / Republic of China calendar** — Taiwan's official civil calendar; -- Gregorian months and days with year numbering offset by 1911 (year 1 = 1912 CE). -- Matches Unicode CLDR/ICU `roc`. -- -- * **Thai solar calendar** (Suriyakati, Buddhist Era) — Thailand's official civil -- calendar since 1941; Gregorian months and days with the Buddhist Era offset of -- 543 years. Matches Unicode CLDR/ICU `buddhist`. -- -- * **Revised Bengali calendar** (Bangladesh) — Bangladesh's national calendar as -- fixed by the Bangla Academy; arithmetic solar calendar locked to the Gregorian -- year. -- * **Nanakshahi calendar** (Sikh) — the tropical solar calendar designed by Pal -- Singh Purewal, adopted by the SGPC in 2003. -- -- * **Amazigh (Berber) calendar** — the agrarian calendar of North Africa; the -- Julian calendar with years offset by 950. Yennayer (new year) is an official -- public holiday in Algeria and Morocco. -- -- * **Japanese era calendar** (nengō / wareki) — the modern Japanese civil calendar; -- Gregorian months and days with years counted within the reigning emperor's era. -- Matches Unicode CLDR/ICU `japanese`. -- -- -- For ICU/CLDR calendar equivalences see the ICU calendar guide: -- . -- -- @module calendrica-modern-civil -- @release 0.2 2026-10-13 local M = {} local basic = require("calendrica-basic") local gregorian = require("calendrica-gregorian") local julian = require("calendrica-julian") local standard_month = basic.standard_month local standard_day = basic.standard_day local standard_year = basic.standard_year local quotient = basic.quotient local gregorian_leap_year = gregorian.gregorian_leap_year local fixed_from_gregorian = gregorian.fixed_from_gregorian local gregorian_from_fixed = gregorian.gregorian_from_fixed -- === Indian National calendar (reformed Śaka) === -- -- Not from "Calendrical Calculations". -- Source: Report of the Calendar Reform Committee (Council of Scientific and -- Industrial Research, India, 1955), which defines the reformed Śaka calendar -- adopted as India's national calendar on 22 March 1957. Same calendar as -- Unicode CLDR/ICU `indian`. -- -- Rules: -- * The Śaka year Y begins on Chaitra 1, which falls on Gregorian March 22 in a -- common year and March 21 when Gregorian year (Y + 78) is a leap year. -- * A Śaka year is a leap year exactly when Gregorian year (Y + 78) is a leap -- year; in that case Chaitra has 31 days instead of 30. -- * Month lengths: Chaitra 30 (31 in leap years); Vaishakha..Bhadra (months 2–6) -- 31 days each; Ashwin..Phalguna (months 7–12) 30 days each. -- Year length is therefore 365 (366 in leap years), tracking the Gregorian year. -- Offset between the Śaka era and the Gregorian year that contains Chaitra 1. local INDIAN_SAKA_OFFSET = 78 -- Indian National date constructor. local function indian_national_date(year, month, day) return {year, month, day} end -- Fixed date of Chaitra 1 for Śaka year `year`. local function indian_national_new_year(year) local greg_year = year + INDIAN_SAKA_OFFSET local first_day = gregorian_leap_year(greg_year) and 21 or 22 return fixed_from_gregorian(gregorian.gregorian_date(greg_year, gregorian.MARCH, first_day)) end -- Days from Chaitra 1 up to (but not including) the first of month `month`, -- given whether the year is a leap year. local function indian_national_days_before_month(month, leap) local chaitra_length = leap and 31 or 30 if month == 1 then return 0 elseif month <= 6 then -- Chaitra, then (month - 2) full 31-day months (Vaishakha..). return chaitra_length + 31 * (month - 2) else -- Chaitra + five 31-day months + (month - 7) full 30-day months. return chaitra_length + 31 * 5 + 30 * (month - 7) end end --- Fixed date equivalent to the Indian National (reformed Śaka) date `i_date`. -- @tparam table i_date Indian National date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_indian_national(i_date) local year = standard_year(i_date) local month = standard_month(i_date) local day = standard_day(i_date) local leap = gregorian_leap_year(year + INDIAN_SAKA_OFFSET) return indian_national_new_year(year) + indian_national_days_before_month(month, leap) + day - 1 end --- Indian National (reformed Śaka) date {year, month, day} for fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.indian_national_from_fixed(date) -- Chaitra 1 lies in the Gregorian year of `date` unless `date` precedes it, -- in which case the Śaka year began the previous Gregorian year. local year = standard_year(gregorian_from_fixed(date)) - INDIAN_SAKA_OFFSET if date < indian_national_new_year(year) then year = year - 1 end local leap = gregorian_leap_year(year + INDIAN_SAKA_OFFSET) local day_count = date - indian_national_new_year(year) -- 0-based day of year. local chaitra_length = leap and 31 or 30 local month, day if day_count < chaitra_length then month = 1 day = day_count + 1 elseif day_count < chaitra_length + 31 * 5 then local rem = day_count - chaitra_length month = 2 + quotient(rem, 31) day = rem % 31 + 1 else local rem = day_count - chaitra_length - 31 * 5 month = 7 + quotient(rem, 30) day = rem % 30 + 1 end return indian_national_date(year, month, day) end -- === Minguo / Republic of China calendar === -- -- Not from "Calendrical Calculations". -- Source: Republic of China civil calendar (in official use in Taiwan). The -- calendar is the Gregorian calendar with years counted from the founding of the -- Republic in 1912, so ROC year = Gregorian year - 1911. Same calendar as Unicode -- CLDR/ICU `roc`. local ROC_OFFSET = 1911 -- Minguo date constructor. local function roc_date(year, month, day) return {year, month, day} end --- Fixed date equivalent to the Minguo (ROC) date `r_date`. -- @tparam table r_date Minguo date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_roc(r_date) return fixed_from_gregorian(gregorian.gregorian_date( standard_year(r_date) + ROC_OFFSET, standard_month(r_date), standard_day(r_date) )) end --- Minguo (ROC) date {year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.roc_from_fixed(date) local g = gregorian_from_fixed(date) return roc_date(standard_year(g) - ROC_OFFSET, standard_month(g), standard_day(g)) end -- === Thai solar calendar (Suriyakati / Buddhist Era) === -- -- Not from "Calendrical Calculations". -- Source: Thai solar civil calendar as reformed in 1941, when the year was aligned -- to begin on 1 January. It is the Gregorian calendar with years counted in the -- Buddhist Era, so Thai year = Gregorian year + 543. Same calendar as Unicode -- CLDR/ICU `buddhist` (applied proleptically here; the pre-1941 April new year is -- not modelled). local THAI_SOLAR_OFFSET = 543 -- Thai solar date constructor. local function thai_solar_date(year, month, day) return {year, month, day} end --- Fixed date equivalent to the Thai solar (Buddhist Era) date `t_date`. -- @tparam table t_date Thai solar date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_thai_solar(t_date) return fixed_from_gregorian(gregorian.gregorian_date( standard_year(t_date) - THAI_SOLAR_OFFSET, standard_month(t_date), standard_day(t_date) )) end --- Thai solar (Buddhist Era) date {year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.thai_solar_from_fixed(date) local g = gregorian_from_fixed(date) return thai_solar_date(standard_year(g) + THAI_SOLAR_OFFSET, standard_month(g), standard_day(g)) end -- === Shared machinery for Gregorian-locked solar civil calendars === -- -- The revised Bengali and Nanakshahi calendars share the same arithmetic shape: a run -- of leading 31-day months, then 30-day months, with one designated month that gains a -- day in a leap year. They differ in the details (see the specs below), which are -- captured by the `spec` fields. The new year is pinned to a fixed Gregorian month and -- day, so a calendar year is a leap year exactly when the Gregorian February that falls -- inside it — that of Gregorian year (year + offset + 1) — is a leap February. That also -- keeps every new year on its fixed Gregorian date and makes the conversions exact -- inverses. -- -- A `spec` is {offset, new_year_month, new_year_day, long_months, leap_month, -- leap_month_base}: `long_months` leading months of 31 days; every other month has 30 -- days except `leap_month`, which has `leap_month_base` days (`leap_month_base + 1` in a -- leap year). -- Fixed date of the first day of the year for `year` under `spec`. local function solar_civil_new_year(spec, year) return fixed_from_gregorian(gregorian.gregorian_date( year + spec.offset, spec.new_year_month, spec.new_year_day)) end -- True if `year` is a leap year under `spec`. local function solar_civil_leap(spec, year) return gregorian_leap_year(year + spec.offset + 1) end -- Length of month `month` under `spec`, given whether the year is a leap year. local function solar_civil_month_length(spec, month, leap) if month <= spec.long_months then return 31 elseif month == spec.leap_month then return spec.leap_month_base + (leap and 1 or 0) else return 30 end end -- Fixed date from a {year, month, day} date under `spec`. local function fixed_from_solar_civil(spec, date) local year = standard_year(date) local month = standard_month(date) local leap = solar_civil_leap(spec, year) local days_before = 0 for m = 1, month - 1 do days_before = days_before + solar_civil_month_length(spec, m, leap) end return solar_civil_new_year(spec, year) + days_before + standard_day(date) - 1 end -- {year, month, day} under `spec` from a fixed `date`. local function solar_civil_from_fixed(spec, date) local year = standard_year(gregorian_from_fixed(date)) - spec.offset if date < solar_civil_new_year(spec, year) then year = year - 1 end local leap = solar_civil_leap(spec, year) local day_count = date - solar_civil_new_year(spec, year) -- 0-based day of year. local month = 1 local len = solar_civil_month_length(spec, month, leap) while day_count >= len do day_count = day_count - len month = month + 1 len = solar_civil_month_length(spec, month, leap) end return {year, month, day_count + 1} end -- === Revised Bengali calendar (Bangladesh) === -- -- Not from "Calendrical Calculations". -- Source: the Bangladesh national calendar as revised by the Bangla Academy (2019). -- Boishakh 1 (Pohela Boishakh) is fixed to 14 April; months 1–6 (Boishakh–Ashwin) -- have 31 days and months 7–12 have 30 days, except Falgun (month 11) which has 29 days -- (30 in a leap year). This keeps 21 February on 8 Falgun (Ekushey) and 16 December on -- 1 Poush. The Bengali year (Bangabda) is the Gregorian year − 593 from Boishakh 1 -- onward. See (National calendar -- of Bangladesh). local BENGALI_REVISED_SPEC = { offset = 593, new_year_month = gregorian.APRIL, new_year_day = 14, long_months = 6, -- Boishakh–Ashwin have 31 days. leap_month = 11, -- Falgun. leap_month_base = 29, -- Falgun has 29 days (30 in a leap year). } --- Fixed date equivalent to the revised Bengali date `b_date`. -- @tparam table b_date Revised Bengali date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_bengali_revised(b_date) return fixed_from_solar_civil(BENGALI_REVISED_SPEC, b_date) end --- Revised Bengali date {year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.bengali_revised_from_fixed(date) return solar_civil_from_fixed(BENGALI_REVISED_SPEC, date) end -- === Nanakshahi calendar (Sikh) === -- -- Not from "Calendrical Calculations". -- Source: the Nanakshahi calendar designed by Pal Singh Purewal and adopted by the -- Shiromani Gurdwara Parbandhak Committee in 2003 (original tropical-solar version). -- Chet 1 is fixed to 14 March; months 1–5 (Chet–Sawan) have 31 days, months 6–12 -- have 30 days, and Phagun (month 12) gains a day in a leap year. The Nanakshahi -- year is the Gregorian year − 1468 from Chet 1 onward (era from Guru Nanak's birth, -- 1469 CE). See . local NANAKSHAHI_SPEC = { offset = 1468, new_year_month = gregorian.MARCH, new_year_day = 14, long_months = 5, -- Chet–Sawan have 31 days. leap_month = 12, -- Phagun. leap_month_base = 30, -- Phagun has 30 days (31 in a leap year). } --- Fixed date equivalent to the Nanakshahi date `n_date`. -- @tparam table n_date Nanakshahi date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_nanakshahi(n_date) return fixed_from_solar_civil(NANAKSHAHI_SPEC, n_date) end --- Nanakshahi date {year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.nanakshahi_from_fixed(date) return solar_civil_from_fixed(NANAKSHAHI_SPEC, date) end -- === Amazigh (Berber) calendar === -- -- Not from "Calendrical Calculations". -- Source: the North African agrarian calendar, which is the Julian calendar with -- year numbering offset by 950 (e.g. Gregorian 1950 → Berber year 2900). Month and -- day are exactly those of the Julian calendar; only the year label differs, so this -- is a thin wrapper over `calendrica-julian`. See -- . local AMAZIGH_OFFSET = 950 --- Fixed date equivalent to the Amazigh (Berber) date `a_date`. -- @tparam table a_date Amazigh date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_amazigh(a_date) return julian.fixed_from_julian(julian.julian_date( standard_year(a_date) - AMAZIGH_OFFSET, standard_month(a_date), standard_day(a_date))) end --- Amazigh (Berber) date {year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.amazigh_from_fixed(date) local j = julian.julian_from_fixed(date) return {standard_year(j) + AMAZIGH_OFFSET, standard_month(j), standard_day(j)} end -- === Japanese era (nengō / wareki) calendar === -- -- Not from "Calendrical Calculations". The modern Japanese civil calendar: the -- Gregorian calendar with years counted within the reign era (nengō) of the current -- emperor. Months and days are Gregorian; only the year is expressed as an era name -- plus the year within that era (the first year of an era is year 1, *gannen*). -- -- The model mirrors current Unicode CLDR/ICU: only the modern gengō eras (Meiji onward) -- are defined, and earlier dates inherit the Gregorian eras (`inheritEras` → gregorian). -- The historical pre-Meiji eras were deliberately removed from CLDR/ICU: applied to a -- proleptic Gregorian timeline they are anachronistic (Japan used a lunisolar calendar -- before 1873) and of dubious accuracy (see CLDR-11400 and ICU-23341). For the historical -- Japanese calendar, which was lunisolar, use the Japanese calendar in -- `calendrica-chinese` instead. -- -- Accordingly the `era` field is, on or after 1868-10-23, one of `"meiji"`, `"taisho"`, -- `"showa"`, `"heisei"`, `"reiwa"`; before that it is the inherited Gregorian era `"ce"` -- (with the Gregorian year) or `"bce"` (year counted backwards, 1 BCE = Gregorian year 0). -- -- References: Japanese era names ; -- ICU calendar guide ; -- era simplification and -- . local JAPANESE_CE = "ce" local JAPANESE_BCE = "bce" -- Modern gengō eras and their Gregorian start dates, matching current CLDR/ICU. local JAPANESE_ERA_DEFS = { { id = "meiji", name = "Meiji", start = { 1868, 10, 23 } }, { id = "taisho", name = "Taishō", start = { 1912, 7, 30 } }, { id = "showa", name = "Shōwa", start = { 1926, 12, 25 } }, { id = "heisei", name = "Heisei", start = { 1989, 1, 8 } }, { id = "reiwa", name = "Reiwa", start = { 2019, 5, 1 } }, } -- Build derived fields (start fixed date and start Gregorian year) and a lookup. local JAPANESE_ERAS = {} local JAPANESE_ERA_BY_ID = {} for i, def in ipairs(JAPANESE_ERA_DEFS) do local era = { id = def.id, name = def.name, start_year = def.start[1], start_fixed = fixed_from_gregorian( gregorian.gregorian_date(def.start[1], def.start[2], def.start[3])), } JAPANESE_ERAS[i] = era JAPANESE_ERA_BY_ID[def.id] = era end local JAPANESE_ERA_NAMES = { [JAPANESE_CE] = "CE", [JAPANESE_BCE] = "BCE" } for _, era in ipairs(JAPANESE_ERAS) do JAPANESE_ERA_NAMES[era.id] = era.name end -- Construct a Japanese era date {era, year, month, day}. local function japanese_era_date(era, year, month, day) return { era, year, month, day } end -- Gregorian year for era `era_id` at era-year `year`. local function japanese_gregorian_year_of(era_id, year) if era_id == JAPANESE_CE then return year elseif era_id == JAPANESE_BCE then return 1 - year else local era = JAPANESE_ERA_BY_ID[era_id] or error("unknown Japanese era: " .. tostring(era_id), 3) return era.start_year + year - 1 end end --- Fixed date equivalent to the Japanese era date `j_date`. -- `j_date` is `{era, year, month, day}`, where `era` is a gengō era id (`"meiji"`, -- `"taisho"`, `"showa"`, `"heisei"`, `"reiwa"`) or an inherited Gregorian era (`"ce"`, -- `"bce"`), `year` is the year within the era (the first year is 1), and `month`/`day` -- are Gregorian. -- @tparam table j_date Japanese era date {era, year, month, day}. -- @treturn number Fixed date. -- @raise If the era identifier is unknown. function M.fixed_from_japanese_era(j_date) return fixed_from_gregorian(gregorian.gregorian_date( japanese_gregorian_year_of(j_date[1], j_date[2]), j_date[3], j_date[4])) end --- Japanese era date {era, year, month, day} corresponding to fixed `date`. -- Dates before the Meiji era use the inherited Gregorian eras `"ce"`/`"bce"`. -- @tparam number date Fixed date. -- @treturn table {era, year, month, day} function M.japanese_era_from_fixed(date) local g = gregorian_from_fixed(date) local month = standard_month(g) local day = standard_day(g) for i = #JAPANESE_ERAS, 1, -1 do if date >= JAPANESE_ERAS[i].start_fixed then return japanese_era_date( JAPANESE_ERAS[i].id, standard_year(g) - JAPANESE_ERAS[i].start_year + 1, month, day) end end -- Before Meiji: inherit the Gregorian era. local greg_year = standard_year(g) if greg_year >= 1 then return japanese_era_date(JAPANESE_CE, greg_year, month, day) else return japanese_era_date(JAPANESE_BCE, 1 - greg_year, month, day) end end --- Ordered list of modern gengō eras. -- Each entry is `{id, name, start_year, start_fixed}`. M.japanese_eras = JAPANESE_ERAS --- Display name for Japanese era identifier `era_id`, or `nil` if unknown. -- Covers the gengō eras plus the inherited `"ce"` and `"bce"`. -- @tparam string era_id Era identifier. -- @treturn string|nil Display name, e.g. `"Reiwa"`, `"CE"`. function M.japanese_era_name(era_id) return JAPANESE_ERA_NAMES[era_id] end return M