Skip to content

calendaryjs-plugin-lunarv0.3.0

Solar ↔ lunar date conversion (1900–2100) plus lunar recurring events. Ships two national variants of the East Asian lunisolar calendar — Chinese (default) and Vietnamese (âm lịch) — selected like an Intl/Temporal calendar id.

Terminal window
npm i calendaryjs calendaryjs-plugin-lunar

The plugin adds a lunar.date(month, day) builder selector:

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"),
],
});

lunar.date(month, day) takes lunarMonth (1–12) and lunarDay (1–30) and compiles to a plain lunar event ({ type: "lunar", lunarMonth, lunarDay }) — the storage form. Pass { leap: true } to target a leap month.

Calendar variants — Chinese vs Vietnamese

Section titled “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". Opt into Vietnamese âm lịch per plugin instance — it applies to lunar 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 }

The Vietnamese variant uses the Ho Ngoc Duc astronomical algorithm (the de-facto Vietnamese standard); the Chinese variant keeps the published almanac table — deliberately not “one algorithm at UTC+8”, which would drift from the official Chinese calendar on 155 days in 1900–2100.

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 }

In a leap year a lunar month repeats — 2023 has a leap 2nd month, so 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");

Have a known solar date (a death date for a giỗ, a birthday)? lunar.fromSolar computes the flag for you:

import { lunar } from "calendaryjs-plugin-lunar";
lunar.fromSolar(new Date(2023, 3, 5)); // Apr 5, 2023
// → { lunarMonth: 2, lunarDay: 15, isLeapMonth: true }
// Pin the variant when your source date is Vietnamese:
lunar.fromSolar(deathDate, { calendar: "vietnamese" });

Opt in with enrichDays: true and every day from getDay() / getDays() carries a lunar object (computed with the instance’s calendar variant):

const cal = calendary().use(lunar({ enrichDays: true, calendar: "vietnamese" }));
cal.getDay("2025-01-29").lunar;
// → { year: 2025, month: 1, day: 1, isLeapMonth: false } — mùng 1 Tết

Plugin optionsenrichDays?: boolean (default false) · calendar?: "chinese" | "vietnamese" (default "chinese" — the instance-wide default; a per-event calendar field wins over it).

Event type lunar

Field Type Meaning
lunarMonth 1–12 Lunar month
lunarDay 1–30 Lunar day
isLeapMonth boolean? Prefer the leap month (falls back when the year has none)
calendar "chinese" | "vietnamese"? Pin the variant per event — wins over the instance default

…plus every standard event property.

Exportslunar() · lunar.date(month, day, { leap?, calendar? }) · lunar.fromSolar(date, { calendar? }) · solarToLunar · lunarToSolar · isValidLunarDate (each conversion takes an optional { calendar }).