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 12 commits into
base: ethiopic-calendar
Choose a base branch
from
4 changes: 3 additions & 1 deletion src/ethiopic/lib/addMonths.test.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
test.todo("addMonths should correctly add months to an Ethiopic date");
test.todo("should add positive months correctly in Ethiopic calendar");
test.todo("should add negative months correctly in Ethiopic calendar");
test.todo("should handle month overflow correctly in Ethiopic calendar");
20 changes: 16 additions & 4 deletions src/ethiopic/lib/addMonths.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { daysInMonth } from "../utils/daysInMonth.js";
import { toEthiopicDate, toGregorianDate } from "../utils/index.js";

/**
Expand All @@ -9,8 +10,19 @@ import { toEthiopicDate, toGregorianDate } from "../utils/index.js";
*/
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 });
}
6 changes: 5 additions & 1 deletion src/ethiopic/lib/addYears.test.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
test.todo("addYears should correctly add years to an Ethiopic date");
test.todo("should add positive years correctly in Ethiopic calendar");
test.todo("should add negative years correctly in Ethiopic calendar");
test.todo(
"should maintain month and day when adding years in Ethiopic calendar"
);
17 changes: 2 additions & 15 deletions src/ethiopic/lib/addYears.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,7 @@
import {
toEthiopicDate,
isEthiopicLeapYear,
toGregorianDate
} from "../utils/index.js";
import { toEthiopicDate, toGregorianDate } from "../utils/index.js";

/**
* Adds years to an Ethiopic date
*
* @param {Date} date - The original date
* @param {number} amount - The number of years to add
* @returns {Date} The new date
*/
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 });
return toGregorianDate({ year: newYear, month, day });
}
5 changes: 5 additions & 0 deletions src/ethiopic/lib/differenceInCalendarMonths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
test.todo(
"should calculate difference in months within the same Ethiopic year"
);
test.todo("should calculate difference in months across Ethiopic years");
test.todo("should return zero for same Ethiopic date");
11 changes: 3 additions & 8 deletions src/ethiopic/lib/differenceInCalendarMonths.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toEthiopicDate, isEthiopicLeapYear } from "../utils/index.js";
import { toEthiopicDate } from "../utils/index.js";

/**
* Difference in calendar months
Expand All @@ -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)
);
}
5 changes: 4 additions & 1 deletion src/ethiopic/lib/endOfMonth.test.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
test.todo("should return the correct end of the month date for a given date");
test.todo("Should return the last day of a 30-day Ethiopic month");
test.todo(
"Should handle Pagume (13th month) correctly in leap and non-leap years"
);
11 changes: 4 additions & 7 deletions src/ethiopic/lib/endOfMonth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import {
toEthiopicDate,
isEthiopicLeapYear,
toGregorianDate
} from "../utils/index.js";
import { daysInMonth } from "../utils/daysInMonth.js";
import { toEthiopicDate, toGregorianDate } from "../utils/index.js";

/**
* End of month
Expand All @@ -12,6 +9,6 @@ import {
*/
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 });
}
5 changes: 4 additions & 1 deletion src/ethiopic/lib/endOfYear.test.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
test.todo("should return the correct end of the year date for a given date");
describe("endOfYear", () => {
test.todo("should return last day of Ethiopic year for non-leap year");
test.todo("should return last day of Ethiopic year for leap year");
});
6 changes: 6 additions & 0 deletions src/ethiopic/lib/format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
describe("format", () => {
test.todo("should format date in Ethiopic calendar");
test.todo("should format Ethiopic month names correctly");
test.todo("should format time components correctly");
test.todo("should format full date with Ethiopic day names");
});
100 changes: 100 additions & 0 deletions src/ethiopic/lib/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Locale } from "date-fns";

import { toEthiopicDate } from "../utils/index.js";

export interface FormatOptions {
locale?: Locale;
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
firstWeekContainsDate?: 1 | 2 | 3 | 4 | 5 | 6 | 7;
useAdditionalWeekYearTokens?: boolean;
useAdditionalDayOfYearTokens?: boolean;
}

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)}, ${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?: FormatOptions
): string {
// Handle time formats using original date-fns format
if (formatStr.includes("hh:mm") || formatStr.includes("a")) {
// Use regular date formatting for time components
return new Intl.DateTimeFormat("en-US", {
hour: "numeric",
minute: "numeric",
hour12: formatStr.includes("a")
}).format(date);
}

return formatEthiopianDate(date, formatStr);
}export const ethMonths = [
"መስከረም",
"ጥቅምት",
"ህዳር",
"ታህሳስ",
"ጥር",
"የካቲት",
"መጋቢት",
"ሚያዚያ",
"ግንቦት",
"ሰኔ",
"ሐምሌ",
"ነሀሴ",
"ጳጉሜ"
];export const shortDays = ["እ", "ሰ", "ማ", "ረ", "ሐ", "ዓ", "ቅ"];
export const longDays = ["እሁድ", "ሰኞ", "ማክሰኞ", "ረቡዕ", "ሐሙስ", "ዓርብ", "ቅዳሜ"];
5 changes: 5 additions & 0 deletions src/ethiopic/lib/formatNumber.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
describe("formatNumber", () => {
test.todo("should format numbers using Ethiopic numerals");
test.todo("should format numbers using latin numerals by default");
test.todo("should handle zero and negative numbers correctly");
});
8 changes: 8 additions & 0 deletions src/ethiopic/lib/formatNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function formatNumber(value: number, numerals: string = "latn"): string {
// Use Intl.NumberFormat to create a formatter with the specified numbering system
const formatter = new Intl.NumberFormat("en-US", {
numberingSystem: numerals
});

return formatter.format(value);
}
5 changes: 4 additions & 1 deletion src/ethiopic/lib/getMonth.test.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
test.todo("should return the correct zero-based month index for a given date");
describe("getMonth", () => {
test.todo("should return correct Ethiopic month (0-based)");
test.todo("should handle Pagume (13th month) correctly");
});
7 changes: 6 additions & 1 deletion src/ethiopic/lib/getWeek.test.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
test.todo("should return the correct week number for a given date");
describe("getWeek", () => {
test.todo("should return correct Ethiopic week number");
test.todo("should handle different week start options");
test.todo("should handle dates at Ethiopic year boundaries");
test.todo("should maintain week continuity in Ethiopic calendar");
});
65 changes: 54 additions & 11 deletions src/ethiopic/lib/getWeek.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { toGregorianDate, toEthiopicDate } from "../utils/index.js";
import type { GetWeekOptions } from "date-fns";

import { differenceInCalendarDays } from "./differenceInCalendarDays.js";
import { toGregorianDate, toEthiopicDate } from "../utils/index.js";

/**
* Get week
Expand All @@ -11,16 +11,59 @@ import { differenceInCalendarDays } from "./differenceInCalendarDays.js";
* week (0 - Sunday). Default is `0`
* @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 { year } = toEthiopicDate(date);

// Get the first day of the current year
const firstDayOfYear = toGregorianDate({
year: year,
month: 1,
day: 1
});

// Get the first day of next year
const firstDayOfNextYear = toGregorianDate({
year: year + 1,
month: 1,
day: 1
});
const diffInDays = differenceInCalendarDays(date, startOfYear);
return Math.floor((diffInDays + weekStartsOn) / 7) + 1;

// Adjust to the start of the week (Monday)
const getWeekStart = (date: Date) => {
const daysSinceMonday = (date.getDay() + 6) % 7;
const weekStart = new Date(date);
weekStart.setDate(date.getDate() - daysSinceMonday);
return weekStart;
};

const firstWeekStart = getWeekStart(firstDayOfYear);
const nextYearFirstWeekStart = getWeekStart(firstDayOfNextYear);

// If the date is in the last week of the year, check if it belongs to week 1 of next year
if (date >= nextYearFirstWeekStart) {
return 1;
}

// Calculate days since the first week start
const daysSinceStart = Math.floor(
(date.getTime() - firstWeekStart.getTime()) / (24 * 60 * 60 * 1000)
);

// If the date is before the first week of its year, it belongs to the last week of previous year
if (date < firstWeekStart) {
const prevYearFirstDay = toGregorianDate({
year: year - 1,
month: 1,
day: 1
});
const prevYearFirstWeekStart = getWeekStart(prevYearFirstDay);
const daysSincePrevStart = Math.floor(
(date.getTime() - prevYearFirstWeekStart.getTime()) /
(24 * 60 * 60 * 1000)
);
return Math.floor(daysSincePrevStart / 7) + 1;
}

const data = Math.floor(daysSinceStart / 7) + 1;
return data;
}
6 changes: 5 additions & 1 deletion src/ethiopic/lib/getYear.test.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
test.todo("should return the correct Ethiopic year for a given date");
describe("getYear", () => {
test.todo("should return correct Ethiopic year");

test.todo("should handle Ethiopic year boundary correctly");
});
6 changes: 5 additions & 1 deletion src/ethiopic/lib/isSameMonth.test.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
test.todo("isSameMonth should return true if two dates are in the same month");
describe("isSameMonth", () => {
test.todo("should return true for dates in same Ethiopic month");
test.todo("should return false for dates in different Ethiopic months");
test.todo("should handle Ethiopic month boundaries correctly");
});
6 changes: 5 additions & 1 deletion src/ethiopic/lib/isSameYear.test.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
test.todo("isSameYear should return true if two dates are in the same year");
describe("isSameYear", () => {
test.todo("should return true for dates in same Ethiopic year");
test.todo("should return false for dates in different Ethiopic years");
test.todo("should handle Ethiopic year boundary correctly");
});
6 changes: 3 additions & 3 deletions src/ethiopic/lib/newDate.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
describe("newDate", () => {
test.todo("should create a new Ethiopic date");
});
test.todo("creates date with valid Ethiopic values");
test.todo("handles Pagume (13th month)");
test.todo("throws error for invalid month");
11 changes: 10 additions & 1 deletion src/ethiopic/lib/newDate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,14 @@ 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 (month < 1 || month > 13) {
throw new Error(
"Month must be between 0 and 12 (1-13 in Ethiopic calendar)"
);
}

return toGregorianDate({ year, month, day: date });
}
6 changes: 3 additions & 3 deletions src/ethiopic/lib/setYear.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
describe("setYear", () => {
test.todo("set the Ethiopic year for a given date");
});
test.todo("sets year correctly");
test.todo("maintains month and day when possible");
test.todo("adjusts day for leap year changes");
Loading