calendaryjs-plugin-lunar
v0.3.0
Published
Lunar (lunisolar) calendar plugin for calendaryjs — solar ↔ lunar date conversion (1900–2100) and lunar recurring events using the standard astronomical lunisolar table.
Maintainers
Readme
calendaryjs-plugin-lunar
New to calendaryjs? Start with the core README — this plugin builds on its engine and builder.
Adds the East Asian lunisolar calendar to calendaryjs: declare events by lunar
date, and convert solar ↔ lunar (1900–2100) with leap months handled. Ships two
national variants — Chinese (default, the published almanac table) and
Vietnamese (âm lịch, astronomical at UTC+7) — selected like an
Intl/Temporal calendar id.
Install
npm i calendaryjs calendaryjs-plugin-lunarUse it
Register the plugin, then declare lunar events with the lunar.date() selector — the
engine converts them to solar dates automatically:
import { calendary } from "calendaryjs";
import { every } from "calendaryjs/builder";
import { lunar } from "calendaryjs-plugin-lunar";
const cal = calendary().use(lunar());
cal.addGroup({
id: "lunar-holidays",
events: [
every("year").on(lunar.date(1, 1)).title("Lunar New Year"),
every("year").on(lunar.date(8, 15)).title("Mid-Autumn Festival"),
],
});
cal.getEventsInRange("2025-01-01", "2025-12-31"); // → solar datesPrefer raw config? The selector compiles to a plain lunar event:
{ type: "lunar", id: "tet", lunarMonth: 1, lunarDay: 1, title: "Lunar New Year" }Calendar variants — Chinese vs Vietnamese
The Chinese calendar is computed at 120°E (UTC+8), the Vietnamese one at 105°E (UTC+7). They are distinct national standards, not timezones: whenever a new moon falls between 23:00 and 24:00 Vietnam time, the Vietnamese month starts a day earlier — occasionally moving Tết (1968, 1985, 2007, 2030) or a leap-month boundary (1984, 1985, 1987, 1995, 2031).
The default is "chinese" (the standard almanac table). Opt into Vietnamese
âm lịch per plugin instance — it applies to events and day enrichment alike:
const cal = calendary().use(lunar({ calendar: "vietnamese" }));
// 2019-03-17 is 12/2 âm lịch (Vietnam) but 11/2 in the Chinese calendar:
solarToLunar({ year: 2019, month: 3, day: 17 }, { calendar: "vietnamese" });
// → { year: 2019, month: 2, day: 12, isLeapMonth: false }
solarToLunar({ year: 2019, month: 3, day: 17 });
// → { year: 2019, month: 2, day: 11, isLeapMonth: false }The Vietnamese variant uses the Ho Ngoc Duc astronomical algorithm — the de-facto Vietnamese standard. The Chinese variant keeps the published table (deliberately not "the same algorithm at UTC+8", which would drift from the official Chinese calendar on 155 days in 1900–2100).
Sharing events? Pin the calendar on the event. The instance option is your
app's default; a stored/shared event can carry its own calendar field, which
wins over the instance default — so a .cdy collection is self-describing
and renders the same dates for every consumer:
// Builder: pin per event
every("year")
.on(lunar.date(2, 12, { calendar: "vietnamese" }))
.title("Giỗ bà");// .cdy — self-describing, regardless of how the consumer registers lunar()
{
"plugins": ["calendaryjs-plugin-lunar"],
"events": [
{
"type": "lunar",
"id": "gio-ba",
"title": "Giỗ bà",
"lunarMonth": 2,
"lunarDay": 12,
"calendar": "vietnamese"
}
]
}Convert dates
Every conversion takes an optional { calendar } as the last argument
(default "chinese"):
import { lunarToSolar, solarToLunar } from "calendaryjs-plugin-lunar";
lunarToSolar({ year: 2025, month: 1, day: 1, isLeapMonth: false });
// → { year: 2025, month: 1, day: 29 }
solarToLunar({ year: 2025, month: 1, day: 29 }, { calendar: "vietnamese" });
// → { year: 2025, month: 1, day: 1, isLeapMonth: false }Leap months
In a leap year a lunar month repeats (2023 has a leap 2nd month — two “month 2”s).
Events resolve to the regular month by default; pass { leap: true } (raw:
isLeapMonth: true) to prefer the leap month. In years without it the event falls
back to the regular month, matching Temporal's monthCode.
every("year")
.on(lunar.date(2, 15, { leap: true }))
.title("Leap 2/15");Working from a known solar date (a death date for a giỗ, a birthday)? Let
lunar.fromSolar compute the flag instead of guessing it:
lunar.fromSolar(new Date(2023, 3, 5)); // Apr 5, 2023
// → { lunarMonth: 2, lunarDay: 15, isLeapMonth: true }Reference
Event type lunar — lunarMonth (1–12) · lunarDay (1–30) · isLeapMonth?
(prefer the leap month; see above) · calendar? (pin the variant per event —
wins over the instance default), plus every standard
event property.
Plugin options — enrichDays?: boolean · calendar?: "chinese" | "vietnamese"
(default "chinese").
Exports — lunar() · lunar.date(month, day, { leap?, calendar? }) ·
lunar.fromSolar(date, { calendar? }) · lunarToSolar · solarToLunar ·
isValidLunarDate (each takes an optional { calendar }).
License
MIT. This plugin requires the calendaryjs core, which is licensed
PolyForm Noncommercial — so commercial use of the combined work requires a
commercial license for the core. See Commercial licensing.
