Skip to content

feat: ethiopic calendar #2658

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

Open
wants to merge 24 commits into
base: gpbl/simplify-types-datelib
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @gpbl
1 change: 1 addition & 0 deletions ethiopic.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./dist/cjs/ethiopic/index.d.ts";
4 changes: 4 additions & 0 deletions ethiopic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable no-undef */
const ethiopic = require("./dist/cjs/ethiopic/index.js");
module.exports = ethiopic;
16 changes: 16 additions & 0 deletions examples/Ethiopic.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +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(2024, 11, 22);

beforeAll(() => jest.setSystemTime(today));
afterAll(() => jest.useRealTimers());

test("should render ታህሳስ ፳፻፲፯", () => {
render(<Ethiopic />);
expect(grid("ታህሳስ ፳፻፲፯")).toBeInTheDocument();
});
7 changes: 7 additions & 0 deletions examples/Ethiopic.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 Ethiopic() {
return <DayPicker />;
}
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" />;
}
3 changes: 3 additions & 0 deletions examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export * from "./DisableNavigation";
export * from "./Dropdown";
export * from "./DropdownMonths";
export * from "./DropdownMultipleMonths";
export * from "./Ethiopic";
export * from "./Fixedweeks";
export * from "./FocusRecursive";
export * from "./FocusedDisabledNav";
Expand Down Expand Up @@ -57,6 +58,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
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@
"default": "./dist/cjs/persian.js"
}
},
"./ethiopic": {
"import": {
"types": "./dist/esm/ethiopic/index.d.ts",
"default": "./dist/esm/ethiopic/index.js"
},
"require": {
"types": "./dist/cjs/ethiopic/index.d.ts",
"default": "./dist/cjs/ethiopic/index.js"
}
},
"./locale": {
"import": {
"types": "./dist/esm/locale.d.ts",
Expand Down
5 changes: 5 additions & 0 deletions src/ethiopic/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
describe("DayPicker", () => {
test.todo("should render with default props");
test.todo("should render with custom locale");
test.todo("should render with custom numerals");
});
60 changes: 60 additions & 0 deletions src/ethiopic/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from "react";

import type { Locale } from "date-fns";

import {
DateLib,
DateLibOptions,
DayPicker as DayPickerComponent
} from "../index.js";
import type { DayPickerProps } from "../types/props.js";

import * as ethiopicDateLib from "./lib/index.js";

/**
* Render the Ethiopic Calendar.
*
* @see https://daypicker.dev/docs/localization#ethiopic-calendar
*/
export function DayPicker(
props: DayPickerProps & {
/**
* The locale to use in the calendar.
*
* @default `am-ET`
*/
locale?: Locale;
/**
* The numeral system to use when formatting dates.
*
* - `latn`: Latin (Western Arabic)
* - `geez`: Ge'ez (Ethiopic numerals)
*
* @defaultValue `latn` Latin (Western Arabic)
* @see https://daypicker.dev/docs/translation#numeral-systems
*/
numerals?: DayPickerProps["numerals"];
}
) {
const dateLib = getDateLib({
locale: props.locale,
weekStartsOn: 1,
firstWeekContainsDate: props.firstWeekContainsDate,
useAdditionalWeekYearTokens: props.useAdditionalWeekYearTokens,
useAdditionalDayOfYearTokens: props.useAdditionalDayOfYearTokens,
timeZone: props.timeZone
});
return (
<DayPickerComponent
{...props}
locale={props.locale ?? ({} as Locale)}
numerals={props.numerals ?? "latn"}
dateLib={dateLib}
/>
);
}

/** Returns the date library used in the calendar. */
export const getDateLib = (options?: DateLibOptions) => {
return new DateLib(options, ethiopicDateLib);
};
81 changes: 81 additions & 0 deletions src/ethiopic/lib/addMonths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { toEthiopicDate, toGregorianDate } from "../utils";

import { addMonths } from "./addMonths";

describe("addMonths", () => {
test("should add positive months correctly", () => {
// 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", () => {
// 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: 29 additions & 0 deletions src/ethiopic/lib/addMonths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { daysInMonth } from "../utils/daysInMonth.js";
import { toEthiopicDate, toGregorianDate } from "../utils/index.js";

/**
* Adds the specified number of months to the given Ethiopian date. Handles
* month overflow and year boundaries correctly.
*
* @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);
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 });
}
50 changes: 50 additions & 0 deletions src/ethiopic/lib/addYears.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { toEthiopicDate, toGregorianDate } from "../utils/index.js";

import { addYears } from "./addYears";

describe("addYears", () => {
test("should add positive years correctly", () => {
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", () => {
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", () => {
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: 29 additions & 0 deletions src/ethiopic/lib/addYears.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
isEthiopicLeapYear,
toEthiopicDate,
toGregorianDate
} from "../utils/index.js";

/**
* Adds the specified number of years to the given Ethiopian date. Handles leap
* year transitions for Pagume month.
*
* @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 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
});
}
Loading
Loading