diff --git a/.github/wiki/_Sidebar.md b/.github/wiki/_Sidebar.md index 0e9d49e..c3bfc02 100644 --- a/.github/wiki/_Sidebar.md +++ b/.github/wiki/_Sidebar.md @@ -10,6 +10,12 @@ **Reference** - [API Reference](API-Reference) +- [installHijri](api/installHijri) +- [toHijri](api/toHijri) +- [fromHijri](api/fromHijri) +- [formatHijri](api/formatHijri) +- [hijriYear / hijriMonth / hijriDay](api/hijriYear-hijriMonth-hijriDay) +- [isValidHijri](api/isValidHijri) - [Architecture](Architecture) - [Benchmarks](benchmarks/index) diff --git a/.github/wiki/api/functions/default.md b/.github/wiki/api/functions/default.md index 82b356c..033f092 100644 --- a/.github/wiki/api/functions/default.md +++ b/.github/wiki/api/functions/default.md @@ -8,7 +8,7 @@ > **default**(`momentInstance`): `void` -Defined in: [src/index.ts:150](https://github.com/acamarata/moment-hijri-plus/blob/50b666503568cb2ef81d0a7fc2aeefa6cb395aa5/src/index.ts#L150) +Defined in: [src/index.ts:150](https://github.com/acamarata/moment-hijri-plus/blob/b96b21a86195492a30c50860eaa4dcadad9946ab/src/index.ts#L150) Install the Hijri plugin into the provided Moment.js instance. diff --git a/.github/wiki/examples/basic-usage.md b/.github/wiki/examples/basic-usage.md index c7a6bdc..00e023f 100644 --- a/.github/wiki/examples/basic-usage.md +++ b/.github/wiki/examples/basic-usage.md @@ -1,10 +1,10 @@ -# Basic Usage Examples +# Basic Usage ## Setup ```typescript import moment from 'moment'; -import { installHijri } from 'moment-hijri-plus'; +import installHijri from 'moment-hijri-plus'; installHijri(moment); ``` @@ -14,7 +14,7 @@ installHijri(moment); ```typescript const today = moment(); const h = today.toHijri(); -// Returns null if date is outside UAQ range; guard before using +// Returns null if date is outside the UAQ range; guard before use. if (h !== null) { console.log(`${h.hd} / ${h.hm} / ${h.hy}`); @@ -27,16 +27,16 @@ if (h !== null) { // 23 March 2023 = 1 Ramadan 1444 AH const m = moment('2023-03-23'); -console.log(m.iYear()); // 1444 -console.log(m.iMonth()); // 9 (Ramadan is the 9th month) -console.log(m.iDate()); // 1 +console.log(m.hijriYear()); // 1444 +console.log(m.hijriMonth()); // 9 (Ramadan is the 9th month) +console.log(m.hijriDay()); // 1 ``` ## Convert from Hijri to Gregorian ```typescript const gregorian = moment.fromHijri(1444, 9, 1); -console.log(gregorian.format('YYYY-MM-DD')); // '2023-03-23' +console.log(gregorian.format('YYYY-MM-DD')); // '2023-03-23' ``` ## Format with Hijri tokens @@ -44,10 +44,10 @@ console.log(gregorian.format('YYYY-MM-DD')); // '2023-03-23' ```typescript const m = moment('2023-03-23'); -m.format('iD iMMMM iYYYY'); // '1 Ramadan 1444' -m.format('iDD/iMM/iYYYY'); // '01/09/1444' -m.format('YYYY-MM-DD'); // '2023-03-23' (Gregorian tokens still work) -m.format('YYYY (iYYYY/iM/iD)'); // '2023 (1444/9/1)' +m.formatHijri('iD iMMMM iYYYY'); // '1 Ramadan 1444' +m.formatHijri('iDD/iMM/iYYYY'); // '01/09/1444' +m.formatHijri('YYYY-MM-DD'); // no Hijri tokens; passes through to moment.format +m.formatHijri('YYYY (iYYYY/iM/iD)'); // '2023 (1444/9/1)' ``` ## Use FCNA calendar @@ -55,21 +55,21 @@ m.format('YYYY (iYYYY/iM/iD)'); // '2023 (1444/9/1)' ```typescript const m = moment('2023-03-23'); -const uaqYear = m.iYear(); // UAQ (default) -const fcnaYear = m.iYear({ calendar: 'fcna' }); // FCNA +const uaqYear = m.hijriYear(); // UAQ (default) +const fcnaYear = m.hijriYear({ calendar: 'fcna' }); // FCNA console.log(uaqYear, fcnaYear); -// Near month boundaries, UAQ and FCNA may differ by one day +// Near month boundaries, UAQ and FCNA may differ by one day. ``` ## CJS usage ```javascript const moment = require('moment'); -const { installHijri } = require('moment-hijri-plus'); +const installHijri = require('moment-hijri-plus').default; installHijri(moment); const m = moment('2023-03-23'); -console.log(m.iYear()); // 1444 +console.log(m.hijriYear()); // 1444 ``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5743612..4159d1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index 0f099b3..33ae12e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ [](https://github.com/acamarata/moment-hijri-plus/actions/workflows/ci.yml) [](https://opensource.org/licenses/MIT) -Moment.js plugin for Hijri calendar conversion and formatting. Delegates all calendar logic to [hijri-core](https://github.com/acamarata/hijri-core), a zero-dependency Hijri engine with pluggable calendar support. +Moment.js plugin for Hijri calendar conversion and formatting. Delegates all calendar +logic to [hijri-core](https://github.com/acamarata/hijri-core), a zero-dependency Hijri +engine with pluggable calendar support (Umm al-Qura and FCNA/ISNA). ## Installation @@ -12,148 +14,55 @@ Moment.js plugin for Hijri calendar conversion and formatting. Delegates all cal pnpm add moment moment-hijri-plus hijri-core ``` -Both `moment` and `hijri-core` are peer dependencies and must be installed alongside this package. +Both `moment` and `hijri-core` are peer dependencies. ## Quick Start -```javascript +```typescript import moment from 'moment'; import installHijri from 'moment-hijri-plus'; -// Install the plugin once at startup. installHijri(moment); -// Convert a Gregorian date to Hijri. const m = moment(new Date(2023, 2, 23)); // 23 March 2023 -const hijri = m.toHijri(); -// => { hy: 1444, hm: 9, hd: 1 } (1 Ramadan 1444 AH) +m.toHijri(); // { hy: 1444, hm: 9, hd: 1 } (1 Ramadan 1444 AH) +m.formatHijri('iD iMMMM iYYYY AH'); // '1 Ramadan 1444 AH' -// Format using Hijri tokens. -m.formatHijri('iD iMMMM iYYYY AH'); -// => '1 Ramadan 1444 AH' - -// Construct a moment from a Hijri date. -const start = moment.fromHijri(1446, 1, 1); -// => moment representing 7 July 2024 (1 Muharram 1446 AH) +moment.fromHijri(1446, 1, 1); // moment for 7 July 2024 ``` -## API +## API Summary -### Instance methods +Call `installHijri(moment)` once at startup to add these methods. -All methods are added to `moment.Moment` by calling `installHijri(moment)` once. - -| Method | Signature | Description | +| Method | Returns | Description | | --- | --- | --- | -| `toHijri` | `(options?) => HijriDate \| null` | Convert to Hijri. Returns `null` if the date is outside the calendar range. | -| `hijriYear` | `(options?) => number \| null` | Hijri year, or `null` if out of range. | -| `hijriMonth` | `(options?) => number \| null` | Hijri month (1-12), or `null` if out of range. | -| `hijriDay` | `(options?) => number \| null` | Hijri day, or `null` if out of range. | -| `isValidHijri` | `(options?) => boolean` | `true` if the date falls within the supported Hijri range. | -| `formatHijri` | `(formatStr, options?) => string` | Format using Hijri tokens. Returns `''` if out of range. Non-Hijri tokens pass through to `moment.format()`. | +| `toHijri(options?)` | `HijriDate \| null` | Convert to Hijri date object | +| `hijriYear(options?)` | `number \| null` | Hijri year | +| `hijriMonth(options?)` | `number \| null` | Hijri month (1-12) | +| `hijriDay(options?)` | `number \| null` | Hijri day | +| `isValidHijri(options?)` | `boolean` | True if date is within calendar range | +| `formatHijri(fmt, options?)` | `string` | Format with Hijri tokens; non-Hijri tokens pass through | +| `moment.fromHijri(hy, hm, hd, options?)` | `Moment` | Construct moment from Hijri date | -### Static factory +Pass `{ calendar: 'fcna' }` to switch from the default Umm al-Qura calendar to FCNA/ISNA. -| Method | Signature | Description | -| --- | --- | --- | -| `moment.fromHijri` | `(hy, hm, hd, options?) => Moment` | Create a moment from a Hijri date. Throws if the date is invalid or out of range. | - -### Options - -```typescript -interface ConversionOptions { - calendar?: string; // 'uaq' (default) | 'fcna' -} -``` - -## Calendar Systems - -| ID | Name | Description | -| --- | --- | --- | -| `uaq` | Umm al-Qura | Official calendar of Saudi Arabia. Tabular, covers AH 1318-1500 (1900-2076 CE). Default. | -| `fcna` | FCNA/ISNA | Fiqh Council of North America calculated calendar. | - -Pass the calendar ID via `options`: - -```javascript -m.toHijri({ calendar: 'fcna' }); -moment.fromHijri(1444, 9, 1, { calendar: 'fcna' }); -``` - -## Format Tokens - -`formatHijri()` recognises the following tokens. All other tokens are passed through to `moment.format()`, so you can mix Hijri and Gregorian tokens freely. - -| Token | Example | Description | -| --- | --- | --- | -| `iYYYY` | `1444` | Hijri year, 4 digits | -| `iYY` | `44` | Hijri year, 2 digits | -| `iMMMM` | `Ramadan` | Month long name | -| `iMMM` | `Ramadan` | Month medium name | -| `iMM` | `09` | Month, zero-padded | -| `iM` | `9` | Month, no padding | -| `iDD` | `01` | Day, zero-padded | -| `iD` | `1` | Day, no padding | -| `iEEEE` | `Yawm al-Khamis` | Weekday long name | -| `iEEE` | `Kham` | Weekday short name | -| `iE` | `5` | Weekday numeric (1=Sun, 7=Sat) | -| `ioooo` | `AH` | Era, long | -| `iooo` | `AH` | Era, short | - -### Mixed format example - -```javascript -m.formatHijri('iD iMMMM iYYYY [CE:] MMMM YYYY'); -// => '1 Ramadan 1444 CE: March 2023' -``` - -Bracket escaping (`[...]`) is handled by moment's own formatter for the Gregorian portion. - -## TypeScript - -The plugin augments `moment.Moment` and `moment.MomentStatic` via module declaration merging, so type safety applies after the plugin is installed. No extra imports are needed for the types. - -```typescript -import moment from 'moment'; -import installHijri from 'moment-hijri-plus'; -import type { HijriDate, ConversionOptions } from 'moment-hijri-plus'; - -installHijri(moment); - -const hijri: HijriDate | null = moment().toHijri(); -``` +Full API reference, format token table, and examples are in the +[project wiki](https://github.com/acamarata/moment-hijri-plus/wiki). ## Note on Moment.js -Moment.js is in maintenance mode. The authors recommend Luxon, Day.js, or date-fns for new projects. This package targets existing codebases already using Moment.js. If you are starting a new project, [dayjs-hijri-plus](https://github.com/acamarata/dayjs-hijri-plus) is a compatible alternative that works with Day.js. - -## Architecture - -A thin plugin wrapper over [hijri-core](https://github.com/acamarata/hijri-core). The plugin augments the Moment.js prototype with Hijri methods, each delegating to the registered calendar engine. Zero global state. - -For more detail see the [Architecture wiki page](https://github.com/acamarata/moment-hijri-plus/wiki/Architecture). - -## Documentation - -Full API reference, architecture notes, and calendar algorithm details are in the [project wiki](https://github.com/acamarata/moment-hijri-plus/wiki). +Moment.js is in maintenance mode. For new projects, +[dayjs-hijri-plus](https://github.com/acamarata/dayjs-hijri-plus) offers the same Hijri +support on Day.js. This package targets existing codebases already using Moment.js. ## Related -- [hijri-core](https://github.com/acamarata/hijri-core): zero-dependency Hijri calendar engine used by this plugin -- [luxon-hijri](https://github.com/acamarata/luxon-hijri): same Hijri support for Luxon +- [hijri-core](https://github.com/acamarata/hijri-core): Hijri calendar engine used internally +- [dayjs-hijri-plus](https://github.com/acamarata/dayjs-hijri-plus): same API for Day.js +- [luxon-hijri](https://github.com/acamarata/luxon-hijri): same API for Luxon - [pray-calc](https://github.com/acamarata/pray-calc): Islamic prayer time calculation -## Compatibility - -- Node.js 20, 22, 24 -- Moment.js 2.x (peer dependency) -- ESM and CJS builds included -- TypeScript definitions bundled - -## Acknowledgments - -Calendar data and algorithms provided by [hijri-core](https://github.com/acamarata/hijri-core). The Umm al-Qura table is derived from data published by the King Abdulaziz City for Science and Technology (KACST). FCNA new moon calculations follow Jean Meeus, "Astronomical Algorithms," 2nd ed., Chapter 49. - ## License MIT. Copyright (c) 2026 Aric Camarata. diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..530d1ed --- /dev/null +++ b/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/coverage/lcov-report/favicon.png differ diff --git a/coverage/lcov-report/index.html b/coverage/lcov-report/index.html new file mode 100644 index 0000000..d21189c --- /dev/null +++ b/coverage/lcov-report/index.html @@ -0,0 +1,116 @@ + + + + +
++ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| index.ts | +
+
+ |
+ 92.68% | +152/164 | +58.06% | +18/31 | +100% | +9/9 | +92.68% | +152/164 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 | 1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +7x +7x +7x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +12x +12x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +5x +5x +5x +5x +5x +5x +5x +5x +7x +7x +2x +7x + +7x +1x +1x +1x +7x + + +7x +1x +7x + +7x +1x +7x + +7x +1x +1x +1x +7x + + +7x + + +7x +7x +7x +1x +7x + +7x +5x +5x +5x +1x +1x +1x +1x +1x +3x +3x +3x +3x +3x +3x +3x +3x +3x + + +3x +1x +1x +2x +2x +2x +3x +1x +1x +1x +1x +1x + | import moment from 'moment';
+import type { Moment as MomentInstance } from 'moment';
+import { toHijri, toGregorian, hmLong, hmMedium, hwLong, hwShort, hwNumeric } from 'hijri-core';
+import type { HijriDate, ConversionOptions } from './types';
+
+declare module 'moment' {
+ interface MomentStatic {
+ /**
+ * Construct a moment from a Hijri date.
+ * Throws if the date is invalid or outside the supported range.
+ * Call installHijri(moment) before use.
+ */
+ fromHijri(hy: number, hm: number, hd: number, options?: ConversionOptions): MomentInstance;
+ }
+
+ interface Moment {
+ /**
+ * Convert this moment to a Hijri date.
+ * Returns null if the date falls outside the supported calendar range.
+ */
+ toHijri(options?: ConversionOptions): HijriDate | null;
+
+ /** Return the Hijri year, or null if out of range. */
+ hijriYear(options?: ConversionOptions): number | null;
+
+ /** Return the Hijri month (1-12), or null if out of range. */
+ hijriMonth(options?: ConversionOptions): number | null;
+
+ /** Return the Hijri day, or null if out of range. */
+ hijriDay(options?: ConversionOptions): number | null;
+
+ /** Return true if this moment falls within the supported Hijri range. */
+ isValidHijri(options?: ConversionOptions): boolean;
+
+ /**
+ * Format this moment using Hijri-aware format tokens.
+ *
+ * Hijri tokens: iYYYY iYY iMMMM iMMM iMM iM iDD iD iEEEE iEEE iE ioooo iooo
+ * All other tokens are passed through to moment's own format().
+ *
+ * Returns an empty string if the date is outside the Hijri range.
+ */
+ formatHijri(formatStr: string, options?: ConversionOptions): string;
+ }
+}
+
+// Regex matching all Hijri format tokens. Ordered longest-first so iYYYY is
+// matched before iYY, iMMMM before iMMM, iDD before iD, iEEEE before iEEE.
+const HIJRI_TOKEN_RE = /iYYYY|iYY|iMMMM|iMMM|iMM|iM|iDD|iD|iEEEE|iEEE|iE|ioooo|iooo/g;
+
+/**
+ * Escape a literal string so moment.format() treats it as literal text.
+ * Wraps the value in square brackets, escaping any ] characters within.
+ */
+function escapeLiteral(value: string): string {
+ return '[' + value.replace(/]/g, '][]') + ']';
+}
+
+/**
+ * Install the Hijri plugin into the provided moment instance.
+ *
+ * @example
+ * import moment from 'moment';
+ * import installHijri from 'moment-hijri-plus';
+ * installHijri(moment);
+ */
+function install(momentInstance: typeof moment): void {
+ momentInstance.fn.toHijri = function (opts?: ConversionOptions): HijriDate | null {
+ return toHijri(this.toDate(), opts);
+ };
+
+ momentInstance.fn.hijriYear = function (opts?: ConversionOptions): number | null {
+ return this.toHijri(opts)?.hy ?? null;
+ };
+
+ momentInstance.fn.hijriMonth = function (opts?: ConversionOptions): number | null {
+ return this.toHijri(opts)?.hm ?? null;
+ };
+
+ momentInstance.fn.hijriDay = function (opts?: ConversionOptions): number | null {
+ return this.toHijri(opts)?.hd ?? null;
+ };
+
+ momentInstance.fn.isValidHijri = function (opts?: ConversionOptions): boolean {
+ return this.toHijri(opts) !== null;
+ };
+
+ momentInstance.fn.formatHijri = function (formatStr: string, opts?: ConversionOptions): string {
+ const hijri = this.toHijri(opts);
+ if (!hijri) return '';
+ const dow = this.day();
+ // Replace Hijri tokens with escaped literals, then pass the residual string
+ // to moment.format() so all standard tokens (YYYY, MMM, etc.) resolve correctly.
+ // Escaping is required because values like "Ramadan" would otherwise be
+ // interpreted by moment as format tokens (R, a, m, etc.).
+ const residual = formatStr.replace(HIJRI_TOKEN_RE, (token: string): string => {
+ switch (token) {
+ case 'iYYYY':
+ return escapeLiteral(String(hijri.hy).padStart(4, '0'));
+ case 'iYY':
+ return escapeLiteral(String(hijri.hy % 100).padStart(2, '0'));
+ case 'iMMMM':
+ // Non-null: hijri.hm is 1-12; hm-1 is always 0-11, within hmLong bounds.
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return escapeLiteral(hmLong[hijri.hm - 1]!);
+ case 'iMMM':
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return escapeLiteral(hmMedium[hijri.hm - 1]!);
+ case 'iMM':
+ return escapeLiteral(String(hijri.hm).padStart(2, '0'));
+ case 'iM':
+ return escapeLiteral(String(hijri.hm));
+ case 'iDD':
+ return escapeLiteral(String(hijri.hd).padStart(2, '0'));
+ case 'iD':
+ return escapeLiteral(String(hijri.hd));
+ case 'iEEEE':
+ // Non-null: dow is always 0-6 (day of week), within hwLong bounds.
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return escapeLiteral(hwLong[dow]!);
+ case 'iEEE':
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return escapeLiteral(hwShort[dow]!);
+ case 'iE':
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return escapeLiteral(String(hwNumeric[dow]!));
+ // Era tokens: both iooo and ioooo map to the common abbreviation.
+ case 'iooo':
+ case 'ioooo':
+ return escapeLiteral('AH');
+ default:
+ return token;
+ }
+ });
+ return this.format(residual);
+ };
+
+ // Attach fromHijri as a property on the constructor. We use bracket notation and a type
+ // assertion because MomentStatic augmentation produces a DTS visibility error with some
+ // TypeScript configurations; attaching at runtime is equivalent and safe.
+ (momentInstance as unknown as Record<string, unknown>)['fromHijri'] = function (
+ hy: number,
+ hm: number,
+ hd: number,
+ opts?: ConversionOptions,
+ ): moment.Moment {
+ let greg: Date | null;
+ try {
+ greg = toGregorian(hy, hm, hd, opts);
+ } catch {
+ throw new Error(`Invalid or out-of-range Hijri date: ${hy}/${hm}/${hd}`);
+ }
+ if (!greg) {
+ throw new Error(`Invalid or out-of-range Hijri date: ${hy}/${hm}/${hd}`);
+ }
+ // Construct from explicit year/month/day to avoid UTC-to-local timezone
+ // shift when the Date object represents midnight UTC.
+ return momentInstance([greg.getUTCFullYear(), greg.getUTCMonth(), greg.getUTCDate()]);
+ };
+}
+
+export default install;
+export type { HijriDate, ConversionOptions, CalendarEngine } from 'hijri-core';
+export { registerCalendar, getCalendar, listCalendars } from 'hijri-core';
+ |