<p align="center">
  <img src="https://cdn.jsdelivr.net/npm/calendaryjs/assets/logo.svg" alt="calendaryjs" width="380" />
</p>

# calendaryjs-plugin-lunar

> **New to calendaryjs?** Start with the [core README](https://www.npmjs.com/package/calendaryjs) — 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.

<p align="center">
  <a href="https://www.npmjs.com/package/calendaryjs-plugin-lunar"><img alt="npm" src="https://img.shields.io/npm/v/calendaryjs-plugin-lunar?style=for-the-badge&color=f59e0b&labelColor=16120F" /></a>
  <a href="https://socket.dev/npm/package/calendaryjs-plugin-lunar"><img alt="Socket" src="https://img.shields.io/badge/Socket-security-f59e0b?style=for-the-badge&labelColor=16120F" /></a>
  <img alt="Zero dependencies" src="https://img.shields.io/badge/deps-zero-22c55e?style=for-the-badge&labelColor=16120F" />
</p>

## Install

```bash
npm i calendaryjs calendaryjs-plugin-lunar
```

## Use it

Register the plugin, then declare lunar events with the `lunar.date()` selector — the
engine converts them to solar dates automatically:

```ts
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 dates
```

Prefer raw config? The selector compiles to a plain `lunar` event:

```ts
{ 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:

```ts
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:

```ts
// Builder: pin per event
every("year")
  .on(lunar.date(2, 12, { calendar: "vietnamese" }))
  .title("Giỗ bà");
```

```json
// .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"`):

```ts
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`.

```ts
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:

```ts
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](https://www.npmjs.com/package/calendaryjs#event-properties).

**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`](https://www.npmjs.com/package/calendaryjs) core, which is licensed
PolyForm Noncommercial — so **commercial use of the combined work requires a
commercial license for the core**. See [Commercial licensing](https://calendaryjs.dev/license/).
