kiahk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/CopticCalendar.d.ts +10 -0
- package/dist/CopticCalendar.js +63 -0
- package/dist/CopticDate.d.ts +9 -0
- package/dist/CopticDate.js +28 -0
- package/dist/Feast.d.ts +24 -0
- package/dist/Feast.js +17 -0
- package/dist/GregorianDate.d.ts +11 -0
- package/dist/GregorianDate.js +34 -0
- package/dist/algorithms.d.ts +41 -0
- package/dist/algorithms.js +95 -0
- package/dist/coptic-months-data.d.ts +10 -0
- package/dist/coptic-months-data.js +15 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +24 -0
- package/dist/feasts-data.d.ts +7 -0
- package/dist/feasts-data.js +73 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +6 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WizardLabz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/amir-magdy-of-wizardlabz/kiahk/master/assets/kiahk.png" alt="Kiahk logo" width="160">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# kiahk (JavaScript / TypeScript)
|
|
6
|
+
|
|
7
|
+
Coptic calendar arithmetic — date conversion, Easter, and feast days. The reference port of [kiahk](https://github.com/amir-magdy-of-wizardlabz/kiahk). Identical results to all other ports against `core/test-vectors.json`.
|
|
8
|
+
|
|
9
|
+
Works in **Node.js and the browser** — no Node-only APIs at runtime.
|
|
10
|
+
|
|
11
|
+
**Live demo:** <https://raw.githack.com/amir-magdy-of-wizardlabz/kiahk/master/demo/index.html>
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install kiahk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or from this repo for development:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd js
|
|
23
|
+
npm install
|
|
24
|
+
npm run build
|
|
25
|
+
npm test
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import { CopticDate, GregorianDate, CopticCalendar } from 'kiahk'
|
|
32
|
+
|
|
33
|
+
// Convert Gregorian → Coptic
|
|
34
|
+
const g = new GregorianDate(2025, 1, 11)
|
|
35
|
+
const c = g.toCoptic()
|
|
36
|
+
console.log(c.year, c.month, c.day) // 1741 5 3
|
|
37
|
+
|
|
38
|
+
// Convert Coptic → Gregorian
|
|
39
|
+
const c2 = new CopticDate(1742, 1, 1)
|
|
40
|
+
const g2 = c2.toGregorian()
|
|
41
|
+
console.log(g2.year, g2.month, g2.day) // 2025 9 11
|
|
42
|
+
|
|
43
|
+
// Coptic Easter for a Gregorian year
|
|
44
|
+
const easter = CopticCalendar.easterDate(2025)
|
|
45
|
+
console.log(easter.year, easter.month, easter.day) // 2025 4 20
|
|
46
|
+
|
|
47
|
+
// All major feasts for a Gregorian year, sorted by date
|
|
48
|
+
for (const feast of CopticCalendar.yearFeasts(2025)) {
|
|
49
|
+
const d = feast.gregorianDate
|
|
50
|
+
const pad = n => String(n).padStart(2, '0')
|
|
51
|
+
console.log(`${d.year}-${pad(d.month)}-${pad(d.day)} ${feast.name('en')}`)
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Sample output:**
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
1741 5 3
|
|
59
|
+
2025 9 11
|
|
60
|
+
2025 4 20
|
|
61
|
+
2025-01-07 Nativity of Christ
|
|
62
|
+
2025-01-19 Epiphany (Theophany)
|
|
63
|
+
2025-02-10 Nineveh Fast
|
|
64
|
+
2025-02-24 Great Lent (start)
|
|
65
|
+
2025-04-07 Annunciation
|
|
66
|
+
2025-04-13 Palm Sunday
|
|
67
|
+
2025-04-20 Easter Sunday
|
|
68
|
+
2025-05-29 Ascension
|
|
69
|
+
2025-06-08 Pentecost
|
|
70
|
+
2025-08-22 Assumption of Mary
|
|
71
|
+
2025-09-27 Feast of the Cross
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Render a date in English and Arabic
|
|
75
|
+
|
|
76
|
+
The library exposes Coptic month names in `en` + `ar` via `CopticCalendar.monthName(month, locale)`. The full 13-entry table is also re-exported as `COPTIC_MONTHS` for callers that prefer raw data.
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
import { GregorianDate, CopticCalendar } from 'kiahk'
|
|
80
|
+
|
|
81
|
+
const g = new GregorianDate(2025, 4, 20)
|
|
82
|
+
const c = g.toCoptic()
|
|
83
|
+
console.log(`${c.day} ${CopticCalendar.monthName(c.month, 'en')} ${c.year} AM`)
|
|
84
|
+
console.log(`${c.day} ${CopticCalendar.monthName(c.month, 'ar')} ${c.year} للشهداء`)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Sample output:**
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
12 Parmouti 1741 AM
|
|
91
|
+
12 برمودة 1741 للشهداء
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API at a glance
|
|
95
|
+
|
|
96
|
+
| Type / method | Purpose |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `new GregorianDate(y, m, d)` | Validating constructor; throws `InvalidGregorianDateException` on bad input |
|
|
99
|
+
| `GregorianDate#toCoptic() → CopticDate` | Convert |
|
|
100
|
+
| `GregorianDate#toNativeDate()` / `GregorianDate.fromNativeDate(d)` | Interop with JS `Date` |
|
|
101
|
+
| `new CopticDate(y, m, d)` | Validating constructor; throws `InvalidCopticDateException` on bad input |
|
|
102
|
+
| `CopticDate#toGregorian() → GregorianDate` | Convert |
|
|
103
|
+
| `Feast` | `id`, `type`, `category`, `gregorianDate`, `name(locale)` |
|
|
104
|
+
| `feast.name('fr')` | Throws `UnsupportedLocaleException` for unknown locale |
|
|
105
|
+
| `CopticCalendar.easterDate(year) → GregorianDate` | Coptic Easter |
|
|
106
|
+
| `CopticCalendar.moveableFeast(feastId, year) → Feast` | One moveable feast |
|
|
107
|
+
| `CopticCalendar.yearFeasts(year) → Feast[]` | All feasts, sorted ascending |
|
|
108
|
+
| `CopticCalendar.monthName(month, locale) → string` | Coptic month name; throws `InvalidCopticMonthException` / `UnsupportedLocaleException` |
|
|
109
|
+
| `COPTIC_MONTHS` | 13-entry array (mirrors `core/coptic_months.json`) |
|
|
110
|
+
|
|
111
|
+
Supported locales for `Feast#name(...)` and `CopticCalendar.monthName(...)`: `en`, `ar`.
|
|
112
|
+
|
|
113
|
+
## Run tests
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
cd js
|
|
117
|
+
npm test
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
Licensed under the [MIT License](../LICENSE).
|
|
123
|
+
|
|
124
|
+
Maintained by Amir Magdy at WizardLabz.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { GregorianDate } from './GregorianDate.js';
|
|
2
|
+
import { Feast } from './Feast.js';
|
|
3
|
+
export declare class CopticCalendar {
|
|
4
|
+
private constructor();
|
|
5
|
+
static monthName(month: number, locale: string): string;
|
|
6
|
+
static easterDate(gregorianYear: number): GregorianDate;
|
|
7
|
+
static moveableFeast(feastId: string, gregorianYear: number): Feast;
|
|
8
|
+
static fixedFeasts(gregorianYear: number): Feast[];
|
|
9
|
+
static yearFeasts(gregorianYear: number): Feast[];
|
|
10
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { computeEaster, addDays } from './algorithms.js';
|
|
2
|
+
import { GregorianDate } from './GregorianDate.js';
|
|
3
|
+
import { CopticDate } from './CopticDate.js';
|
|
4
|
+
import { Feast } from './Feast.js';
|
|
5
|
+
import { FEASTS } from './feasts-data.js';
|
|
6
|
+
import { COPTIC_MONTHS } from './coptic-months-data.js';
|
|
7
|
+
import { InvalidCopticMonthException, UnsupportedLocaleException } from './errors.js';
|
|
8
|
+
export class CopticCalendar {
|
|
9
|
+
constructor() { }
|
|
10
|
+
static monthName(month, locale) {
|
|
11
|
+
if (!Number.isInteger(month) || month < 1 || month > 13) {
|
|
12
|
+
throw new InvalidCopticMonthException(month);
|
|
13
|
+
}
|
|
14
|
+
const record = COPTIC_MONTHS[month - 1];
|
|
15
|
+
if (!(locale in record.names))
|
|
16
|
+
throw new UnsupportedLocaleException(locale);
|
|
17
|
+
return record.names[locale];
|
|
18
|
+
}
|
|
19
|
+
static easterDate(gregorianYear) {
|
|
20
|
+
const [y, m, d] = computeEaster(gregorianYear);
|
|
21
|
+
return new GregorianDate(y, m, d);
|
|
22
|
+
}
|
|
23
|
+
static moveableFeast(feastId, gregorianYear) {
|
|
24
|
+
const data = FEASTS.find(f => f.id === feastId && f.type === 'moveable');
|
|
25
|
+
if (!data)
|
|
26
|
+
throw new Error(`Unknown moveable feast: ${feastId}`);
|
|
27
|
+
const easter = this.easterDate(gregorianYear);
|
|
28
|
+
const [y, m, d] = addDays(easter.year, easter.month, easter.day, data.easter_offset);
|
|
29
|
+
const gDate = new GregorianDate(y, m, d);
|
|
30
|
+
return new Feast(data, gDate, gDate.toCoptic());
|
|
31
|
+
}
|
|
32
|
+
static fixedFeasts(gregorianYear) {
|
|
33
|
+
// A Gregorian year spans two Coptic years — check both
|
|
34
|
+
const cYearStart = new GregorianDate(gregorianYear, 1, 1).toCoptic().year;
|
|
35
|
+
const result = [];
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
for (const copticYear of [cYearStart, cYearStart + 1]) {
|
|
38
|
+
for (const data of FEASTS.filter(f => f.type === 'fixed')) {
|
|
39
|
+
try {
|
|
40
|
+
const c = new CopticDate(copticYear, data.coptic_month, data.coptic_day);
|
|
41
|
+
const g = c.toGregorian();
|
|
42
|
+
if (g.year === gregorianYear && !seen.has(data.id)) {
|
|
43
|
+
seen.add(data.id);
|
|
44
|
+
result.push(new Feast(data, g, c));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch { /* invalid date for this coptic year, skip */ }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
static yearFeasts(gregorianYear) {
|
|
53
|
+
const moveable = FEASTS
|
|
54
|
+
.filter(f => f.type === 'moveable')
|
|
55
|
+
.map(data => this.moveableFeast(data.id, gregorianYear));
|
|
56
|
+
const fixed = this.fixedFeasts(gregorianYear);
|
|
57
|
+
return [...fixed, ...moveable].sort((a, b) => {
|
|
58
|
+
const ag = a.gregorianDate, bg = b.gregorianDate;
|
|
59
|
+
return new Date(ag.year, ag.month - 1, ag.day).getTime()
|
|
60
|
+
- new Date(bg.year, bg.month - 1, bg.day).getTime();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { GregorianDate } from './GregorianDate.js';
|
|
2
|
+
export declare class CopticDate {
|
|
3
|
+
readonly year: number;
|
|
4
|
+
readonly month: number;
|
|
5
|
+
readonly day: number;
|
|
6
|
+
constructor(year: number, month: number, day: number);
|
|
7
|
+
toGregorian(): GregorianDate;
|
|
8
|
+
toString(): string;
|
|
9
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { copticToGregorian } from './algorithms.js';
|
|
2
|
+
import { InvalidCopticDateException } from './errors.js';
|
|
3
|
+
import { GregorianDate } from './GregorianDate.js';
|
|
4
|
+
function daysInCopticMonth(month, year) {
|
|
5
|
+
if (month >= 1 && month <= 12)
|
|
6
|
+
return 30;
|
|
7
|
+
if (month === 13)
|
|
8
|
+
return year % 4 === 3 ? 6 : 5;
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
export class CopticDate {
|
|
12
|
+
constructor(year, month, day) {
|
|
13
|
+
const maxDay = daysInCopticMonth(month, year);
|
|
14
|
+
if (month < 1 || month > 13 || day < 1 || day > maxDay) {
|
|
15
|
+
throw new InvalidCopticDateException(year, month, day);
|
|
16
|
+
}
|
|
17
|
+
this.year = year;
|
|
18
|
+
this.month = month;
|
|
19
|
+
this.day = day;
|
|
20
|
+
}
|
|
21
|
+
toGregorian() {
|
|
22
|
+
const [y, m, d] = copticToGregorian(this.year, this.month, this.day);
|
|
23
|
+
return new GregorianDate(y, m, d);
|
|
24
|
+
}
|
|
25
|
+
toString() {
|
|
26
|
+
return `${this.year}/${String(this.month).padStart(2, '0')}/${String(this.day).padStart(2, '0')}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/dist/Feast.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { GregorianDate } from './GregorianDate.js';
|
|
2
|
+
import type { CopticDate } from './CopticDate.js';
|
|
3
|
+
export type FeastType = 'moveable' | 'fixed';
|
|
4
|
+
export type FeastCategory = 'major' | 'minor';
|
|
5
|
+
export interface FeastData {
|
|
6
|
+
id: string;
|
|
7
|
+
names: Record<string, string>;
|
|
8
|
+
type: FeastType;
|
|
9
|
+
category: FeastCategory;
|
|
10
|
+
easter_offset?: number;
|
|
11
|
+
coptic_month?: number;
|
|
12
|
+
coptic_day?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class Feast {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly type: FeastType;
|
|
17
|
+
readonly category: FeastCategory;
|
|
18
|
+
readonly easterOffset: number | null;
|
|
19
|
+
readonly gregorianDate: GregorianDate;
|
|
20
|
+
readonly copticDate: CopticDate;
|
|
21
|
+
private readonly _names;
|
|
22
|
+
constructor(data: FeastData, gregorianDate: GregorianDate, copticDate: CopticDate);
|
|
23
|
+
name(locale: string): string;
|
|
24
|
+
}
|
package/dist/Feast.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { UnsupportedLocaleException } from './errors.js';
|
|
2
|
+
export class Feast {
|
|
3
|
+
constructor(data, gregorianDate, copticDate) {
|
|
4
|
+
this.id = data.id;
|
|
5
|
+
this.type = data.type;
|
|
6
|
+
this.category = data.category;
|
|
7
|
+
this.easterOffset = data.easter_offset ?? null;
|
|
8
|
+
this.gregorianDate = gregorianDate;
|
|
9
|
+
this.copticDate = copticDate;
|
|
10
|
+
this._names = data.names;
|
|
11
|
+
}
|
|
12
|
+
name(locale) {
|
|
13
|
+
if (!(locale in this._names))
|
|
14
|
+
throw new UnsupportedLocaleException(locale);
|
|
15
|
+
return this._names[locale];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { CopticDate } from './CopticDate.js';
|
|
2
|
+
export declare class GregorianDate {
|
|
3
|
+
readonly year: number;
|
|
4
|
+
readonly month: number;
|
|
5
|
+
readonly day: number;
|
|
6
|
+
constructor(year: number, month: number, day: number);
|
|
7
|
+
toCoptic(): CopticDate;
|
|
8
|
+
toNativeDate(): Date;
|
|
9
|
+
static fromNativeDate(date: Date): GregorianDate;
|
|
10
|
+
toString(): string;
|
|
11
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { gregorianToCoptic } from './algorithms.js';
|
|
2
|
+
import { InvalidGregorianDateException } from './errors.js';
|
|
3
|
+
import { CopticDate } from './CopticDate.js';
|
|
4
|
+
const DAYS_IN_MONTH = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
5
|
+
function isLeap(y) { return (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0; }
|
|
6
|
+
function daysInGregorianMonth(month, year) {
|
|
7
|
+
if (month === 2 && isLeap(year))
|
|
8
|
+
return 29;
|
|
9
|
+
return DAYS_IN_MONTH[month] ?? 0;
|
|
10
|
+
}
|
|
11
|
+
export class GregorianDate {
|
|
12
|
+
constructor(year, month, day) {
|
|
13
|
+
const maxDay = daysInGregorianMonth(month, year);
|
|
14
|
+
if (month < 1 || month > 12 || day < 1 || day > maxDay) {
|
|
15
|
+
throw new InvalidGregorianDateException(year, month, day);
|
|
16
|
+
}
|
|
17
|
+
this.year = year;
|
|
18
|
+
this.month = month;
|
|
19
|
+
this.day = day;
|
|
20
|
+
}
|
|
21
|
+
toCoptic() {
|
|
22
|
+
const [y, m, d] = gregorianToCoptic(this.year, this.month, this.day);
|
|
23
|
+
return new CopticDate(y, m, d);
|
|
24
|
+
}
|
|
25
|
+
toNativeDate() {
|
|
26
|
+
return new Date(this.year, this.month - 1, this.day);
|
|
27
|
+
}
|
|
28
|
+
static fromNativeDate(date) {
|
|
29
|
+
return new GregorianDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
|
30
|
+
}
|
|
31
|
+
toString() {
|
|
32
|
+
return `${this.year}-${String(this.month).padStart(2, '0')}-${String(this.day).padStart(2, '0')}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar conversion primitives for the Coptic (Alexandrian) calendar.
|
|
3
|
+
*
|
|
4
|
+
* The Coptic calendar:
|
|
5
|
+
* - 12 months of 30 days, plus a 13th "little month" (Nasie) of 5 days
|
|
6
|
+
* (6 in leap years).
|
|
7
|
+
* - Leap rule: Coptic year Y is a leap year iff Y mod 4 == 3 (Julian rule).
|
|
8
|
+
* - Era: Anno Martyrum (AM), starting 29 August 284 CE (Julian) =
|
|
9
|
+
* 11 September 284 CE (proleptic Gregorian) = JDN 1825030
|
|
10
|
+
* (= 1 Tout 1 AM).
|
|
11
|
+
*
|
|
12
|
+
* Algorithm references:
|
|
13
|
+
* - Reingold & Dershowitz, "Calendrical Calculations" (3rd ed., 2008),
|
|
14
|
+
* §4.1 (Coptic calendar).
|
|
15
|
+
* - Fourmilab/Meeus port in the Python `convertdate` package
|
|
16
|
+
* (https://github.com/fitnr/convertdate, src/convertdate/coptic.py).
|
|
17
|
+
* - Wikipedia: https://en.wikipedia.org/wiki/Coptic_calendar
|
|
18
|
+
*/
|
|
19
|
+
/** Gregorian date → Julian Day Number (Fliegel & Van Flandern). */
|
|
20
|
+
export declare function gregorianToJdn(year: number, month: number, day: number): number;
|
|
21
|
+
/** Julian Day Number → Gregorian date. */
|
|
22
|
+
export declare function jdnToGregorian(jdn: number): [number, number, number];
|
|
23
|
+
/** Coptic date → Julian Day Number. */
|
|
24
|
+
export declare function copticToJdn(cYear: number, cMonth: number, cDay: number): number;
|
|
25
|
+
/** Julian Day Number → Coptic date. */
|
|
26
|
+
export declare function jdnToCoptic(jdn: number): [number, number, number];
|
|
27
|
+
/** Gregorian → Coptic. */
|
|
28
|
+
export declare function gregorianToCoptic(gYear: number, gMonth: number, gDay: number): [number, number, number];
|
|
29
|
+
/** Coptic → Gregorian. */
|
|
30
|
+
export declare function copticToGregorian(cYear: number, cMonth: number, cDay: number): [number, number, number];
|
|
31
|
+
/**
|
|
32
|
+
* Coptic / Orthodox Easter (Meeus's Julian computus + Julian-to-Gregorian shift).
|
|
33
|
+
*
|
|
34
|
+
* The Meeus formula yields Easter Sunday in the *Julian* calendar. To express
|
|
35
|
+
* the same instant on the *Gregorian* calendar we add the Julian-Gregorian
|
|
36
|
+
* offset (13 days for any date in the 20th–21st century: 1900-03-01 through
|
|
37
|
+
* 2100-02-28). Since this library targets the modern era we hard-code +13.
|
|
38
|
+
*/
|
|
39
|
+
export declare function computeEaster(gregorianYear: number): [number, number, number];
|
|
40
|
+
/** Add N days to a Gregorian date, returning [year, month, day]. */
|
|
41
|
+
export declare function addDays(year: number, month: number, day: number, days: number): [number, number, number];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar conversion primitives for the Coptic (Alexandrian) calendar.
|
|
3
|
+
*
|
|
4
|
+
* The Coptic calendar:
|
|
5
|
+
* - 12 months of 30 days, plus a 13th "little month" (Nasie) of 5 days
|
|
6
|
+
* (6 in leap years).
|
|
7
|
+
* - Leap rule: Coptic year Y is a leap year iff Y mod 4 == 3 (Julian rule).
|
|
8
|
+
* - Era: Anno Martyrum (AM), starting 29 August 284 CE (Julian) =
|
|
9
|
+
* 11 September 284 CE (proleptic Gregorian) = JDN 1825030
|
|
10
|
+
* (= 1 Tout 1 AM).
|
|
11
|
+
*
|
|
12
|
+
* Algorithm references:
|
|
13
|
+
* - Reingold & Dershowitz, "Calendrical Calculations" (3rd ed., 2008),
|
|
14
|
+
* §4.1 (Coptic calendar).
|
|
15
|
+
* - Fourmilab/Meeus port in the Python `convertdate` package
|
|
16
|
+
* (https://github.com/fitnr/convertdate, src/convertdate/coptic.py).
|
|
17
|
+
* - Wikipedia: https://en.wikipedia.org/wiki/Coptic_calendar
|
|
18
|
+
*/
|
|
19
|
+
/** Gregorian date → Julian Day Number (Fliegel & Van Flandern). */
|
|
20
|
+
export function gregorianToJdn(year, month, day) {
|
|
21
|
+
const a = Math.floor((14 - month) / 12);
|
|
22
|
+
const y = year + 4800 - a;
|
|
23
|
+
const m = month + 12 * a - 3;
|
|
24
|
+
return day + Math.floor((153 * m + 2) / 5) + 365 * y +
|
|
25
|
+
Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400) - 32045;
|
|
26
|
+
}
|
|
27
|
+
/** Julian Day Number → Gregorian date. */
|
|
28
|
+
export function jdnToGregorian(jdn) {
|
|
29
|
+
const a = jdn + 32044;
|
|
30
|
+
const b = Math.floor((4 * a + 3) / 146097);
|
|
31
|
+
const c = a - Math.floor((146097 * b) / 4);
|
|
32
|
+
const d = Math.floor((4 * c + 3) / 1461);
|
|
33
|
+
const e = c - Math.floor((1461 * d) / 4);
|
|
34
|
+
const m = Math.floor((5 * e + 2) / 153);
|
|
35
|
+
const day = e - Math.floor((153 * m + 2) / 5) + 1;
|
|
36
|
+
const month = m + 3 - 12 * Math.floor(m / 10);
|
|
37
|
+
const year = 100 * b + d - 4800 + Math.floor(m / 10);
|
|
38
|
+
return [year, month, day];
|
|
39
|
+
}
|
|
40
|
+
/** JDN of 1 Tout, year 1 AM (Coptic epoch). */
|
|
41
|
+
const COPTIC_EPOCH = 1825030;
|
|
42
|
+
/** Coptic date → Julian Day Number. */
|
|
43
|
+
export function copticToJdn(cYear, cMonth, cDay) {
|
|
44
|
+
// Days before year cYear (within the AM era):
|
|
45
|
+
// 365 * (cYear - 1) full years + one extra day for every leap year
|
|
46
|
+
// in [1, cYear - 1] (Coptic leap year: Y mod 4 == 3).
|
|
47
|
+
// The count of leap years in [1, cYear - 1] equals floor(cYear / 4).
|
|
48
|
+
return COPTIC_EPOCH - 1
|
|
49
|
+
+ 365 * (cYear - 1)
|
|
50
|
+
+ Math.floor(cYear / 4)
|
|
51
|
+
+ 30 * (cMonth - 1)
|
|
52
|
+
+ cDay;
|
|
53
|
+
}
|
|
54
|
+
/** Julian Day Number → Coptic date. */
|
|
55
|
+
export function jdnToCoptic(jdn) {
|
|
56
|
+
const r = jdn - COPTIC_EPOCH; // 0 = 1 Tout 1 AM
|
|
57
|
+
// Solve for cYear given r = 365*(cYear-1) + floor(cYear/4) + dayOfYear (0..365).
|
|
58
|
+
// Closed form: cYear = floor((4*r + 1463) / 1461).
|
|
59
|
+
const cYear = Math.floor((4 * r + 1463) / 1461);
|
|
60
|
+
const dayOfYear = r - 365 * (cYear - 1) - Math.floor(cYear / 4); // 0-indexed
|
|
61
|
+
const cMonth = Math.floor(dayOfYear / 30) + 1;
|
|
62
|
+
const cDay = dayOfYear - 30 * (cMonth - 1) + 1;
|
|
63
|
+
return [cYear, cMonth, cDay];
|
|
64
|
+
}
|
|
65
|
+
/** Gregorian → Coptic. */
|
|
66
|
+
export function gregorianToCoptic(gYear, gMonth, gDay) {
|
|
67
|
+
return jdnToCoptic(gregorianToJdn(gYear, gMonth, gDay));
|
|
68
|
+
}
|
|
69
|
+
/** Coptic → Gregorian. */
|
|
70
|
+
export function copticToGregorian(cYear, cMonth, cDay) {
|
|
71
|
+
return jdnToGregorian(copticToJdn(cYear, cMonth, cDay));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Coptic / Orthodox Easter (Meeus's Julian computus + Julian-to-Gregorian shift).
|
|
75
|
+
*
|
|
76
|
+
* The Meeus formula yields Easter Sunday in the *Julian* calendar. To express
|
|
77
|
+
* the same instant on the *Gregorian* calendar we add the Julian-Gregorian
|
|
78
|
+
* offset (13 days for any date in the 20th–21st century: 1900-03-01 through
|
|
79
|
+
* 2100-02-28). Since this library targets the modern era we hard-code +13.
|
|
80
|
+
*/
|
|
81
|
+
export function computeEaster(gregorianYear) {
|
|
82
|
+
const a = gregorianYear % 4;
|
|
83
|
+
const b = gregorianYear % 7;
|
|
84
|
+
const c = gregorianYear % 19;
|
|
85
|
+
const d = (19 * c + 15) % 30;
|
|
86
|
+
const e = (2 * a + 4 * b - d + 34) % 7;
|
|
87
|
+
const f = Math.floor((d + e + 114) / 31); // Julian-calendar month
|
|
88
|
+
const g = ((d + e + 114) % 31) + 1; // Julian-calendar day
|
|
89
|
+
const jdn = gregorianToJdn(gregorianYear, f, g) + 13;
|
|
90
|
+
return jdnToGregorian(jdn);
|
|
91
|
+
}
|
|
92
|
+
/** Add N days to a Gregorian date, returning [year, month, day]. */
|
|
93
|
+
export function addDays(year, month, day, days) {
|
|
94
|
+
return jdnToGregorian(gregorianToJdn(year, month, day) + days);
|
|
95
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-maintained mirror of core/coptic_months.json. Keep order identical
|
|
3
|
+
* (months 1..13) for cross-port test parity. Inlined (not read from disk)
|
|
4
|
+
* so this module works in both Node and browser environments.
|
|
5
|
+
*/
|
|
6
|
+
export interface CopticMonthRecord {
|
|
7
|
+
month: number;
|
|
8
|
+
names: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
export declare const COPTIC_MONTHS: CopticMonthRecord[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const COPTIC_MONTHS = [
|
|
2
|
+
{ month: 1, names: { en: 'Thout', ar: 'توت' } },
|
|
3
|
+
{ month: 2, names: { en: 'Paopi', ar: 'بابة' } },
|
|
4
|
+
{ month: 3, names: { en: 'Hathor', ar: 'هاتور' } },
|
|
5
|
+
{ month: 4, names: { en: 'Koiak', ar: 'كيهك' } },
|
|
6
|
+
{ month: 5, names: { en: 'Tobi', ar: 'طوبة' } },
|
|
7
|
+
{ month: 6, names: { en: 'Meshir', ar: 'أمشير' } },
|
|
8
|
+
{ month: 7, names: { en: 'Paremhat', ar: 'برمهات' } },
|
|
9
|
+
{ month: 8, names: { en: 'Parmouti', ar: 'برمودة' } },
|
|
10
|
+
{ month: 9, names: { en: 'Pashons', ar: 'بشنس' } },
|
|
11
|
+
{ month: 10, names: { en: 'Paoni', ar: 'بؤونة' } },
|
|
12
|
+
{ month: 11, names: { en: 'Epip', ar: 'أبيب' } },
|
|
13
|
+
{ month: 12, names: { en: 'Mesori', ar: 'مسرى' } },
|
|
14
|
+
{ month: 13, names: { en: 'Nasie', ar: 'نسيء' } }
|
|
15
|
+
];
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class InvalidCopticDateException extends Error {
|
|
2
|
+
constructor(year: number, month: number, day: number);
|
|
3
|
+
}
|
|
4
|
+
export declare class InvalidGregorianDateException extends Error {
|
|
5
|
+
constructor(year: number, month: number, day: number);
|
|
6
|
+
}
|
|
7
|
+
export declare class UnsupportedLocaleException extends Error {
|
|
8
|
+
constructor(locale: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class InvalidCopticMonthException extends Error {
|
|
11
|
+
constructor(month: number);
|
|
12
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class InvalidCopticDateException extends Error {
|
|
2
|
+
constructor(year, month, day) {
|
|
3
|
+
super(`Invalid Coptic date: ${year}/${month}/${day}`);
|
|
4
|
+
this.name = 'InvalidCopticDateException';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class InvalidGregorianDateException extends Error {
|
|
8
|
+
constructor(year, month, day) {
|
|
9
|
+
super(`Invalid Gregorian date: ${year}/${month}/${day}`);
|
|
10
|
+
this.name = 'InvalidGregorianDateException';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class UnsupportedLocaleException extends Error {
|
|
14
|
+
constructor(locale) {
|
|
15
|
+
super(`Unsupported locale: ${locale}`);
|
|
16
|
+
this.name = 'UnsupportedLocaleException';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class InvalidCopticMonthException extends Error {
|
|
20
|
+
constructor(month) {
|
|
21
|
+
super(`Invalid Coptic month: ${month} (expected 1..13)`);
|
|
22
|
+
this.name = 'InvalidCopticMonthException';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FeastData } from './Feast.js';
|
|
2
|
+
/**
|
|
3
|
+
* Hand-maintained mirror of core/feasts.json. Keep order identical for
|
|
4
|
+
* cross-port test parity. Inlined (not read from disk) so this module works
|
|
5
|
+
* in both Node and browser environments.
|
|
6
|
+
*/
|
|
7
|
+
export declare const FEASTS: FeastData[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-maintained mirror of core/feasts.json. Keep order identical for
|
|
3
|
+
* cross-port test parity. Inlined (not read from disk) so this module works
|
|
4
|
+
* in both Node and browser environments.
|
|
5
|
+
*/
|
|
6
|
+
export const FEASTS = [
|
|
7
|
+
{
|
|
8
|
+
id: 'nativity',
|
|
9
|
+
names: { en: 'Nativity of Christ', ar: 'عيد الميلاد المجيد' },
|
|
10
|
+
type: 'fixed', category: 'major',
|
|
11
|
+
coptic_month: 4, coptic_day: 29
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'epiphany',
|
|
15
|
+
names: { en: 'Epiphany (Theophany)', ar: 'عيد الغطاس' },
|
|
16
|
+
type: 'fixed', category: 'major',
|
|
17
|
+
coptic_month: 5, coptic_day: 11
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'annunciation',
|
|
21
|
+
names: { en: 'Annunciation', ar: 'عيد البشارة' },
|
|
22
|
+
type: 'fixed', category: 'major',
|
|
23
|
+
coptic_month: 7, coptic_day: 29
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'assumption',
|
|
27
|
+
names: { en: 'Assumption of Mary', ar: 'عيد انتقال العذراء' },
|
|
28
|
+
type: 'fixed', category: 'major',
|
|
29
|
+
coptic_month: 12, coptic_day: 16
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'cross',
|
|
33
|
+
names: { en: 'Feast of the Cross', ar: 'عيد الصليب' },
|
|
34
|
+
type: 'fixed', category: 'major',
|
|
35
|
+
coptic_month: 1, coptic_day: 17
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'nineveh_fast',
|
|
39
|
+
names: { en: 'Nineveh Fast', ar: 'صوم نينوى' },
|
|
40
|
+
type: 'moveable', category: 'major',
|
|
41
|
+
easter_offset: -69
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'great_lent',
|
|
45
|
+
names: { en: 'Great Lent (start)', ar: 'بداية الصوم الكبير' },
|
|
46
|
+
type: 'moveable', category: 'major',
|
|
47
|
+
easter_offset: -55
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'palm_sunday',
|
|
51
|
+
names: { en: 'Palm Sunday', ar: 'أحد الشعانين' },
|
|
52
|
+
type: 'moveable', category: 'major',
|
|
53
|
+
easter_offset: -7
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'easter',
|
|
57
|
+
names: { en: 'Easter Sunday', ar: 'عيد القيامة المجيد' },
|
|
58
|
+
type: 'moveable', category: 'major',
|
|
59
|
+
easter_offset: 0
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'ascension',
|
|
63
|
+
names: { en: 'Ascension', ar: 'عيد الصعود' },
|
|
64
|
+
type: 'moveable', category: 'major',
|
|
65
|
+
easter_offset: 39
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'pentecost',
|
|
69
|
+
names: { en: 'Pentecost', ar: 'عيد العنصرة' },
|
|
70
|
+
type: 'moveable', category: 'major',
|
|
71
|
+
easter_offset: 49
|
|
72
|
+
}
|
|
73
|
+
];
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { CopticDate } from './CopticDate.js';
|
|
2
|
+
export { GregorianDate } from './GregorianDate.js';
|
|
3
|
+
export type { FeastType, FeastCategory } from './Feast.js';
|
|
4
|
+
export { Feast } from './Feast.js';
|
|
5
|
+
export { CopticCalendar } from './CopticCalendar.js';
|
|
6
|
+
export type { CopticMonthRecord } from './coptic-months-data.js';
|
|
7
|
+
export { COPTIC_MONTHS } from './coptic-months-data.js';
|
|
8
|
+
export { InvalidCopticDateException, InvalidGregorianDateException, InvalidCopticMonthException, UnsupportedLocaleException } from './errors.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { CopticDate } from './CopticDate.js';
|
|
2
|
+
export { GregorianDate } from './GregorianDate.js';
|
|
3
|
+
export { Feast } from './Feast.js';
|
|
4
|
+
export { CopticCalendar } from './CopticCalendar.js';
|
|
5
|
+
export { COPTIC_MONTHS } from './coptic-months-data.js';
|
|
6
|
+
export { InvalidCopticDateException, InvalidGregorianDateException, InvalidCopticMonthException, UnsupportedLocaleException } from './errors.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kiahk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Coptic calendar arithmetic — date conversion, Easter, and feast days. Works in Node and the browser.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest",
|
|
28
|
+
"prepublishOnly": "npm test && npm run build"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"coptic",
|
|
32
|
+
"calendar",
|
|
33
|
+
"easter",
|
|
34
|
+
"coptic-calendar",
|
|
35
|
+
"kiahk",
|
|
36
|
+
"kiahk-calendar",
|
|
37
|
+
"orthodox",
|
|
38
|
+
"feast",
|
|
39
|
+
"aboqty",
|
|
40
|
+
"قبطي",
|
|
41
|
+
"تقويم",
|
|
42
|
+
"كيهك",
|
|
43
|
+
"عيد",
|
|
44
|
+
"القيامة",
|
|
45
|
+
"القيامه",
|
|
46
|
+
"أبوقطي"
|
|
47
|
+
],
|
|
48
|
+
"author": "WizardLabz",
|
|
49
|
+
"contributors": [
|
|
50
|
+
{ "name": "Amir Magdy" }
|
|
51
|
+
],
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/amir-magdy-of-wizardlabz/kiahk.git",
|
|
56
|
+
"directory": "js"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://github.com/amir-magdy-of-wizardlabz/kiahk#readme",
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/amir-magdy-of-wizardlabz/kiahk/issues"
|
|
61
|
+
},
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public",
|
|
64
|
+
"provenance": true
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"typescript": "^5.4.0",
|
|
68
|
+
"vitest": "^1.6.0",
|
|
69
|
+
"@types/node": "^20.0.0"
|
|
70
|
+
}
|
|
71
|
+
}
|