Skip to content

Commit 2d9a8e9

Browse files
amirhmoradiwikus
authored and
wikus
committed
feat: add Amazigh calendar system
1 parent 722037a commit 2d9a8e9

File tree

8 files changed

+295
-2
lines changed

8 files changed

+295
-2
lines changed

Diff for: README.md

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Day.js Calendar Systems Plugin extends Day.js library to allow the use of differ
1818
* Persian (a.k.a.: Jalaali, Shamsi, Khorshidi),
1919
* Arabic (a.k.a: Hijri, Islamic, Umalqura, Ghamari),
2020
* Hebrew (a.k.a: Jewish),
21+
* Amazigh (a.k.a: Berber),
2122
* and more to come (PRs are welcome).
2223

2324
With this plugin, Day.js will be available to more than 200 million additional users worldwide (Estimated number of non-gregorian calendar users).
@@ -40,6 +41,7 @@ With the `@calidy/dayjs-calendarsystems` plugin, we bring the capacity to run an
4041
- 🌍 🗓️ 🇮🇷 Persian Calendar system available.
4142
- 🌍 🗓️ 🇸🇦 Islamic (Hijri, Umalqura) Calendar system. Note: we will use the default "islamic-umalqura" calendar system for "islamic" calendar system.
4243
- 🌍 🗓️ 🇮🇱 Hebrew (Jewish) Calendar system.
44+
- 🌍 🗓️ ⵣ **[Need more testing]** Amazigh (Berber) Calendar system.
4345
- 🌍 🗓️ 🇪🇹 **[WIP]** Ethiopian Calendar system.
4446
- 🌍 🗓️ 🇮🇳 **[TODO]** Indian Calendar system.
4547
- 🌍 🗓️ 🇨🇳 **[TODO]** Chinese Calendar system.

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@calidy/dayjs-calendarsystems",
3-
"version": "1.4.0",
3+
"version": "1.5.0",
44
"description": "Calendar Systems Management for Day.js",
55
"main": "dayjs-calendarsystems.cjs.min.js",
66
"umd:main": "dayjs-calendarsystems.umd.min.js",

Diff for: src/calendarSystems/AmazighCalendarSystem.js

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/**
2+
* Amazigh Calendar System
3+
*
4+
* @file AmazighCalendarSystem.js
5+
* @project dayjs-calendarsystems
6+
* @license see LICENSE file included in the project
7+
* @author Calidy.com, Amir Moradi (https://calidy.com/)
8+
* @description see README.md file included in the project
9+
*
10+
*/
11+
12+
import CalendarSystemBase from "./CalendarSystemBase";
13+
import * as CalendarUtils from "../calendarUtils/fourmilabCalendar";
14+
import { generateMonthNames } from "../calendarUtils/IntlUtils";
15+
16+
export default class AmazighCalendarSystem extends CalendarSystemBase {
17+
constructor(locale = "en") {
18+
super();
19+
this.firstDayOfWeek = 1; // Monday
20+
this.locale = locale;
21+
this.intlCalendar = "gregory"; // Using Gregorian as base for month names
22+
this.firstMonthNameEnglish = "Yennayer";
23+
this.monthNamesLocalized = generateMonthNames(
24+
locale,
25+
"amazigh",
26+
"Yennayer"
27+
);
28+
}
29+
30+
/**
31+
* Converts a Julian Day Number to an Amazigh date.
32+
* @param {number} jdn - The Julian Day Number.
33+
* @returns {Object} An object containing the Amazigh year, month, and day.
34+
*/
35+
convertFromJulian(jdn) {
36+
// Constants for JDN of the Julian calendar start and the Amazigh calendar start year
37+
const JDN_JULIAN_START = 2299160.5; // October 15, 1582, Gregorian calendar start (end of Julian calendar)
38+
const AMZ_YEAR_START = 950; // Amazigh calendar start year in BC
39+
const DAYS_IN_YEAR = 365.25; // Average days in a year accounting for leap years in Julian calendar
40+
const GREGORIAN_START_YEAR = 1582; // Year the Gregorian calendar starts
41+
const YENNAYER_JDN_OFFSET = 13; // Offset for Yennayer in the Gregorian calendar as of the 21st century
42+
43+
// Calculate the Gregorian year for the given JDN
44+
let year = GREGORIAN_START_YEAR + Math.floor((jdn - JDN_JULIAN_START) / DAYS_IN_YEAR);
45+
// Adjust the year based on the Amazigh calendar start year
46+
let amazighYear = year + (AMZ_YEAR_START - (year < 0 ? 1 : 0)); // Adjust for no year 0 in historical counting
47+
48+
// Calculate the JDN for January 1st of the given year
49+
let jdnJan1 = jdn - ((jdn - JDN_JULIAN_START) % DAYS_IN_YEAR);
50+
// Calculate the day of the year from JDN
51+
let dayOfYear = jdn - jdnJan1 + 1; // +1 since January 1st is day 1
52+
53+
// Adjust dayOfYear based on the Yennayer offset
54+
dayOfYear -= YENNAYER_JDN_OFFSET;
55+
56+
// Correct the year and dayOfYear if the adjustment crosses into the previous year
57+
if (dayOfYear <= 0) {
58+
amazighYear -= 1;
59+
dayOfYear += DAYS_IN_YEAR; // Add the days in a year to the negative dayOfYear
60+
}
61+
62+
// Determine the month and day from dayOfYear
63+
let month = 0, day = dayOfYear;
64+
const daysInMonths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; // Days in each month for Julian calendar
65+
while (day > daysInMonths[month]) {
66+
day -= daysInMonths[month];
67+
month += 1;
68+
}
69+
70+
// Adjust for the Amazigh calendar specifics if necessary
71+
// Note: This example uses a simplified approach and might need adjustments for leap years and accurate month lengths
72+
73+
return [
74+
amazighYear,
75+
month + 1, // +1 to make the month 1-based
76+
day
77+
];
78+
}
79+
80+
81+
// Convert Amazigh date to Julian Day
82+
convertToJulian(calendarYear, calendarMonth, calendarDay) {
83+
// Convert Amazigh year to Gregorian year
84+
const gregorianYear = calendarYear + 950;
85+
// Adjusting for Yennayer starting on January 14th in the Gregorian calendar
86+
const isBeforeYennayer = calendarMonth === 0 && calendarDay < 14;
87+
const adjustedYear = gregorianYear - (isBeforeYennayer ? 1 : 0);
88+
const adjustedMonth = isBeforeYennayer ? 12 : calendarMonth + 1; // Adjust month to 1-based for calculation
89+
const adjustedDay = calendarDay + (isBeforeYennayer ? 18 : 13); // Adjust days for Yennayer start, considering the current 13-day discrepancy
90+
91+
// Convert adjusted Gregorian date to Julian Day
92+
return CalendarUtils.gregorian_to_jd(adjustedYear, adjustedMonth, adjustedDay);
93+
}
94+
95+
96+
// Convert from Gregorian date to Amazigh date
97+
convertFromGregorian(date) {
98+
const julianDay = CalendarUtils.gregorian_to_jd(date.getFullYear(), date.getMonth() + 1, date.getDate());
99+
const gregorianYear = date.getFullYear();
100+
const gregorianMonth = date.getMonth() + 1; // 1-based month
101+
const gregorianDay = date.getDate();
102+
103+
// Calculate the Amazigh year
104+
let amazighYear = gregorianYear - 950;
105+
if (gregorianMonth < 1 || (gregorianMonth === 1 && gregorianDay < 14)) {
106+
amazighYear -= 1; // Adjust for Yennayer
107+
}
108+
109+
// Convert Julian day back to Gregorian to adjust for Yennayer offset
110+
const { year, month, day } = CalendarUtils.jd_to_gregorian(julianDay - 13);
111+
112+
return {
113+
year: year - 950,
114+
month: month - 1, // Convert to 0-based month index
115+
day: day,
116+
};
117+
}
118+
119+
convertToGregorian(calendarYear, calendarMonth, calendarDay) {
120+
// Calculate the Gregorian year for the given Amazigh year.
121+
const baseYear = -950; // Starting point of the Amazigh calendar in the Gregorian calendar (950 BC).
122+
let gregorianYear = calendarYear + baseYear;
123+
124+
// Adjust for the current discrepancy between the Julian and Gregorian calendars.
125+
const discrepancyDays = 13; // As of the 21st century, there's a 13-day difference between the calendars.
126+
const yennayerGregorianDate = new Date(gregorianYear, 0, 14 + discrepancyDays); // January 14th + discrepancy in days.
127+
128+
// Calculate the Julian Day Number for Yennayer of the given Gregorian year.
129+
let julianDayYennayer = this.convertToJulian(yennayerGregorianDate.getFullYear(), yennayerGregorianDate.getMonth(), yennayerGregorianDate.getDate());
130+
131+
// Considering the Amazigh calendar follows the Julian calendar with months having the same length,
132+
// we calculate the total number of days since Yennayer to the Amazigh date.
133+
let daysSinceYennayer = 0;
134+
for (let month = 0; month < calendarMonth; month++) {
135+
daysSinceYennayer += month === 1 ? 28 : (month < 7 ? (month % 2 === 0 ? 31 : 30) : (month % 2 === 0 ? 30 : 31));
136+
}
137+
daysSinceYennayer += calendarDay - 1; // Subtract one since Yennayer is considered day 1.
138+
139+
// Calculate the total Julian Day and convert it back to a Gregorian date.
140+
let julianDay = julianDayYennayer + daysSinceYennayer;
141+
const gregorianDateArray = CalendarUtils.jd_to_gregorian(julianDay);
142+
return {
143+
year: gregorianDateArray[0],
144+
month: gregorianDateArray[1] - 1, // -1 because the Gregorian month is 0-based
145+
day: gregorianDateArray[2],
146+
};
147+
}
148+
149+
isLeapYear(year) {
150+
// Adjust if Amazigh leap year rules differ, using Gregorian as placeholder
151+
const adjustedYear = year + 950;
152+
return (adjustedYear % 4 === 0 && adjustedYear % 100 !== 0) || adjustedYear % 400 === 0;
153+
}
154+
monthNames(
155+
locale = "en",
156+
calendar = "amazigh",
157+
firstMonthName = "Yennayer"
158+
) {
159+
return generateMonthNames(locale, calendar, firstMonthName);
160+
}
161+
162+
getLocalizedMonthName(monthIndex) {
163+
return this.monthNamesLocalized[monthIndex];
164+
}
165+
}
166+

Diff for: src/calendarUtils/IntlUtils.js

+41
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ function getMonthNames(locale, calendar = "persian") {
3131
if (!monthNamesCache[cacheKey]) {
3232
const monthNames = [];
3333
for (let i = 0; i < 12; i++) {
34+
if (calendar === "amazigh") {
35+
// Use the function to get localized Amazigh month names
36+
monthNames.push(getLocalizedAmazighMonthName(i, locale));
37+
} else {
38+
// For other calendars, use Intl.DateTimeFormat
3439
const date = new Date(2023, i, 1);
3540
const formatter = new Intl.DateTimeFormat(`${locale}-u-ca-${calendar}`, {
3641
month: "long",
@@ -58,6 +63,42 @@ function shiftAndSortMonthNames(locale, firstMonthIndex, calendar = "persian") {
5863
return shiftedSortedMonthNamesCache[cacheKey];
5964
}
6065

66+
/**
67+
* Enhanced utility functions to include locale-specific Amazigh calendar month names in English, Tamazight and Arabic.
68+
*/
69+
// Define Amazigh month names in Tamazight (Tifinagh script), Arabic, and English.
70+
const amazighMonthNamesByLocale = {
71+
"tzm": [ // Tamazight in Tifinagh script
72+
"ⵢⴻⵏⵏⴰⵢⴻⵔ", "ⴼⵓⵔⴰⵔ", "ⵎⴻⵖⵔⴻⵙ", "ⵢⴻⴱⵔⵉⵔ", "ⵎⴰⵢⵢⵓ", "ⵢⵓⵏⵢⵓ",
73+
"ⵢⵓⵍⵢⵓⵣ", "ⵖⵓⵛⵜ", "ⵙⵓⵜⴻⵏⴱⵉⵔ", "ⴽⵜⵓⴱⵔ", "ⵏⵓⵏⴻⵎⴱⵉⵔ", "ⴷⵓⵊⴻⵎⴱⵉⵔ"
74+
],
75+
"ar": [ // Arabic
76+
"يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو",
77+
"يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"
78+
],
79+
"default": [ // English and other locales
80+
"Yennayer", "Furar", "Meghres", "Yebrir", "Mayyu", "Yunyu",
81+
"Yulyu", "Ghuct", "Cutenber", "Ktuber", "Nunember", "Dujember"
82+
]
83+
};
84+
85+
/**
86+
* Returns the localized name of a month for the Amazigh calendar based on the locale.
87+
* @param {number} monthIndex - The index of the month (0 = Yennayer, 11 = Dujember).
88+
* @param {string} locale - The locale code (e.g., "tzm" for Tamazight, "ar" for Arabic).
89+
* @returns {string} The localized name of the month.
90+
*/
91+
function getLocalizedAmazighMonthName(monthIndex, locale) {
92+
// Determine which set of month names to use based on the locale
93+
if (amazighMonthNamesByLocale[locale]) {
94+
return amazighMonthNamesByLocale[locale][monthIndex];
95+
} else {
96+
// Default to English if the locale is not specifically handled
97+
return amazighMonthNamesByLocale["default"][monthIndex];
98+
}
99+
}
100+
101+
61102
export function generateMonthNames(
62103
locale,
63104
calendar = "persian",

Diff for: test/AmazighCalendarSystem.test.js

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import AmazighCalendarSystem from "../src/calendarSystems/AmazighCalendarSystem";
2+
// Assuming you have or will create equivalent utility functions for Amazigh calendar conversions
3+
import * as CalendarUtils from "../src/calendarUtils/fourmilabCalendar";
4+
5+
describe("AmazighCalendarSystem", () => {
6+
let amazighCalendar;
7+
8+
beforeEach(() => {
9+
amazighCalendar = new AmazighCalendarSystem();
10+
});
11+
12+
test("convertFromGregorian should return the correct Amazigh date", () => {
13+
const date = new Date(2023, 4, 14); // May 14, 2023
14+
// Assuming jd_to_amazigh and gregorian_to_jd functions are correctly implemented for the Amazigh calendar
15+
const [ay, am, ad] = amazighCalendar.convertFromJulian(
16+
CalendarUtils.gregorian_to_jd(
17+
date.getFullYear(),
18+
date.getMonth() + 1,
19+
date.getDate()
20+
)
21+
);
22+
const convertedDate = amazighCalendar.convertFromGregorian(date);
23+
expect(convertedDate.year).toEqual(ay);
24+
expect(convertedDate.month).toEqual(am - 1); // -1 because the Amazigh month is 0-based
25+
expect(convertedDate.day).toEqual(ad);
26+
});
27+
28+
test("convertToGregorian should return the correct Gregorian date", () => {
29+
const date = { year: 2973, month: 1, day: 25 }; // Amazigh date
30+
// Assuming jd_to_gregorian and amazigh_to_jd functions are correctly implemented
31+
const [gy, gm, gd] = CalendarUtils.jd_to_gregorian(
32+
amazighCalendar.convertToJulian(date.year, date.month + 1, date.day) + 0.5
33+
);
34+
const convertedDate = amazighCalendar.convertToGregorian(
35+
date.year,
36+
date.month,
37+
date.day
38+
);
39+
expect(convertedDate.year).toEqual(gy);
40+
expect(convertedDate.month).toEqual(gm - 1); // -1 because the jd_to_gregorian month is 1-based
41+
expect(convertedDate.day).toEqual(gd);
42+
});
43+
44+
test("monthNames should return Amazigh month names", () => {
45+
const monthNames = amazighCalendar.monthNames();
46+
expect(monthNames).toEqual([
47+
"Yennayer", "Furar", "Meghres", "Yebrir", "Mayyu", "Yunyu",
48+
"Yulyu", "Ghuct", "Cutenber", "Ktuber", "Nunember", "Dujember"
49+
]);
50+
});
51+
52+
test("getLocalizedMonthName should return the correct localized month name", () => {
53+
// assuming the AmazighCalendarSystem defaults to 'en' locale or handles localization internally
54+
expect(amazighCalendar.getLocalizedMonthName(0)).toEqual("Yennayer");
55+
expect(amazighCalendar.getLocalizedMonthName(1)).toEqual("Furar");
56+
});
57+
});

Diff for: test/startOf.test.js

+8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import GregoryCalendarSystem from "../src/calendarSystems/GregoryCalendarSystem"
44
import PersianCalendarSystem from "../src/calendarSystems/PersianCalendarSystem";
55
import HijriCalendarSystem from "../src/calendarSystems/HijriCalendarSystem";
66
import HebrewCalendarSystem from "../src/calendarSystems/HebrewCalendarSystem";
7+
import AmazighCalendarSystem from "../src/calendarSystems/AmazighCalendarSystem";
78

89
describe("startOf method with different calendar systems", () => {
910
beforeAll(() => {
@@ -12,6 +13,7 @@ describe("startOf method with different calendar systems", () => {
1213
dayjs.registerCalendarSystem("persian", new PersianCalendarSystem());
1314
dayjs.registerCalendarSystem("islamic", new HijriCalendarSystem());
1415
dayjs.registerCalendarSystem("hebrew", new HebrewCalendarSystem());
16+
dayjs.registerCalendarSystem("amazigh", new AmazighCalendarSystem());
1517
});
1618

1719
test("should return the start of the year in Gregorian calendar", () => {
@@ -37,4 +39,10 @@ describe("startOf method with different calendar systems", () => {
3739
const startOfYear = date.startOf("year");
3840
expect(startOfYear.format("YYYY-MM-DD")).toBe("5783-01-01"); // Tishri 1, 5783
3941
});
42+
43+
test("should return the start of the year in Amazigh calendar", () => {
44+
const date = dayjs("2024-02-28").toCalendarSystem("amazigh");
45+
const startOfYear = date.startOf("year");
46+
expect(startOfYear.format("YYYY-MM-DD")).toBe("2974-01-01"); // Yennayer 1, 2974
47+
});
4048
});

Diff for: types/calendarSystems/AmazighCalendarSystem.d.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { CalendarSystemBase } from './CalendarSystemBase';
2+
3+
type DateLikeObject = { year: number; month: number; day: number; };
4+
type DayjsLikeObject = { $y: number; $M: number; $D: number; };
5+
6+
export default class AmazighCalendarSystem extends CalendarSystemBase {
7+
firstDayOfWeek: number;
8+
locale: string;
9+
monthNamesLocalized: string[];
10+
11+
constructor(locale?: string)
12+
13+
convertFromGregorian(date: Date | DateLikeObject | DayjsLikeObject | string | number | undefined | null): { year: number; month: number; day: number; };
14+
convertToGregorian(amazighYear: number, amazighMonth: number, amazighDay: number): { year: number; month: number; day: number; };
15+
convertToJulian(year: number, month: number, day: number): number;
16+
isLeapYear(): boolean;
17+
monthNames(locale: string, calendar: string, firstMonthName: string): string[];
18+
getLocalizedMonthName(monthIndex: number): string;
19+
}

Diff for: types/index.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { CalendarSystemBase } from './calendarSystems/CalendarSystemBase';
66
// 'buddhist' | 'chinese' | 'coptic' | 'dangi' | 'ethioaa' | 'ethiopic' | 'gregory' | 'hebrew' | 'indian' | 'islamic' | 'islamic-civil' | 'islamic-rgsa' | 'islamic-tbla' | 'islamic-umalqura' | 'islamicc' | 'iso8601' | 'japanese' | 'persian' | 'roc'
77
// Our supported Calendar Systems (alphabetical order)
88
// ! Note that we use the default "islamic-umalqura" calendar system for "islamic" calendar system
9-
export type CalendarSystem = 'ethiopic' | 'gregory' | 'hebrew' | 'islamic' | 'persian';
9+
export type CalendarSystem = 'amazigh' | 'ethiopic' | 'gregory' | 'hebrew' | 'islamic' | 'persian';
1010

1111
declare module 'dayjs' {
1212
interface Dayjs {

0 commit comments

Comments
 (0)