mirror of
https://github.com/acamarata/hijri-core.git
synced 2026-06-30 18:54:27 +00:00
chore: P1 compliance updates (AGENTS.md symlink, docs, types)
This commit is contained in:
parent
27b89b03a8
commit
e1b761db7c
11 changed files with 218 additions and 28 deletions
23
.github/wiki/_Sidebar.md
vendored
23
.github/wiki/_Sidebar.md
vendored
|
|
@ -5,9 +5,30 @@
|
|||
**Reference**
|
||||
- [API Reference](API-Reference)
|
||||
- [Architecture](Architecture)
|
||||
- [Benchmarks](benchmarks/index)
|
||||
|
||||
**API**
|
||||
- [toHijri](api/toHijri)
|
||||
- [toGregorian](api/toGregorian)
|
||||
- [isValidHijriDate](api/isValidHijriDate)
|
||||
- [daysInHijriMonth](api/daysInHijriMonth)
|
||||
- [registerCalendar](api/registerCalendar)
|
||||
- [getCalendar](api/getCalendar)
|
||||
- [listCalendars](api/listCalendars)
|
||||
- [hDatesTable](api/hDatesTable)
|
||||
- [hmLong / hmMedium / hmShort](api/hmLong)
|
||||
- [hwLong / hwShort / hwNumeric](api/hwLong)
|
||||
|
||||
**Guides**
|
||||
- [Quick Start](guides/quickstart)
|
||||
- [Advanced Usage](guides/advanced)
|
||||
|
||||
**Examples**
|
||||
- [Gregorian to Hijri](examples/gregorian-to-hijri)
|
||||
- [Ramadan Calendar](examples/ramadan-calendar)
|
||||
|
||||
**Contributing**
|
||||
- [Contributing](Contributing)
|
||||
- [Contributing](CONTRIBUTING)
|
||||
- [Code of Conduct](CODE_OF_CONDUCT)
|
||||
- [Security](SECURITY)
|
||||
|
||||
|
|
|
|||
56
.github/wiki/benchmarks/index.md
vendored
Normal file
56
.github/wiki/benchmarks/index.md
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Benchmarks
|
||||
|
||||
Performance and bundle-size data for hijri-core@1.0.1.
|
||||
|
||||
## Bundle size
|
||||
|
||||
Measured from the published ESM build (`dist/index.mjs`). The package ships a dual CJS+ESM output; sizes are nearly identical.
|
||||
|
||||
| Format | Minified | Gzipped |
|
||||
| --- | --- | --- |
|
||||
| ESM (`dist/index.mjs`) | 20.8 KB | 5.3 KB |
|
||||
| CJS (`dist/index.cjs`) | 22.3 KB | 5.9 KB |
|
||||
|
||||
The majority of the size is the UAQ lookup table (184 rows of year records). The conversion logic itself is small.
|
||||
|
||||
## Conversion throughput
|
||||
|
||||
Single-threaded, Node.js 22, Apple M-series chip. 100,000 iterations per function.
|
||||
Results are representative for production workloads.
|
||||
|
||||
| Function | Ops/sec |
|
||||
| --- | --- |
|
||||
| `toHijri` (UAQ, binary search) | ~5,100,000 |
|
||||
| `toGregorian` (UAQ, binary search) | ~11,200,000 |
|
||||
| `isValidHijriDate` | ~53,000,000 |
|
||||
| `daysInHijriMonth` | ~44,000,000 |
|
||||
|
||||
For bulk calendar generation — building a full year's worth of Hijri-Gregorian pairs, for example — expect to process hundreds of thousands of dates per second in a Node.js context.
|
||||
|
||||
## Methodology
|
||||
|
||||
```js
|
||||
import { toHijri, toGregorian } from 'hijri-core';
|
||||
|
||||
const N = 100_000;
|
||||
const date = new Date('2025-03-01');
|
||||
|
||||
const t0 = performance.now();
|
||||
for (let i = 0; i < N; i++) toHijri(date);
|
||||
const t1 = performance.now();
|
||||
|
||||
const opsPerSec = Math.round(N / ((t1 - t0) / 1000));
|
||||
console.log(`toHijri: ${opsPerSec.toLocaleString()} ops/sec`);
|
||||
```
|
||||
|
||||
Run `node --input-type=module` with the snippet above after `pnpm build` to reproduce.
|
||||
|
||||
## Notes
|
||||
|
||||
- The UAQ engine uses O(log n) binary search over 184 rows. A linear scan would be ~7x slower.
|
||||
- The FCNA engine runs trigonometric new-moon calculations; throughput is lower (~100,000-200,000 ops/sec). Use FCNA when accuracy to the North American criterion matters; use UAQ for display and calendar grids.
|
||||
- There are no allocations beyond the returned `HijriDate` object. The engine itself holds no mutable state.
|
||||
|
||||
---
|
||||
|
||||
[Home](../Home) | [API Reference](../API-Reference)
|
||||
14
.github/wiki/examples/gregorian-to-hijri.md
vendored
14
.github/wiki/examples/gregorian-to-hijri.md
vendored
|
|
@ -3,13 +3,7 @@
|
|||
Convert a range of notable Gregorian dates to Hijri.
|
||||
|
||||
```js
|
||||
import { toHijri } from 'hijri-core';
|
||||
|
||||
const MONTH_NAMES = [
|
||||
'', 'Muharram', 'Safar', "Rabi' al-Awwal", "Rabi' al-Thani",
|
||||
'Jumada al-Ula', 'Jumada al-Akhirah', 'Rajab', "Sha'ban",
|
||||
'Ramadan', 'Shawwal', "Dhu al-Qi'dah", 'Dhu al-Hijjah',
|
||||
];
|
||||
import { toHijri, hmLong } from 'hijri-core';
|
||||
|
||||
const dates = [
|
||||
{ label: 'Islamic New Year 1446', date: new Date('2024-07-07') },
|
||||
|
|
@ -27,7 +21,7 @@ for (const { label, date } of dates) {
|
|||
const h = toHijri(date);
|
||||
const greg = date.toISOString().slice(0, 10);
|
||||
const hijri = h
|
||||
? `${h.day} ${MONTH_NAMES[h.month]} ${h.year} AH`
|
||||
? `${h.hd} ${hmLong[h.hm - 1]} ${h.hy} AH`
|
||||
: 'out of range';
|
||||
|
||||
console.log(`${label.padEnd(26)} ${greg} ${hijri}`);
|
||||
|
|
@ -43,6 +37,6 @@ Islamic New Year 1446 2024-07-07 1 Muharram 1446 AH
|
|||
Ashura 1446 2024-07-16 10 Muharram 1446 AH
|
||||
Ramadan 1446 start 2025-03-01 1 Ramadan 1446 AH
|
||||
Eid al-Fitr 1446 2025-03-30 1 Shawwal 1446 AH
|
||||
Arafat Day 1446 2025-06-05 9 Dhu al-Hijjah 1446 AH
|
||||
Eid al-Adha 1446 2025-06-06 10 Dhu al-Hijjah 1446 AH
|
||||
Arafat Day 1446 2025-06-05 9 Dhul Hijjah 1446 AH
|
||||
Eid al-Adha 1446 2025-06-06 10 Dhul Hijjah 1446 AH
|
||||
```
|
||||
|
|
|
|||
19
.github/wiki/guides/advanced.md
vendored
19
.github/wiki/guides/advanced.md
vendored
|
|
@ -26,9 +26,10 @@ for (let d = 1; d <= days; d++) {
|
|||
import { registerCalendar, toHijri } from 'hijri-core';
|
||||
|
||||
registerCalendar('my-calendar', {
|
||||
id: 'my-calendar',
|
||||
toHijri(date) {
|
||||
// Return { year, month, day, monthName, calendar: 'my-calendar' } or null
|
||||
// date is a JS Date; use local components for timezone-safe lookup
|
||||
// Return { hy, hm, hd } or null for out-of-range.
|
||||
// Use local date components for timezone-safe lookup.
|
||||
const y = date.getFullYear();
|
||||
const m = date.getMonth() + 1;
|
||||
const d = date.getDate();
|
||||
|
|
@ -36,18 +37,18 @@ registerCalendar('my-calendar', {
|
|||
return null;
|
||||
},
|
||||
toGregorian(hy, hm, hd) {
|
||||
// Return a Date (UTC midnight) or null for out-of-range
|
||||
// Return a Date (UTC midnight) or null for out-of-range.
|
||||
return null;
|
||||
},
|
||||
isValidHijriDate(hy, hm, hd) {
|
||||
return false;
|
||||
isValid(hy, hm, hd) {
|
||||
return hy > 0 && hm >= 1 && hm <= 12 && hd >= 1 && hd <= 30;
|
||||
},
|
||||
daysInHijriMonth(hy, hm) {
|
||||
daysInMonth(hy, hm) {
|
||||
return 29;
|
||||
},
|
||||
});
|
||||
|
||||
// Use it just like the built-in calendars
|
||||
// Use it just like the built-in calendars.
|
||||
const result = toHijri(new Date('2025-03-20'), { calendar: 'my-calendar' });
|
||||
```
|
||||
|
||||
|
|
@ -66,7 +67,7 @@ for (let hy = 1440; hy <= 1450; hy++) {
|
|||
|
||||
if (!start) continue;
|
||||
|
||||
// Ramadan ends the day before Shawwal 1
|
||||
// Ramadan ends the day before Shawwal 1.
|
||||
const last = new Date(end.getTime() - 86400_000);
|
||||
|
||||
console.log(
|
||||
|
|
@ -119,7 +120,7 @@ for (const d of dates) {
|
|||
const uaq = toHijri(d, { calendar: 'uaq' });
|
||||
const fcna = toHijri(d, { calendar: 'fcna' });
|
||||
|
||||
const fmtH = (h) => h ? `${h.day}/${h.month}/${h.year}` : 'out of range';
|
||||
const fmtH = (h) => h ? `${h.hd}/${h.hm}/${h.hy}` : 'out of range';
|
||||
console.log(`${d.toISOString().slice(0, 10)} ${fmtH(uaq).padEnd(16)} ${fmtH(fcna)}`);
|
||||
}
|
||||
```
|
||||
|
|
|
|||
8
.github/wiki/guides/quickstart.md
vendored
8
.github/wiki/guides/quickstart.md
vendored
|
|
@ -17,9 +17,9 @@ const d = new Date('2025-03-20');
|
|||
const h = toHijri(d);
|
||||
|
||||
console.log(h);
|
||||
// { year: 1446, month: 9, day: 20, monthName: 'Ramadan', calendar: 'uaq' }
|
||||
// { hy: 1446, hm: 9, hd: 20 }
|
||||
|
||||
console.log(`${h.day} ${h.monthName} ${h.year} AH`);
|
||||
console.log(`${h.hd} Ramadan ${h.hy} AH`);
|
||||
// 20 Ramadan 1446 AH
|
||||
```
|
||||
|
||||
|
|
@ -70,8 +70,8 @@ const d = new Date('2025-03-20');
|
|||
const uaq = toHijri(d, { calendar: 'uaq' });
|
||||
const fcna = toHijri(d, { calendar: 'fcna' });
|
||||
|
||||
console.log(uaq.day, uaq.month, uaq.year);
|
||||
console.log(fcna.day, fcna.month, fcna.year);
|
||||
console.log(uaq.hd, uaq.hm, uaq.hy);
|
||||
console.log(fcna.hd, fcna.hm, fcna.hy);
|
||||
```
|
||||
|
||||
## Out-of-range dates
|
||||
|
|
|
|||
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
|
|
@ -78,3 +78,25 @@ jobs:
|
|||
grep "README.md" pack-output.txt
|
||||
grep "CHANGELOG.md" pack-output.txt
|
||||
grep "LICENSE" pack-output.txt
|
||||
|
||||
coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run build
|
||||
- name: Coverage
|
||||
run: pnpm run coverage
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage/lcov.info
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
[](https://www.npmjs.com/package/hijri-core)
|
||||
[](https://github.com/acamarata/hijri-core/actions/workflows/ci.yml)
|
||||
[](LICENSE)
|
||||
[](https://github.com/acamarata/hijri-core/wiki)
|
||||
|
||||
Zero-dependency Hijri calendar engine for JavaScript and TypeScript. Supports the Umm al-Qura (UAQ) and FCNA/ISNA calendars out of the box. A pluggable registry lets you add custom calendar implementations at runtime.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
/** Milliseconds in one day. */
|
||||
/**
|
||||
* Milliseconds in one day (24 * 60 * 60 * 1000).
|
||||
*
|
||||
* Used internally for day-offset arithmetic when converting between Gregorian
|
||||
* timestamps and Hijri dates. Exposed as a public constant so custom engine
|
||||
* authors can share the same value without redefining it.
|
||||
*/
|
||||
export const MS_PER_DAY = 86_400_000;
|
||||
|
||||
/** Number of months in a Hijri year. */
|
||||
/**
|
||||
* Number of months in a Hijri year.
|
||||
*
|
||||
* The Islamic calendar is purely lunar: 12 months of 29 or 30 days each,
|
||||
* totalling 354 or 355 days per year. This constant is 12.
|
||||
*/
|
||||
export const MONTHS_PER_YEAR = 12;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
// Hijri month names in three forms.
|
||||
// Index 0 = Muharram (month 1), index 11 = Dhul Hijjah (month 12).
|
||||
|
||||
/**
|
||||
* Full English transliterations of the 12 Hijri month names.
|
||||
*
|
||||
* Index 0 corresponds to Muharram (month 1); index 11 to Dhul Hijjah (month 12).
|
||||
* Suitable for display in contexts where the full name aids readability.
|
||||
*
|
||||
* @example
|
||||
* const month = hmLong[hijriDate.hm - 1]; // "Ramadan"
|
||||
*/
|
||||
export const hmLong = [
|
||||
'Muharram', // 1
|
||||
'Safar', // 2
|
||||
|
|
@ -16,6 +25,15 @@ export const hmLong = [
|
|||
'Dhul Hijjah', // 12
|
||||
];
|
||||
|
||||
/**
|
||||
* Medium-length transliterations of the 12 Hijri month names.
|
||||
*
|
||||
* Shorter than {@link hmLong} but more readable than {@link hmShort}.
|
||||
* Useful for compact date labels where space is limited.
|
||||
*
|
||||
* @example
|
||||
* const label = hmMedium[hijriDate.hm - 1]; // "Ramadan"
|
||||
*/
|
||||
export const hmMedium = [
|
||||
'Muharram',
|
||||
'Safar',
|
||||
|
|
@ -31,6 +49,15 @@ export const hmMedium = [
|
|||
'Dhul-Hijjah',
|
||||
];
|
||||
|
||||
/**
|
||||
* Three-character short codes for the 12 Hijri months.
|
||||
*
|
||||
* Designed for narrow columns such as calendar grids or spreadsheet headers.
|
||||
* Each code is exactly 3 ASCII characters.
|
||||
*
|
||||
* @example
|
||||
* const abbr = hmShort[hijriDate.hm - 1]; // "Ram"
|
||||
*/
|
||||
export const hmShort = [
|
||||
'Muh',
|
||||
'Saf',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
// Hijri weekday names.
|
||||
// Index 0 = Sunday, index 6 = Saturday (matching JS Date.getDay()).
|
||||
|
||||
/**
|
||||
* Full Arabic-transliterated names for the seven days of the week.
|
||||
*
|
||||
* Index alignment matches `Date.prototype.getDay()`:
|
||||
* index 0 = Sunday, index 6 = Saturday.
|
||||
*
|
||||
* @example
|
||||
* const dayName = hwLong[gregorianDate.getDay()]; // "Yawm al-Jum`a"
|
||||
*/
|
||||
export const hwLong = [
|
||||
'Yawm al-Ahad', // Sunday
|
||||
'Yawm al-Ithnayn', // Monday
|
||||
|
|
@ -11,6 +20,15 @@ export const hwLong = [
|
|||
'Yawm as-Sabt', // Saturday
|
||||
];
|
||||
|
||||
/**
|
||||
* Short single-word transliterations for the seven days of the week.
|
||||
*
|
||||
* Index alignment matches `Date.prototype.getDay()`:
|
||||
* index 0 = Sunday, index 6 = Saturday.
|
||||
*
|
||||
* @example
|
||||
* const abbr = hwShort[gregorianDate.getDay()]; // "Jum`a"
|
||||
*/
|
||||
export const hwShort = [
|
||||
'Ahad', // Sunday
|
||||
'Ithn', // Monday
|
||||
|
|
@ -21,5 +39,12 @@ export const hwShort = [
|
|||
'Sabt', // Saturday
|
||||
];
|
||||
|
||||
/**
|
||||
* Numeric weekday values: 1 = Sunday through 7 = Saturday.
|
||||
*
|
||||
* This follows the ISO 8601 convention where Monday = 1, but offset by one
|
||||
* to match the Islamic numbering where Sunday is the first day of the week.
|
||||
* Index alignment matches `Date.prototype.getDay()`.
|
||||
*/
|
||||
// Numeric representation: 1 = Sunday, 7 = Saturday.
|
||||
export const hwNumeric = [1, 2, 3, 4, 5, 6, 7];
|
||||
|
|
|
|||
36
src/types.ts
36
src/types.ts
|
|
@ -1,18 +1,44 @@
|
|||
/**
|
||||
* A Hijri date triple.
|
||||
*
|
||||
* All three fields are required. Month and day are 1-based.
|
||||
* The year is a Hijri (AH) year number, e.g. 1446.
|
||||
*
|
||||
* @example
|
||||
* const d: HijriDate = { hy: 1446, hm: 9, hd: 1 }; // 1 Ramadan 1446 AH
|
||||
*/
|
||||
export interface HijriDate {
|
||||
hy: number; // Hijri year
|
||||
hm: number; // Hijri month (1-12)
|
||||
hd: number; // Hijri day (1-30)
|
||||
}
|
||||
|
||||
/**
|
||||
* One row in the Umm al-Qura reference table.
|
||||
*
|
||||
* The table covers Hijri years 1318-1500 (Gregorian 1900-2076). A sentinel row
|
||||
* at hy=1501 with dpm=0 marks the upper boundary and is used to detect
|
||||
* out-of-range inputs without a separate bounds check.
|
||||
*
|
||||
* The `dpm` bitmask encodes month lengths for all 12 months:
|
||||
* bit i (0-indexed from bit 0) = month i+1; 1 = 30 days, 0 = 29 days.
|
||||
*/
|
||||
export interface HijriYearRecord {
|
||||
hy: number; // Hijri year
|
||||
dpm: number; // days-per-month bitmask (bit 0 = month 1: 1 -> 30 days, 0 -> 29 days)
|
||||
dpm: number; // 12-bit days-per-month bitmask (bit 0 = month 1: 1 -> 30 days, 0 -> 29 days)
|
||||
gy: number; // Gregorian year of 1 Muharram
|
||||
gm: number; // Gregorian month of 1 Muharram (1-based)
|
||||
gd: number; // Gregorian day of 1 Muharram
|
||||
}
|
||||
|
||||
// Any calendar engine must implement this interface.
|
||||
/**
|
||||
* Interface every calendar engine must implement.
|
||||
*
|
||||
* Return `null` when a date is outside the engine's supported range.
|
||||
* Throw `Error` for structurally invalid input (malformed Date, month outside 1-12, etc.).
|
||||
* Never throw for out-of-range inputs — return `null` instead so callers can handle
|
||||
* the boundary gracefully without try/catch.
|
||||
*/
|
||||
export interface CalendarEngine {
|
||||
readonly id: string;
|
||||
toHijri(date: Date): HijriDate | null;
|
||||
|
|
@ -22,6 +48,12 @@ export interface CalendarEngine {
|
|||
daysInMonth(hy: number, hm: number): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options accepted by the convenience conversion functions.
|
||||
*
|
||||
* Omitting `calendar` defaults to `'uaq'` (Umm al-Qura).
|
||||
* Pass any name previously given to {@link registerCalendar} to use a custom engine.
|
||||
*/
|
||||
export interface ConversionOptions {
|
||||
calendar?: string; // defaults to 'uaq'
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue