diff --git a/examples/Ethiopic.test.tsx b/examples/Ethiopic.test.tsx
index c9f3964d1..94641697d 100644
--- a/examples/Ethiopic.test.tsx
+++ b/examples/Ethiopic.test.tsx
@@ -1,16 +1,16 @@
import React from "react";
+import { grid } from "@/test/elements";
import { render } from "@/test/render";
import { Ethiopic } from "./Ethiopic.jsx";
-const today = new Date(2021, 10, 25);
+const today = new Date(2024, 11, 22);
beforeAll(() => jest.setSystemTime(today));
afterAll(() => jest.useRealTimers());
-beforeEach(() => {
+test("should render ታህሳስ ፳፻፲፯", () => {
render();
+ expect(grid("ታህሳስ ፳፻፲፯")).toBeInTheDocument();
});
-
-test.todo("should render the Ethiopic calendar");
diff --git a/examples/EthiopicGeez.test.tsx b/examples/EthiopicGeez.test.tsx
new file mode 100644
index 000000000..655d5f991
--- /dev/null
+++ b/examples/EthiopicGeez.test.tsx
@@ -0,0 +1,21 @@
+import React from "react";
+
+import { grid } from "@/test/elements";
+import { render } from "@/test/render";
+
+import { EthiopicGeez } from "./EthiopicGeez";
+
+const today = new Date(2024, 11, 22);
+
+beforeAll(() => jest.setSystemTime(today));
+afterAll(() => jest.useRealTimers());
+
+test("should render Tahsas 2017 with latin numerals", () => {
+ render();
+ expect(grid("Tahsas 2017")).toBeInTheDocument();
+});
+
+test("should render December 2024 with latin numerals", () => {
+ render();
+ expect(grid("December 2024")).toBeInTheDocument();
+});
diff --git a/examples/EthiopicGeez.tsx b/examples/EthiopicGeez.tsx
new file mode 100644
index 000000000..ea4fd7fd8
--- /dev/null
+++ b/examples/EthiopicGeez.tsx
@@ -0,0 +1,7 @@
+import React from "react";
+
+import { DayPicker } from "react-day-picker/ethiopic";
+
+export function EthiopicGeez() {
+ return ;
+}
diff --git a/examples/index.ts b/examples/index.ts
index bde95af30..060c0045e 100644
--- a/examples/index.ts
+++ b/examples/index.ts
@@ -50,6 +50,8 @@ export * from "./PastDatesDisabled";
export * from "./Persian";
export * from "./PersianFormatted";
export * from "./PersianEn";
+export * from "./Ethiopic";
+export * from "./EthiopicGeez";
export * from "./Range";
export * from "./RangeExcludeDisabled";
export * from "./RangeLong";
diff --git a/src/ethiopic/index.tsx b/src/ethiopic/index.tsx
index c0c637325..e84329ed1 100644
--- a/src/ethiopic/index.tsx
+++ b/src/ethiopic/index.tsx
@@ -49,7 +49,7 @@ export function DayPicker(
) {
const dateLib = getDateLib({
locale: props.locale,
- weekStartsOn: props.broadcastCalendar ? 1 : props.weekStartsOn,
+ weekStartsOn: 1,
firstWeekContainsDate: props.firstWeekContainsDate,
useAdditionalWeekYearTokens: props.useAdditionalWeekYearTokens,
useAdditionalDayOfYearTokens: props.useAdditionalDayOfYearTokens,
@@ -59,7 +59,7 @@ export function DayPicker(
);
diff --git a/src/ethiopic/lib/addDays.ts b/src/ethiopic/lib/addDays.ts
index 91974defc..0cda9dd84 100644
--- a/src/ethiopic/lib/addDays.ts
+++ b/src/ethiopic/lib/addDays.ts
@@ -1,3 +1,5 @@
+import { addDays as addDaysFns } from "date-fns";
+
/**
* Adds days to an Ethiopic date
*
@@ -6,7 +8,5 @@
* @returns {Date} The new date
*/
export function addDays(date: Date, amount: number): Date {
- const julianDay = Math.floor(date.getTime() / 86400000 + 2440587.5);
- const newJulianDay = julianDay + amount;
- return new Date((newJulianDay - 2440587.5) * 86400000);
+ return addDaysFns(date, amount);
}
diff --git a/src/ethiopic/lib/addMonths.test.ts b/src/ethiopic/lib/addMonths.test.ts
index 1b8806873..0786c8345 100644
--- a/src/ethiopic/lib/addMonths.test.ts
+++ b/src/ethiopic/lib/addMonths.test.ts
@@ -1 +1,81 @@
-test.todo("addMonths should correctly add months to an Ethiopic date");
+import { toEthiopicDate, toGregorianDate } from "../utils";
+
+import { addMonths } from "./addMonths";
+
+describe("addMonths in Ethiopian calendar", () => {
+ test("should add positive months correctly in Ethiopian calendar", () => {
+ // Test case 1: Adding within same year
+ const date1 = toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 22
+ }); // Greg: Jan 1, 2024
+ const result1 = addMonths(date1, 2);
+ const ethResult1 = toEthiopicDate(result1);
+ expect(ethResult1).toEqual({
+ year: 2016,
+ month: 6, // Yekatit(6)
+ day: 22
+ }); // Greg: Mar 1, 2024
+
+ // Test case 2: Adding across gregorian year boundary
+ const date2 = toGregorianDate({
+ year: 2016,
+ month: 5,
+ day: 22
+ }); // Greg: Feb 1, 2024
+ const result2 = addMonths(date2, 3);
+ const ethResult2 = toEthiopicDate(result2);
+ expect(ethResult2).toEqual({
+ year: 2016,
+ month: 8, // Meyazia(8)
+ day: 22
+ }); // Greg: Apr 30, 2024
+ });
+
+ test("should add negative months correctly in Ethiopian calendar", () => {
+ // Test case 1: Subtracting within same year
+ const date1 = toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 21
+ }); // Greg: Dec 31, 2023
+ const result1 = addMonths(date1, -2);
+ const ethResult1 = toEthiopicDate(result1);
+ expect(ethResult1).toEqual({
+ year: 2016,
+ month: 2, // Tikimt(2)
+ day: 21
+ }); // Greg: Oct 31, 2023
+
+ // Test case 2: Subtracting across gregorian year boundary
+ const date2 = toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 21
+ }); // Greg: Dec 31, 2023
+ const result2 = addMonths(date2, -3);
+ const ethResult2 = toEthiopicDate(result2);
+ expect(ethResult2).toEqual({
+ year: 2016,
+ month: 1, // Meskerem(1)
+ day: 21
+ }); // Greg: Oct 1, 2023
+ });
+
+ test("should handle day overflow in the 13th month Ethiopian calendar", () => {
+ // Test case 2: Day overflow in Pagume (13th month)
+ const date2 = toGregorianDate({
+ year: 2016,
+ month: 12,
+ day: 25
+ }); // Greg: Aug 31, 2024
+ const result2 = addMonths(date2, 1);
+ const ethResult2 = toEthiopicDate(result2);
+ expect(ethResult2).toEqual({
+ year: 2016,
+ month: 13, // Pagume
+ day: 5 // Adjusted from 26 to 5 (Pagume has only 5 or 6 days)
+ }); // Greg: Sep 5, 2024
+ });
+});
diff --git a/src/ethiopic/lib/addMonths.ts b/src/ethiopic/lib/addMonths.ts
index 85d2b4220..34c8d464a 100644
--- a/src/ethiopic/lib/addMonths.ts
+++ b/src/ethiopic/lib/addMonths.ts
@@ -1,16 +1,29 @@
+import { daysInMonth } from "../utils/daysInMonth.js";
import { toEthiopicDate, toGregorianDate } from "../utils/index.js";
/**
- * Adds months to an Ethiopic date
+ * Adds the specified number of months to the given Ethiopian date. Handles
+ * month overflow and year boundaries correctly.
*
- * @param {Date} date - The original date
- * @param {number} amount - The number of months to add
- * @returns {Date} The new date
+ * @param date - The starting gregorian date
+ * @param amount - The number of months to add (can be negative)
+ * @returns A new gregorian date with the months added
*/
export function addMonths(date: Date, amount: number): Date {
const { year, month, day } = toEthiopicDate(date);
- const totalMonths = month + amount - 1;
- const newYear = year + Math.floor(totalMonths / 12);
- const newMonth = (totalMonths % 12) + 1;
- return toGregorianDate({ year: newYear, month: newMonth, day });
+ let newMonth = month + amount;
+ const yearAdjustment = Math.floor((newMonth - 1) / 13);
+ newMonth = ((newMonth - 1) % 13) + 1;
+
+ if (newMonth < 1) {
+ newMonth += 13;
+ }
+
+ const newYear = year + yearAdjustment;
+
+ // Adjust day if it exceeds the month length
+ const monthLength = daysInMonth(newMonth, newYear);
+ const newDay = Math.min(day, monthLength);
+
+ return toGregorianDate({ year: newYear, month: newMonth, day: newDay });
}
diff --git a/src/ethiopic/lib/addWeeks.ts b/src/ethiopic/lib/addWeeks.ts
index 5363453f0..3c7b12ab6 100644
--- a/src/ethiopic/lib/addWeeks.ts
+++ b/src/ethiopic/lib/addWeeks.ts
@@ -7,6 +7,7 @@ import { addDays } from "./addDays.js";
* @param {number} amount - The number of weeks to add
* @returns {Date} The new date
*/
+//TODO: We can use the addWeeks from Date-fns
export function addWeeks(date: Date, amount: number): Date {
return addDays(date, amount * 7);
}
diff --git a/src/ethiopic/lib/addYears.test.ts b/src/ethiopic/lib/addYears.test.ts
index 09bfb117a..dc23f3e9a 100644
--- a/src/ethiopic/lib/addYears.test.ts
+++ b/src/ethiopic/lib/addYears.test.ts
@@ -1 +1,50 @@
-test.todo("addYears should correctly add years to an Ethiopic date");
+import { toEthiopicDate, toGregorianDate } from "../utils/index.js";
+
+import { addYears } from "./addYears";
+
+describe("addYears in Ethiopian calendar", () => {
+ test("should add positive years correctly in Ethiopian calendar", () => {
+ const date = toGregorianDate({
+ year: 2015,
+ month: 4,
+ day: 22
+ }); // Greg: Jan 1, 2023
+ const result = addYears(date, 2);
+ const ethResult = toEthiopicDate(result);
+ expect(ethResult).toEqual({
+ year: 2017,
+ month: 4, // Tahsas(4)
+ day: 22
+ }); // Greg: Jan 1, 2025
+ });
+
+ test("should add negative years correctly in Ethiopian calendar", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 21
+ }); // Greg: Dec 31, 2023
+ const result = addYears(date, -2);
+ const ethResult = toEthiopicDate(result);
+ expect(ethResult).toEqual({
+ year: 2014,
+ month: 4, // Tahsas(4)
+ day: 21
+ }); // Greg: Dec 31, 2021
+ });
+
+ test("should maintain month and day when adding years from leap year in Ethiopian calendar", () => {
+ const date = toGregorianDate({
+ year: 2015,
+ month: 13, // Pagume
+ day: 6
+ }); // Greg: Sep 6, 2023, Leap year day
+ const result = addYears(date, 1);
+ const ethResult = toEthiopicDate(result);
+ expect(ethResult).toEqual({
+ year: 2016,
+ month: 13, // Pagume
+ day: 5
+ }); // Greg: Sep 6, 2024
+ });
+});
diff --git a/src/ethiopic/lib/addYears.ts b/src/ethiopic/lib/addYears.ts
index e80d26fd2..1eae6e303 100644
--- a/src/ethiopic/lib/addYears.ts
+++ b/src/ethiopic/lib/addYears.ts
@@ -1,20 +1,29 @@
import {
- toEthiopicDate,
isEthiopicLeapYear,
+ toEthiopicDate,
toGregorianDate
} from "../utils/index.js";
/**
- * Adds years to an Ethiopic date
+ * Adds the specified number of years to the given Ethiopian date. Handles leap
+ * year transitions for Pagume month.
*
- * @param {Date} date - The original date
- * @param {number} amount - The number of years to add
- * @returns {Date} The new date
+ * @param date - The starting gregorian date
+ * @param amount - The number of years to add (can be negative)
+ * @returns A new gregorian date with the years added
*/
export function addYears(date: Date, amount: number): Date {
- const { year, month, day } = toEthiopicDate(date);
- const newYear = year + amount;
- const newDay =
- month === 13 && day === 6 && !isEthiopicLeapYear(newYear) ? 5 : day;
- return toGregorianDate({ year: newYear, month, day: newDay });
+ const etDate = toEthiopicDate(date);
+ const day =
+ isEthiopicLeapYear(etDate.year) &&
+ etDate.month === 13 &&
+ etDate.day === 6 &&
+ amount % 4 !== 0
+ ? 5
+ : etDate.day;
+ return toGregorianDate({
+ month: etDate.month,
+ day: day,
+ year: etDate.year + amount
+ });
}
diff --git a/src/ethiopic/lib/differenceInCalendarDays.ts b/src/ethiopic/lib/differenceInCalendarDays.ts
index 8e642ff67..800c92547 100644
--- a/src/ethiopic/lib/differenceInCalendarDays.ts
+++ b/src/ethiopic/lib/differenceInCalendarDays.ts
@@ -1,4 +1,4 @@
-import { toEthiopicDate, isEthiopicLeapYear } from "../utils/index.js";
+import { differenceInCalendarDays as differenceInCalendarDaysNative } from "date-fns";
/**
* Difference in calendar days
@@ -7,17 +7,11 @@ import { toEthiopicDate, isEthiopicLeapYear } from "../utils/index.js";
* @param {Date} dateRight - The earlier date
* @returns {number} The number of calendar days between the two dates
*/
+//TODO: We can use the differenceInCalendarDays from Date-fns
export function differenceInCalendarDays(
dateLeft: Date,
dateRight: Date
): number {
- const leftYear = toEthiopicDate(dateLeft).year;
- const rightYear = toEthiopicDate(dateRight).year;
- const leapDays = Array.from(
- { length: leftYear - rightYear },
- (_, i) => rightYear + i
- ).filter(isEthiopicLeapYear).length;
- return (
- Math.floor((dateLeft.getTime() - dateRight.getTime()) / 86400000) + leapDays
- );
+ const result = differenceInCalendarDaysNative(dateLeft, dateRight);
+ return result;
}
diff --git a/src/ethiopic/lib/differenceInCalendarMonths.test.ts b/src/ethiopic/lib/differenceInCalendarMonths.test.ts
new file mode 100644
index 000000000..ae234a29e
--- /dev/null
+++ b/src/ethiopic/lib/differenceInCalendarMonths.test.ts
@@ -0,0 +1,42 @@
+import { toGregorianDate } from "../utils";
+
+import { differenceInCalendarMonths } from "./differenceInCalendarMonths";
+
+describe("differenceInCalendarMonths in Ethiopian calendar", () => {
+ test("should calculate difference in months within the same Ethiopian year", () => {
+ const date1 = toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 1
+ }); // Greg: Dec 11, 2023
+ const date2 = toGregorianDate({
+ year: 2016,
+ month: 7,
+ day: 1
+ }); // Greg: Mar 10, 2024
+ expect(differenceInCalendarMonths(date2, date1)).toBe(3);
+ });
+
+ test("should calculate difference in months across Ethiopian years", () => {
+ const date1 = toGregorianDate({
+ year: 2015,
+ month: 11,
+ day: 1
+ }); // Greg: Jul 8, 2023
+ const date2 = toGregorianDate({
+ year: 2016,
+ month: 2,
+ day: 1
+ }); // Greg: Oct 12, 2023
+ expect(differenceInCalendarMonths(date2, date1)).toBe(4);
+ });
+
+ test("should return zero for same Ethiopian date", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 15
+ }); // Greg: Dec 25, 2023
+ expect(differenceInCalendarMonths(date, date)).toBe(0);
+ });
+});
diff --git a/src/ethiopic/lib/differenceInCalendarMonths.ts b/src/ethiopic/lib/differenceInCalendarMonths.ts
index 03cc06d8d..97563770c 100644
--- a/src/ethiopic/lib/differenceInCalendarMonths.ts
+++ b/src/ethiopic/lib/differenceInCalendarMonths.ts
@@ -1,4 +1,4 @@
-import { toEthiopicDate, isEthiopicLeapYear } from "../utils/index.js";
+import { toEthiopicDate } from "../utils/index.js";
/**
* Difference in calendar months
@@ -13,13 +13,8 @@ export function differenceInCalendarMonths(
): number {
const ethiopicLeft = toEthiopicDate(dateLeft);
const ethiopicRight = toEthiopicDate(dateRight);
- const leapDays = Array.from(
- { length: ethiopicLeft.year - ethiopicRight.year },
- (_, i) => ethiopicRight.year + i
- ).filter(isEthiopicLeapYear).length;
return (
- (ethiopicLeft.year - ethiopicRight.year) * 12 +
- (ethiopicLeft.month - ethiopicRight.month) +
- leapDays
+ (ethiopicLeft.year - ethiopicRight.year) * 13 +
+ (ethiopicLeft.month - ethiopicRight.month)
);
}
diff --git a/src/ethiopic/lib/eachMonthOfInterval.test.ts b/src/ethiopic/lib/eachMonthOfInterval.test.ts
index 7cadbf2d7..92a414b64 100644
--- a/src/ethiopic/lib/eachMonthOfInterval.test.ts
+++ b/src/ethiopic/lib/eachMonthOfInterval.test.ts
@@ -1,3 +1,90 @@
-test.todo(
- "should return an array of dates representing the start of each month in the interval"
-);
+import { toGregorianDate } from "../utils";
+
+import { eachMonthOfInterval } from "./eachMonthOfInterval";
+
+describe("eachMonthOfInterval in Ethiopian calendar", () => {
+ test("should return an array of dates representing the start of each month in the interval", () => {
+ const start = toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 1
+ }); // January 1, 2024 (2016-04-01 E.C.)
+ const end = toGregorianDate({
+ year: 2016,
+ month: 6,
+ day: 30
+ }); // March 9, 2024 (2016-06-30 E.C.)
+
+ const result = eachMonthOfInterval({ start, end });
+
+ expect(result).toEqual([
+ toGregorianDate({ year: 2016, month: 4, day: 1 }), // 2016-04-01 E.C.
+ toGregorianDate({ year: 2016, month: 5, day: 1 }), // 2016-05-01 E.C.
+ toGregorianDate({ year: 2016, month: 6, day: 1 }) // 2016-06-01 E.C.
+ ]);
+ });
+
+ test("should handle intervals spanning Ethiopian new year", () => {
+ const start = toGregorianDate({
+ year: 2015,
+ month: 13,
+ day: 1
+ }); // September 1, 2023 (2015-13-01 E.C.)
+ const end = toGregorianDate({
+ year: 2016,
+ month: 2,
+ day: 30
+ }); // October 15, 2023 (2016-02-30 E.C.)
+
+ const result = eachMonthOfInterval({ start, end });
+
+ expect(result).toEqual([
+ toGregorianDate({ year: 2015, month: 13, day: 1 }), // 2015-13-01 E.C.
+ toGregorianDate({ year: 2016, month: 1, day: 1 }), // 2016-01-01 E.C.
+ toGregorianDate({ year: 2016, month: 2, day: 1 }) // 2016-02-01 E.C.
+ ]);
+ });
+
+ test("should handle single month intervals", () => {
+ const start = toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 1
+ }); // 2016-04-01 E.C.
+ const end = toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 30
+ }); // 2016-04-30 E.C.
+
+ const result = eachMonthOfInterval({ start, end });
+
+ expect(result).toEqual([
+ toGregorianDate({ year: 2016, month: 4, day: 1 }) // 2016-04-01 E.C.
+ ]);
+ });
+
+ test("should handle intervals spanning multiple years", () => {
+ const start = toGregorianDate({
+ year: 2016,
+ month: 10,
+ day: 1
+ }); // 2016-01-01 E.C.
+ const end = toGregorianDate({
+ year: 2017,
+ month: 2,
+ day: 5
+ }); // 2016-13-05 E.C.
+
+ const result = eachMonthOfInterval({ start, end });
+
+ // Should return 13 dates (one for each Ethiopian month in the year)
+ expect(result).toHaveLength(6);
+ expect(result[0]).toEqual(
+ toGregorianDate({ year: 2016, month: 10, day: 1 })
+ ); // First month
+ expect(result[5]).toEqual(
+ toGregorianDate({ year: 2017, month: 2, day: 1 })
+ ); // Pagume
+ });
+});
diff --git a/src/ethiopic/lib/eachMonthOfInterval.ts b/src/ethiopic/lib/eachMonthOfInterval.ts
index 2acd82d2e..f56d6bc0a 100644
--- a/src/ethiopic/lib/eachMonthOfInterval.ts
+++ b/src/ethiopic/lib/eachMonthOfInterval.ts
@@ -28,7 +28,7 @@ export function eachMonthOfInterval(interval: Interval): Date[] {
);
currentMonth++;
- if (currentMonth > 12) {
+ if (currentMonth > 13) {
currentMonth = 1;
currentYear++;
}
diff --git a/src/ethiopic/lib/endOfMonth.test.ts b/src/ethiopic/lib/endOfMonth.test.ts
index 4fd3d089c..e414c4ed6 100644
--- a/src/ethiopic/lib/endOfMonth.test.ts
+++ b/src/ethiopic/lib/endOfMonth.test.ts
@@ -1 +1,50 @@
-test.todo("should return the correct end of the month date for a given date");
+import { toEthiopicDate, toGregorianDate } from "../utils";
+
+import { endOfMonth } from "./endOfMonth";
+
+describe("endOfMonth in Ethiopian calendar", () => {
+ test("Should return the last day of a 30-day Ethiopian month", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 15
+ }); // Greg: Dec 25, 2023
+ const result = endOfMonth(date);
+ const ethResult = toEthiopicDate(result);
+ expect(ethResult).toEqual({
+ year: 2016,
+ month: 4,
+ day: 30
+ }); // Greg: Jan 9, 2024
+ });
+
+ test("Should handle Pagume (13th month) correctly in leap and non-leap years", () => {
+ // Non-leap year (5 days)
+ const nonLeapDate = toGregorianDate({
+ year: 2016,
+ month: 13,
+ day: 1
+ }); // Greg: Sep 6, 2024
+ const nonLeapResult = endOfMonth(nonLeapDate);
+ const ethNonLeapResult = toEthiopicDate(nonLeapResult);
+ expect(ethNonLeapResult).toEqual({
+ year: 2016,
+ month: 13,
+ day: 5
+ }); // Greg: Sep 10, 2024
+
+ // Leap year (6 days)
+ const leapDate = toGregorianDate({
+ year: 2015,
+ month: 13,
+ day: 1
+ }); // Greg: Sep 6, 2023
+ const leapResult = endOfMonth(leapDate);
+ const ethLeapResult = toEthiopicDate(leapResult);
+ expect(ethLeapResult).toEqual({
+ year: 2015,
+ month: 13,
+ day: 6
+ }); // Greg: Sep 11, 2023
+ });
+});
diff --git a/src/ethiopic/lib/endOfMonth.ts b/src/ethiopic/lib/endOfMonth.ts
index dbc8c9a2c..20e085481 100644
--- a/src/ethiopic/lib/endOfMonth.ts
+++ b/src/ethiopic/lib/endOfMonth.ts
@@ -1,17 +1,15 @@
-import {
- toEthiopicDate,
- isEthiopicLeapYear,
- toGregorianDate
-} from "../utils/index.js";
+import { daysInMonth } from "../utils/daysInMonth.js";
+import { toEthiopicDate, toGregorianDate } from "../utils/index.js";
/**
- * End of month
+ * Returns the last day of the Ethiopian month for the given date.
*
- * @param {Date} date - The original date
- * @returns {Date} The end of the month
+ * @param date - The gregorian date to get the end of month for
+ * @returns A new gregorian date representing the last day of the Ethiopian
+ * month
*/
export function endOfMonth(date: Date): Date {
const { year, month } = toEthiopicDate(date);
- const daysInMonth = month === 13 ? (isEthiopicLeapYear(year) ? 6 : 5) : 30;
- return toGregorianDate({ year, month, day: daysInMonth });
+ const day = daysInMonth(month, year);
+ return toGregorianDate({ year, month, day: day });
}
diff --git a/src/ethiopic/lib/endOfWeek.ts b/src/ethiopic/lib/endOfWeek.ts
index 696761e92..bc4f4ce95 100644
--- a/src/ethiopic/lib/endOfWeek.ts
+++ b/src/ethiopic/lib/endOfWeek.ts
@@ -1,20 +1,14 @@
-import { addDays } from "./addDays.js";
+import { endOfWeek as endOfWeekFns, EndOfWeekOptions } from "date-fns";
/**
* End of week
*
* @param {Date} date - The original date
- * @param {Object} [options] - The options object
- * @param {number} [options.weekStartsOn=0] - The index of the first day of the
- * week (0 - Sunday). Default is `0`
+ * @param {EndOfWeekOptions} [options] - The options object
* @returns {Date} The end of the week
*/
-export function endOfWeek(
- date: Date,
- options?: { weekStartsOn?: number }
-): Date {
- const weekStartsOn = options?.weekStartsOn ?? 0;
- const day = date.getDay();
- const diff = (7 - day + weekStartsOn - 1) % 7;
- return addDays(date, diff);
+export function endOfWeek(date: Date, options?: EndOfWeekOptions): Date {
+ const weekStartsOn = options?.weekStartsOn ?? 0; // Default to Monday (1)
+ const endOfWeek = endOfWeekFns(date, { weekStartsOn });
+ return endOfWeek;
}
diff --git a/src/ethiopic/lib/endOfYear.test.ts b/src/ethiopic/lib/endOfYear.test.ts
index 6b88f61e8..f39c034c1 100644
--- a/src/ethiopic/lib/endOfYear.test.ts
+++ b/src/ethiopic/lib/endOfYear.test.ts
@@ -1 +1,35 @@
-test.todo("should return the correct end of the year date for a given date");
+import { toEthiopicDate, toGregorianDate } from "../utils";
+
+import { endOfYear } from "./endOfYear";
+
+describe("endOfYear in Ethiopian calendar", () => {
+ test("Should return last day of Ethiopian year for non-leap year", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 1,
+ day: 1
+ }); // Greg: Sep 12, 2023
+ const result = endOfYear(date);
+ const ethResult = toEthiopicDate(result);
+ expect(ethResult).toEqual({
+ year: 2016,
+ month: 13, // Pagume
+ day: 5
+ }); // Greg: Sep 10, 2024
+ });
+
+ test("Should return last day of Ethiopian year for leap year", () => {
+ const date = toGregorianDate({
+ year: 2015,
+ month: 1,
+ day: 1
+ }); // Greg: Sep 12, 2022
+ const result = endOfYear(date);
+ const ethResult = toEthiopicDate(result);
+ expect(ethResult).toEqual({
+ year: 2015,
+ month: 13, // Pagume
+ day: 6
+ }); // Greg: Sep 11, 2023
+ });
+});
diff --git a/src/ethiopic/lib/format.test.ts b/src/ethiopic/lib/format.test.ts
new file mode 100644
index 000000000..f9fe86063
--- /dev/null
+++ b/src/ethiopic/lib/format.test.ts
@@ -0,0 +1,36 @@
+import { toGregorianDate } from "../utils";
+
+import { format } from "./format";
+
+describe("format", () => {
+ test("Should format date in Ethiopian calendar", () => {
+ // Ethiopian date: 06/07/2016 (dd/mm/yyyy)
+ const date = toGregorianDate({ year: 2016, month: 7, day: 6 });
+ expect(format(date, "yyyy-MM-dd")).toBe("2016-07-06");
+ expect(format(date, "d")).toBe("6");
+ expect(format(date, "yyyy-MM")).toBe("2016-07");
+ });
+
+ test("Should format Ethiopian month names correctly", () => {
+ // Ethiopian date: 06 መጋቢት 2016
+ const date = toGregorianDate({ year: 2016, month: 7, day: 6 });
+ expect(format(date, "LLLL yyyy")).toBe("መጋቢት 2016");
+ expect(format(date, "LLLL")).toBe("መጋቢት");
+ expect(format(date, "PPP")).toBe(" መጋቢት 6, 2016");
+ });
+
+ test("Should format time components correctly", () => {
+ // Ethiopian date: 06 መጋቢት 2016, 14:30
+ const date = toGregorianDate({ year: 2016, month: 7, day: 6 });
+ date.setHours(14, 30);
+ expect(format(date, "hh:mm a")).toBe("2:30 PM");
+ });
+
+ test("Should format full date with Ethiopian day names", () => {
+ // Ethiopian date: ዓርብ, 06 መጋቢት 2016
+ const date = toGregorianDate({ year: 2016, month: 7, day: 6 });
+ expect(format(date, "PPPP")).toBe("ዓርብ, መጋቢት 6, 2016");
+ expect(format(date, "cccc")).toBe("ዓርብ");
+ expect(format(date, "cccccc")).toBe("ዓ");
+ });
+});
diff --git a/src/ethiopic/lib/format.ts b/src/ethiopic/lib/format.ts
new file mode 100644
index 000000000..ff7298885
--- /dev/null
+++ b/src/ethiopic/lib/format.ts
@@ -0,0 +1,109 @@
+import type { FormatOptions as DateFnsFormatOptions } from "date-fns";
+
+import type { DateLibOptions } from "../../classes/DateLib.js";
+import { toEthiopicDate } from "../utils/index.js";
+
+import { formatNumber } from "./formatNumber.js";
+
+/** Options for formatting dates in the Ethiopian calendar */
+export type FormatOptions = DateFnsFormatOptions;
+
+function getEtDayName(day: Date, short: boolean = true): string {
+ const dayOfWeek = day.getDay();
+ return short ? shortDays[dayOfWeek] : longDays[dayOfWeek];
+}
+
+function getEtMonthName(m: number): string {
+ if (m > 0 && m <= 13) {
+ return ethMonths[m - 1];
+ }
+ return "";
+}
+
+function formatEthiopianDate(
+ dateObj: Date | undefined,
+ formatStr: string
+): string {
+ const etDate = dateObj ? toEthiopicDate(dateObj) : undefined;
+
+ if (!etDate) return "";
+
+ switch (formatStr) {
+ case "LLLL yyyy":
+ case "LLLL y":
+ return `${getEtMonthName(etDate.month)} ${etDate.year}`;
+
+ case "LLLL":
+ return getEtMonthName(etDate.month);
+
+ case "yyyy-MM-dd":
+ return `${etDate.year}-${etDate.month
+ .toString()
+ .padStart(2, "0")}-${etDate.day.toString().padStart(2, "0")}`;
+
+ case "yyyy-MM":
+ return `${etDate.year}-${etDate.month.toString().padStart(2, "0")}`;
+
+ case "d":
+ return etDate.day.toString();
+ case "PPP":
+ return ` ${getEtMonthName(etDate.month)} ${etDate.day}, ${etDate.year}`;
+ case "PPPP":
+ if (!dateObj) return "";
+ return `${getEtDayName(dateObj, false)}, ${getEtMonthName(etDate.month)} ${
+ etDate.day
+ }, ${etDate.year}`;
+
+ case "cccc":
+ return dateObj ? getEtDayName(dateObj, false) : "";
+ case "cccccc":
+ return dateObj ? getEtDayName(dateObj) : "";
+
+ default:
+ return `${etDate.day}/${etDate.month}/${etDate.year}`;
+ }
+}
+
+export function format(
+ date: Date,
+ formatStr: string,
+ options?: DateFnsFormatOptions
+): string {
+ const extendedOptions = options as DateLibOptions;
+
+ if (formatStr.includes("hh:mm") || formatStr.includes("a")) {
+ return new Intl.DateTimeFormat(extendedOptions?.locale?.code ?? "en-US", {
+ hour: "numeric",
+ minute: "numeric",
+ hour12: formatStr.includes("a")
+ }).format(date);
+ }
+
+ const formatted = formatEthiopianDate(date, formatStr);
+
+ if (extendedOptions?.numerals && extendedOptions.numerals === "geez") {
+ return formatted.replace(/\d+/g, (match) =>
+ formatNumber(parseInt(match), "geez")
+ );
+ }
+
+ return formatted;
+}
+
+export const ethMonths = [
+ "መስከረም",
+ "ጥቅምት",
+ "ህዳር",
+ "ታህሳስ",
+ "ጥር",
+ "የካቲት",
+ "መጋቢት",
+ "ሚያዚያ",
+ "ግንቦት",
+ "ሰኔ",
+ "ሐምሌ",
+ "ነሀሴ",
+ "ጳጉሜ"
+];
+export const shortDays = ["እ", "ሰ", "ማ", "ረ", "ሐ", "ዓ", "ቅ"];
+export const longDays = ["እሁድ", "ሰኞ", "ማክሰኞ", "ረቡዕ", "ሐሙስ", "ዓርብ", "ቅዳሜ"];
diff --git a/src/ethiopic/lib/formatNumber.test.ts b/src/ethiopic/lib/formatNumber.test.ts
new file mode 100644
index 000000000..879c9c2fa
--- /dev/null
+++ b/src/ethiopic/lib/formatNumber.test.ts
@@ -0,0 +1,47 @@
+import { formatNumber } from "./formatNumber";
+
+describe("formatNumber", () => {
+ test("Should format numbers using Ethiopian numerals", () => {
+ expect(formatNumber(1, "geez")).toBe("፩");
+ expect(formatNumber(10, "geez")).toBe("፲");
+ expect(formatNumber(12, "geez")).toBe("፲፪");
+ expect(formatNumber(21, "geez")).toBe("፳፩");
+ expect(formatNumber(100, "geez")).toBe("፻");
+ expect(formatNumber(1000, "geez")).toBe("፲፻");
+ expect(formatNumber(10000, "geez")).toBe("፼");
+
+ // years
+ expect(formatNumber(1998, "geez")).toBe("፲፱፻፺፰");
+ expect(formatNumber(2000, "geez")).toBe("፳፻");
+ expect(formatNumber(2002, "geez")).toBe("፳፻፪");
+
+ // Complex numbers
+ expect(formatNumber(140000, "geez")).toBe("፲፬፼");
+ expect(formatNumber(123, "geez")).toBe("፻፳፫");
+ expect(formatNumber(1234, "geez")).toBe("፲፪፻፴፬");
+ expect(formatNumber(38965, "geez")).toBe("፫፼፹፱፻፷፭");
+ });
+
+ test("Should format numbers using latin numerals by default", () => {
+ expect(formatNumber(123)).toBe("123");
+ expect(formatNumber(1234)).toBe("1,234");
+ expect(formatNumber(12345)).toBe("12,345");
+
+ // Explicit latin format
+ expect(formatNumber(123, "latn")).toBe("123");
+ expect(formatNumber(1234, "latn")).toBe("1,234");
+ expect(formatNumber(12345, "latn")).toBe("12,345");
+ });
+
+ test("Should handle zero and negative numbers correctly", () => {
+ // Zero
+ expect(formatNumber(0, "geez")).toBe("-"); // Special case in Geez
+ expect(formatNumber(0, "latn")).toBe("0");
+
+ // Negative numbers
+ expect(formatNumber(-123, "geez")).toBe("-፻፳፫");
+ expect(formatNumber(-1234, "geez")).toBe("-፲፪፻፴፬");
+ expect(formatNumber(-123, "latn")).toBe("-123");
+ expect(formatNumber(-1234, "latn")).toBe("-1,234");
+ });
+});
diff --git a/src/ethiopic/lib/formatNumber.ts b/src/ethiopic/lib/formatNumber.ts
new file mode 100644
index 000000000..ccfe1325d
--- /dev/null
+++ b/src/ethiopic/lib/formatNumber.ts
@@ -0,0 +1,33 @@
+import { toGeezNumerals } from "../utils/geezConverter.js";
+
+/**
+ * Formats a number using either Latin or Ethiopic (Geez) numerals
+ *
+ * @example
+ * ```ts
+ * formatNumber(123) // '123'
+ * formatNumber(123, 'geez') // '፻፳፫'
+ * formatNumber(2023, 'geez') // '፳፻፳፫'
+ * ```;
+ *
+ * @param value - The number to format
+ * @param numerals - The numeral system to use:
+ *
+ * - 'latn': Latin numerals (1, 2, 3...)
+ * - 'geez': Ethiopic numerals (፩, ፪, ፫...)
+ *
+ * @returns The formatted number string
+ */
+export function formatNumber(value: number, numerals: string = "latn"): string {
+ console.log("numerals", value);
+ if (numerals === "geez") {
+ return toGeezNumerals(value);
+ }
+
+ // Use Intl.NumberFormat for other numeral systems
+ const formatter = new Intl.NumberFormat("en-US", {
+ numberingSystem: numerals
+ });
+
+ return formatter.format(value);
+}
diff --git a/src/ethiopic/lib/getMonth.test.ts b/src/ethiopic/lib/getMonth.test.ts
index addfd9364..bb5f40d0e 100644
--- a/src/ethiopic/lib/getMonth.test.ts
+++ b/src/ethiopic/lib/getMonth.test.ts
@@ -1 +1,24 @@
-test.todo("should return the correct zero-based month index for a given date");
+import { toGregorianDate } from "../utils";
+
+import { getMonth } from "./getMonth";
+
+describe("getMonth in Ethiopian calendar", () => {
+ test("should return 0-based month number", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 1, // Meskerem
+ day: 1
+ }); // Greg: Sep 12, 2023
+
+ expect(getMonth(date)).toBe(0);
+ });
+
+ test("should handle Pagume (13th month)", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 13, // Pagume
+ day: 1
+ }); // Greg: Sep 6, 2024
+ expect(getMonth(date)).toBe(12);
+ });
+});
diff --git a/src/ethiopic/lib/getWeek.test.ts b/src/ethiopic/lib/getWeek.test.ts
index f3a007310..698494045 100644
--- a/src/ethiopic/lib/getWeek.test.ts
+++ b/src/ethiopic/lib/getWeek.test.ts
@@ -1 +1,59 @@
-test.todo("should return the correct week number for a given date");
+import { toGregorianDate } from "../utils";
+
+import { getWeek } from "./getWeek";
+
+describe("getWeek in Ethiopian calendar", () => {
+ test("should return 1 for first week of year", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 1,
+ day: 1
+ }); // Greg: Sep 12, 2023
+ expect(getWeek(date)).toBe(1);
+ });
+
+ test("should handle dates in last month of year to be either 52 or 1", () => {
+ // part of the last week of the previous year
+ const date52 = toGregorianDate({
+ year: 2016,
+ month: 13,
+ day: 1
+ }); // Greg: Sep 10, 202
+ expect(getWeek(date52)).toBe(52);
+
+ // part of the first week of the new year
+ const date1 = toGregorianDate({
+ year: 2016,
+ month: 13,
+ day: 5
+ }); // Greg: Sep 10, 202
+ expect(getWeek(date1)).toBe(1);
+ });
+
+ test("should handle dates at week boundaries", () => {
+ // Sunday week 1
+ const sunday = toGregorianDate({
+ year: 2016,
+ month: 1,
+ day: 6
+ }); // Greg: Sep 17, 2023
+ expect(getWeek(sunday)).toBe(1);
+
+ // Monday week 2
+ const monday = toGregorianDate({
+ year: 2016,
+ month: 1,
+ day: 7
+ }); // Greg: Sep 18, 2023
+ expect(getWeek(monday)).toBe(2);
+ });
+
+ test("should handle dates in middle of year", () => {
+ const midYear = toGregorianDate({
+ year: 2016,
+ month: 6,
+ day: 15
+ }); // Greg: Feb 23, 2024
+ expect(getWeek(midYear)).toBe(24);
+ });
+});
diff --git a/src/ethiopic/lib/getWeek.ts b/src/ethiopic/lib/getWeek.ts
index 6032b828a..04aef487c 100644
--- a/src/ethiopic/lib/getWeek.ts
+++ b/src/ethiopic/lib/getWeek.ts
@@ -1,26 +1,40 @@
+import {
+ differenceInDays,
+ getWeek as getWeekFns,
+ GetWeekOptions
+} from "date-fns";
+
import { toGregorianDate, toEthiopicDate } from "../utils/index.js";
-import { differenceInCalendarDays } from "./differenceInCalendarDays.js";
+import { startOfWeek } from "./startOfWeek.js";
/**
- * Get week
+ * Get week number for Ethiopian calendar
*
* @param {Date} date - The original date
- * @param {Object} [options] - The options object
- * @param {number} [options.weekStartsOn=0] - The index of the first day of the
- * week (0 - Sunday). Default is `0`
+ * @param {GetWeekOptions} [options] - The options object
* @returns {number} The week number
*/
-export function getWeek(
- date: Date,
- options?: { weekStartsOn?: number }
-): number {
- const weekStartsOn = options?.weekStartsOn ?? 0; // Default to Sunday
- const startOfYear = toGregorianDate({
- year: toEthiopicDate(date).year,
+export function getWeek(date: Date, options?: GetWeekOptions): number {
+ const weekStartsOn = options?.weekStartsOn ?? 1; // Default to Tuesday for Ethiopian calendar
+ const etDate = toEthiopicDate(date);
+ const currentWeekStart = startOfWeek(date, { weekStartsOn });
+
+ // Get the first day of the current year
+ const firstDayOfYear = toGregorianDate({
+ year: etDate.year,
month: 1,
day: 1
});
- const diffInDays = differenceInCalendarDays(date, startOfYear);
- return Math.floor((diffInDays + weekStartsOn) / 7) + 1;
+
+ const firstWeekStart = startOfWeek(firstDayOfYear, { weekStartsOn });
+
+ // If date is before the first week of its year
+ if (date < firstWeekStart) {
+ return getWeekFns(date, { weekStartsOn, firstWeekContainsDate: 1 });
+ }
+
+ // Calculate week number based on days since first week
+ const daysSinceFirstWeek = differenceInDays(currentWeekStart, firstWeekStart);
+ return Math.floor(daysSinceFirstWeek / 7) + 1;
}
diff --git a/src/ethiopic/lib/getYear.test.ts b/src/ethiopic/lib/getYear.test.ts
index 32fb406fc..a922a89d4 100644
--- a/src/ethiopic/lib/getYear.test.ts
+++ b/src/ethiopic/lib/getYear.test.ts
@@ -1 +1,32 @@
-test.todo("should return the correct Ethiopic year for a given date");
+import { toGregorianDate } from "../utils";
+
+import { getYear } from "./getYear";
+
+describe("getYear in Ethiopian calendar", () => {
+ test("Should return correct Ethiopian year", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 15
+ }); // Greg: Dec 25, 2023
+ expect(getYear(date)).toBe(2016);
+ });
+
+ test("Should handle Ethiopian year boundary correctly", () => {
+ // Last day of Ethiopian year 2015
+ const lastDay2015 = toGregorianDate({
+ year: 2015,
+ month: 13,
+ day: 6
+ }); // Greg: Sep 11, 2023
+ expect(getYear(lastDay2015)).toBe(2015);
+
+ // First day of Ethiopian year 2016
+ const firstDay2016 = toGregorianDate({
+ year: 2016,
+ month: 1,
+ day: 1
+ }); // Greg: Sep 12, 2023
+ expect(getYear(firstDay2016)).toBe(2016);
+ });
+});
diff --git a/src/ethiopic/lib/index.ts b/src/ethiopic/lib/index.ts
index 0809e9e8a..88a2219f1 100644
--- a/src/ethiopic/lib/index.ts
+++ b/src/ethiopic/lib/index.ts
@@ -8,10 +8,11 @@ export * from "./eachMonthOfInterval.js";
export * from "./endOfMonth.js";
export * from "./endOfWeek.js";
export * from "./endOfYear.js";
+export * from "./format.js";
+export * from "./formatNumber.js";
export * from "./getMonth.js";
export * from "./getWeek.js";
export * from "./getYear.js";
-export * from "./isSameDay.js";
export * from "./isSameMonth.js";
export * from "./isSameYear.js";
export * from "./newDate.js";
diff --git a/src/ethiopic/lib/isAfter.ts b/src/ethiopic/lib/isAfter.ts
new file mode 100644
index 000000000..c5e93022a
--- /dev/null
+++ b/src/ethiopic/lib/isAfter.ts
@@ -0,0 +1,5 @@
+import { isAfter as isAfterNative } from "date-fns";
+
+export function isAfter(date: Date, other: Date): boolean {
+ return isAfterNative(date, other);
+}
diff --git a/src/ethiopic/lib/isSameDay.test.ts b/src/ethiopic/lib/isSameDay.test.ts
deleted file mode 100644
index 4363ac2fd..000000000
--- a/src/ethiopic/lib/isSameDay.test.ts
+++ /dev/null
@@ -1 +0,0 @@
-test.todo("isSameDay should return true if two dates are on the same day");
diff --git a/src/ethiopic/lib/isSameDay.ts b/src/ethiopic/lib/isSameDay.ts
deleted file mode 100644
index 90364abd9..000000000
--- a/src/ethiopic/lib/isSameDay.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { toEthiopicDate } from "../utils/index.js";
-
-/**
- * Is same day
- *
- * @param {Date} dateLeft - The first date
- * @param {Date} dateRight - The second date
- * @returns {boolean} True if the two dates are on the same day
- */
-export function isSameDay(dateLeft: Date, dateRight: Date): boolean {
- const left = toEthiopicDate(dateLeft);
- const right = toEthiopicDate(dateRight);
- return (
- left.year === right.year &&
- left.month === right.month &&
- left.day === right.day
- );
-}
diff --git a/src/ethiopic/lib/isSameMonth.test.ts b/src/ethiopic/lib/isSameMonth.test.ts
index d5355b629..79cb1659c 100644
--- a/src/ethiopic/lib/isSameMonth.test.ts
+++ b/src/ethiopic/lib/isSameMonth.test.ts
@@ -1 +1,48 @@
-test.todo("isSameMonth should return true if two dates are in the same month");
+import { toGregorianDate } from "../utils";
+
+import { isSameMonth } from "./isSameMonth";
+
+describe("isSameMonth in Ethiopian calendar", () => {
+ test("Should return true for dates in same Ethiopian month", () => {
+ const date1 = toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 1
+ }); // Greg: Dec 11, 2023
+ const date2 = toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 30
+ }); // Greg: Jan 9, 2024
+ expect(isSameMonth(date1, date2)).toBe(true);
+ });
+
+ test("Should return false for dates in different Ethiopian months", () => {
+ const date1 = toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 30
+ }); // Greg: Jan 9, 2024
+ const date2 = toGregorianDate({
+ year: 2016,
+ month: 5,
+ day: 1
+ }); // Greg: Jan 10, 2024
+ expect(isSameMonth(date1, date2)).toBe(false);
+ });
+
+ test("Should handle Ethiopian month boundaries correctly", () => {
+ // Same month number but different years
+ const date1 = toGregorianDate({
+ year: 2015,
+ month: 13,
+ day: 6
+ }); // Greg: Sep 11, 2023
+ const date2 = toGregorianDate({
+ year: 2016,
+ month: 13,
+ day: 5
+ }); // Greg: Sep 10, 2024
+ expect(isSameMonth(date1, date2)).toBe(false);
+ });
+});
diff --git a/src/ethiopic/lib/isSameYear.test.ts b/src/ethiopic/lib/isSameYear.test.ts
index a5bc214df..b0e6b7f51 100644
--- a/src/ethiopic/lib/isSameYear.test.ts
+++ b/src/ethiopic/lib/isSameYear.test.ts
@@ -1 +1,47 @@
-test.todo("isSameYear should return true if two dates are in the same year");
+import { toGregorianDate } from "../utils";
+
+import { isSameYear } from "./isSameYear";
+
+describe("isSameYear in Ethiopian calendar", () => {
+ test("Should return true for dates in same Ethiopian year", () => {
+ const date1 = toGregorianDate({
+ year: 2016,
+ month: 1,
+ day: 1
+ }); // Greg: Sep 12, 2023
+ const date2 = toGregorianDate({
+ year: 2016,
+ month: 13,
+ day: 5
+ }); // Greg: Sep 10, 2024
+ expect(isSameYear(date1, date2)).toBe(true);
+ });
+
+ test("Should return false for dates in different Ethiopian years", () => {
+ const date1 = toGregorianDate({
+ year: 2015,
+ month: 6,
+ day: 15
+ }); // Greg: Feb 23, 2023
+ const date2 = toGregorianDate({
+ year: 2016,
+ month: 6,
+ day: 15
+ }); // Greg: Feb 23, 2024
+ expect(isSameYear(date1, date2)).toBe(false);
+ });
+
+ test("Should handle Ethiopian year boundary correctly", () => {
+ const lastDayOf2015 = toGregorianDate({
+ year: 2015,
+ month: 13,
+ day: 6
+ }); // Greg: Sep 11, 2023
+ const firstDayOf2016 = toGregorianDate({
+ year: 2016,
+ month: 1,
+ day: 1
+ }); // Greg: Sep 12, 2023
+ expect(isSameYear(lastDayOf2015, firstDayOf2016)).toBe(false);
+ });
+});
diff --git a/src/ethiopic/lib/isSameYear.ts b/src/ethiopic/lib/isSameYear.ts
index 28c146e44..58d9fae9f 100644
--- a/src/ethiopic/lib/isSameYear.ts
+++ b/src/ethiopic/lib/isSameYear.ts
@@ -1,11 +1,11 @@
import { toEthiopicDate } from "../utils/index.js";
/**
- * Is same year
+ * Checks if two dates fall in the same Ethiopian year.
*
- * @param {Date} dateLeft - The first date
- * @param {Date} dateRight - The second date
- * @returns {boolean} True if the two dates are in the same year
+ * @param dateLeft - The first gregorian date to compare
+ * @param dateRight - The second gregorian date to compare
+ * @returns True if the dates are in the same Ethiopian year
*/
export function isSameYear(dateLeft: Date, dateRight: Date): boolean {
const left = toEthiopicDate(dateLeft);
diff --git a/src/ethiopic/lib/newDate.test.ts b/src/ethiopic/lib/newDate.test.ts
index d798aaad6..fb836a51b 100644
--- a/src/ethiopic/lib/newDate.test.ts
+++ b/src/ethiopic/lib/newDate.test.ts
@@ -1,3 +1,48 @@
-describe("newDate", () => {
- test.todo("should create a new Ethiopic date");
+import { toEthiopicDate } from "../utils/index.js";
+
+import { newDate } from "./newDate";
+
+describe("newDate in Ethiopian calendar", () => {
+ test("creates date with valid Ethiopian values", () => {
+ const date = newDate(2016, 4, 15); // Tahsas 15, 2016
+ const ethDate = toEthiopicDate(date);
+ expect(ethDate).toEqual({
+ year: 2016,
+ month: 5, // Tahsas
+ day: 15
+ }); // Greg: Dec 25, 2023
+ });
+
+ test("handles Pagume (13th month)", () => {
+ // Non-leap year Pagume
+ const nonLeapDate = newDate(2016, 12, 5);
+ const ethNonLeapDate = toEthiopicDate(nonLeapDate);
+ expect(ethNonLeapDate).toEqual({
+ year: 2016,
+ month: 13, // Pagume
+ day: 5
+ }); // Greg: Sep 10, 2024
+
+ // Leap year Pagume
+ const leapDate = newDate(2015, 12, 6);
+ const ethLeapDate = toEthiopicDate(leapDate);
+ expect(ethLeapDate).toEqual({
+ year: 2015,
+ month: 13, // Pagume
+ day: 6
+ }); // Greg: Sep 11, 2023
+ });
+
+ test("throws error for invalid month and day", () => {
+ //invalid month
+ expect(() => newDate(2016, 14, 1)).toThrow();
+ //invalid day
+ expect(() => newDate(2016, 13, 7)).toThrow();
+ //invalid month and day
+ expect(() => newDate(2016, 5, 32)).toThrow();
+ //invalid month and day
+ expect(() => newDate(2016, -1, -1)).toThrow();
+ //not a leap year
+ expect(() => newDate(2016, 13, 6)).toThrow();
+ });
});
diff --git a/src/ethiopic/lib/newDate.ts b/src/ethiopic/lib/newDate.ts
index c494ccefc..cc7d1c769 100644
--- a/src/ethiopic/lib/newDate.ts
+++ b/src/ethiopic/lib/newDate.ts
@@ -1,4 +1,5 @@
import { toGregorianDate } from "../utils/index.js";
+import { isEthiopicDateValid } from "../utils/isEthiopicDateValid.js";
/**
* Creates a new Ethiopic date
@@ -9,5 +10,15 @@ import { toGregorianDate } from "../utils/index.js";
* @returns {Date} The corresponding Gregorian date
*/
export function newDate(year: number, monthIndex: number, date: number): Date {
- return toGregorianDate({ year, month: monthIndex + 1, day: date });
+ // Convert from 0-based month index to 1-based Ethiopic month
+ const month = monthIndex + 1;
+
+ if (!isEthiopicDateValid({ year, month, day: date })) {
+ throw new Error("Invalid Ethiopic date");
+ }
+ return toGregorianDate({
+ year: year,
+ month: month,
+ day: date
+ });
}
diff --git a/src/ethiopic/lib/setMonth.test.ts b/src/ethiopic/lib/setMonth.test.ts
index 1004cfcca..05beb2a30 100644
--- a/src/ethiopic/lib/setMonth.test.ts
+++ b/src/ethiopic/lib/setMonth.test.ts
@@ -1,3 +1,56 @@
-describe("setMonth", () => {
- test.todo("set the Ethiopic month for a given date");
+import { toGregorianDate } from "../utils";
+
+import { setMonth } from "./setMonth";
+
+describe("setMonth in Ethiopian calendar", () => {
+ test("should set month to a regular month", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 1, // Meskerem
+ day: 1
+ }); // Greg: Sep 12, 2023
+
+ const result = setMonth(date, 3); // Set to 4th month (Tahsas)
+ expect(result).toEqual(
+ toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 1
+ })
+ );
+ });
+
+ test("should handle setting to Pagume (13th month)", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 1,
+ day: 1
+ }); // Greg: Sep 12, 2023
+
+ const result = setMonth(date, 12); // Set to Pagume (0-based index)
+ expect(result).toEqual(
+ toGregorianDate({
+ year: 2016,
+ month: 13,
+ day: 1
+ })
+ );
+ });
+
+ test("should preserve the day when setting month", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 1,
+ day: 15
+ }); // Greg: Sep 26, 2023
+
+ const result = setMonth(date, 5); // Set to 6th month (Yekatit)
+ expect(result).toEqual(
+ toGregorianDate({
+ year: 2016,
+ month: 6,
+ day: 15
+ })
+ );
+ });
});
diff --git a/src/ethiopic/lib/setYear.test.ts b/src/ethiopic/lib/setYear.test.ts
index cb6524ee8..6ffd0b4a2 100644
--- a/src/ethiopic/lib/setYear.test.ts
+++ b/src/ethiopic/lib/setYear.test.ts
@@ -1,3 +1,65 @@
-describe("setYear", () => {
- test.todo("set the Ethiopic year for a given date");
+import { toEthiopicDate, toGregorianDate } from "../utils/index.js";
+
+import { setYear } from "./setYear";
+
+describe("setYear in Ethiopian calendar", () => {
+ test("sets year correctly", () => {
+ const date = toGregorianDate({
+ year: 2015,
+ month: 4,
+ day: 15
+ }); // Greg: Dec 25, 2022
+ const result = setYear(date, 2016);
+ const ethResult = toEthiopicDate(result);
+ expect(ethResult).toEqual({
+ year: 2016,
+ month: 4,
+ day: 15
+ }); // Greg: Dec 25, 2023
+ });
+
+ test("maintains month and day when possible", () => {
+ const date = toGregorianDate({
+ year: 2015,
+ month: 7,
+ day: 21
+ }); // Greg: Mar 30, 2023
+ const result = setYear(date, 2016);
+ const ethResult = toEthiopicDate(result);
+ expect(ethResult).toEqual({
+ year: 2016,
+ month: 7,
+ day: 21
+ }); // Greg: Mar 29, 2024
+ });
+
+ test("adjusts day for leap year changes", () => {
+ // From leap year to non-leap year (Pagume 6 -> Pagume 5)
+ const leapDate = toGregorianDate({
+ year: 2015,
+ month: 13,
+ day: 6
+ }); // Greg: Sep 11, 2023
+ const nonLeapResult = setYear(leapDate, 2016);
+ const ethNonLeapResult = toEthiopicDate(nonLeapResult);
+ expect(ethNonLeapResult).toEqual({
+ year: 2016,
+ month: 13,
+ day: 5
+ }); // Greg: Sep 10, 2024
+
+ // From non-leap year to leap year (maintains Pagume 5)
+ const nonLeapDate = toGregorianDate({
+ year: 2016,
+ month: 13,
+ day: 5
+ }); // Greg: Sep 10, 2024
+ const leapResult = setYear(nonLeapDate, 2015);
+ const ethLeapResult = toEthiopicDate(leapResult);
+ expect(ethLeapResult).toEqual({
+ year: 2015,
+ month: 13,
+ day: 5
+ }); // Greg: Sep 10, 2023
+ });
});
diff --git a/src/ethiopic/lib/setYear.ts b/src/ethiopic/lib/setYear.ts
index e86877735..2af2e86c3 100644
--- a/src/ethiopic/lib/setYear.ts
+++ b/src/ethiopic/lib/setYear.ts
@@ -1,3 +1,4 @@
+import { daysInMonth } from "../utils/daysInMonth.js";
import { toEthiopicDate, toGregorianDate } from "../utils/index.js";
/**
@@ -9,5 +10,10 @@ import { toEthiopicDate, toGregorianDate } from "../utils/index.js";
*/
export function setYear(date: Date, year: number): Date {
const { month, day } = toEthiopicDate(date);
- return toGregorianDate({ year, month, day });
+
+ // Check if the day is valid in the new year (handles leap year changes)
+ const maxDays = daysInMonth(month, year);
+ const newDay = Math.min(day, maxDays);
+
+ return toGregorianDate({ year, month, day: newDay });
}
diff --git a/src/ethiopic/lib/startOfDay.test.ts b/src/ethiopic/lib/startOfDay.test.ts
index 195e1963b..fc91529f5 100644
--- a/src/ethiopic/lib/startOfDay.test.ts
+++ b/src/ethiopic/lib/startOfDay.test.ts
@@ -1,3 +1,71 @@
-describe("startOfDay", () => {
- test.todo("should return the start of the Ethiopic day for a given date");
+import { toGregorianDate } from "../utils";
+
+import { startOfDay } from "./startOfDay";
+
+describe("startOfDay in Ethiopian calendar", () => {
+ test("should return the start of the Ethiopic day for a given date", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 1,
+ day: 1
+ }); // Greg: Sep 12, 2023
+ date.setHours(12, 34, 56, 789); // Add time components
+
+ const result = startOfDay(date);
+
+ // Should preserve the date but reset time to start of day
+ expect(result).toEqual(
+ toGregorianDate({
+ year: 2016,
+ month: 1,
+ day: 1
+ })
+ );
+ expect(result.getHours()).toBe(0);
+ expect(result.getMinutes()).toBe(0);
+ expect(result.getSeconds()).toBe(0);
+ expect(result.getMilliseconds()).toBe(0);
+ });
+
+ test("should handle dates with time near midnight", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 1,
+ day: 1
+ });
+ date.setHours(23, 59, 59, 999);
+
+ const result = startOfDay(date);
+
+ expect(result).toEqual(
+ toGregorianDate({
+ year: 2016,
+ month: 1,
+ day: 1
+ })
+ );
+ expect(result.getHours()).toBe(0);
+ expect(result.getMinutes()).toBe(0);
+ expect(result.getSeconds()).toBe(0);
+ expect(result.getMilliseconds()).toBe(0);
+ });
+
+ test("should preserve the original date components", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 13, // Pagume
+ day: 5
+ }); // Greg: Sep 10, 2024
+ date.setHours(15, 30, 45, 500);
+
+ const result = startOfDay(date);
+
+ expect(result).toEqual(
+ toGregorianDate({
+ year: 2016,
+ month: 13,
+ day: 5
+ })
+ );
+ });
});
diff --git a/src/ethiopic/lib/startOfMonth.test.ts b/src/ethiopic/lib/startOfMonth.test.ts
index b9e725910..5a45c6274 100644
--- a/src/ethiopic/lib/startOfMonth.test.ts
+++ b/src/ethiopic/lib/startOfMonth.test.ts
@@ -1 +1,50 @@
-test.todo("startOfMonth should return the start of the Ethiopic month");
+import { toEthiopicDate, toGregorianDate } from "../utils/index.js";
+
+import { startOfMonth } from "./startOfMonth";
+
+describe("startOfMonth in Ethiopian calendar", () => {
+ test("returns first day of Ethiopian month", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 4,
+ day: 15
+ }); // Greg: Dec 25, 2023
+ const result = startOfMonth(date);
+ const ethResult = toEthiopicDate(result);
+ expect(ethResult).toEqual({
+ year: 2016,
+ month: 4,
+ day: 1
+ }); // Greg: Dec 11, 2023
+ });
+
+ test("maintains year and month", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 7,
+ day: 30
+ }); // Greg: Apr 7, 2024
+ const result = startOfMonth(date);
+ const ethResult = toEthiopicDate(result);
+ expect(ethResult).toEqual({
+ year: 2016,
+ month: 7,
+ day: 1
+ }); // Greg: Mar 10, 2024
+ });
+
+ test("handles Pagume (13th month)", () => {
+ const date = toGregorianDate({
+ year: 2015,
+ month: 13,
+ day: 6
+ }); // Greg: Sep 11, 2023
+ const result = startOfMonth(date);
+ const ethResult = toEthiopicDate(result);
+ expect(ethResult).toEqual({
+ year: 2015,
+ month: 13,
+ day: 1
+ }); // Greg: Sep 6, 2023
+ });
+});
diff --git a/src/ethiopic/lib/startOfWeek.test.ts b/src/ethiopic/lib/startOfWeek.test.ts
index 37eb654e9..b1f0484f5 100644
--- a/src/ethiopic/lib/startOfWeek.test.ts
+++ b/src/ethiopic/lib/startOfWeek.test.ts
@@ -1 +1,38 @@
-test.todo("startOfWeek should return the start of the week");
+import { startOfWeek } from "./startOfWeek";
+
+describe("startOfWeek", () => {
+ test("should return Monday when given a Monday", () => {
+ const monday = new Date(2024, 2, 18); // Monday, March 18, 2024
+ const result = startOfWeek(monday);
+ expect(result.getDay()).toBe(1); // Monday is 1
+ expect(result.toISOString()).toBe(monday.toISOString());
+ });
+
+ test("should return previous Monday when given a Wednesday", () => {
+ const wednesday = new Date(2024, 2, 20); // Wednesday, March 20, 2024
+ const result = startOfWeek(wednesday);
+ const expected = new Date(2024, 2, 18); // Should return Monday, March 18, 2024
+ expect(result.toISOString()).toBe(expected.toISOString());
+ });
+
+ test("should return previous Monday when given a Sunday", () => {
+ const sunday = new Date(2024, 2, 24); // Sunday, March 24, 2024
+ const result = startOfWeek(sunday);
+ const expected = new Date(2024, 2, 18); // Should return Monday, March 18, 2024
+ expect(result.toISOString()).toBe(expected.toISOString());
+ });
+
+ test("should handle month boundaries correctly", () => {
+ const saturday = new Date(2024, 2, 2); // Saturday, March 2, 2024
+ const result = startOfWeek(saturday);
+ const expected = new Date(2024, 1, 26); // Should return Monday, February 26, 2024
+ expect(result.toISOString()).toBe(expected.toISOString());
+ });
+
+ test("should handle year boundaries correctly", () => {
+ const wednesday = new Date(2024, 0, 3); // Wednesday, January 3, 2024
+ const result = startOfWeek(wednesday);
+ const expected = new Date(2024, 0, 1); // Should return Monday, January 1, 2024
+ expect(result.toISOString()).toBe(expected.toISOString());
+ });
+});
diff --git a/src/ethiopic/lib/startOfWeek.ts b/src/ethiopic/lib/startOfWeek.ts
index ebf774cec..443c1e818 100644
--- a/src/ethiopic/lib/startOfWeek.ts
+++ b/src/ethiopic/lib/startOfWeek.ts
@@ -1,12 +1,13 @@
-import { addDays } from "./addDays.js";
+import { StartOfWeekOptions, startOfWeek as startOfWeekFns } from "date-fns";
/**
* Start of week
*
* @param {Date} date - The original date
+ * @param {StartOfWeekOptions} [options] - The options object
* @returns {Date} The start of the week
*/
-export function startOfWeek(date: Date): Date {
- const day = date.getDay();
- return addDays(date, -day); // Subtract days to get to Sunday (start of week)
+export function startOfWeek(date: Date, options?: StartOfWeekOptions): Date {
+ const weekStartsOn = options?.weekStartsOn ?? 1; // Default to Monday (1)
+ return startOfWeekFns(date, { weekStartsOn: weekStartsOn });
}
diff --git a/src/ethiopic/lib/startOfYear.test.ts b/src/ethiopic/lib/startOfYear.test.ts
index 7031f7887..c99dd4c9a 100644
--- a/src/ethiopic/lib/startOfYear.test.ts
+++ b/src/ethiopic/lib/startOfYear.test.ts
@@ -1 +1,50 @@
-test.todo("startOfYear should return the start of the Ethiopic year");
+import { toEthiopicDate, toGregorianDate } from "../utils/index.js";
+
+import { startOfYear } from "./startOfYear";
+
+describe("startOfYear in Ethiopian calendar", () => {
+ test("returns first day of Ethiopian year", () => {
+ const date = toGregorianDate({
+ year: 2016,
+ month: 6,
+ day: 15
+ }); // Greg: Feb 23, 2024
+ const result = startOfYear(date);
+ const ethResult = toEthiopicDate(result);
+ expect(ethResult).toEqual({
+ year: 2016,
+ month: 1,
+ day: 1
+ }); // Greg: Sep 12, 2023
+ });
+
+ test("handles leap year correctly", () => {
+ // From middle of leap year
+ const leapDate = toGregorianDate({
+ year: 2015,
+ month: 7,
+ day: 15
+ }); // Greg: Mar 24, 2023
+ const leapResult = startOfYear(leapDate);
+ const ethLeapResult = toEthiopicDate(leapResult);
+ expect(ethLeapResult).toEqual({
+ year: 2015,
+ month: 1,
+ day: 1
+ }); // Greg: Sep 12, 2022
+
+ // From middle of non-leap year
+ const nonLeapDate = toGregorianDate({
+ year: 2016,
+ month: 7,
+ day: 15
+ }); // Greg: Mar 24, 2024
+ const nonLeapResult = startOfYear(nonLeapDate);
+ const ethNonLeapResult = toEthiopicDate(nonLeapResult);
+ expect(ethNonLeapResult).toEqual({
+ year: 2016,
+ month: 1,
+ day: 1
+ }); // Greg: Sep 12, 2023
+ });
+});
diff --git a/src/ethiopic/utils/EthiopicDate.ts b/src/ethiopic/utils/EthiopicDate.ts
index ae2ea143e..4f490c2bb 100644
--- a/src/ethiopic/utils/EthiopicDate.ts
+++ b/src/ethiopic/utils/EthiopicDate.ts
@@ -1,5 +1,17 @@
+/**
+ * Represents a date in the Ethiopic calendar system.
+ *
+ * The Ethiopic calendar has:
+ *
+ * - 13 months
+ * - 12 months of 30 days each
+ * - A 13th month (Pagume) of 5 or 6 days
+ */
export interface EthiopicDate {
+ /** The Ethiopic year */
year: number;
+ /** The month number (1-13) */
month: number;
+ /** The day of the month (1-30, or 1-5/6 for month 13) */
day: number;
}
diff --git a/src/ethiopic/utils/consts.ts b/src/ethiopic/utils/consts.ts
deleted file mode 100644
index c8e6f9069..000000000
--- a/src/ethiopic/utils/consts.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-/** Julian Day number offset for the Ethiopic calendar */
-export const ETHIOPIC_EPOCH_OFFSET = -285019;
-
-// TODO: Add more constants
diff --git a/src/ethiopic/utils/daysInMonth.ts b/src/ethiopic/utils/daysInMonth.ts
new file mode 100644
index 000000000..16ee608a9
--- /dev/null
+++ b/src/ethiopic/utils/daysInMonth.ts
@@ -0,0 +1,20 @@
+import { isEthiopicLeapYear } from "./isEthiopicLeapYear.js";
+
+/**
+ * Returns the number of days in the specified month of the Ethiopic calendar.
+ *
+ * In the Ethiopic calendar:
+ *
+ * - Months 1-12 have 30 days each
+ * - Month 13 (Pagume) has 5 days in regular years, 6 days in leap years
+ *
+ * @param month - The month number (1-13)
+ * @param year - The Ethiopic year
+ * @returns The number of days in the specified month
+ */
+export function daysInMonth(month: number, year: number): number {
+ if (month === 13) {
+ return isEthiopicLeapYear(year) ? 6 : 5;
+ }
+ return 30;
+}
diff --git a/src/ethiopic/utils/geezConverter.ts b/src/ethiopic/utils/geezConverter.ts
new file mode 100644
index 000000000..1ad62c9f5
--- /dev/null
+++ b/src/ethiopic/utils/geezConverter.ts
@@ -0,0 +1,53 @@
+/**
+ * Converts a number to Geez (Ethiopic) numerals.
+ *
+ * @param num - The number to convert
+ * @returns The number in Geez numerals
+ * @throws {Error} When input is 0 (Geez has no zero representation)
+ */
+export function toGeezNumerals(num: number): string {
+ const geezDigits = ["፩", "፪", "፫", "፬", "፭", "፮", "፯", "፰", "፱"];
+ const geezTens = ["፲", "፳", "፴", "፵", "፶", "፷", "፸", "፹", "፺"];
+ const geezHundreds = "፻";
+ const geezThousands = "፼";
+
+ if (num === 0) return "-";
+ if (num < 0) return `-${toGeezNumerals(-num)}`;
+
+ let result = "";
+ let remaining = num;
+
+ // Handle thousands (10,000 and above)
+ if (remaining >= 10000) {
+ const thousandsValue = Math.floor(remaining / 10000);
+ result +=
+ thousandsValue === 1
+ ? geezThousands
+ : toGeezNumerals(thousandsValue) + geezThousands;
+ remaining %= 10000;
+ }
+
+ // Handle hundreds (100 - 9,900)
+ if (remaining >= 100) {
+ const hundredsValue = Math.floor(remaining / 100);
+ result +=
+ hundredsValue === 1
+ ? geezHundreds
+ : toGeezNumerals(hundredsValue) + geezHundreds;
+ remaining %= 100;
+ }
+
+ // Handle tens (10 - 90)
+ if (remaining >= 10) {
+ const tensValue = Math.floor(remaining / 10);
+ result += geezTens[tensValue - 1];
+ remaining %= 10;
+ }
+
+ // Handle ones (1 - 9)
+ if (remaining > 0) {
+ result += geezDigits[remaining - 1];
+ }
+
+ return result;
+}
diff --git a/src/ethiopic/utils/index.ts b/src/ethiopic/utils/index.ts
index 1dc38356a..e9ede1a3d 100644
--- a/src/ethiopic/utils/index.ts
+++ b/src/ethiopic/utils/index.ts
@@ -2,3 +2,5 @@ export * from "./EthiopicDate.js";
export * from "./isEthiopicLeapYear.js";
export * from "./toEthiopicDate.js";
export * from "./toGregorianDate.js";
+export * from "./geezConverter.js";
+export * from "./daysInMonth.js";
diff --git a/src/ethiopic/utils/isEthiopicDateValid.ts b/src/ethiopic/utils/isEthiopicDateValid.ts
new file mode 100644
index 000000000..7fcf80616
--- /dev/null
+++ b/src/ethiopic/utils/isEthiopicDateValid.ts
@@ -0,0 +1,10 @@
+import { EthiopicDate } from "./EthiopicDate.js";
+import { daysInMonth } from "./daysInMonth.js";
+
+export function isEthiopicDateValid(date: EthiopicDate): boolean {
+ if (date.month < 1) return false;
+ if (date.day < 1) return false;
+ if (date.month > 13) return false;
+ if (date.day > daysInMonth(date.month, date.year)) return false;
+ return true;
+}
diff --git a/src/ethiopic/utils/isEthiopicLeapYear.test.ts b/src/ethiopic/utils/isEthiopicLeapYear.test.ts
index ece0ec678..67dcf014a 100644
--- a/src/ethiopic/utils/isEthiopicLeapYear.test.ts
+++ b/src/ethiopic/utils/isEthiopicLeapYear.test.ts
@@ -1,4 +1,18 @@
+import { isEthiopicLeapYear } from "./isEthiopicLeapYear";
+
describe("isEthiopicLeapYear", () => {
- test.todo("should return true for a leap year");
- test.todo("should return false for a non-leap year");
+ test("should return true for leap years", () => {
+ // In Ethiopic calendar, years that give remainder 3 when divided by 4 are leap years
+ expect(isEthiopicLeapYear(2003)).toBe(true);
+ expect(isEthiopicLeapYear(2007)).toBe(true);
+ expect(isEthiopicLeapYear(2011)).toBe(true);
+ expect(isEthiopicLeapYear(2015)).toBe(true);
+ });
+
+ test("should return false for non-leap years", () => {
+ expect(isEthiopicLeapYear(2001)).toBe(false);
+ expect(isEthiopicLeapYear(2002)).toBe(false);
+ expect(isEthiopicLeapYear(2004)).toBe(false);
+ expect(isEthiopicLeapYear(2005)).toBe(false);
+ });
});
diff --git a/src/ethiopic/utils/toEthiopicDate.test.ts b/src/ethiopic/utils/toEthiopicDate.test.ts
index 3d30b8285..b195b5fc2 100644
--- a/src/ethiopic/utils/toEthiopicDate.test.ts
+++ b/src/ethiopic/utils/toEthiopicDate.test.ts
@@ -1,3 +1,62 @@
-test.todo("should convert a Gregorian date to an Ethiopic date correctly");
-test.todo("should handle leap years correctly");
-test.todo("should handle dates before the Ethiopic epoch correctly");
+import { toEthiopicDate } from "./toEthiopicDate";
+
+describe("toEthiopicDate", () => {
+ test("should convert a Gregorian date to an Ethiopic date correctly", () => {
+ const testCases = [
+ {
+ gregorian: new Date(2024, 2, 9), // March 9, 2024
+ ethiopic: { year: 2016, month: 6, day: 30 }
+ },
+ {
+ gregorian: new Date(2024, 0, 1), // January 1, 2024
+ ethiopic: { year: 2016, month: 4, day: 22 }
+ },
+ {
+ gregorian: new Date(2023, 8, 11), // September 11, 2023
+ ethiopic: { year: 2015, month: 13, day: 6 } // Ethiopian New Year
+ },
+ {
+ gregorian: new Date(2024, 8, 10), // September 10, 2024
+ ethiopic: { year: 2016, month: 13, day: 5 } // Ethiopian New Year
+ }
+ ];
+
+ testCases.forEach(({ gregorian, ethiopic }) => {
+ expect(toEthiopicDate(gregorian)).toEqual(ethiopic);
+ });
+ });
+
+ test("should handle leap years correctly", () => {
+ const testCases = [
+ {
+ gregorian: new Date(2024, 1, 29), // February 29, 2024 (Gregorian leap year)
+ ethiopic: { year: 2016, month: 6, day: 21 }
+ },
+ {
+ gregorian: new Date(2023, 8, 11), // August 29, 2024
+ ethiopic: { year: 2015, month: 13, day: 6 } // Pagume (Ethiopian 13th month)
+ }
+ ];
+
+ testCases.forEach(({ gregorian, ethiopic }) => {
+ expect(toEthiopicDate(gregorian)).toEqual(ethiopic);
+ });
+ });
+
+ test("should handle dates before the Ethiopic epoch correctly", () => {
+ const testCases = [
+ {
+ gregorian: new Date(1900, 0, 1), // January 1, 1900
+ ethiopic: { year: 1892, month: 4, day: 23 }
+ },
+ {
+ gregorian: new Date(1800, 5, 15), // June 15, 1800
+ ethiopic: { year: 1792, month: 10, day: 9 }
+ }
+ ];
+
+ testCases.forEach(({ gregorian, ethiopic }) => {
+ expect(toEthiopicDate(gregorian)).toEqual(ethiopic);
+ });
+ });
+});
diff --git a/src/ethiopic/utils/toEthiopicDate.ts b/src/ethiopic/utils/toEthiopicDate.ts
index a100b0d87..1bae6a05d 100644
--- a/src/ethiopic/utils/toEthiopicDate.ts
+++ b/src/ethiopic/utils/toEthiopicDate.ts
@@ -1,5 +1,47 @@
-import type { EthiopicDate } from "./EthiopicDate.js";
-import { ETHIOPIC_EPOCH_OFFSET } from "./consts.js";
+import { differenceInCalendarDays } from "date-fns";
+
+import { EthiopicDate } from "./EthiopicDate.js";
+
+/**
+ * Calculates the number of days between January 1, 0001 and the given date.
+ *
+ * @param date - A JavaScript Date object to calculate days from
+ * @returns The number of days since January 1, 0001. Returns 0 if the input is
+ * not a valid Date.
+ */
+export function getDayNoGregorian(date: Date): number {
+ if (!(date instanceof Date)) {
+ return 0;
+ }
+ // Create the start date as January 1, 0001.
+ // Using an ISO string avoids issues with the Date constructor and two-digit years.
+ const adStart = new Date("0001-01-01");
+
+ // Calculate the number of days between the two dates, then add 1.
+ const dayNumber = differenceInCalendarDays(date, adStart) + 1;
+
+ return dayNumber;
+}
+
+function createEthiopicDate(dn: number): EthiopicDate {
+ const num = Math.floor(dn / 1461);
+ const num2 = dn % 1461;
+ const num3 = Math.floor(num2 / 365);
+ const num4 = num2 % 365;
+ if (num2 !== 1460) {
+ return {
+ year: num * 4 + num3,
+ month: Math.floor(num4 / 30) + 1,
+ day: (num4 % 30) + 1
+ };
+ } else {
+ return {
+ year: num * 4 + num3 - 1,
+ month: 13,
+ day: 6
+ };
+ }
+}
/**
* Converts a Gregorian date to an Ethiopic date.
@@ -9,13 +51,5 @@ import { ETHIOPIC_EPOCH_OFFSET } from "./consts.js";
* @returns An EthiopicDate object.
*/
export function toEthiopicDate(gregorianDate: Date): EthiopicDate {
- const julianDay = Math.floor(gregorianDate.getTime() / 86400000 + 2440587.5);
- const ethiopicDayNumber = julianDay + ETHIOPIC_EPOCH_OFFSET;
-
- const year = Math.floor((4 * ethiopicDayNumber + 1463) / 1461);
- const dayOfYear = ethiopicDayNumber - (365 * year + Math.floor(year / 4));
- const month = Math.floor(dayOfYear / 30) + 1;
- const day = (dayOfYear % 30) + 1;
-
- return { year, month, day };
+ return createEthiopicDate(getDayNoGregorian(gregorianDate) - 2431);
}
diff --git a/src/ethiopic/utils/toGregorianDate.test.ts b/src/ethiopic/utils/toGregorianDate.test.ts
index 2937f87bf..e39211c54 100644
--- a/src/ethiopic/utils/toGregorianDate.test.ts
+++ b/src/ethiopic/utils/toGregorianDate.test.ts
@@ -1,8 +1,43 @@
+import { EthiopicDate } from "./EthiopicDate";
+import { toGregorianDate } from "./toGregorianDate";
+
describe("toGregorianDate", () => {
- test.todo("convert an Ethiopic date to the correct Gregorian date");
- test.todo("handle leap years correctly");
- test.todo("handle the last day of the year correctly");
- test.todo("handle the first day of the year correctly");
- test.todo("handle invalid dates gracefully");
- test.todo("handle edge cases for month and day values");
+ test("convert an Ethiopic date to the correct Gregorian date", () => {
+ const ethiopicDate: EthiopicDate = { year: 2015, month: 1, day: 1 };
+ const gregorianDate = toGregorianDate(ethiopicDate);
+ expect(gregorianDate).toEqual(new Date(2022, 8, 11)); // September 11, 2022
+ });
+
+ test("handle leap years correctly", () => {
+ const ethiopicDate: EthiopicDate = { year: 2015, month: 13, day: 6 };
+ const gregorianDate = toGregorianDate(ethiopicDate);
+ expect(gregorianDate).toEqual(new Date(2023, 8, 11)); // September 11, 2023
+ });
+
+ test("handle the last day of the year correctly", () => {
+ const ethiopicDate: EthiopicDate = { year: 2014, month: 13, day: 5 };
+ const gregorianDate = toGregorianDate(ethiopicDate);
+ expect(gregorianDate).toEqual(new Date(2022, 8, 10)); // September 10, 2022
+ });
+
+ test("handle the first day of the year correctly", () => {
+ const ethiopicDate: EthiopicDate = { year: 2014, month: 1, day: 1 };
+ const gregorianDate = toGregorianDate(ethiopicDate);
+ expect(gregorianDate).toEqual(new Date(2021, 8, 11)); // September 11, 2021
+ });
+
+ test("handle invalid dates gracefully", () => {
+ const invalidDate: EthiopicDate = { year: 2015, month: 14, day: 1 };
+ expect(() => toGregorianDate(invalidDate)).toThrow();
+ });
+
+ test("handle edge cases for month and day values", () => {
+ // Test month 13 with regular year (5 days)
+ const pagume: EthiopicDate = { year: 2014, month: 13, day: 1 };
+ expect(toGregorianDate(pagume)).toEqual(new Date(2022, 8, 6)); // September 6, 2022
+
+ // Test last day of a regular month
+ const lastDayOfMonth: EthiopicDate = { year: 2015, month: 1, day: 30 };
+ expect(toGregorianDate(lastDayOfMonth)).toEqual(new Date(2022, 9, 10)); // October 10, 2022
+ });
});
diff --git a/src/ethiopic/utils/toGregorianDate.ts b/src/ethiopic/utils/toGregorianDate.ts
index 5dd0ec151..64118de1b 100644
--- a/src/ethiopic/utils/toGregorianDate.ts
+++ b/src/ethiopic/utils/toGregorianDate.ts
@@ -1,6 +1,61 @@
+import { getDaysInMonth } from "date-fns";
+
import type { EthiopicDate } from "./EthiopicDate.js";
-import { ETHIOPIC_EPOCH_OFFSET } from "./consts.js";
+import { isEthiopicDateValid } from "./isEthiopicDateValid.js";
+
+export function getDayNoEthiopian(etDate: EthiopicDate): number {
+ const num = Math.floor(etDate.year / 4);
+ const num2 = etDate.year % 4;
+ return num * 1461 + num2 * 365 + (etDate.month - 1) * 30 + etDate.day - 1;
+}
+
+function gregorianDateFromDayNo(dayNum: number): Date {
+ let year = 1,
+ month = 1,
+ day;
+
+ const num400 = Math.floor(dayNum / 146097); // number of full 400-year periods
+ dayNum %= 146097;
+ if (dayNum === 0) {
+ return new Date(400 * num400, 12 - 1, 31);
+ }
+
+ const num100 = Math.min(Math.floor(dayNum / 36524), 3); // number of full 100-year periods, but not more than 3
+ dayNum -= num100 * 36524;
+ if (dayNum === 0) {
+ return new Date(400 * num400 + 100 * num100, 12 - 1, 31);
+ }
+
+ const num4 = Math.floor(dayNum / 1461); // number of full 4-year periods
+ dayNum %= 1461;
+ if (dayNum === 0) {
+ return new Date(400 * num400 + 100 * num100 + 4 * num4, 12 - 1, 31);
+ }
+ const num1 = Math.min(Math.floor(dayNum / 365), 3); // number of full years, but not more than 3
+ dayNum -= num1 * 365;
+ if (dayNum === 0) {
+ return new Date(400 * num400 + 100 * num100 + 4 * num4 + num1, 12 - 1, 31);
+ }
+
+ year += 400 * num400 + 100 * num100 + 4 * num4 + num1;
+
+ while (dayNum > 0) {
+ const tempDate = new Date(year, month - 1);
+ const daysInMonth = getDaysInMonth(tempDate);
+
+ if (dayNum <= daysInMonth) {
+ day = dayNum;
+ break;
+ }
+
+ dayNum -= daysInMonth;
+ month++;
+ }
+
+ // Remember in JavaScript Date object, months are 0-based.
+ return new Date(year, month - 1, day);
+}
/**
* Converts an Ethiopic date to a Gregorian date.
*
@@ -8,12 +63,9 @@ import { ETHIOPIC_EPOCH_OFFSET } from "./consts.js";
* @returns A JavaScript Date object representing the Gregorian date.
*/
export function toGregorianDate(ethiopicDate: EthiopicDate): Date {
- const yearStartJulianDay =
- 365 * ethiopicDate.year +
- Math.floor(ethiopicDate.year / 4) -
- ETHIOPIC_EPOCH_OFFSET;
- const julianDay =
- yearStartJulianDay + (ethiopicDate.month - 1) * 30 + (ethiopicDate.day - 1);
- const gregorianTime = (julianDay - 2440587.5) * 86400000;
- return new Date(gregorianTime);
+ if (!isEthiopicDateValid(ethiopicDate)) {
+ throw new Error("Invalid Ethiopic date");
+ }
+
+ return gregorianDateFromDayNo(getDayNoEthiopian(ethiopicDate) + 2431);
}
diff --git a/src/types/shared.ts b/src/types/shared.ts
index 27cb3a8a4..49650554b 100644
--- a/src/types/shared.ts
+++ b/src/types/shared.ts
@@ -416,7 +416,7 @@ export type MoveFocusBy =
* - `arab`: Arabic-Indic
* - `arabext`: Eastern Arabic-Indic (Persian)
* - `deva`: Devanagari
- * - `ethio`: Ethiopic
+ * - `geez`: Ethiopic
* - `beng`: Bengali
* - `guru`: Gurmukhi
* - `gujr`: Gujarati
@@ -433,7 +433,7 @@ export type Numerals =
| "arab"
| "arabext"
| "deva"
- | "ethio"
+ | "geez"
| "beng"
| "guru"
| "gujr"
diff --git a/website/docs/docs/localization.mdx b/website/docs/docs/localization.mdx
index 4ca7d51fa..4427ddf72 100644
--- a/website/docs/docs/localization.mdx
+++ b/website/docs/docs/localization.mdx
@@ -127,6 +127,40 @@ export function PersianCalendar() {
+## Ethiopic Calendar
+
+DayPicker supports the [Ethiopic calendar](https://en.wikipedia.org/wiki/Ethiopian_calendar), which is the principal calendar used in Ethiopia and Eritrea.
+
+- To use the Ethiopic calendar, import `DayPicker` from `react-day-picker/ethiopic`:
+
+```diff
+- import { DayPicker } from 'react-day-picker';
++ import { DayPicker } from 'react-day-picker/ethiopic';
+```
+
+
+
+
+
+### Numerals (Geez)
+
+By default, the Ethiopic calendar uses Latin numerals (Western Arabic numbers).
+
+- To use the Geez numeral system, pass the `numerals` prop:
+
+```tsx
+import { DayPicker } from "react-day-picker/ethiopic";
+
+export function EthiopicGeez() {
+ return ;
+}
+```
+
+
+
+
+
+
## Broadcast Calendar
| Prop Name | Type | Description |