Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ethiopic date lib #2662

Open
wants to merge 20 commits into
base: ethiopic-calendar
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions examples/Ethiopic.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<Ethiopic />);
expect(grid("ታህሳስ ፳፻፲፯")).toBeInTheDocument();
});

test.todo("should render the Ethiopic calendar");
21 changes: 21 additions & 0 deletions examples/EthiopicGeez.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<EthiopicGeez />);
expect(grid("Tahsas 2017")).toBeInTheDocument();
});

test("should render December 2024 with latin numerals", () => {
render(<EthiopicGeez />);
expect(grid("December 2024")).toBeInTheDocument();
});
7 changes: 7 additions & 0 deletions examples/EthiopicGeez.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react";

import { DayPicker } from "react-day-picker/ethiopic";

export function EthiopicGeez() {
return <DayPicker numerals="geez" />;
}
2 changes: 2 additions & 0 deletions examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
4 changes: 2 additions & 2 deletions src/ethiopic/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -59,7 +59,7 @@ export function DayPicker(
<DayPickerComponent
{...props}
locale={props.locale ?? ({} as Locale)}
numerals={props.numerals ?? "ethio"}
numerals={props.numerals ?? "latn"}
dateLib={dateLib}
/>
);
Expand Down
6 changes: 3 additions & 3 deletions src/ethiopic/lib/addDays.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { addDays as addDaysFns } from "date-fns";

/**
* Adds days to an Ethiopic date
*
Expand All @@ -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);
}
82 changes: 81 additions & 1 deletion src/ethiopic/lib/addMonths.test.ts
Original file line number Diff line number Diff line change
@@ -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
});
});
29 changes: 21 additions & 8 deletions src/ethiopic/lib/addMonths.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
1 change: 1 addition & 0 deletions src/ethiopic/lib/addWeeks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
51 changes: 50 additions & 1 deletion src/ethiopic/lib/addYears.test.ts
Original file line number Diff line number Diff line change
@@ -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
});
});
29 changes: 19 additions & 10 deletions src/ethiopic/lib/addYears.ts
Original file line number Diff line number Diff line change
@@ -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
});
}
14 changes: 4 additions & 10 deletions src/ethiopic/lib/differenceInCalendarDays.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toEthiopicDate, isEthiopicLeapYear } from "../utils/index.js";
import { differenceInCalendarDays as differenceInCalendarDaysNative } from "date-fns";

/**
* Difference in calendar days
Expand All @@ -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;
}
42 changes: 42 additions & 0 deletions src/ethiopic/lib/differenceInCalendarMonths.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading