🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

kenat

Package Overview
Dependencies
Maintainers
1
Versions
25
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

kenat - npm Package Compare versions

Comparing version
3.2.0
to
4.0.0
dist/index.cjs

Sorry, the diff of this file is too big to display

+802
/** Shared domain types used across Kenat's modules. */
type Lang = 'amharic' | 'english' | string;
interface EthiopianDate {
year: number;
month: number;
day: number;
}
interface GregorianDate {
year: number;
month: number;
day: number;
}
type TimePeriod = 'day' | 'night';
interface LocalizedText {
amharic: string;
english: string;
[lang: string]: string;
}
interface Holiday {
key: string;
tags: string[];
movable: boolean;
name?: string;
description?: string;
ethiopian: EthiopianDate;
gregorian?: GregorianDate;
}
interface DateRange {
start: EthiopianDate;
end: EthiopianDate;
}
type DiffUnit = 'years' | 'months' | 'days';
interface DiffBreakdown {
sign: 1 | -1;
totalDays: number;
years?: number;
months?: number;
days?: number;
}
interface BahireHasabResult {
ameteAlem: number;
meteneRabiet: number;
evangelist: {
name: string;
remainder: number;
};
newYear: {
dayName: string;
tinteQemer: number;
};
medeb: number;
wenber: number;
abektie: number;
metqi: number;
bealeMetqi: {
date: EthiopianDate;
weekday: string;
};
mebajaHamer: number;
nineveh: EthiopianDate;
movableFeasts: Record<string, Holiday>;
}
/**
* Calculates all Bahire Hasab values for a given Ethiopian year, including all movable feasts.
*
* @param {number} ethiopianYear - The Ethiopian year to calculate for.
* @param {Object} [options={}] - Options for language.
* @param {string} [options.lang='amharic'] - The language for names.
* @returns {Object} An object containing all the calculated Bahire Hasab values.
*/
declare function getBahireHasab(ethiopianYear: number, options?: {
lang?: Lang;
}): BahireHasabResult;
interface TimeFormatOptions {
lang?: Lang;
useGeez?: boolean;
showPeriodLabel?: boolean;
zeroAsDash?: boolean;
}
interface TimeDuration {
hours?: number;
minutes?: number;
}
declare class Time {
hour: number;
minute: number;
period: TimePeriod;
/**
* Constructs a Time instance representing an Ethiopian time.
* @param {number} hour - The Ethiopian hour (1-12).
* @param {number} [minute=0] - The minute (0-59).
* @param {string} [period='day'] - The period ('day' or 'night').
* @throws {InvalidTimeError} If any time component is invalid.
*/
constructor(hour: number, minute?: number, period?: TimePeriod);
/**
* Creates a Time instance from a Gregorian 24-hour time.
* @param {number} hour - The Gregorian hour (0-23).
* @param {number} [minute=0] - The minute (0-59).
* @returns {Time} A new Time instance.
* @throws {InvalidTimeError} If the Gregorian time is invalid.
*/
static fromGregorian(hour: number, minute?: number): Time;
/**
* Converts the Ethiopian time to Gregorian 24-hour format.
* @returns {{hour: number, minute: number}}
*/
toGregorian(): {
hour: number;
minute: number;
};
/**
* Creates a `Time` object from a string representation.
*
* This static method parses a time string, which can include hours, minutes, and an optional period (day/night).
* It supports both Arabic numerals (e.g., "1", "30") and Ethiopic numerals (e.g., "፩", "፴") for hours and minutes,
* assuming a `toArabic` utility function is available to convert Ethiopic numerals to Arabic numbers.
*
* The time string must contain a colon (`:`) separating the hour and minute.
*
* @param {string} timeString - The string representation of the time.
* Expected formats:
* - "HH:MM" (e.g., "6:30", "፮:፴")
* - "HH:MM period" (e.g., "6:30 night", "፮:፴ ማታ")
* Where:
* - HH: Hour (Arabic or Ethiopic numeral).
* - MM: Minute (Arabic or Ethiopic numeral).
* - period: Optional. Case-insensitive. Recognized values are "night" or "ማታ".
* If the period is omitted, or if a third part is present but not recognized as "night" or "ማታ",
* the time is assumed to be in the 'day' period.
*
* @returns {Time} A new `Time` object representing the parsed time.
*
* @throws {InvalidTimeError} If the `timeString` is:
* - Not a string or an empty string.
* - Missing the colon (`:`) separator.
* - Formatted incorrectly (e.g., not enough parts after splitting).
* - Contains non-numeric values for hour or minute that cannot be parsed into numbers
* (neither as Arabic nor as Ethiopic numerals via `toArabic`).
*
*/
static fromString(timeString: string): Time;
/**
* Adds a duration to the current time.
* @param {{hours?: number, minutes?: number}} duration - Object with hours and/or minutes to add.
* @returns {Time} A new Time instance with the added duration.
*/
add(duration: TimeDuration): Time;
/**
* Subtracts a duration from the current time.
* @param {{hours?: number, minutes?: number}} duration - Object with hours and/or minutes to subtract.
* @returns {Time} A new Time instance with the subtracted duration.
*/
subtract(duration: TimeDuration): Time;
/**
* Calculates the difference between this time and another.
* @param {Time} otherTime - Another Time instance to compare against.
* @returns {{hours: number, minutes: number}} An object with the absolute difference.
*/
diff(otherTime: Time): {
hours: number;
minutes: number;
};
/**
* Formats the time as a string.
* @param {Object} [options] - Formatting options.
* @param {string} [options.lang] - The language for the period label. Defaults to 'english' if useGeez is false, otherwise 'amharic'.
* @param {boolean} [options.useGeez=true] - Whether to use Ge'ez numerals.
* @param {boolean} [options.showPeriodLabel=true] - Whether to show the period label.
* @param {boolean} [options.zeroAsDash=true] - Whether to represent zero minutes as a dash.
* @returns {string} The formatted time string.
*/
format(options?: TimeFormatOptions): string;
}
interface KenatTimeInput {
hour: number;
minute: number;
period: 'day' | 'night';
}
type KenatInput = string | EthiopianDate | Date;
interface FormatOptions {
calendar?: 'ethiopian' | 'gregorian';
lang?: Lang;
showWeekday?: boolean;
useGeez?: boolean;
includeTime?: boolean;
}
interface ToStringOptions {
calendar?: 'ethiopian' | 'gregorian';
}
interface ToISOStringOptions {
calendar?: 'ethiopian' | 'gregorian';
}
interface GetDateOptions {
calendar?: 'ethiopian' | 'gregorian';
}
interface CalendarDay {
ethiopian: EthiopianDate & {
display: string;
};
gregorian: GregorianDate & {
display: string;
};
}
interface StaticCalendarOptions {
useGeez?: boolean;
weekdayLang?: Lang;
weekStart?: number;
holidayFilter?: string | string[] | null;
mode?: 'christian' | 'muslim' | 'public' | null;
}
interface StaticMonthCalendar {
month: number;
monthName: string;
year: number | string;
headers: string[];
days: unknown[];
}
interface DistanceOptions {
units?: DiffUnit[];
output?: 'object' | 'string';
lang?: 'english' | 'amharic';
}
interface DistanceToHolidayOptions extends DistanceOptions {
direction?: 'auto' | 'future' | 'past';
}
/**
* Kenat - Ethiopian Calendar Date Wrapper
*
* A lightweight class to work with both Gregorian and Ethiopian calendars.
* It wraps JavaScript's built-in `Date` object and converts Gregorian dates to Ethiopian equivalents.
*
*/
declare class Kenat {
ethiopian: EthiopianDate;
time: Time;
/**
* Constructs a Kenat instance.
* Can be initialized with:
* - An Ethiopian date string (e.g., '2016/1/1', '2016-1-1').
* - An object with { year, month, day }.
* - A native JavaScript Date object (will be converted from Gregorian).
* - No arguments, for the current date.
*
* @param {string|Object|Date} [input] - The date input.
* @param {Object} [timeObj] - An optional time object.
* @throws {InvalidEthiopianDateError} If the provided Ethiopian date is invalid.
* @throws {InvalidDateFormatError} If the provided date string format is invalid.
* @throws {UnrecognizedInputError} If the input format is unrecognized.
*/
constructor(input?: KenatInput, timeObj?: KenatTimeInput | null);
/**
* Creates and returns a new instance of the Kenat class representing the current moment.
*
* @returns {Kenat} A new Kenat instance set to the current date and time.
*/
static now(): Kenat;
/**
* Converts the current Ethiopian date stored in this.ethiopian to its Gregorian equivalent.
*
* @returns {{ year: number, month: number, day: number }} The Gregorian date corresponding to the Ethiopian date.
*/
getGregorian(): GregorianDate;
/**
* Returns the Ethiopian equivalent of the stored Gregorian date.
*
* @returns {{ year: number, month: number, day: number }} An object representing the Ethiopian date.
*/
getEthiopian(): EthiopianDate;
/**
* Returns the date in whichever calendar is requested, so callers with a
* user-configurable calendar preference don't need an if/else between
* getEthiopian() and getGregorian() at every call site.
*
* @param {Object} [options={}] - Options.
* @param {'ethiopian'|'gregorian'} [options.calendar='ethiopian'] - Which calendar's date to return.
* @returns {{ year: number, month: number, day: number }}
*/
getDate(options?: GetDateOptions): EthiopianDate | GregorianDate;
/**
* Sets the time and returns a new Kenat instance.
* Supports method chaining.
*
* @param {number} hour - The hour value to set.
* @param {number} minute - The minute value to set.
* @param {string} period - The period of the day (e.g., 'day' or 'night').
* @returns {Kenat} A new Kenat instance with the updated time.
*/
setTime(hour: number, minute: number, period: 'day' | 'night'): Kenat;
/**
* Calculates and returns the Bahire Hasab values for the current instance's year.
*
* @returns {Object} An object containing all the calculated Bahire Hasab values
* (ameteAlem, evangelist, wenber, metqi, nineveh, etc.).
*/
getBahireHasab(): BahireHasabResult;
/**
* Returns a string representation of the date and time, e.g. "መስከረም 1 2016 12:00 ጠዋት".
*
* @param {Object} [options={}] - Formatting options.
* @param {'ethiopian'|'gregorian'} [options.calendar='ethiopian'] - Which calendar to render the date in.
* 'gregorian' renders the Gregorian date with an English month name, e.g. "September 12, 2023 12:00 ጠዋት".
* In both cases the time-of-day portion still reflects Kenat's Ethiopian 12-hour clock; use
* toISOString({ calendar: 'gregorian' }) if you need the time converted to Gregorian 24-hour format.
* @returns {string} The formatted date and time string.
*/
toString(options?: ToStringOptions): string;
/**
* Formats the date according to the specified options.
*
* @param {Object} [options={}] - Formatting options.
* @param {'ethiopian'|'gregorian'} [options.calendar='ethiopian'] - Which calendar to render the date in. When
* 'gregorian', the date is rendered using English Gregorian month names; `useGeez` is ignored.
* @param {string} [options.lang='amharic'] - Language to use for formatting ('amharic', 'english', etc.).
* @param {boolean} [options.showWeekday=false] - Whether to include the weekday in the formatted string.
* @param {boolean} [options.useGeez=false] - Whether to use Geez numerals (only applies if lang is 'amharic').
* @param {boolean} [options.includeTime=false] - Whether to include the time in the formatted string.
* @returns {string} The formatted date string.
*/
format(options?: FormatOptions): string;
/**
* Formats the Ethiopian date in Geez numerals and Amharic month name.
*
* @returns {string} The formatted date string in the format: "{Amharic Month Name} {Geez Day} {Geez Year}".
*
* formatInGeezAmharic(); // "የካቲት ፲ ፳፻፲፭"
*/
formatInGeezAmharic(): string;
/**
* Formats the Ethiopian date with weekday name.
*
* @param {'amharic'|'english'} [lang='amharic'] - Language for month and weekday names.
* @param {boolean} [useGeez=false] - Whether to show numerals in Geez.
* @returns {string} Formatted string with weekday, e.g. "ማክሰኞ, መስከረም ፳፩ ፳፻፲፯"
*/
formatWithWeekday(lang?: Lang, useGeez?: boolean): string;
/**
* Returns the Ethiopian date in "yyyy/mm/dd" short format.
* @returns {string}
*/
formatShort(): string;
/**
* Returns an ISO-style date string: "YYYY-MM-DD" or "YYYY-MM-DDTHH:mm".
*
* @param {Object} [options={}] - Formatting options.
* @param {'ethiopian'|'gregorian'} [options.calendar='ethiopian'] - Which calendar's date to render.
* 'gregorian' produces a genuine ISO 8601 string: the date is Gregorian and the time is converted
* from Kenat's Ethiopian 12-hour clock to Gregorian 24-hour time (e.g. 12:00 day -> 06:00), with
* no non-standard suffix - useful for interop (e.g. `<input type="date">` or `new Date(...)`).
* The default 'ethiopian' calendar keeps the existing Ethiopian-date, Ethiopian-time, ISO-*like*
* string (including the non-standard `+12h` suffix for night times), unchanged for backward compatibility.
* @returns {string}
*/
toISOString(options?: ToISOStringOptions): string;
/**
* Checks if the current date is a holiday.
* @param {Object} [options={}] - Options for language.
* @param {string} [options.lang='amharic'] - The language for the holiday names and descriptions.
* @returns {Array<Object>} An array of holiday objects for the current date, or an empty array if it's not a holiday.
*/
isHoliday(options?: {
lang?: Lang;
}): Holiday[];
/**
* Generates a calendar for a given Ethiopian month and year, mapping each Ethiopian date
* to its corresponding Gregorian date and providing formatted display strings.
*
* @param {number} [year=this.ethiopian.year] - The Ethiopian year for the calendar.
* @param {number} [month=this.ethiopian.month] - The Ethiopian month (1-13).
* @param {boolean} [useGeez=false] - Whether to display dates in Geez numerals.
* @returns {Array<Object>} An array of objects, each representing a day in the month with
* Ethiopian and Gregorian date information and display strings.
*/
getMonthCalendar(year?: number, month?: number, useGeez?: boolean): CalendarDay[];
/**
* Prints the calendar grid for the current Ethiopian month.
*
* @param {boolean} [useGeez=false] - If true, displays the calendar using Geez numerals.
* @returns {void}
*/
printThisMonth(useGeez?: boolean): void;
static getMonthCalendar(year: number, month: number, options?: StaticCalendarOptions): StaticMonthCalendar;
/**
* Generates a full-year calendar as an array of month objects for the specified year.
*
* @param {number} year - The year for which to generate the calendar.
* @param {Object} [options={}] - Optional configuration for calendar generation.
* @param {boolean} [options.useGeez=false] - Whether to use the Geez calendar system.
* @param {string} [options.weekdayLang='amharic'] - Language for weekday names (e.g., 'amharic').
* @param {number} [options.weekStart=0] - The starting day of the week (0 = Sunday, 1 = Monday, etc.).
* @param {function|null} [options.holidayFilter=null] - Optional filter function for holidays.
* @returns {Array<Object>} An array of 13 month objects, each containing:
* - {number} month: The month number (1-13).
* - {string} monthName: The name of the month.
* - {number} year: The year of the month.
* - {Array<string>} headers: The headers for the days of the week.
* - {Array<Array<Object>>} days: The grid of day objects for the month.
*/
static getYearCalendar(year: number, options?: StaticCalendarOptions): StaticMonthCalendar[];
/**
* Generates an array of Kenat instances for a given date range.
* @param {Kenat} startDate - The start of the range.
* @param {Kenat} endDate - The end of the range.
* @returns {Kenat[]} An array of Kenat objects.
* @throws {InvalidInputTypeError} If start or end dates are not Kenat instances.
*/
static generateDateRange(startDate: Kenat, endDate: Kenat): Kenat[];
/**
* Adds a specified amount of time to the current date.
* Supports method chaining for fluent API.
*
* @param {number} amount - The amount to add.
* @param {string} unit - The unit of time ('days', 'months', 'years').
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
add(amount: number, unit: 'days' | 'months' | 'years'): Kenat;
/**
* Subtracts a specified amount of time from the current date.
* Supports method chaining for fluent API.
*
* @param {number} amount - The amount to subtract.
* @param {string} unit - The unit of time ('days', 'months', 'years').
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
subtract(amount: number, unit: 'days' | 'months' | 'years'): Kenat;
/**
* Adds a specified number of days to the current Ethiopian date.
* Maintains backward compatibility.
*
* @param {number} days - The number of days to add.
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
addDays(days: number): Kenat;
/**
* Returns a new Kenat instance with the date advanced by the specified number of months.
* Maintains backward compatibility.
*
* @param {number} months - The number of months to add to the current date.
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
addMonths(months: number): Kenat;
/**
* Returns a new Kenat instance with the year increased by the specified number of years.
* Maintains backward compatibility.
*
* @param {number} years - The number of years to add to the current date.
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
addYears(years: number): Kenat;
/**
* Calculates the difference in days between this object's Ethiopian date and another object's Ethiopian date.
*
* @param {Object} other - An object with a `getEthiopian` method that returns an Ethiopian date.
* @returns {number} The number of days difference between the two Ethiopian dates.
*/
diffInDays(other: Kenat): number;
/**
* Calculates the difference in months between this instance's Ethiopian date and another Ethiopian date.
*
* @param {Object} other - An object with a `getEthiopian` method that returns an Ethiopian date.
* @returns {number} The number of months difference between the two Ethiopian dates.
*/
diffInMonths(other: Kenat): number;
/**
* Calculates the difference in years between this instance's Ethiopian date and another.
*
* @param {Object} other - An object with a getEthiopian() method returning an Ethiopian date.
* @returns {number} The number of years difference between the two Ethiopian dates.
*/
diffInYears(other: Kenat): number;
getCurrentTime(): Time;
/**
* Checks if the current Kenat instance's date is before another Kenat instance's date.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the current date is before the other date.
*/
isBefore(other: Kenat): boolean;
/**
* Checks if the current Kenat instance's date is after another Kenat instance's date.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the current date is after the other date.
*/
isAfter(other: Kenat): boolean;
/**
* Checks if the current Kenat instance's date is the same as another Kenat instance's date.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the dates are the same.
*/
isSameDay(other: Kenat): boolean;
/**
* Checks if the current Kenat instance's date is in the same month and year as another.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the month and year are the same.
*/
isSameMonth(other: Kenat): boolean;
/**
* Checks if the current Kenat instance's date is in the same year as another.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the year is the same.
*/
isSameYear(other: Kenat): boolean;
/**
* Returns a plain object representation of the Kenat instance for JSON serialization.
* @returns {{ ethiopian: {year: number, month: number, day: number}, gregorian: {year: number, month: number, day: number}, time: {hour: number, minute: number, period: string}|null }}
*/
toJSON(): {
ethiopian: {
year: number;
month: number;
day: number;
};
gregorian: GregorianDate;
time: {
hour: number;
minute: number;
period: TimePeriod;
} | null;
};
/**
* Returns a new Kenat instance set to the start of the specified unit.
* Supports method chaining.
*
* @param {string} unit - The unit ('day', 'month', 'year').
* @returns {Kenat} A new Kenat instance.
*/
startOf(unit: 'day' | 'month' | 'year'): Kenat;
/**
* Returns a new Kenat instance set to the end of the specified unit.
* Supports method chaining.
*
* @param {string} unit - The unit ('day', 'month', 'year').
* @returns {Kenat} A new Kenat instance.
*/
endOf(unit: 'day' | 'month' | 'year'): Kenat;
/**
* Returns a new Kenat instance set to the first day of the current month.
* Maintains backward compatibility.
* @returns {Kenat} A new Kenat instance.
*/
startOfMonth(): Kenat;
/**
* Returns a new Kenat instance set to the last day of the current month.
* Maintains backward compatibility.
* @returns {Kenat} A new Kenat instance.
*/
endOfMonth(): Kenat;
/**
* Checks if the current Ethiopian year is a leap year.
* @returns {boolean} True if it is a leap year.
*/
isLeapYear(): boolean;
/**
* Returns the weekday number for the current date.
* @returns {number} The day of the week (0 for Sunday, 6 for Saturday).
*/
weekday(): number;
/**
* Returns a breakdown of distance from this date to another date.
* @param {Kenat|{year:number,month:number,day:number}|string|Date} other - target date
* @param {{units?: ('years'|'months'|'days')[], output?: 'object'|'string', lang?: 'english'|'amharic'}} [options]
* @returns {Object|string}
*/
distanceTo(other: Kenat | EthiopianDate | string | Date, options?: DistanceOptions): DiffBreakdown | string;
/**
* Returns a breakdown of distance from today to this date.
*/
distanceFromToday(options?: DistanceOptions): DiffBreakdown | string;
/**
* Returns distance from today to the specified holiday occurrence.
* @param {string} holidayKey
* @param {{direction?: 'auto'|'future'|'past', units?: ('years'|'months'|'days')[], output?: 'object'|'string', lang?: 'english'|'amharic'}} [options]
*/
static distanceToHoliday(holidayKey: string, options?: DistanceToHolidayOptions): DiffBreakdown | string;
/**
* Formats a breakdown result to human string like "1 year 2 months 5 days".
*/
static formatDistance(breakdown: DiffBreakdown, options?: DistanceOptions): string;
static _coerceToKenat(input: Kenat | EthiopianDate | string | Date): Kenat;
static _getHolidayOccurrence(holidayKey: string, refEth: EthiopianDate, which: 'next' | 'prev'): Kenat;
}
/**
* Calculates a human-friendly breakdown between two Ethiopian dates.
* Iteratively accumulates years, then months, then days to avoid off-by-one issues.
*
* @param {Object} a - First Ethiopian date { year, month, day }.
* @param {Object} b - Second Ethiopian date { year, month, day }.
* @param {Object} [options]
* @param {Array<'years'|'months'|'days'>} [options.units=['years','months','days']] - Units to include, in order.
* @returns {{ sign: 1|-1, years?: number, months?: number, days?: number, totalDays: number }}
*/
declare function diffBreakdown(a: EthiopianDate, b: EthiopianDate, options?: {
units?: DiffUnit[];
}): DiffBreakdown;
interface MonthGridConfig {
year?: number;
month?: number;
weekStart?: number;
useGeez?: boolean;
weekdayLang?: Lang;
holidayFilter?: string | string[] | null;
mode?: 'christian' | 'muslim' | 'public' | null;
showAllSaints?: boolean;
}
/** A holiday-like entry attached to a day cell; saint/Jummah entries are looser than a full `Holiday`. */
interface DayHolidayEntry {
key: string;
name?: string;
description?: string;
tags: string[];
isNigs?: boolean;
ethiopian?: Holiday['ethiopian'];
gregorian?: Holiday['gregorian'];
movable?: boolean;
}
interface MonthGridDay {
ethiopian: {
year: number | string;
month: number | string;
day: number | string;
};
gregorian: {
year: number;
month: number;
day: number;
};
weekday: number;
weekdayName: string;
isToday: boolean;
holidays: DayHolidayEntry[];
}
interface MonthGridResult {
headers: string[];
days: (MonthGridDay | null)[];
year: number | string;
month: number;
monthName: string;
up: () => MonthGridResult;
down: () => MonthGridResult;
}
declare class MonthGrid {
year: number;
month: number;
weekStart: number;
useGeez: boolean;
weekdayLang: Lang;
holidayFilter: string | string[] | null;
mode: 'christian' | 'muslim' | 'public' | null;
showAllSaints: boolean;
constructor(config?: MonthGridConfig);
_validateConfig(config: MonthGridConfig): void;
static create(config?: MonthGridConfig): MonthGridResult;
generate(): MonthGridResult;
_getRawDays(): CalendarDay[];
_getFilteredHolidays(): Holiday[];
_getSaintsMap(): Record<number, DayHolidayEntry[]>;
_mergeDays(rawDays: any[], holidaysList: Holiday[], saintsMap: Record<number, DayHolidayEntry[]>): (MonthGridDay | null)[];
_getWeekdayHeaders(): string[];
_getLocalizedMonthName(): string;
_getLocalizedYear(): number | string;
up(): this;
down(): this;
}
/**
* Converts an Ethiopian date to its corresponding Gregorian date.
*
* @param {number} ethYear - The Ethiopian year.
* @param {number} ethMonth - The Ethiopian month (1-13).
* @param {number} ethDay - The Ethiopian day of the month.
* @returns {{ year: number, month: number, day: number }} The equivalent Gregorian date.
* @throws {InvalidInputTypeError} If any input is not a number.
* @throws {InvalidEthiopianDateError} If the provided Ethiopian date is invalid.
*/
declare function toGC(ethYear: number, ethMonth: number, ethDay: number): GregorianDate;
/**
* Converts a Gregorian date to the Ethiopian calendar (EC) date.
*
* @param {number} gYear - The Gregorian year (e.g., 2024).
* @param {number} gMonth - The Gregorian month (1-12).
* @param {number} gDay - The Gregorian day of the month (1-31).
* @returns {{ year: number, month: number, day: number }} The corresponding Ethiopian calendar date.
* @throws {InvalidInputTypeError} If any input is not a number.
* @throws {InvalidGregorianDateError} If the input date is invalid or out of supported range.
*/
declare function toEC(gYear: number, gMonth: number, gDay: number): EthiopianDate;
/**
* ethiopianNumberConverter.js
*
* Converts Arabic numerals (natural numbers) to their equivalent Ethiopic numerals.
* Supports numbers from 1 up to 99999999.
*
* Example:
* toGeez(1); // '፩'
* toGeez(30); // '፴'
* toGeez(123); // '፻፳፫'
* toGeez(10000); // '፼'
*
* @author Melaku Demeke
* @license MIT
*/
/**
* Converts a natural number to Ethiopic numeral string.
*
* @param {number|string} input - The number to convert (positive integer only).
* @returns {string} Ethiopic numeral string.
* @throws {GeezConverterError} If input is not a valid positive integer.
*/
declare function toGeez(input: number | string): string;
/**
* Converts a Ge'ez numeral string to its Arabic numeral equivalent.
*
* @param {string} geezStr - The Ge'ez numeral string to convert.
* @returns {number} The Arabic numeral representation of the input string.
* @throws {GeezConverterError} If the input is not a valid Ge'ez numeral string.
*/
declare function toArabic(geezStr: string): number;
declare function getHoliday(holidayKey: string, ethYear: number, options?: {
lang?: Lang;
}): Holiday | null;
declare function getHolidaysInMonth(ethYear: number, ethMonth: number, options?: {
lang?: Lang;
filter?: string | string[] | null;
}): Holiday[];
declare function getHolidaysForYear(ethYear: number, options?: {
lang?: Lang;
filter?: string | string[] | null;
}): Holiday[];
declare const monthNames: {
english: string[];
amharic: string[];
gregorian: string[];
};
declare const HolidayTags: {
readonly PUBLIC: "public";
readonly RELIGIOUS: "religious";
readonly CHRISTIAN: "christian";
readonly MUSLIM: "muslim";
readonly STATE: "state";
readonly CULTURAL: "cultural";
readonly OTHER: "other";
};
declare const HolidayNames: Readonly<Record<string, string>>;
declare const FastingKeys: {
readonly ABIY_TSOME: "ABIY_TSOME";
readonly TSOME_HAWARYAT: "TSOME_HAWARYAT";
readonly TSOME_NEBIYAT: "TSOME_NEBIYAT";
readonly NINEVEH: "NINEVEH";
readonly FILSETA: "FILSETA";
readonly RAMADAN: "RAMADAN";
readonly TSOME_DIHENET: "TSOME_DIHENET";
};
/**
* Calculates the start and end dates of a specific fasting period for a given year.
* @param {'ABIY_TSOME' | 'TSOME_HAWARYAT' | 'TSOME_NEBIYAT' | 'NINEVEH' | 'RAMADAN'} fastKey - The key for the fast.
* @param {number} ethiopianYear - The Ethiopian year.
* @returns {{start: object, end: object}|null} An object with start and end PLAIN date objects.
*/
declare function getFastingPeriod(fastKey: string, ethiopianYear: number): DateRange | null;
/**
* Returns fasting information (names, descriptions, period) for a given fast and year.
* @param {'ABIY_TSOME'|'TSOME_HAWARYAT'|'TSOME_NEBIYAT'|'NINEVEH'|'RAMADAN'} fastKey
* @param {number} ethiopianYear
* @param {{lang?: 'amharic'|'english'}} options
* @returns {{ key: string, name: string, description: string, period: { start: object, end: object } } | null}
*/
declare function getFastingInfo(fastKey: string, ethiopianYear: number, options?: {
lang?: Lang;
}): {
key: "TSOME_DIHENET";
name: string;
description: string;
tags: string[];
period: null;
} | {
key: string;
name: string;
description: string;
tags: string[];
period: DateRange;
} | null;
/**
* Return an array of day numbers in the given Ethiopian month that belong to a fasting period.
* For TSOME_DIHENET, it returns all Wednesdays and Fridays excluding the 50-day period after Easter (through Pentecost).
* For fixed/range fasts, it returns the days intersecting the fast period.
*
* @param {string} fastKey - One of FastingKeys
* @param {number} year - Ethiopian year
* @param {number} month - Ethiopian month (1-13)
* @returns {number[]}
*/
declare function getFastingDays(fastKey: string, year: number, month: number): number[];
export { type BahireHasabResult, type CalendarDay, type DateRange, type DayHolidayEntry, type DiffBreakdown, type DiffUnit, type DistanceOptions, type DistanceToHolidayOptions, type EthiopianDate, FastingKeys, type FormatOptions, type GetDateOptions, type GregorianDate, type Holiday, HolidayNames, HolidayTags, type KenatInput, type KenatTimeInput, type Lang, type LocalizedText, MonthGrid, type MonthGridConfig, type MonthGridDay, type MonthGridResult, type StaticCalendarOptions, type StaticMonthCalendar, Time, type TimeDuration, type TimeFormatOptions, type TimePeriod, type ToISOStringOptions, type ToStringOptions, Kenat as default, diffBreakdown, getBahireHasab, getFastingDays, getFastingInfo, getFastingPeriod, getHoliday, getHolidaysForYear, getHolidaysInMonth, monthNames, toArabic, toEC, toGC, toGeez };
/** Shared domain types used across Kenat's modules. */
type Lang = 'amharic' | 'english' | string;
interface EthiopianDate {
year: number;
month: number;
day: number;
}
interface GregorianDate {
year: number;
month: number;
day: number;
}
type TimePeriod = 'day' | 'night';
interface LocalizedText {
amharic: string;
english: string;
[lang: string]: string;
}
interface Holiday {
key: string;
tags: string[];
movable: boolean;
name?: string;
description?: string;
ethiopian: EthiopianDate;
gregorian?: GregorianDate;
}
interface DateRange {
start: EthiopianDate;
end: EthiopianDate;
}
type DiffUnit = 'years' | 'months' | 'days';
interface DiffBreakdown {
sign: 1 | -1;
totalDays: number;
years?: number;
months?: number;
days?: number;
}
interface BahireHasabResult {
ameteAlem: number;
meteneRabiet: number;
evangelist: {
name: string;
remainder: number;
};
newYear: {
dayName: string;
tinteQemer: number;
};
medeb: number;
wenber: number;
abektie: number;
metqi: number;
bealeMetqi: {
date: EthiopianDate;
weekday: string;
};
mebajaHamer: number;
nineveh: EthiopianDate;
movableFeasts: Record<string, Holiday>;
}
/**
* Calculates all Bahire Hasab values for a given Ethiopian year, including all movable feasts.
*
* @param {number} ethiopianYear - The Ethiopian year to calculate for.
* @param {Object} [options={}] - Options for language.
* @param {string} [options.lang='amharic'] - The language for names.
* @returns {Object} An object containing all the calculated Bahire Hasab values.
*/
declare function getBahireHasab(ethiopianYear: number, options?: {
lang?: Lang;
}): BahireHasabResult;
interface TimeFormatOptions {
lang?: Lang;
useGeez?: boolean;
showPeriodLabel?: boolean;
zeroAsDash?: boolean;
}
interface TimeDuration {
hours?: number;
minutes?: number;
}
declare class Time {
hour: number;
minute: number;
period: TimePeriod;
/**
* Constructs a Time instance representing an Ethiopian time.
* @param {number} hour - The Ethiopian hour (1-12).
* @param {number} [minute=0] - The minute (0-59).
* @param {string} [period='day'] - The period ('day' or 'night').
* @throws {InvalidTimeError} If any time component is invalid.
*/
constructor(hour: number, minute?: number, period?: TimePeriod);
/**
* Creates a Time instance from a Gregorian 24-hour time.
* @param {number} hour - The Gregorian hour (0-23).
* @param {number} [minute=0] - The minute (0-59).
* @returns {Time} A new Time instance.
* @throws {InvalidTimeError} If the Gregorian time is invalid.
*/
static fromGregorian(hour: number, minute?: number): Time;
/**
* Converts the Ethiopian time to Gregorian 24-hour format.
* @returns {{hour: number, minute: number}}
*/
toGregorian(): {
hour: number;
minute: number;
};
/**
* Creates a `Time` object from a string representation.
*
* This static method parses a time string, which can include hours, minutes, and an optional period (day/night).
* It supports both Arabic numerals (e.g., "1", "30") and Ethiopic numerals (e.g., "፩", "፴") for hours and minutes,
* assuming a `toArabic` utility function is available to convert Ethiopic numerals to Arabic numbers.
*
* The time string must contain a colon (`:`) separating the hour and minute.
*
* @param {string} timeString - The string representation of the time.
* Expected formats:
* - "HH:MM" (e.g., "6:30", "፮:፴")
* - "HH:MM period" (e.g., "6:30 night", "፮:፴ ማታ")
* Where:
* - HH: Hour (Arabic or Ethiopic numeral).
* - MM: Minute (Arabic or Ethiopic numeral).
* - period: Optional. Case-insensitive. Recognized values are "night" or "ማታ".
* If the period is omitted, or if a third part is present but not recognized as "night" or "ማታ",
* the time is assumed to be in the 'day' period.
*
* @returns {Time} A new `Time` object representing the parsed time.
*
* @throws {InvalidTimeError} If the `timeString` is:
* - Not a string or an empty string.
* - Missing the colon (`:`) separator.
* - Formatted incorrectly (e.g., not enough parts after splitting).
* - Contains non-numeric values for hour or minute that cannot be parsed into numbers
* (neither as Arabic nor as Ethiopic numerals via `toArabic`).
*
*/
static fromString(timeString: string): Time;
/**
* Adds a duration to the current time.
* @param {{hours?: number, minutes?: number}} duration - Object with hours and/or minutes to add.
* @returns {Time} A new Time instance with the added duration.
*/
add(duration: TimeDuration): Time;
/**
* Subtracts a duration from the current time.
* @param {{hours?: number, minutes?: number}} duration - Object with hours and/or minutes to subtract.
* @returns {Time} A new Time instance with the subtracted duration.
*/
subtract(duration: TimeDuration): Time;
/**
* Calculates the difference between this time and another.
* @param {Time} otherTime - Another Time instance to compare against.
* @returns {{hours: number, minutes: number}} An object with the absolute difference.
*/
diff(otherTime: Time): {
hours: number;
minutes: number;
};
/**
* Formats the time as a string.
* @param {Object} [options] - Formatting options.
* @param {string} [options.lang] - The language for the period label. Defaults to 'english' if useGeez is false, otherwise 'amharic'.
* @param {boolean} [options.useGeez=true] - Whether to use Ge'ez numerals.
* @param {boolean} [options.showPeriodLabel=true] - Whether to show the period label.
* @param {boolean} [options.zeroAsDash=true] - Whether to represent zero minutes as a dash.
* @returns {string} The formatted time string.
*/
format(options?: TimeFormatOptions): string;
}
interface KenatTimeInput {
hour: number;
minute: number;
period: 'day' | 'night';
}
type KenatInput = string | EthiopianDate | Date;
interface FormatOptions {
calendar?: 'ethiopian' | 'gregorian';
lang?: Lang;
showWeekday?: boolean;
useGeez?: boolean;
includeTime?: boolean;
}
interface ToStringOptions {
calendar?: 'ethiopian' | 'gregorian';
}
interface ToISOStringOptions {
calendar?: 'ethiopian' | 'gregorian';
}
interface GetDateOptions {
calendar?: 'ethiopian' | 'gregorian';
}
interface CalendarDay {
ethiopian: EthiopianDate & {
display: string;
};
gregorian: GregorianDate & {
display: string;
};
}
interface StaticCalendarOptions {
useGeez?: boolean;
weekdayLang?: Lang;
weekStart?: number;
holidayFilter?: string | string[] | null;
mode?: 'christian' | 'muslim' | 'public' | null;
}
interface StaticMonthCalendar {
month: number;
monthName: string;
year: number | string;
headers: string[];
days: unknown[];
}
interface DistanceOptions {
units?: DiffUnit[];
output?: 'object' | 'string';
lang?: 'english' | 'amharic';
}
interface DistanceToHolidayOptions extends DistanceOptions {
direction?: 'auto' | 'future' | 'past';
}
/**
* Kenat - Ethiopian Calendar Date Wrapper
*
* A lightweight class to work with both Gregorian and Ethiopian calendars.
* It wraps JavaScript's built-in `Date` object and converts Gregorian dates to Ethiopian equivalents.
*
*/
declare class Kenat {
ethiopian: EthiopianDate;
time: Time;
/**
* Constructs a Kenat instance.
* Can be initialized with:
* - An Ethiopian date string (e.g., '2016/1/1', '2016-1-1').
* - An object with { year, month, day }.
* - A native JavaScript Date object (will be converted from Gregorian).
* - No arguments, for the current date.
*
* @param {string|Object|Date} [input] - The date input.
* @param {Object} [timeObj] - An optional time object.
* @throws {InvalidEthiopianDateError} If the provided Ethiopian date is invalid.
* @throws {InvalidDateFormatError} If the provided date string format is invalid.
* @throws {UnrecognizedInputError} If the input format is unrecognized.
*/
constructor(input?: KenatInput, timeObj?: KenatTimeInput | null);
/**
* Creates and returns a new instance of the Kenat class representing the current moment.
*
* @returns {Kenat} A new Kenat instance set to the current date and time.
*/
static now(): Kenat;
/**
* Converts the current Ethiopian date stored in this.ethiopian to its Gregorian equivalent.
*
* @returns {{ year: number, month: number, day: number }} The Gregorian date corresponding to the Ethiopian date.
*/
getGregorian(): GregorianDate;
/**
* Returns the Ethiopian equivalent of the stored Gregorian date.
*
* @returns {{ year: number, month: number, day: number }} An object representing the Ethiopian date.
*/
getEthiopian(): EthiopianDate;
/**
* Returns the date in whichever calendar is requested, so callers with a
* user-configurable calendar preference don't need an if/else between
* getEthiopian() and getGregorian() at every call site.
*
* @param {Object} [options={}] - Options.
* @param {'ethiopian'|'gregorian'} [options.calendar='ethiopian'] - Which calendar's date to return.
* @returns {{ year: number, month: number, day: number }}
*/
getDate(options?: GetDateOptions): EthiopianDate | GregorianDate;
/**
* Sets the time and returns a new Kenat instance.
* Supports method chaining.
*
* @param {number} hour - The hour value to set.
* @param {number} minute - The minute value to set.
* @param {string} period - The period of the day (e.g., 'day' or 'night').
* @returns {Kenat} A new Kenat instance with the updated time.
*/
setTime(hour: number, minute: number, period: 'day' | 'night'): Kenat;
/**
* Calculates and returns the Bahire Hasab values for the current instance's year.
*
* @returns {Object} An object containing all the calculated Bahire Hasab values
* (ameteAlem, evangelist, wenber, metqi, nineveh, etc.).
*/
getBahireHasab(): BahireHasabResult;
/**
* Returns a string representation of the date and time, e.g. "መስከረም 1 2016 12:00 ጠዋት".
*
* @param {Object} [options={}] - Formatting options.
* @param {'ethiopian'|'gregorian'} [options.calendar='ethiopian'] - Which calendar to render the date in.
* 'gregorian' renders the Gregorian date with an English month name, e.g. "September 12, 2023 12:00 ጠዋት".
* In both cases the time-of-day portion still reflects Kenat's Ethiopian 12-hour clock; use
* toISOString({ calendar: 'gregorian' }) if you need the time converted to Gregorian 24-hour format.
* @returns {string} The formatted date and time string.
*/
toString(options?: ToStringOptions): string;
/**
* Formats the date according to the specified options.
*
* @param {Object} [options={}] - Formatting options.
* @param {'ethiopian'|'gregorian'} [options.calendar='ethiopian'] - Which calendar to render the date in. When
* 'gregorian', the date is rendered using English Gregorian month names; `useGeez` is ignored.
* @param {string} [options.lang='amharic'] - Language to use for formatting ('amharic', 'english', etc.).
* @param {boolean} [options.showWeekday=false] - Whether to include the weekday in the formatted string.
* @param {boolean} [options.useGeez=false] - Whether to use Geez numerals (only applies if lang is 'amharic').
* @param {boolean} [options.includeTime=false] - Whether to include the time in the formatted string.
* @returns {string} The formatted date string.
*/
format(options?: FormatOptions): string;
/**
* Formats the Ethiopian date in Geez numerals and Amharic month name.
*
* @returns {string} The formatted date string in the format: "{Amharic Month Name} {Geez Day} {Geez Year}".
*
* formatInGeezAmharic(); // "የካቲት ፲ ፳፻፲፭"
*/
formatInGeezAmharic(): string;
/**
* Formats the Ethiopian date with weekday name.
*
* @param {'amharic'|'english'} [lang='amharic'] - Language for month and weekday names.
* @param {boolean} [useGeez=false] - Whether to show numerals in Geez.
* @returns {string} Formatted string with weekday, e.g. "ማክሰኞ, መስከረም ፳፩ ፳፻፲፯"
*/
formatWithWeekday(lang?: Lang, useGeez?: boolean): string;
/**
* Returns the Ethiopian date in "yyyy/mm/dd" short format.
* @returns {string}
*/
formatShort(): string;
/**
* Returns an ISO-style date string: "YYYY-MM-DD" or "YYYY-MM-DDTHH:mm".
*
* @param {Object} [options={}] - Formatting options.
* @param {'ethiopian'|'gregorian'} [options.calendar='ethiopian'] - Which calendar's date to render.
* 'gregorian' produces a genuine ISO 8601 string: the date is Gregorian and the time is converted
* from Kenat's Ethiopian 12-hour clock to Gregorian 24-hour time (e.g. 12:00 day -> 06:00), with
* no non-standard suffix - useful for interop (e.g. `<input type="date">` or `new Date(...)`).
* The default 'ethiopian' calendar keeps the existing Ethiopian-date, Ethiopian-time, ISO-*like*
* string (including the non-standard `+12h` suffix for night times), unchanged for backward compatibility.
* @returns {string}
*/
toISOString(options?: ToISOStringOptions): string;
/**
* Checks if the current date is a holiday.
* @param {Object} [options={}] - Options for language.
* @param {string} [options.lang='amharic'] - The language for the holiday names and descriptions.
* @returns {Array<Object>} An array of holiday objects for the current date, or an empty array if it's not a holiday.
*/
isHoliday(options?: {
lang?: Lang;
}): Holiday[];
/**
* Generates a calendar for a given Ethiopian month and year, mapping each Ethiopian date
* to its corresponding Gregorian date and providing formatted display strings.
*
* @param {number} [year=this.ethiopian.year] - The Ethiopian year for the calendar.
* @param {number} [month=this.ethiopian.month] - The Ethiopian month (1-13).
* @param {boolean} [useGeez=false] - Whether to display dates in Geez numerals.
* @returns {Array<Object>} An array of objects, each representing a day in the month with
* Ethiopian and Gregorian date information and display strings.
*/
getMonthCalendar(year?: number, month?: number, useGeez?: boolean): CalendarDay[];
/**
* Prints the calendar grid for the current Ethiopian month.
*
* @param {boolean} [useGeez=false] - If true, displays the calendar using Geez numerals.
* @returns {void}
*/
printThisMonth(useGeez?: boolean): void;
static getMonthCalendar(year: number, month: number, options?: StaticCalendarOptions): StaticMonthCalendar;
/**
* Generates a full-year calendar as an array of month objects for the specified year.
*
* @param {number} year - The year for which to generate the calendar.
* @param {Object} [options={}] - Optional configuration for calendar generation.
* @param {boolean} [options.useGeez=false] - Whether to use the Geez calendar system.
* @param {string} [options.weekdayLang='amharic'] - Language for weekday names (e.g., 'amharic').
* @param {number} [options.weekStart=0] - The starting day of the week (0 = Sunday, 1 = Monday, etc.).
* @param {function|null} [options.holidayFilter=null] - Optional filter function for holidays.
* @returns {Array<Object>} An array of 13 month objects, each containing:
* - {number} month: The month number (1-13).
* - {string} monthName: The name of the month.
* - {number} year: The year of the month.
* - {Array<string>} headers: The headers for the days of the week.
* - {Array<Array<Object>>} days: The grid of day objects for the month.
*/
static getYearCalendar(year: number, options?: StaticCalendarOptions): StaticMonthCalendar[];
/**
* Generates an array of Kenat instances for a given date range.
* @param {Kenat} startDate - The start of the range.
* @param {Kenat} endDate - The end of the range.
* @returns {Kenat[]} An array of Kenat objects.
* @throws {InvalidInputTypeError} If start or end dates are not Kenat instances.
*/
static generateDateRange(startDate: Kenat, endDate: Kenat): Kenat[];
/**
* Adds a specified amount of time to the current date.
* Supports method chaining for fluent API.
*
* @param {number} amount - The amount to add.
* @param {string} unit - The unit of time ('days', 'months', 'years').
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
add(amount: number, unit: 'days' | 'months' | 'years'): Kenat;
/**
* Subtracts a specified amount of time from the current date.
* Supports method chaining for fluent API.
*
* @param {number} amount - The amount to subtract.
* @param {string} unit - The unit of time ('days', 'months', 'years').
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
subtract(amount: number, unit: 'days' | 'months' | 'years'): Kenat;
/**
* Adds a specified number of days to the current Ethiopian date.
* Maintains backward compatibility.
*
* @param {number} days - The number of days to add.
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
addDays(days: number): Kenat;
/**
* Returns a new Kenat instance with the date advanced by the specified number of months.
* Maintains backward compatibility.
*
* @param {number} months - The number of months to add to the current date.
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
addMonths(months: number): Kenat;
/**
* Returns a new Kenat instance with the year increased by the specified number of years.
* Maintains backward compatibility.
*
* @param {number} years - The number of years to add to the current date.
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
addYears(years: number): Kenat;
/**
* Calculates the difference in days between this object's Ethiopian date and another object's Ethiopian date.
*
* @param {Object} other - An object with a `getEthiopian` method that returns an Ethiopian date.
* @returns {number} The number of days difference between the two Ethiopian dates.
*/
diffInDays(other: Kenat): number;
/**
* Calculates the difference in months between this instance's Ethiopian date and another Ethiopian date.
*
* @param {Object} other - An object with a `getEthiopian` method that returns an Ethiopian date.
* @returns {number} The number of months difference between the two Ethiopian dates.
*/
diffInMonths(other: Kenat): number;
/**
* Calculates the difference in years between this instance's Ethiopian date and another.
*
* @param {Object} other - An object with a getEthiopian() method returning an Ethiopian date.
* @returns {number} The number of years difference between the two Ethiopian dates.
*/
diffInYears(other: Kenat): number;
getCurrentTime(): Time;
/**
* Checks if the current Kenat instance's date is before another Kenat instance's date.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the current date is before the other date.
*/
isBefore(other: Kenat): boolean;
/**
* Checks if the current Kenat instance's date is after another Kenat instance's date.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the current date is after the other date.
*/
isAfter(other: Kenat): boolean;
/**
* Checks if the current Kenat instance's date is the same as another Kenat instance's date.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the dates are the same.
*/
isSameDay(other: Kenat): boolean;
/**
* Checks if the current Kenat instance's date is in the same month and year as another.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the month and year are the same.
*/
isSameMonth(other: Kenat): boolean;
/**
* Checks if the current Kenat instance's date is in the same year as another.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the year is the same.
*/
isSameYear(other: Kenat): boolean;
/**
* Returns a plain object representation of the Kenat instance for JSON serialization.
* @returns {{ ethiopian: {year: number, month: number, day: number}, gregorian: {year: number, month: number, day: number}, time: {hour: number, minute: number, period: string}|null }}
*/
toJSON(): {
ethiopian: {
year: number;
month: number;
day: number;
};
gregorian: GregorianDate;
time: {
hour: number;
minute: number;
period: TimePeriod;
} | null;
};
/**
* Returns a new Kenat instance set to the start of the specified unit.
* Supports method chaining.
*
* @param {string} unit - The unit ('day', 'month', 'year').
* @returns {Kenat} A new Kenat instance.
*/
startOf(unit: 'day' | 'month' | 'year'): Kenat;
/**
* Returns a new Kenat instance set to the end of the specified unit.
* Supports method chaining.
*
* @param {string} unit - The unit ('day', 'month', 'year').
* @returns {Kenat} A new Kenat instance.
*/
endOf(unit: 'day' | 'month' | 'year'): Kenat;
/**
* Returns a new Kenat instance set to the first day of the current month.
* Maintains backward compatibility.
* @returns {Kenat} A new Kenat instance.
*/
startOfMonth(): Kenat;
/**
* Returns a new Kenat instance set to the last day of the current month.
* Maintains backward compatibility.
* @returns {Kenat} A new Kenat instance.
*/
endOfMonth(): Kenat;
/**
* Checks if the current Ethiopian year is a leap year.
* @returns {boolean} True if it is a leap year.
*/
isLeapYear(): boolean;
/**
* Returns the weekday number for the current date.
* @returns {number} The day of the week (0 for Sunday, 6 for Saturday).
*/
weekday(): number;
/**
* Returns a breakdown of distance from this date to another date.
* @param {Kenat|{year:number,month:number,day:number}|string|Date} other - target date
* @param {{units?: ('years'|'months'|'days')[], output?: 'object'|'string', lang?: 'english'|'amharic'}} [options]
* @returns {Object|string}
*/
distanceTo(other: Kenat | EthiopianDate | string | Date, options?: DistanceOptions): DiffBreakdown | string;
/**
* Returns a breakdown of distance from today to this date.
*/
distanceFromToday(options?: DistanceOptions): DiffBreakdown | string;
/**
* Returns distance from today to the specified holiday occurrence.
* @param {string} holidayKey
* @param {{direction?: 'auto'|'future'|'past', units?: ('years'|'months'|'days')[], output?: 'object'|'string', lang?: 'english'|'amharic'}} [options]
*/
static distanceToHoliday(holidayKey: string, options?: DistanceToHolidayOptions): DiffBreakdown | string;
/**
* Formats a breakdown result to human string like "1 year 2 months 5 days".
*/
static formatDistance(breakdown: DiffBreakdown, options?: DistanceOptions): string;
static _coerceToKenat(input: Kenat | EthiopianDate | string | Date): Kenat;
static _getHolidayOccurrence(holidayKey: string, refEth: EthiopianDate, which: 'next' | 'prev'): Kenat;
}
/**
* Calculates a human-friendly breakdown between two Ethiopian dates.
* Iteratively accumulates years, then months, then days to avoid off-by-one issues.
*
* @param {Object} a - First Ethiopian date { year, month, day }.
* @param {Object} b - Second Ethiopian date { year, month, day }.
* @param {Object} [options]
* @param {Array<'years'|'months'|'days'>} [options.units=['years','months','days']] - Units to include, in order.
* @returns {{ sign: 1|-1, years?: number, months?: number, days?: number, totalDays: number }}
*/
declare function diffBreakdown(a: EthiopianDate, b: EthiopianDate, options?: {
units?: DiffUnit[];
}): DiffBreakdown;
interface MonthGridConfig {
year?: number;
month?: number;
weekStart?: number;
useGeez?: boolean;
weekdayLang?: Lang;
holidayFilter?: string | string[] | null;
mode?: 'christian' | 'muslim' | 'public' | null;
showAllSaints?: boolean;
}
/** A holiday-like entry attached to a day cell; saint/Jummah entries are looser than a full `Holiday`. */
interface DayHolidayEntry {
key: string;
name?: string;
description?: string;
tags: string[];
isNigs?: boolean;
ethiopian?: Holiday['ethiopian'];
gregorian?: Holiday['gregorian'];
movable?: boolean;
}
interface MonthGridDay {
ethiopian: {
year: number | string;
month: number | string;
day: number | string;
};
gregorian: {
year: number;
month: number;
day: number;
};
weekday: number;
weekdayName: string;
isToday: boolean;
holidays: DayHolidayEntry[];
}
interface MonthGridResult {
headers: string[];
days: (MonthGridDay | null)[];
year: number | string;
month: number;
monthName: string;
up: () => MonthGridResult;
down: () => MonthGridResult;
}
declare class MonthGrid {
year: number;
month: number;
weekStart: number;
useGeez: boolean;
weekdayLang: Lang;
holidayFilter: string | string[] | null;
mode: 'christian' | 'muslim' | 'public' | null;
showAllSaints: boolean;
constructor(config?: MonthGridConfig);
_validateConfig(config: MonthGridConfig): void;
static create(config?: MonthGridConfig): MonthGridResult;
generate(): MonthGridResult;
_getRawDays(): CalendarDay[];
_getFilteredHolidays(): Holiday[];
_getSaintsMap(): Record<number, DayHolidayEntry[]>;
_mergeDays(rawDays: any[], holidaysList: Holiday[], saintsMap: Record<number, DayHolidayEntry[]>): (MonthGridDay | null)[];
_getWeekdayHeaders(): string[];
_getLocalizedMonthName(): string;
_getLocalizedYear(): number | string;
up(): this;
down(): this;
}
/**
* Converts an Ethiopian date to its corresponding Gregorian date.
*
* @param {number} ethYear - The Ethiopian year.
* @param {number} ethMonth - The Ethiopian month (1-13).
* @param {number} ethDay - The Ethiopian day of the month.
* @returns {{ year: number, month: number, day: number }} The equivalent Gregorian date.
* @throws {InvalidInputTypeError} If any input is not a number.
* @throws {InvalidEthiopianDateError} If the provided Ethiopian date is invalid.
*/
declare function toGC(ethYear: number, ethMonth: number, ethDay: number): GregorianDate;
/**
* Converts a Gregorian date to the Ethiopian calendar (EC) date.
*
* @param {number} gYear - The Gregorian year (e.g., 2024).
* @param {number} gMonth - The Gregorian month (1-12).
* @param {number} gDay - The Gregorian day of the month (1-31).
* @returns {{ year: number, month: number, day: number }} The corresponding Ethiopian calendar date.
* @throws {InvalidInputTypeError} If any input is not a number.
* @throws {InvalidGregorianDateError} If the input date is invalid or out of supported range.
*/
declare function toEC(gYear: number, gMonth: number, gDay: number): EthiopianDate;
/**
* ethiopianNumberConverter.js
*
* Converts Arabic numerals (natural numbers) to their equivalent Ethiopic numerals.
* Supports numbers from 1 up to 99999999.
*
* Example:
* toGeez(1); // '፩'
* toGeez(30); // '፴'
* toGeez(123); // '፻፳፫'
* toGeez(10000); // '፼'
*
* @author Melaku Demeke
* @license MIT
*/
/**
* Converts a natural number to Ethiopic numeral string.
*
* @param {number|string} input - The number to convert (positive integer only).
* @returns {string} Ethiopic numeral string.
* @throws {GeezConverterError} If input is not a valid positive integer.
*/
declare function toGeez(input: number | string): string;
/**
* Converts a Ge'ez numeral string to its Arabic numeral equivalent.
*
* @param {string} geezStr - The Ge'ez numeral string to convert.
* @returns {number} The Arabic numeral representation of the input string.
* @throws {GeezConverterError} If the input is not a valid Ge'ez numeral string.
*/
declare function toArabic(geezStr: string): number;
declare function getHoliday(holidayKey: string, ethYear: number, options?: {
lang?: Lang;
}): Holiday | null;
declare function getHolidaysInMonth(ethYear: number, ethMonth: number, options?: {
lang?: Lang;
filter?: string | string[] | null;
}): Holiday[];
declare function getHolidaysForYear(ethYear: number, options?: {
lang?: Lang;
filter?: string | string[] | null;
}): Holiday[];
declare const monthNames: {
english: string[];
amharic: string[];
gregorian: string[];
};
declare const HolidayTags: {
readonly PUBLIC: "public";
readonly RELIGIOUS: "religious";
readonly CHRISTIAN: "christian";
readonly MUSLIM: "muslim";
readonly STATE: "state";
readonly CULTURAL: "cultural";
readonly OTHER: "other";
};
declare const HolidayNames: Readonly<Record<string, string>>;
declare const FastingKeys: {
readonly ABIY_TSOME: "ABIY_TSOME";
readonly TSOME_HAWARYAT: "TSOME_HAWARYAT";
readonly TSOME_NEBIYAT: "TSOME_NEBIYAT";
readonly NINEVEH: "NINEVEH";
readonly FILSETA: "FILSETA";
readonly RAMADAN: "RAMADAN";
readonly TSOME_DIHENET: "TSOME_DIHENET";
};
/**
* Calculates the start and end dates of a specific fasting period for a given year.
* @param {'ABIY_TSOME' | 'TSOME_HAWARYAT' | 'TSOME_NEBIYAT' | 'NINEVEH' | 'RAMADAN'} fastKey - The key for the fast.
* @param {number} ethiopianYear - The Ethiopian year.
* @returns {{start: object, end: object}|null} An object with start and end PLAIN date objects.
*/
declare function getFastingPeriod(fastKey: string, ethiopianYear: number): DateRange | null;
/**
* Returns fasting information (names, descriptions, period) for a given fast and year.
* @param {'ABIY_TSOME'|'TSOME_HAWARYAT'|'TSOME_NEBIYAT'|'NINEVEH'|'RAMADAN'} fastKey
* @param {number} ethiopianYear
* @param {{lang?: 'amharic'|'english'}} options
* @returns {{ key: string, name: string, description: string, period: { start: object, end: object } } | null}
*/
declare function getFastingInfo(fastKey: string, ethiopianYear: number, options?: {
lang?: Lang;
}): {
key: "TSOME_DIHENET";
name: string;
description: string;
tags: string[];
period: null;
} | {
key: string;
name: string;
description: string;
tags: string[];
period: DateRange;
} | null;
/**
* Return an array of day numbers in the given Ethiopian month that belong to a fasting period.
* For TSOME_DIHENET, it returns all Wednesdays and Fridays excluding the 50-day period after Easter (through Pentecost).
* For fixed/range fasts, it returns the days intersecting the fast period.
*
* @param {string} fastKey - One of FastingKeys
* @param {number} year - Ethiopian year
* @param {number} month - Ethiopian month (1-13)
* @returns {number[]}
*/
declare function getFastingDays(fastKey: string, year: number, month: number): number[];
export { type BahireHasabResult, type CalendarDay, type DateRange, type DayHolidayEntry, type DiffBreakdown, type DiffUnit, type DistanceOptions, type DistanceToHolidayOptions, type EthiopianDate, FastingKeys, type FormatOptions, type GetDateOptions, type GregorianDate, type Holiday, HolidayNames, HolidayTags, type KenatInput, type KenatTimeInput, type Lang, type LocalizedText, MonthGrid, type MonthGridConfig, type MonthGridDay, type MonthGridResult, type StaticCalendarOptions, type StaticMonthCalendar, Time, type TimeDuration, type TimeFormatOptions, type TimePeriod, type ToISOStringOptions, type ToStringOptions, Kenat as default, diffBreakdown, getBahireHasab, getFastingDays, getFastingInfo, getFastingPeriod, getHoliday, getHolidaysForYear, getHolidaysInMonth, monthNames, toArabic, toEC, toGC, toGeez };

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

+29
-11
{
"name": "kenat",
"version": "3.2.0",
"version": "4.0.0",
"description": "A JavaScript library for the Ethiopian calendar with date and time support.",
"main": "src/index.js",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"unpkg": "./dist/index.global.js",
"type": "module",
"sideEffects": false,
"engines": {
"node": ">=18"
},
"files": [
"dist",
"!dist/**/*.map"
],
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"docs": "jsdoc -c jsdoc.json",
"release:patch": "npm version patch && git push && git push --tags && npm publish",
"release:minor": "npm version minor && git push && git push --tags && npm publish",
"release:major": "npm version major && git push && git push --tags && npm publish",
"prepack": "tsc -p tsconfig.json"
"typecheck": "tsc --noEmit -p tsconfig.json",
"build": "tsup",
"docs": "typedoc",
"release:patch": "npm version patch && git push --follow-tags",
"release:minor": "npm version minor && git push --follow-tags",
"release:major": "npm version major && git push --follow-tags",
"prepack": "npm run typecheck && npm run build",
"prepublishOnly": "npm test"
},

@@ -40,7 +53,8 @@ "keywords": [

"license": "MIT",
"types": "./types/index.d.ts",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./src/index.js"
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}

@@ -57,6 +71,10 @@ },

"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "^26.1.0",
"jest": "^29.7.0",
"jsdoc": "^4.0.4",
"ts-jest": "^29.4.11",
"tsup": "^8.5.1",
"typedoc": "^0.28.19",
"typescript": "^5.9.2"
}
}
+382
-30

@@ -9,3 +9,3 @@ # Kenat / ቀናት ![NPM Version](https://img.shields.io/npm/v/kenat)

![GitHub Repo stars](https://img.shields.io/github/stars/MelakuDemeke/kenat?logo=github&style=flat)
![GitHub forks](https://img.shields.io/github/forks/MelakuDemeke/kenat?logo=github&style=falt)
![GitHub forks](https://img.shields.io/github/forks/MelakuDemeke/kenat?logo=github&style=flat)
![GitHub commit activity](https://img.shields.io/github/commit-activity/m/MelakuDemeke/kenat?logo=github)

@@ -23,14 +23,53 @@ [![npm downloads](https://img.shields.io/npm/dm/kenat.svg?style=flat-square)](https://www.npmjs.com/package/kenat)

## 📚 Table of Contents
- [Features](#-features)
- [Installation](#-installation)
- [Requirements](#requirements)
- [TypeScript Support](#typescript-support)
- [Import Options](#import-options)
- [Quick Start](#-quick-start)
- [Design Principles](#-design-principles)
- [Bahire Hasab & Holiday System](#-bahire-hasab--holiday-system)
- [More API Examples](#-more-api-examples)
- [Date Arithmetic](#date-arithmetic)
- [Comparing Dates](#comparing-dates)
- [Start / End of a Period](#start--end-of-a-period)
- [Date Difference & Distance API](#date-difference--distance-api)
- [Calendar Generation](#calendar-generation)
- [Fasting Periods](#fasting-periods)
- [Holiday Distance Helpers](#holiday-distance-helpers)
- [Geez Numerals & Formatting](#geez-numerals--formatting)
- [Calendar Preference (Ethiopian / Gregorian)](#calendar-preference-ethiopian--gregorian)
- [Time Handling](#time-handling)
- [Serialization](#serialization)
- [API Reference](#-api-reference)
- [Contribution Guide](#-contribution-guide)
- [Author](#-author)
- [Contributors](#-contributors)
- [License](#-license)
---
## ✨ Features
- 🔄 **Bidirectional Conversion**: Seamlessly convert between Ethiopian and Gregorian calendars.
- 🔄 **Bidirectional Conversion**: Seamlessly convert between Ethiopian and Gregorian calendars with high precision.
- 🌓 **Calendar Preference**: Read or format any date as Ethiopian *or* Gregorian with a single `{ calendar }` option — no `if`/`else` needed at call sites.
- 🗂️ **Complete Holiday System**: Pre-loaded with all public, religious (Christian & Muslim), and cultural holidays.
- 🔎 **Advanced Holiday Filtering**: Easily filter holidays by tags (e.g., public, christian, muslim).
- 📖 **Authentic Liturgical Calculations**: Implements Bahire Hasab for movable feasts and fasts.
- 🔠 **Localized Formatting**: Display dates in both Amharic and English.
- 📖 **Authentic Liturgical Calculations**: Implements Bahire Hasab (ባሕረ ሃሳብ) for movable feasts and fasts, including edge-case years.
- 🔠 **Localized Formatting**: Display dates in both Amharic and English with multiple format options.
- 🔢 **Geez Numerals**: Convert numbers and dates to traditional Geez numeral equivalents.
- ➕ **Full Date Arithmetic**: Add or subtract days, months, and years with support for the 13-month calendar.
- ↔️ **Date Difference**: Calculate precise differences between two dates.
- 🕒 **Ethiopian Time**: Convert between 24-hour Gregorian and 12-hour Ethiopian time.
- 🗓️ **Calendar Generation**: Create monthly or yearly calendar grids.
- ⏱️ **Start / End of Period**: Snap any date to the start or end of its day, month, or year (leap-year aware).
- ⚖️ **Date Comparison**: `isBefore`, `isAfter`, `isSameDay`, `isSameMonth`, and `isSameYear` for clean, readable comparisons.
- ↔️ **Date Difference & Distance API**: Calculate precise differences and human-friendly distances between dates.
- 🕒 **Ethiopian Time**: Convert between 24-hour Gregorian and 12-hour Ethiopian time systems.
- 🗓️ **Calendar Generation**: Create monthly or yearly calendar grids with customizable options.
- 🕌 **Fasting Periods**: Calculate Orthodox Christian fasting periods and Islamic Ramadan dates.
- 📅 **Date Range Generation**: Generate arrays of dates between two Ethiopian dates.
- 🏷️ **Holiday Distance Helpers**: Find distances to next/previous holiday occurrences.
- 📊 **Multiple Output Formats**: Support for ISO strings, JSON serialization, custom formatting, and structured data.
- 🧊 **Immutable & Chainable**: Every method that changes a date returns a *new* `Kenat` instance, so chaining is always safe.
- 📦 **Zero Dependencies**: Pure JavaScript, no runtime dependencies.

@@ -43,4 +82,48 @@ ---

npm install kenat
````
```
### Requirements
Node.js 18 or later. Kenat ships zero runtime dependencies and both module formats — `import` (ESM) and `require()` (CJS) both work, so there's no need for a dynamic `import()` workaround in CommonJS projects. A minified browser build (`dist/index.global.js`, global `Kenat`) is also published for plain `<script>` tag / CDN use via [unpkg](https://unpkg.com/kenat/).
### TypeScript Support
Kenat is written in TypeScript and ships its own `.d.ts` declarations — no additional type packages needed, and the types are guaranteed to match the implementation since they're generated from the same source, not hand-maintained separately.
```ts
import Kenat from 'kenat';
import type { EthiopianDate, Holiday } from 'kenat';
const today: Kenat = new Kenat();
const date: EthiopianDate = today.getEthiopian();
```
### Import Options
```js
// Default import (Kenat class)
import Kenat from 'kenat';
// Named imports for utilities
import {
getHolidaysForYear,
getHolidaysInMonth,
getHoliday,
HolidayTags,
HolidayNames,
toGeez,
toArabic,
toEC,
toGC,
diffBreakdown,
getFastingPeriod,
getFastingInfo,
getFastingDays,
getBahireHasab,
monthNames,
MonthGrid,
Time
} from 'kenat';
```
---

@@ -50,3 +133,3 @@

Get today’s Ethiopian date:
Get today's Ethiopian date:

@@ -59,6 +142,15 @@ ```js

console.log(today.getEthiopian());
// → { year: 2017, month: 9, day: 26 }
// → { year: 2018, month: 1, day: 8 }
console.log(today.format({ lang: 'english', showWeekday: true }));
// → "Friday, Ginbot 26 2017"
// → "Thursday, Meskerem 8 2018"
// Get Gregorian equivalent
console.log(today.getGregorian());
// → { year: 2025, month: 9, day: 19 }
// Format with time
const withTime = today.setTime(8, 30, 'day');
console.log(withTime.format({ includeTime: true }));
// → "2018/1/8 8:30 day"
```

@@ -68,2 +160,18 @@

## 🧭 Design Principles
- **Immutable & chainable** — every method that "changes" a date (`.add()`, `.startOf()`, `.setTime()`, ...) returns a **new** `Kenat` instance rather than mutating the original. This makes chaining predictable and safe:
```js
const future = new Kenat()
.add(2, 'years')
.add(3, 'months')
.startOf('month');
```
- **Ethiopian-first, calendar-agnostic where it counts** — dates are stored internally as Ethiopian `{year, month, day}`, but every user-facing accessor and formatter (`getDate`, `format`, `toString`, `toISOString`) accepts a `{ calendar: 'ethiopian' | 'gregorian' }` option, so building calendar-switchable UIs doesn't require branching logic in your own code.
- **No hidden state** — options are always passed explicitly per call (`options = {}`), matching the rest of the library's style; there's no global or instance-level configuration to keep track of.
---
## ⛪ Bahire Hasab & Holiday System

@@ -89,4 +197,4 @@

description: 'የኢየሱስ ክርስቶስን ከሙታን መነሣት ያከብራል።...',
ethiopian: { year: 2017, month: 8, day: 21 },
gregorian: { year: 2025, month: 4, day: 29 }
ethiopian: { year: 2017, month: 8, day: 12 },
gregorian: { year: 2025, month: 4, day: 20 }
}

@@ -154,4 +262,4 @@ ```

description: 'የኢየሱስ ክርስቶስን ከሙታን መነሣት ያከብራል።...',
ethiopian: { year: 2017, month: 8, day: 21 },
gregorian: { year: 2025, month: 4, day: 29 }
ethiopian: { year: 2017, month: 8, day: 12 },
gregorian: { year: 2025, month: 4, day: 20 }
},

@@ -163,2 +271,4 @@ // ... other movable holidays

> The Bahire Hasab engine correctly handles edge-case years where the traditional Metqi calculation lands on 0/30 (e.g. 2006 E.C.), so movable feast dates stay accurate every year.
---

@@ -174,22 +284,164 @@

const lastMonth = today.addMonths(-1);
// Fluent API
const future = today.add(2, 'years').add(3, 'months').add(15, 'days');
```
### Date Difference
### Comparing Dates
```js
const a = new Kenat('2016/1/1');
const b = new Kenat('2016/2/5');
a.isBefore(b); // → true
b.isAfter(a); // → true
a.isSameDay(a); // → true
a.isSameMonth(b); // → false (different months)
a.isSameYear(b); // → true (both 2016)
```
### Start / End of a Period
`startOf`/`endOf` snap a date to the boundary of its day, month, or year — `endOf('year')` correctly returns Pagume 6 in leap years and Pagume 5 otherwise:
```js
const date = new Kenat('2016/5/15');
date.startOfMonth().getEthiopian(); // → { year: 2016, month: 5, day: 1 }
date.endOfMonth().getEthiopian(); // → { year: 2016, month: 5, day: 30 }
date.startOf('year').getEthiopian(); // → { year: 2016, month: 1, day: 1 }
new Kenat('2015/5/1').endOf('year').getEthiopian();
// → { year: 2015, month: 13, day: 6 } (2015 is a leap year, so Pagume has 6 days)
```
### Date Difference & Distance API
```js
const a = new Kenat('2015/5/15');
const b = new Kenat('2012/5/15');
// Precise differences
console.log(a.diffInDays(b)); // → 1095
console.log(a.diffInYears(b)); // → 3
// Human-friendly distance
console.log(a.distanceTo(b, { units: ['years', 'months', 'days'], output: 'string' }));
// → "2 years 9 months 3 days"
// Distance from today to a given date
console.log(new Kenat('2018/1/1').distanceFromToday({ output: 'string' }));
```
### Geez Numerals
### Calendar Generation
```js
// Monthly calendar
const calendar = Kenat.getMonthCalendar(2017, 1, {
useGeez: false,
weekdayLang: 'amharic'
});
// Yearly calendar
const yearCalendar = Kenat.getYearCalendar(2017);
// Date range generation
const start = new Kenat('2017/1/1');
const end = new Kenat('2017/1/10');
const range = Kenat.generateDateRange(start, end);
// Print the current month as a grid to the console
new Kenat().printThisMonth();
```
### Fasting Periods
```js
import { getFastingPeriod, getFastingInfo } from 'kenat';
// Get Lent fasting period
const lent = getFastingPeriod('ABIY_TSOME', 2017);
console.log(lent);
// → { start: { year: 2017, month: 7, day: 16 }, end: { year: 2017, month: 8, day: 12 } }
// Get fasting information
const fastingInfo = getFastingInfo('NINEVEH', 2017);
```
### Holiday Distance Helpers
```js
import { HolidayNames } from 'kenat';
// Distance to next holiday
console.log(Kenat.distanceToHoliday(HolidayNames.fasika, {
direction: 'future',
units: ['days'],
output: 'string'
}));
// → "358 days"
```
### Geez Numerals & Formatting
```js
import { toGeez } from 'kenat';
console.log(toGeez(2017)); // → "፳፻፲፯"
// Format in Geez
const date = new Kenat('2017/1/1');
console.log(date.formatInGeezAmharic()); // → "መስከረም ፩ ፳፻፲፯"
```
### Calendar Preference (Ethiopian / Gregorian)
If your app lets end users choose between the Ethiopian and Gregorian calendar, pass `calendar: 'gregorian'` to `format()`, `toString()`, `toISOString()`, or `getDate()` instead of branching with `if`/`else` at every call site:
```js
const date = new Kenat('2016/1/1');
const userPref = 'gregorian'; // e.g. from a user setting
date.format({ calendar: userPref }); // → "September 12, 2023"
date.getDate({ calendar: userPref }); // → { year: 2023, month: 9, day: 12 }
date.toISOString({ calendar: userPref }); // → "2023-09-12T06:00" (standard ISO 8601)
// Defaults to 'ethiopian' when omitted
date.format(); // → "መስከረም 1 2016"
```
> `toISOString({ calendar: 'gregorian' })` converts Kenat's Ethiopian 12-hour time to Gregorian 24-hour time (12:00 day → 06:00) and never appends the non-standard `+12h` suffix that the default Ethiopian-calendar ISO string uses for night times — the result is a genuine, parser-safe ISO 8601 string.
### Time Handling
```js
// Set Ethiopian time (12-hour system)
const withTime = new Kenat('2017/1/1').setTime(8, 30, 'day');
console.log(withTime.format({ includeTime: true }));
// → "2017/1/1 8:30 day"
// Convert to ISO format
console.log(withTime.toISOString());
// → "2017-01-01T08:30"
```
### Serialization
`Kenat` instances serialize cleanly with `JSON.stringify` via a built-in `toJSON()`:
```js
const date = new Kenat('2017/1/15');
console.log(date.toJSON());
// → {
// ethiopian: { year: 2017, month: 1, day: 15 },
// gregorian: { year: 2024, month: 9, day: 25 },
// time: { hour: 12, minute: 0, period: 'day' }
// }
console.log(JSON.stringify({ createdAt: date }));
// → '{"createdAt":{"ethiopian":{...},"gregorian":{...},"time":{...}}}'
```
---

@@ -199,13 +451,101 @@

Refer to the full documentation site for method details, utility functions, and live examples.
### Core Classes
---
#### `Kenat` (Default Export)
Main class for Ethiopian date operations.
## 🎉 Coming Soon
**Constructor:**
```js
new Kenat() // Current date
new Kenat('2017/1/1') // Ethiopian date string
new Kenat({year: 2017, month: 1, day: 1}) // Date object
new Kenat(new Date()) // Gregorian Date object
```
* ✅ Ethiopian Seasons (Tseday, Bega, Kiremt, Meher)
* ✅ Helpers like `.isSameMonth()` and `.startOfYear()`
* 🚀 Multi-language ports (Python, PHP, Dart)
* ⚙️ `.ics` iCalendar export
**Access & Conversion**
- `getEthiopian()` - Returns `{year, month, day}`
- `getGregorian()` - Returns Gregorian equivalent
- `getDate({ calendar })` - Returns the date in `'ethiopian'` (default) or `'gregorian'`, no if/else needed
- `getCurrentTime()` - Returns the instance's `Time` object
- `setTime(hour, minute, period)` - Returns a new instance with the given Ethiopian time
**Formatting**
- `format(options)` - Format with various options, including `lang`, `showWeekday`, `useGeez`, `includeTime`, and `{ calendar: 'ethiopian' | 'gregorian' }`
- `toString(options)` - String representation of date + time; also accepts `{ calendar }`
- `formatInGeezAmharic()` - Amharic month name with Geez numerals
- `formatWithWeekday(lang, useGeez)` - Formatted string including the weekday name
- `formatShort()` - Compact `"yyyy/mm/dd"` format
- `toISOString(options)` - ISO-style date string; `{ calendar: 'gregorian' }` returns a standard ISO 8601 date
- `toJSON()` - Plain-object representation for `JSON.stringify`
**Comparison**
- `isBefore(other)` / `isAfter(other)` - Chronological comparison
- `isSameDay(other)` / `isSameMonth(other)` / `isSameYear(other)` - Granular equality checks
- `isLeapYear()` - Whether the instance's Ethiopian year is a leap year
- `weekday()` - Day-of-week index (0 = Sunday)
**Arithmetic & Period Boundaries**
- `add(amount, unit)` / `subtract(amount, unit)` - Chainable arithmetic (`unit`: `'days'|'months'|'years'`)
- `addDays(n)` / `addMonths(n)` / `addYears(n)` - Direct arithmetic shortcuts
- `startOf(unit)` / `endOf(unit)` - Snap to the start/end of `'day'`, `'month'`, or `'year'` (leap-year aware)
- `startOfMonth()` / `endOfMonth()` - Shortcuts for `startOf('month')` / `endOf('month')`
- `diffInDays(other)` / `diffInMonths(other)` / `diffInYears(other)` - Precise numeric differences
- `distanceTo(other, options)` - Human-friendly breakdown or string (`{ units, output, lang }`)
- `distanceFromToday(options)` - Distance from today to this instance
**Holidays & Liturgical Calendar**
- `isHoliday(options)` - Check if the date is a holiday
- `getBahireHasab()` - Get liturgical calculations for the instance's year
**Calendar Grids**
- `getMonthCalendar(year, month, useGeez)` - Instance-level month grid (defaults to the instance's own month)
- `printThisMonth(useGeez)` - Pretty-print the current month's calendar grid to the console
**Static Helpers**
- `Kenat.now()` - Same as `new Kenat()`
- `Kenat.getMonthCalendar(year, month, options)` - Generate a month calendar
- `Kenat.getYearCalendar(year, options)` - Generate a full year's calendar
- `Kenat.generateDateRange(start, end)` - Array of `Kenat` instances between two dates
- `Kenat.distanceToHoliday(holidayKey, options)` - Distance to a holiday occurrence
- `Kenat.formatDistance(breakdown, options)` - Format a distance breakdown as a human string
### Utility Functions
#### Date Conversion
- `toEC(year, month, day)` - Gregorian to Ethiopian
- `toGC(year, month, day)` - Ethiopian to Gregorian
- `toGeez(number)` - Convert to Geez numerals
- `toArabic(geezString)` - Convert from Geez numerals
#### Holiday System
- `getHolidaysForYear(year, options)` - Get all holidays for a year
- `getHolidaysInMonth(year, month, lang)` - Get holidays for a month
- `getHoliday(key, year)` - Get a specific holiday
- `HolidayTags` - Constants for filtering holidays
- `HolidayNames` - Constants for holiday keys
#### Fasting & Religious Calendar
- `getFastingPeriod(fastKey, year)` - Get fasting period dates
- `getFastingInfo(fastKey, year)` - Get fasting information
- `getFastingDays(fastKey, year)` - Get the list of individual fasting days
- `getBahireHasab(year)` - Get Bahire Hasab calculations
#### Calendar Generation
- `Kenat.getMonthCalendar(year, month, options)` - Generate month calendar
- `Kenat.getYearCalendar(year, options)` - Generate year calendar
- `Kenat.generateDateRange(start, end)` - Generate date range
- `MonthGrid.create(options)` - Create calendar grid
#### Distance & Arithmetic
- `diffBreakdown(dateA, dateB, options)` - Precise date difference
- `Kenat.distanceToHoliday(holidayKey, options)` - Distance to holiday
### Constants
- `HolidayTags` - Holiday category tags
- `HolidayNames` - Holiday name constants
- `monthNames` - Month name arrays in multiple languages (includes an English `gregorian` set)
- `FastingKeys` - Fasting period keys
For detailed method signatures and advanced usage, refer to the [full documentation](https://www.kenat.systems/).
---

@@ -221,5 +561,6 @@

```
3. Write your changes and add tests in the `/tests` directory.
4. Run `npm test` to ensure everything passes.
5. Open a Pull Request with your improvements or bug fix.
3. Write your changes (in `src/*.ts`) and add tests in the `/tests` directory.
4. Run `npm run typecheck` and `npm test` to ensure everything passes.
5. If you're changing anything in `examples/`, run `npm run build` first — the examples import from `dist/`, not `src/`.
6. Open a Pull Request with your improvements or bug fix.

@@ -231,10 +572,21 @@ ---

**Melaku Demeke**
[GitHub](https://github.com) ・ [LinkedIn](https://linkedin.com)
- [GitHub](https://github.com/MelakuDemeke) ・ [LinkedIn](https://linkedin.com/in/melakudemeke)
- [Website](https://www.kenat.systems/) ・ [NPM Package](https://www.npmjs.com/package/kenat)
### Acknowledgments
Special thanks to the Ethiopian Orthodox Tewahedo Church for preserving the traditional Bahire Hasab calculations and to the open-source community for their contributions and feedback.
---
## 📄 License
## 🙌 Contributors
MIT — see `LICENSE` for details.
Thanks to all the amazing people who have contributed to this project!
[![Contributors](https://contrib.rocks/image?repo=MelakuDemeke/kenat)](https://github.com/MelakuDemeke/kenat/graphs/contributors)
---
## 📄 License
MIT — see [LICENSE](LICENSE) for details.
# These are supported funding model platforms
# github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
# patreon: # Replace with a single Patreon username
# open_collective: # Replace with a single Open Collective username
# ko_fi: # Replace with a single Ko-fi username
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
# liberapay: # Replace with a single Liberapay username
# issuehunt: # Replace with a single IssueHunt username
# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
# polar: # Replace with a single Polar username
# buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
# thanks_dev: # Replace with a single thanks.dev username
# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
github: [MelakuDemeke]
name: Main Branch Test Workflow
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18' # or any version you use
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test

Sorry, the diff of this file is not supported yet

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ethiopian Time Clock</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
text-align: center;
padding: 2rem;
background: #f9f9f9;
}
#digitalClock {
font-size: 2.5rem;
margin-bottom: 1rem;
}
#geezToggle {
margin-bottom: 2rem;
padding: 0.5rem 1rem;
font-size: 1rem;
cursor: pointer;
}
#analogClock {
margin: 0 auto;
background: #fff;
border: 5px solid #333;
border-radius: 50%;
width: 250px;
height: 250px;
position: relative;
}
#analogClock canvas {
display: block;
margin: 0 auto;
background: transparent;
}
</style>
</head>
<body>
<h1>Ethiopian Time Clock</h1>
<button id="geezToggle">Use Geez Numerals: ON</button>
<div id="digitalClock">--:-- --</div>
<div id="analogClock">
<canvas id="clockCanvas" width="250" height="250"></canvas>
</div>
<script type="module">
// --- Start of Embedded Library Code ---
// Note: GeezConverterError is simplified for this standalone file.
class GeezConverterError extends Error {
constructor(message) {
super(message);
this.name = 'GeezConverterError';
}
}
const geezSymbols = {
ones: ['', '፩', '፪', '፫', '፬', '፭', '፮', '፯', '፰', '፱'],
tens: ['', '፲', '፳', '፴', '፵', '፶', '፷', '፸', '፹', '፺'],
hundred: '፻',
tenThousand: '፼'
};
/**
* Converts a natural number to Ethiopic numeral string.
*/
function toGeez(input) {
if (typeof input !== 'number' && typeof input !== 'string') {
throw new GeezConverterError("Input must be a number or a string.");
}
const num = Number(input);
if (isNaN(num) || !Number.isInteger(num) || num < 0) {
throw new GeezConverterError("Input must be a non-negative integer.");
}
if (num === 0) return '0';
function convertBelow100(n) {
if (n <= 0) return '';
const tensDigit = Math.floor(n / 10);
const onesDigit = n % 10;
return geezSymbols.tens[tensDigit] + geezSymbols.ones[onesDigit];
}
if (num < 100) {
return convertBelow100(num);
}
if (num === 100) return geezSymbols.hundred;
if (num < 10000) {
const hundreds = Math.floor(num / 100);
const remainder = num % 100;
const hundredPart = (hundreds > 1 ? convertBelow100(hundreds) : '') + geezSymbols.hundred;
return hundredPart + convertBelow100(remainder);
}
const tenThousandPart = Math.floor(num / 10000);
const remainder = num % 10000;
const tenThousandGeez = (tenThousandPart > 1 ? toGeez(tenThousandPart) : '') + geezSymbols.tenThousand;
return tenThousandGeez + (remainder > 0 ? toGeez(remainder) : '');
}
// Note: InvalidTimeError is simplified for this standalone file.
class InvalidTimeError extends Error {
constructor(message) {
super(message);
this.name = 'InvalidTimeError';
}
}
class Time {
constructor(hour, minute = 0, period = 'day') {
if (hour < 1 || hour > 12) {
throw new InvalidTimeError(`Invalid Ethiopian hour: ${hour}. Must be between 1 and 12.`);
}
if (minute < 0 || minute > 59) {
throw new InvalidTimeError(`Invalid minute: ${minute}. Must be between 0 and 59.`);
}
if (period !== 'day' && period !== 'night') {
throw new InvalidTimeError(`Invalid period: "${period}". Must be 'day' or 'night'.`);
}
this.hour = hour;
this.minute = minute;
this.period = period;
}
static fromGregorian(hour, minute = 0) {
if (hour < 0 || hour > 23) {
throw new InvalidTimeError(`Invalid Gregorian hour: ${hour}. Must be between 0 and 23.`);
}
if (minute < 0 || minute > 59) {
throw new InvalidTimeError(`Invalid minute: ${minute}. Must be between 0 and 59.`);
}
let tempHour = hour - 6;
if (tempHour < 0) {
tempHour += 24;
}
const period = (tempHour < 12) ? 'day' : 'night';
let ethHour = tempHour % 12;
ethHour = (ethHour === 0) ? 12 : ethHour;
return new Time(ethHour, minute, period);
}
format(options = {}) {
const defaultLang = options.useGeez === false ? 'english' : 'amharic';
const { lang = defaultLang, useGeez = true, showPeriodLabel = true, zeroAsDash = true } = options;
const formatNum = (num) => {
if (useGeez) return toGeez(num);
return num.toString().padStart(2, '0');
};
const hourStr = formatNum(this.hour);
let minuteStr;
if (zeroAsDash && this.minute === 0) {
minuteStr = '_';
} else {
minuteStr = useGeez ? toGeez(this.minute) : this.minute.toString().padStart(2, '0');
}
let periodLabel = '';
if (showPeriodLabel) {
if (lang === 'english') {
periodLabel = this.period;
} else {
const amharicLabels = { day: 'ጠዋት', night: 'ማታ' };
periodLabel = amharicLabels[this.period];
}
}
const label = periodLabel ? ` ${periodLabel}` : '';
return `${hourStr}:${minuteStr}${label}`;
}
}
// --- End of Embedded Library Code ---
// --- Start of Clock Application Code ---
const digitalClock = document.getElementById('digitalClock');
const geezToggleBtn = document.getElementById('geezToggle');
const canvas = document.getElementById('clockCanvas');
const ctx = canvas.getContext('2d');
const radius = canvas.height / 2;
ctx.translate(radius, radius);
let useGeez = true;
geezToggleBtn.addEventListener('click', () => {
useGeez = !useGeez;
geezToggleBtn.textContent = `Use Geez Numerals: ${useGeez ? 'ON' : 'OFF'}`;
// No need to call drawClock immediately, the update loop will handle it.
});
function drawClock() {
drawFace(ctx, radius);
drawNumbers(ctx, radius);
drawTime(ctx, radius);
}
function drawFace(ctx, radius) {
ctx.beginPath();
ctx.arc(0, 0, radius, 0, 2 * Math.PI);
ctx.fillStyle = 'white';
ctx.fill();
ctx.strokeStyle = '#333';
ctx.lineWidth = radius * 0.05;
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, radius * 0.05, 0, 2 * Math.PI);
ctx.fillStyle = '#333';
ctx.fill();
}
function drawNumbers(ctx, radius) {
const angIncrement = (2 * Math.PI) / 12;
ctx.font = `${radius * 0.15}px Arial`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
for (let num = 1; num <= 12; num++) {
let numeral = useGeez ? toGeez(num) : num.toString();
// Position numbers on the clock face
let ang = num * angIncrement - (Math.PI / 2); // Adjust to start 1 at the top-right
let x = radius * 0.85 * Math.cos(ang);
let y = radius * 0.85 * Math.sin(ang);
ctx.fillStyle = '#000';
ctx.fillText(numeral, x, y);
}
}
function drawTime(ctx, radius) {
const now = new Date();
const time = Time.fromGregorian(now.getHours(), now.getMinutes());
const hour = time.hour;
const minute = time.minute;
const second = now.getSeconds();
// Hour hand angle calculation
const hourForAngle = hour % 12 + minute / 60; // Get a fractional hour (e.g., 1.5 for 1:30)
const hourAngle = (hourForAngle * Math.PI) / 6 - Math.PI / 2;
drawHand(ctx, hourAngle, radius * 0.5, radius * 0.07);
// Minute hand angle
const minuteAngle = (minute * Math.PI) / 30 - Math.PI / 2;
drawHand(ctx, minuteAngle, radius * 0.75, radius * 0.05);
// Second hand angle
const secondAngle = (second * Math.PI) / 30 - Math.PI / 2;
drawHand(ctx, secondAngle, radius * 0.85, radius * 0.02, 'red');
digitalClock.textContent = time.format({ useGeez, showPeriodLabel: true, zeroAsDash: false });
}
function drawHand(ctx, pos, length, width, color = '#333') {
ctx.beginPath();
ctx.lineWidth = width;
ctx.lineCap = 'round';
ctx.strokeStyle = color;
ctx.moveTo(0, 0);
ctx.lineTo(length * Math.cos(pos), length * Math.sin(pos));
ctx.stroke();
}
function update() {
ctx.clearRect(-radius, -radius, canvas.width, canvas.height);
drawClock();
requestAnimationFrame(update);
}
// Start the clock
update();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kenat Calendar Example</title>
<style>
html,
body {
height: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f9f9f9;
padding: 1rem;
}
.app-container {
display: flex;
gap: 0;
align-items: stretch;
max-width: 1200px;
margin: 0 auto;
background: #fff;
border-radius: 16px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
overflow: hidden;
height: calc(100vh - 2rem);
}
#calendar-container {
flex: 1 1 0%;
padding: 2rem 2rem;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
#calendar-container h1 {
font-size: 2.1rem;
font-weight: 700;
margin-bottom: 1.2rem;
letter-spacing: 0.02em;
color: #2d3a4a;
}
#calendar-container .header-controls {
position: sticky;
top: 0;
background: #fff;
padding: 0.5rem 0;
z-index: 2;
}
#calendar {
flex: 1;
overflow: auto;
padding-bottom: 1rem;
}
#fasting-sidebar {
width: 300px;
background: linear-gradient(135deg, #f3e7e9 0%, #e3eeff 100%);
padding: 2.5rem 2rem 2rem 2rem;
border-right: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
align-items: flex-start;
min-height: 100%;
overflow-y: auto;
}
#fasting-sidebar h2 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 1.2rem;
color: #4a2d7b;
letter-spacing: 0.01em;
}
#fasting-sidebar h3 {
margin-top: 0;
text-align: left;
font-size: 1.1rem;
border-bottom: 1px solid #eee;
padding-bottom: 0.5rem;
margin-bottom: 1.2rem;
color: #2d3a4a;
}
#fasting-list {
list-style: none;
padding: 0;
margin: 0;
text-align: left;
width: 100%;
overflow-y: auto;
max-height: calc(100% - 120px);
}
#fasting-list li {
padding: 1rem 1.2rem;
cursor: pointer;
border-radius: 10px;
margin-bottom: 0.7rem;
border-left: 8px solid transparent;
transition: background 0.2s, border-color 0.2s;
font-size: 1.08rem;
background: #f7f7fa;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.03);
}
#fasting-list li .fast-title {
font-weight: 600;
display: block;
margin-bottom: 4px;
}
#fasting-list li .fast-desc {
font-size: 0.9rem;
color: #3e4b5c;
line-height: 1.3;
}
#fasting-list li:hover {
background: #e3eeff;
}
#fasting-list li.active {
font-weight: bold;
background: linear-gradient(90deg, #e3eeff 60%, #f3e7e9 100%);
border-left-color: #4a2d7b;
color: #4a2d7b;
}
.fasting-day-marker {
background-color: rgba(76, 175, 80, 0.13) !important;
box-shadow: inset 0 0 0 2px #4a2d7b33;
border-radius: 12px;
}
table {
border-collapse: collapse;
margin: 0.5rem auto;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
background-color: #fff;
border-radius: 18px;
overflow: hidden;
width: 100%;
max-width: 720px;
}
th,
td {
border: 1px solid #eef0f3;
padding: 0.35rem;
width: 52px;
height: 56px;
vertical-align: top;
position: relative;
border-radius: 12px;
}
th {
background: linear-gradient(180deg, #f6f7fb 0%, #eef1f7 100%);
font-weight: 700;
border-radius: 12px 12px 0 0;
position: sticky;
top: 0;
z-index: 1;
}
.today {
background-color: #fffbe6;
border: 2px solid #ffd54f;
font-weight: 700;
}
td strong {
font-weight: 700;
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 50%;
}
.today strong {
background-color: #ffecb3;
}
td:hover {
background-color: #f7faff;
}
td.event-day {
cursor: pointer;
}
.official-holiday {
background-color: #e8f3ff;
}
.christian-holiday {
background-color: #eef7ff;
}
.jummah-day {
background-color: #e9f7ef;
}
.holiday-labels {
font-size: 0.75rem;
color: #333;
margin-top: 5px;
line-height: 1.2;
text-align: left;
max-height: 3.6em;
overflow: hidden;
}
.holiday-labels .is-nigs {
font-weight: bold;
color: #b58c00;
}
.holiday-labels .jummah-label {
font-weight: bold;
color: #388e3c;
}
.header-controls,
.controls,
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin: 1rem 0;
align-items: center;
}
.nav-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.controls {
gap: 20px;
}
.filter-controls {
margin-bottom: 1.5rem;
background-color: #fff;
padding: 1rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.filter-controls label,
.controls label {
font-size: 0.9rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
}
.filter-controls label.disabled {
color: #aaa;
cursor: not-allowed;
}
button,
select {
padding: 0.6rem 1.2rem;
font-size: 0.9rem;
cursor: pointer;
border-radius: 6px;
border: 1px solid #ccc;
background-color: #fff;
transition: all 0.15s ease-in-out;
}
button:hover,
select:hover {
border-color: #9fb3ff;
box-shadow: 0 0 0 3px #9fb3ff22;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
.modal-content {
background-color: #fefefe;
margin: auto;
padding: 25px;
border: 1px solid #888;
width: 90%;
max-width: 500px;
border-radius: 10px;
position: relative;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
text-align: left;
}
.modal-close {
color: #aaa;
position: absolute;
top: 10px;
right: 20px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.modal-content h3 {
margin-top: 0;
}
.modal-content .is-nigs-title {
color: #b58c00;
}
.modal-content hr {
border: 0;
border-top: 1px solid #eee;
margin: 15px 0;
}
</style>
</head>
<body>
<div class="app-container">
<div id="fasting-sidebar">
<h2>Fasting Period Selector</h2>
<h3>Choose a Fasting Period</h3>
<ul id="fasting-list"></ul>
</div>
<div id="calendar-container">
<h1>Kenat Calendar</h1>
<div id="calendar">Loading...</div>
</div>
</div>
<div id="holidayModal" class="modal">
<div class="modal-content"><span class="modal-close">&times;</span>
<div id="modalBody"></div>
</div>
</div>
<script type="module">
import Kenat, { MonthGrid, HolidayTags, toArabic } from '../../src/index.js';
import { monthNames, FastingKeys } from '../../src/constants.js';
import { getFastingPeriod, getFastingInfo, getFastingDays } from '../../src/fasting.js';
// State variables
let monthGridInstance;
let useGeez = false;
let weekdayLang = 'amharic';
let weekStart = 1;
let holidayFilter = null;
let activeMode = 'public';
let showAllSaints = false;
let activeFastingRange = null;
let activeFastingKey = null;
const initialDate = new Kenat().getEthiopian();
let currentYear = initialDate.year;
let currentMonth = initialDate.month;
const modal = document.getElementById('holidayModal');
const modalBody = document.getElementById('modalBody');
const closeModal = document.querySelector('.modal-close');
function showHolidayModal(holidays) {
let content = '';
holidays.forEach((h, index) => {
const nigsClass = h.isNigs ? 'is-nigs-title' : '';
const description = h.description ? `<p>${h.description}</p>` : '';
content += `<h3 class="${nigsClass}">${h.name}</h3>${description}`;
if (index < holidays.length - 1) content += '<hr>';
});
modalBody.innerHTML = content;
modal.style.display = 'flex';
}
closeModal.onclick = () => { modal.style.display = 'none'; }
window.onclick = (event) => { if (event.target == modal) modal.style.display = 'none'; }
function isWithinRange(dayObj, range) {
if (!range || !dayObj) return false;
// Accept both Kenat instances and plain objects
let currentDayKenat;
if (dayObj instanceof Kenat) {
currentDayKenat = dayObj;
} else if (dayObj.ethiopianRaw) {
currentDayKenat = new Kenat(dayObj.ethiopianRaw);
} else if (dayObj.ethiopian) {
currentDayKenat = new Kenat(dayObj.ethiopian);
} else {
return false;
}
return (
(currentDayKenat.isSameDay(range.start) || currentDayKenat.isAfter(range.start)) &&
(currentDayKenat.isSameDay(range.end) || currentDayKenat.isBefore(range.end))
);
}
function renderCalendar(gridData) {
let { headers, days, year, month } = gridData;
const holidaysForDay = (item) => item.holidays.map(h => `<div class="${h.isNigs ? 'is-nigs' : ''} ${h.key === 'jummah' ? 'jummah-label' : ''}">${h.name}</div>`).join('');
const yearForComparison = typeof year === 'string' ? toArabic(year) : year;
const yearOptions = Array.from({ length: 201 }, (_, i) => 1900 + i).map(y => `<option value="${y}" ${y === yearForComparison ? 'selected' : ''}>${y}</option>`).join('');
const monthOptions = monthNames.amharic.map((name, i) => `<option value="${i + 1}" ${i + 1 === month ? 'selected' : ''}>${name}</option>`).join('');
let html = `<div class="header-controls"><button id="prevMonth">⬅️</button><div class="nav-controls"><select id="monthSelector">${monthOptions}</select><select id="yearSelector">${yearOptions}</select></div><button id="nextMonth">➡️</button></div><div class="controls" id="topControls"></div><div class="filter-controls" id="filterControls"></div><table><thead><tr>${headers.map(day => `<th>${day}</th>`).join('')}</tr></thead><tbody>`;
for (let i = 0; i < days.length; i += 7) {
html += '<tr>';
for (let j = 0; j < 7; j++) {
const item = days[i + j];
if (!item) {
html += '<td></td>';
} else {
const hasEvents = item.holidays && item.holidays.length > 0;
let dayClasses = [item.isToday ? 'today' : ''];
let inlineStyle = '';
if (hasEvents) {
dayClasses.push('event-day');
if (item.holidays.some(h => h.tags.includes(HolidayTags.PUBLIC))) dayClasses.push('official-holiday');
if (item.holidays.some(h => h.tags.includes(HolidayTags.CHRISTIAN)) && !dayClasses.includes('official-holiday')) dayClasses.push('christian-holiday');
if (item.holidays.some(h => h.key === 'jummah')) dayClasses.push('jummah-day');
}
if (activeFastingRange && isWithinRange(item, activeFastingRange)) {
dayClasses.push('fasting-day-marker');
inlineStyle = `style="background-color: ${activeFastingRange.color}"`;
}
if (!activeFastingRange && activeFastingKey === FastingKeys.TSOME_DIHENET) {
const fdays = getFastingDays(FastingKeys.TSOME_DIHENET, yearForComparison, month);
if (fdays.includes(item.ethiopian.day)) {
dayClasses.push('fasting-day-marker');
inlineStyle = `style="background-color: #ffe0b2"`;
}
}
const holidaysText = hasEvents ? holidaysForDay(item) : '';
const eventAttr = hasEvents ? `data-day-index="${i + j}"` : '';
html += `<td class="${dayClasses.join(' ')}" ${eventAttr} ${inlineStyle}><strong>${item.ethiopian.day}</strong><br/><small>${item.gregorian.month}/${item.gregorian.day}</small>${holidaysText ? `<div class="holiday-labels">${holidaysText}</div>` : ''}</td>`;
}
}
html += '</tr>';
}
html += '</tbody></table>';
document.getElementById('calendar').innerHTML = html;
renderControls();
attachEventListeners(days);
}
function renderControls() {
const topControlsContainer = document.getElementById('topControls');
const filterControlsContainer = document.getElementById('filterControls');
topControlsContainer.innerHTML = `<label for="modeSelector">Mode:</label><select id="modeSelector"><option value="public" ${activeMode === 'public' ? 'selected' : ''}>Public</option><option value="christian" ${activeMode === 'christian' ? 'selected' : ''}>Christian</option><option value="muslim" ${activeMode === 'muslim' ? 'selected' : ''}>Muslim</option><option value="none" ${activeMode === 'none' ? 'selected' : ''}>All</option></select><button id="toggleGeez">Geez (${useGeez ? 'ON' : 'OFF'})</button><button id="toggleLang">Lang (${weekdayLang})</button><button id="toggleWeekStart">Start (${weekStart === 1 ? 'Mon' : 'Sun'})</button>`;
let filterHtml = '';
if (activeMode === 'christian') {
filterHtml += `<label><input type="checkbox" id="showAllSaintsToggle" ${showAllSaints ? 'checked' : ''}> Show All Saints</label>`;
}
const isDisabled = activeMode !== 'none';
const disabledAttr = isDisabled ? 'disabled' : '';
filterHtml += `<label class="${isDisabled ? 'disabled' : ''}"><input type="checkbox" name="holidayTag" value="all" ${!holidayFilter || isDisabled ? 'checked' : ''} ${disabledAttr}> All Holidays</label>`;
for (const key in HolidayTags) {
const value = HolidayTags[key];
const isChecked = !isDisabled && holidayFilter && holidayFilter.includes(value);
filterHtml += `<label class="${isDisabled ? 'disabled' : ''}"><input type="checkbox" name="holidayTag" value="${value}" ${isChecked ? 'checked' : ''} ${disabledAttr}> ${value}</label>`;
}
filterControlsContainer.innerHTML = filterHtml;
}
// Color palette per fasting key
const FAST_COLORS = {
[FastingKeys.ABIY_TSOME]: '#fff59d',
[FastingKeys.TSOME_NEBIYAT]: '#b3e5fc',
[FastingKeys.TSOME_HAWARYAT]: '#c8e6c9',
[FastingKeys.NINEVEH]: '#ffccbc',
[FastingKeys.RAMADAN]: '#d1c4e9',
[FastingKeys.FILSETA]: '#f8bbd0',
[FastingKeys.TSOME_DIHENET]: '#ffe0b2',
};
function buildFastingList() {
const keys = [
FastingKeys.ABIY_TSOME,
FastingKeys.TSOME_NEBIYAT,
FastingKeys.TSOME_HAWARYAT,
FastingKeys.NINEVEH,
FastingKeys.RAMADAN,
FastingKeys.FILSETA,
FastingKeys.TSOME_DIHENET,
];
return keys.map(key => {
const info = getFastingInfo(key, currentYear, { lang: weekdayLang });
return {
key,
name: info?.name || key,
description: info?.description || '',
color: FAST_COLORS[key] || '#e0e0e0',
};
});
}
function renderFastingSidebar() {
const list = document.getElementById('fasting-list');
list.innerHTML = '';
const fastingPeriods = buildFastingList();
fastingPeriods.forEach(fast => {
const li = document.createElement('li');
const shortDesc = fast.description || '';
li.innerHTML = `<span class="fast-title">${fast.name}</span>${shortDesc ? `<div class="fast-desc">${shortDesc}</div>` : ''}`;
li.style.borderLeftColor = fast.color;
if ((activeFastingRange && activeFastingRange.key === fast.key) || (activeFastingKey === fast.key)) {
li.classList.add('active');
}
li.onclick = () => {
const yearToSearch = currentYear;
if (fast.key === FastingKeys.TSOME_DIHENET) {
activeFastingKey = (activeFastingKey === FastingKeys.TSOME_DIHENET) ? null : FastingKeys.TSOME_DIHENET;
activeFastingRange = null;
} else {
const period = getFastingPeriod(fast.key, yearToSearch);
if (period) {
if (activeFastingRange && activeFastingRange.key === fast.key) {
activeFastingRange = null;
activeFastingKey = null;
} else {
activeFastingRange = {
key: fast.key,
start: new Kenat(period.start),
end: new Kenat(period.end),
color: fast.color
};
activeFastingKey = null;
currentYear = activeFastingRange.start.getEthiopian().year;
currentMonth = activeFastingRange.start.getEthiopian().month;
}
} else {
alert(`Fasting period for ${fast.name} not found in ${yearToSearch} E.C.`);
activeFastingRange = null;
activeFastingKey = null;
}
}
rerender();
};
list.appendChild(li);
});
// Fasting info display
let infoBox = document.getElementById('fasting-info-box');
if (!infoBox) {
infoBox = document.createElement('div');
infoBox.id = 'fasting-info-box';
infoBox.style.marginTop = '1.5rem';
infoBox.style.padding = '1rem';
infoBox.style.background = '#f7f7fa';
infoBox.style.borderRadius = '10px';
infoBox.style.boxShadow = '0 1px 4px rgba(0,0,0,0.03)';
infoBox.style.fontSize = '1rem';
infoBox.style.color = '#2d3a4a';
list.parentNode.appendChild(infoBox);
}
if (activeFastingRange) {
const start = activeFastingRange.start.getEthiopian();
const end = activeFastingRange.end.getEthiopian();
const days = Kenat.generateDateRange(activeFastingRange.start, activeFastingRange.end).length;
const selectedInfo = getFastingInfo(activeFastingRange.key, start.year, { lang: weekdayLang });
const fastName = selectedInfo?.name || activeFastingRange.key;
const fastDesc = selectedInfo?.description || '';
// Use a colored dot for the fast color, but keep text readable
infoBox.innerHTML = `<strong>Selected Fast:</strong> <span style="display:inline-flex;align-items:center;"><span style="display:inline-block;width:14px;height:14px;border-radius:50%;background:${activeFastingRange.color};margin-right:7px;border:1px solid #ccc;"></span><span style="color:#2d3a4a;">${fastName}</span></span><br>
<strong>Start:</strong> ${start.month}/${start.day}/${start.year}<br>
<strong>End:</strong> ${end.month}/${end.day}/${end.year}<br>
<strong>Days:</strong> ${days}
${fastDesc ? `<hr><div>${fastDesc}</div>` : ''}`;
infoBox.style.display = 'block';
} else {
infoBox.innerHTML = '';
infoBox.style.display = 'none';
}
}
function attachEventListeners(days) {
// ✨ FIX: The line `activeFastingRange = null;` has been REMOVED from the four
// navigation handlers below to ensure the highlight persists between months.
document.getElementById('prevMonth').onclick = () => {
const newGrid = monthGridInstance.down().generate();
currentYear = (typeof newGrid.year === 'string') ? toArabic(newGrid.year) : newGrid.year;
currentMonth = newGrid.month;
rerender();
};
document.getElementById('nextMonth').onclick = () => {
const newGrid = monthGridInstance.up().generate();
currentYear = (typeof newGrid.year === 'string') ? toArabic(newGrid.year) : newGrid.year;
currentMonth = newGrid.month;
rerender();
};
document.getElementById('monthSelector').onchange = (e) => {
currentMonth = parseInt(e.target.value);
rerender();
};
document.getElementById('yearSelector').onchange = (e) => {
currentYear = parseInt(e.target.value);
rerender();
};
// --- Other event listeners (no changes needed here) ---
document.getElementById('toggleGeez').onclick = () => { useGeez = !useGeez; rerender(); };
document.getElementById('toggleLang').onclick = () => { weekdayLang = weekdayLang === 'amharic' ? 'english' : 'amharic'; rerender(); };
document.getElementById('toggleWeekStart').onclick = () => { weekStart = weekStart === 1 ? 0 : 1; rerender(); };
document.getElementById('modeSelector').onchange = (e) => { activeMode = e.target.value; if (activeMode !== 'none') { holidayFilter = null; } showAllSaints = false; rerender(); };
const saintsToggle = document.getElementById('showAllSaintsToggle');
if (saintsToggle) { saintsToggle.onchange = (e) => { showAllSaints = e.target.checked; rerender(); }; }
document.querySelectorAll('.event-day').forEach(cell => {
cell.onclick = () => {
const dayIndex = parseInt(cell.getAttribute('data-day-index'));
const dayData = days[dayIndex];
if (dayData && dayData.holidays.length > 0) { showHolidayModal(dayData.holidays); }
};
});
const allCheckbox = document.querySelector('input[name="holidayTag"][value="all"]');
const otherCheckboxes = document.querySelectorAll('input[name="holidayTag"]:not([value="all"])');
const updateFilter = () => {
const checkedValues = Array.from(otherCheckboxes).filter(i => i.checked).map(i => i.value);
if (checkedValues.length === 0) { allCheckbox.checked = true; holidayFilter = null; }
else { allCheckbox.checked = false; holidayFilter = checkedValues; }
rerender();
};
if (allCheckbox) {
allCheckbox.onchange = () => {
if (allCheckbox.checked) { otherCheckboxes.forEach(cb => cb.checked = false); holidayFilter = null; rerender(); }
else { allCheckbox.checked = true; }
};
}
otherCheckboxes.forEach(checkbox => { if (checkbox) checkbox.onchange = updateFilter; });
}
function rerender() {
monthGridInstance = new MonthGrid({
year: currentYear,
month: currentMonth,
useGeez, weekdayLang, weekStart,
holidayFilter,
mode: activeMode,
showAllSaints: showAllSaints,
});
renderCalendar(monthGridInstance.generate());
renderFastingSidebar();
}
rerender();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kenat Full Year Calendar</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
text-align: center;
background-color: #f9f9f9;
padding: 1rem;
color: #333;
}
h1,
h2 {
color: #111;
}
.year-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
max-width: 1400px;
margin: 1rem auto;
}
.month-card {
border: 1px solid #e0e0e0;
border-radius: 10px;
padding: 1rem;
background-color: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.month-card h3 {
margin-top: 0;
font-size: 1rem;
}
table {
border-collapse: collapse;
width: 100%;
}
th,
td {
border: 1px solid #f0f0f0;
padding: 0.3rem;
height: 35px;
text-align: center;
font-size: 0.8rem;
}
th {
background-color: #f7f7f7;
font-weight: 600;
font-size: 0.75rem;
}
.today {
background-color: #fffde7;
border: 1px solid #ffc107;
font-weight: bold;
}
.holiday {
background-color: #e3f2fd;
cursor: pointer;
}
.holiday:hover {
background-color: #bbdefb;
}
.main-controls {
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
margin: 1.5rem 0;
align-items: center;
}
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
justify-content: center;
margin-bottom: 1.5rem;
background-color: #fff;
padding: 1rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.filter-controls label {
font-size: 0.9rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
}
button {
padding: 0.6rem 1.2rem;
font-size: 0.9rem;
cursor: pointer;
border-radius: 6px;
border: 1px solid #ccc;
background-color: #fff;
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
.modal-content {
background-color: #fefefe;
margin: auto;
padding: 25px;
border-radius: 10px;
width: 90%;
max-width: 500px;
text-align: left;
position: relative;
}
.modal-close {
color: #aaa;
position: absolute;
top: 10px;
right: 20px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
</style>
</head>
<body>
<div id="calendar-container"></div>
<div id="holidayModal" class="modal">
<div class="modal-content">
<span class="modal-close">&times;</span>
<div id="modalBody"></div>
</div>
</div>
<script type="module">
// FIX: Correctly import Kenat as the default export
import Kenat, { HolidayTags } from '../../src/index.js';
import { toGeez, toArabic } from '../../src/geezConverter.js';
let useGeez = false;
let weekdayLang = 'amharic';
let weekStart = 1;
let currentYear = new Kenat().getEthiopian().year;
let holidayFilter = null; // Start with no filter
const container = document.getElementById('calendar-container');
const modal = document.getElementById('holidayModal');
const modalBody = document.getElementById('modalBody');
const closeModal = document.querySelector('.modal-close');
function showHolidayModal(holidays) {
let content = '';
holidays.forEach((h, index) => {
content += `<h3>${h.name}</h3><p>${h.description}</p>`;
if (index < holidays.length - 1) content += '<hr>';
});
modalBody.innerHTML = content;
modal.style.display = 'flex';
}
closeModal.onclick = () => { modal.style.display = 'none'; };
window.onclick = (event) => { if (event.target == modal) modal.style.display = 'none'; };
function renderFullYearCalendar(year) {
currentYear = year;
// Pass the holidayFilter to getYearCalendar
const yearCalendar = Kenat.getYearCalendar(year, { useGeez, weekdayLang, weekStart, holidayFilter });
const displayYear = useGeez ? toGeez(year) : year;
let html = `<div style="text-align: center;">
<h2>📅 Ethiopian Calendar ${displayYear}</h2>
<div class="main-controls">
<button id="prevYear">⬅️ Prev Year</button>
<div style="display: flex; gap: 10px;">
<button id="toggleGeez">Geez (${useGeez ? 'ON' : 'OFF'})</button>
<button id="toggleLang">Lang (${weekdayLang})</button>
<button id="toggleWeekStart">Start (${weekStart === 1 ? 'Mon' : 'Sun'})</button>
</div>
<button id="nextYear">Next Year ➡️</button>
</div>
<div class="filter-controls" id="filterControls"></div>
</div>`;
html += `<div class="year-grid">`;
yearCalendar.forEach(month => {
const headers = (weekdayLang === 'english') ? month.headers.map(h => h.substring(0, 3)) : month.headers;
html += `
<div class="month-card">
<h3>${month.monthName}</h3>
<table>
<thead><tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr></thead>
<tbody>
`;
for (let i = 0; i < month.days.length; i += 7) {
html += '<tr>';
for (let j = 0; j < 7; j++) {
const item = month.days[i + j];
if (!item) {
html += '<td></td>';
} else {
const isToday = item.isToday ? 'today' : '';
const isHoliday = item.holidays.length > 0 ? 'holiday' : '';
const holidayAttr = isHoliday ? `data-holidays='${JSON.stringify(item.holidays)}'` : '';
html += `<td class="${isToday} ${isHoliday}" ${holidayAttr}><strong>${item.ethiopian.day}</strong></td>`;
}
}
html += '</tr>';
}
html += `</tbody></table></div>`;
});
html += `</div>`;
container.innerHTML = html;
renderFilterControls();
attachEventListeners();
}
function renderFilterControls() {
const controlsContainer = document.getElementById('filterControls');
let controlsHtml = `<label><input type="checkbox" name="holidayTag" value="all" ${!holidayFilter ? 'checked' : ''}> All</label>`;
for (const key in HolidayTags) {
const value = HolidayTags[key];
const isChecked = holidayFilter && holidayFilter.includes(value);
controlsHtml += `<label><input type="checkbox" name="holidayTag" value="${value}" ${isChecked ? 'checked' : ''}> ${value}</label>`;
}
controlsContainer.innerHTML = controlsHtml;
}
function attachEventListeners() {
document.getElementById('prevYear').onclick = () => rerender(currentYear - 1);
document.getElementById('nextYear').onclick = () => rerender(currentYear + 1);
document.getElementById('toggleGeez').onclick = () => { useGeez = !useGeez; rerender(); };
document.getElementById('toggleLang').onclick = () => { weekdayLang = weekdayLang === 'amharic' ? 'english' : 'amharic'; rerender(); };
document.getElementById('toggleWeekStart').onclick = () => { weekStart = weekStart === 1 ? 0 : 1; rerender(); };
document.querySelectorAll('.holiday').forEach(cell => {
cell.onclick = () => {
const holidays = JSON.parse(cell.getAttribute('data-holidays'));
showHolidayModal(holidays);
};
});
// Improved checkbox logic
const allCheckbox = document.querySelector('input[name="holidayTag"][value="all"]');
const otherCheckboxes = document.querySelectorAll('input[name="holidayTag"]:not([value="all"])');
const updateFilter = () => {
const checkedValues = Array.from(otherCheckboxes)
.filter(i => i.checked)
.map(i => i.value);
if (checkedValues.length === 0) {
allCheckbox.checked = true;
holidayFilter = null;
} else {
allCheckbox.checked = false;
holidayFilter = checkedValues;
}
rerender();
};
allCheckbox.onchange = () => {
if (allCheckbox.checked) {
otherCheckboxes.forEach(cb => cb.checked = false);
holidayFilter = null;
rerender();
} else {
allCheckbox.checked = true;
}
};
otherCheckboxes.forEach(checkbox => {
checkbox.onchange = updateFilter;
});
}
function rerender(year = currentYear) {
renderFullYearCalendar(year);
}
rerender();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kenat Calendar Example</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
text-align: center;
background-color: #f9f9f9;
padding: 1rem;
}
table {
border-collapse: collapse;
margin: 1rem auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
th,
td {
border: 1px solid #e0e0e0;
padding: 0.5rem;
width: 85px;
height: 85px;
vertical-align: top;
position: relative;
}
th {
background-color: #f5f5f5;
font-weight: 600;
}
.today {
background-color: #fffde7;
border: 2px solid #ffc107;
font-weight: bold;
}
td.event-day {
cursor: pointer;
}
.official-holiday {
background-color: #e3f2fd;
}
.christian-holiday {
background-color: #ebf5ff;
}
.jummah-day {
background-color: #e8f5e9;
}
.holiday-labels {
font-size: 0.75rem;
color: #333;
margin-top: 5px;
line-height: 1.2;
text-align: left;
max-height: 3.6em;
overflow: hidden;
}
.holiday-labels .is-nigs {
font-weight: bold;
color: #b58c00;
}
.holiday-labels .jummah-label {
font-weight: bold;
color: #388e3c;
}
.header-controls,
.controls,
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin: 1rem 0;
align-items: center;
}
.nav-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.controls {
gap: 20px;
}
.filter-controls {
margin-bottom: 1.5rem;
background-color: #fff;
padding: 1rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.filter-controls label,
.controls label {
font-size: 0.9rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
}
.filter-controls label.disabled {
color: #aaa;
cursor: not-allowed;
}
button,
select {
padding: 0.6rem 1.2rem;
font-size: 0.9rem;
cursor: pointer;
border-radius: 6px;
border: 1px solid #ccc;
background-color: #fff;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
.modal-content {
background-color: #fefefe;
margin: auto;
padding: 25px;
border: 1px solid #888;
width: 90%;
max-width: 500px;
border-radius: 10px;
position: relative;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
text-align: left;
}
.modal-close {
color: #aaa;
position: absolute;
top: 10px;
right: 20px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.modal-content h3 {
margin-top: 0;
}
.modal-content .is-nigs-title {
color: #b58c00;
}
.modal-content hr {
border: 0;
border-top: 1px solid #eee;
margin: 15px 0;
}
</style>
</head>
<body>
<h1>Kenat Calendar</h1>
<div id="calendar">Loading...</div>
<div id="holidayModal" class="modal">
<div class="modal-content"><span class="modal-close">&times;</span>
<div id="modalBody"></div>
</div>
</div>
<script type="module">
import Kenat, { MonthGrid, HolidayTags, toArabic } from '../../src/index.js';
import { monthNames } from '../../src/constants.js';
let monthGridInstance;
let useGeez = false;
let weekdayLang = 'amharic';
let weekStart = 1;
let holidayFilter = null;
let activeMode = 'public';
let showAllSaints = false;
const initialDate = new Kenat().getEthiopian();
let currentYear = initialDate.year;
let currentMonth = initialDate.month;
const modal = document.getElementById('holidayModal');
const modalBody = document.getElementById('modalBody');
const closeModal = document.querySelector('.modal-close');
function showHolidayModal(holidays) {
let content = '';
holidays.forEach((h, index) => {
const nigsClass = h.isNigs ? 'is-nigs-title' : '';
const description = h.description ? `<p>${h.description}</p>` : '';
content += `<h3 class="${nigsClass}">${h.name}</h3>${description}`;
if (index < holidays.length - 1) content += '<hr>';
});
modalBody.innerHTML = content;
modal.style.display = 'flex';
}
closeModal.onclick = () => { modal.style.display = 'none'; }
window.onclick = (event) => { if (event.target == modal) modal.style.display = 'none'; }
function renderCalendar(gridData) {
let { headers, days, year, month } = gridData;
const holidaysForDay = (item) => {
return item.holidays.map(h => {
let specialClass = '';
if (h.isNigs) specialClass = 'is-nigs';
if (h.key === 'jummah') specialClass = 'jummah-label';
return `<div class="${specialClass}">${h.name}</div>`;
}).join('');
};
const yearForComparison = typeof year === 'string' ? toArabic(year) : year;
const yearOptions = Array.from({ length: 201 }, (_, i) => 1900 + i).map(y => `<option value="${y}" ${y === yearForComparison ? 'selected' : ''}>${y}</option>`).join('');
const monthOptions = monthNames.amharic.map((name, i) => `<option value="${i + 1}" ${i + 1 === month ? 'selected' : ''}>${name}</option>`).join('');
let html = `<div class="header-controls"><button id="prevMonth">⬅️</button><div class="nav-controls"><select id="monthSelector">${monthOptions}</select><select id="yearSelector">${yearOptions}</select></div><button id="nextMonth">➡️</button></div><div class="controls" id="topControls"></div><div class="filter-controls" id="filterControls"></div><table><thead><tr>${headers.map(day => `<th>${day}</th>`).join('')}</tr></thead><tbody>`;
for (let i = 0; i < days.length; i += 7) {
html += '<tr>';
for (let j = 0; j < 7; j++) {
const item = days[i + j];
if (!item) {
html += '<td></td>';
} else {
const hasEvents = item.holidays && item.holidays.length > 0;
let dayClasses = [item.isToday ? 'today' : ''];
if (hasEvents) {
dayClasses.push('event-day');
const isOfficial = item.holidays.some(h => h.tags.includes(HolidayTags.PUBLIC) || h.tags.includes(HolidayTags.STATE));
const isChristian = item.holidays.some(h => h.tags.includes(HolidayTags.CHRISTIAN));
const isJummah = item.holidays.some(h => h.key === 'jummah');
if (isOfficial) dayClasses.push('official-holiday');
if (isChristian && !isOfficial) dayClasses.push('christian-holiday');
if (isJummah) dayClasses.push('jummah-day');
}
const holidaysText = hasEvents ? holidaysForDay(item) : '';
const eventAttr = hasEvents ? `data-day-index="${i + j}"` : '';
html += `<td class="${dayClasses.join(' ')}" ${eventAttr}><strong>${item.ethiopian.day}</strong><br/><small>${item.gregorian.month}/${item.gregorian.day}</small>${holidaysText ? `<div class="holiday-labels">${holidaysText}</div>` : ''}</td>`;
}
}
html += '</tr>';
}
html += '</tbody></table>';
document.getElementById('calendar').innerHTML = html;
renderControls();
attachEventListeners();
}
function renderControls() {
const topControlsContainer = document.getElementById('topControls');
const filterControlsContainer = document.getElementById('filterControls');
topControlsContainer.innerHTML = `<label for="modeSelector">Mode:</label><select id="modeSelector"><option value="none" ${activeMode === 'none' ? 'selected' : ''}>All</option><option value="christian" ${activeMode === 'christian' ? 'selected' : ''}>Christian</option><option value="muslim" ${activeMode === 'muslim' ? 'selected' : ''}>Muslim</option></select><button id="toggleGeez">Geez (${useGeez ? 'ON' : 'OFF'})</button><button id="toggleLang">Lang (${weekdayLang})</button><button id="toggleWeekStart">Start (${weekStart === 1 ? 'Mon' : 'Sun'})</button>`;
let filterHtml = '';
if (activeMode === 'christian') {
filterHtml += `<label><input type="checkbox" id="showAllSaintsToggle" ${showAllSaints ? 'checked' : ''}> Show All Saints</label>`;
}
const isDisabled = activeMode !== 'none';
const disabledAttr = isDisabled ? 'disabled' : '';
filterHtml += `<label class="${isDisabled ? 'disabled' : ''}"><input type="checkbox" name="holidayTag" value="all" ${!holidayFilter || isDisabled ? 'checked' : ''} ${disabledAttr}> All Holidays</label>`;
for (const key in HolidayTags) {
const value = HolidayTags[key];
const isChecked = !isDisabled && holidayFilter && holidayFilter.includes(value);
filterHtml += `<label class="${isDisabled ? 'disabled' : ''}"><input type="checkbox" name="holidayTag" value="${value}" ${isChecked ? 'checked' : ''} ${disabledAttr}> ${value}</label>`;
}
filterControlsContainer.innerHTML = filterHtml;
}
function attachEventListeners() {
document.getElementById('prevMonth').onclick = () => {
const newGrid = monthGridInstance.down().generate();
currentYear = (typeof newGrid.year === 'string') ? toArabic(newGrid.year) : newGrid.year;
currentMonth = newGrid.month;
renderCalendar(newGrid);
};
document.getElementById('nextMonth').onclick = () => {
const newGrid = monthGridInstance.up().generate();
currentYear = (typeof newGrid.year === 'string') ? toArabic(newGrid.year) : newGrid.year;
currentMonth = newGrid.month;
renderCalendar(newGrid);
};
document.getElementById('monthSelector').onchange = (e) => { currentMonth = parseInt(e.target.value); rerender(); };
document.getElementById('yearSelector').onchange = (e) => { currentYear = parseInt(e.target.value); rerender(); };
document.getElementById('toggleGeez').onclick = () => { useGeez = !useGeez; rerender(); };
document.getElementById('toggleLang').onclick = () => { weekdayLang = weekdayLang === 'amharic' ? 'english' : 'amharic'; rerender(); };
document.getElementById('toggleWeekStart').onclick = () => { weekStart = weekStart === 1 ? 0 : 1; rerender(); };
document.getElementById('modeSelector').onchange = (e) => {
activeMode = e.target.value;
if (activeMode !== 'none') {
holidayFilter = null;
}
showAllSaints = false;
rerender();
};
const saintsToggle = document.getElementById('showAllSaintsToggle');
if (saintsToggle) {
saintsToggle.onchange = (e) => {
showAllSaints = e.target.checked;
rerender();
};
}
document.querySelectorAll('.event-day').forEach(cell => {
cell.onclick = () => {
const dayIndex = parseInt(cell.getAttribute('data-day-index'));
const dayData = monthGridInstance.generate().days[dayIndex];
if (dayData && dayData.holidays.length > 0) {
showHolidayModal(dayData.holidays);
}
};
});
const allCheckbox = document.querySelector('input[name="holidayTag"][value="all"]');
const otherCheckboxes = document.querySelectorAll('input[name="holidayTag"]:not([value="all"])');
const updateFilter = () => {
const checkedValues = Array.from(otherCheckboxes).filter(i => i.checked).map(i => i.value);
if (checkedValues.length === 0) {
allCheckbox.checked = true;
holidayFilter = null;
} else {
allCheckbox.checked = false;
holidayFilter = checkedValues;
}
rerender();
};
if (allCheckbox) {
allCheckbox.onchange = () => {
if (allCheckbox.checked) {
otherCheckboxes.forEach(cb => cb.checked = false);
holidayFilter = null;
rerender();
} else {
allCheckbox.checked = true;
}
};
}
otherCheckboxes.forEach(checkbox => {
if (checkbox) checkbox.onchange = updateFilter;
});
}
function rerender() {
monthGridInstance = new MonthGrid({
year: currentYear,
month: currentMonth,
useGeez, weekdayLang, weekStart,
holidayFilter,
mode: activeMode,
showAllSaints: showAllSaints,
});
renderCalendar(monthGridInstance.generate());
}
rerender();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kenat Calendar Example</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 font-sans text-center p-4">
<h1 class="text-3xl font-bold mb-6">Kenat Calendar</h1>
<div class="flex flex-wrap gap-4 items-center justify-center mb-4">
<label class="flex items-center space-x-2">
<span>Mode:</span>
<select id="modeSelector" class="px-2 py-1 border rounded">
<option value="none">All</option>
<option value="christian">Christian</option>
<option value="muslim">Muslim</option>
<option value="public" selected>Public</option>
</select>
</label>
<label class="flex items-center space-x-2" id="saintToggleWrapper" style="display: none">
<input type="checkbox" id="showAllSaintsToggle" class="accent-blue-500">
<span>Show All Saints</span>
</label>
</div>
<div id="calendar" class="mb-4">Loading...</div>
<div id="filterControls"
class="flex flex-wrap gap-4 items-center justify-center p-4 bg-white rounded-lg shadow mt-4">
<label class="flex items-center space-x-2">
<input type="checkbox" name="holidayTag" value="all" checked class="accent-blue-500">
<span>All Holidays</span>
</label>
<label class="flex items-center space-x-2">
<input type="checkbox" name="holidayTag" value="public" class="accent-blue-500">
<span>Public</span>
</label>
<label class="flex items-center space-x-2">
<input type="checkbox" name="holidayTag" value="christian" class="accent-blue-500">
<span>Christian</span>
</label>
<label class="flex items-center space-x-2">
<input type="checkbox" name="holidayTag" value="muslim" class="accent-blue-500">
<span>Muslim</span>
</label>
</div>
<div id="holidayModal" class="fixed hidden inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
<div class="bg-white p-6 rounded-lg max-w-lg w-full relative shadow-lg">
<button class="absolute top-2 right-4 text-gray-600 text-xl font-bold" id="closeModal">&times;</button>
<div id="modalBody" class="text-left"></div>
</div>
</div>
<script type="module">
import Kenat, { MonthGrid, HolidayTags, toArabic } from '../../../src/index.js';
import { monthNames } from '../../../src/constants.js';
let monthGridInstance;
let useGeez = false;
let weekdayLang = 'amharic';
let weekStart = 1;
let holidayFilter = null;
let activeMode = 'public';
let showAllSaints = false;
const initialDate = new Kenat().getEthiopian();
let currentYear = initialDate.year;
let currentMonth = initialDate.month;
const modal = document.getElementById('holidayModal');
const modalBody = document.getElementById('modalBody');
const closeModal = document.getElementById('closeModal');
const saintToggleWrapper = document.getElementById('saintToggleWrapper');
const saintToggle = document.getElementById('showAllSaintsToggle');
function showHolidayModal(holidays) {
modalBody.innerHTML = holidays.map((h, i) => {
return `<h3 class="text-lg font-bold mb-1 ${h.isNigs ? 'text-yellow-700' : ''}">${h.name}</h3><p class="text-sm text-gray-600 mb-2">${h.description || ''}</p>${i < holidays.length - 1 ? '<hr class="my-2">' : ''}`;
}).join('');
modal.classList.remove('hidden');
}
closeModal.onclick = () => modal.classList.add('hidden');
window.onclick = e => { if (e.target === modal) modal.classList.add('hidden'); };
function renderCalendar(gridData) {
const { headers, days, year, month } = gridData;
const yearOptions = Array.from({ length: 201 }, (_, i) => 1900 + i).map(y => `<option value="${y}" ${y === +year ? 'selected' : ''}>${y}</option>`).join('');
const monthOptions = monthNames.amharic.map((name, i) => `<option value="${i + 1}" ${i + 1 === month ? 'selected' : ''}>${name}</option>`).join('');
let html = `
<div class="flex justify-center items-center gap-4 mb-4">
<button id="prevMonth" class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300">⬅️</button>
<select id="monthSelector" class="px-2 py-1 border rounded">${monthOptions}</select>
<select id="yearSelector" class="px-2 py-1 border rounded">${yearOptions}</select>
<button id="nextMonth" class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300">➡️</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full bg-white rounded shadow">
<thead class="bg-gray-100 text-gray-700">
<tr>${headers.map(d => `<th class="py-2">${d}</th>`).join('')}</tr>
</thead>
<tbody>
`;
for (let i = 0; i < days.length; i += 7) {
html += '<tr>';
for (let j = 0; j < 7; j++) {
const item = days[i + j];
if (!item) {
html += '<td class="border h-20 w-20"></td>';
} else {
const holidayNames = item.holidays.map(h => `<div class="text-xs ${h.isNigs ? 'text-yellow-800 font-bold' : ''}">${h.name}</div>`).join('');
const classes = [
'border p-1 align-top h-20 w-20 text-xs',
item.isToday ? 'bg-yellow-100 border-yellow-500' : '',
item.holidays.length ? 'cursor-pointer bg-blue-50 hover:bg-blue-100' : ''
].join(' ');
html += `<td class="${classes}" data-day-index="${i + j}"><div class="font-semibold">${item.ethiopian.day}</div><div class="text-gray-500">${item.gregorian.month}/${item.gregorian.day}</div>${holidayNames}</td>`;
}
}
html += '</tr>';
}
html += '</tbody></table></div>';
document.getElementById('calendar').innerHTML = html;
attachListeners();
}
function attachListeners() {
document.getElementById('prevMonth').onclick = () => {
const newGrid = monthGridInstance.down().generate();
currentYear = +newGrid.year;
currentMonth = newGrid.month;
renderCalendar(newGrid);
};
document.getElementById('nextMonth').onclick = () => {
const newGrid = monthGridInstance.up().generate();
currentYear = +newGrid.year;
currentMonth = newGrid.month;
renderCalendar(newGrid);
};
document.getElementById('monthSelector').onchange = (e) => {
currentMonth = +e.target.value;
rerender();
};
document.getElementById('yearSelector').onchange = (e) => {
currentYear = +e.target.value;
rerender();
};
document.getElementById('modeSelector').onchange = (e) => {
activeMode = e.target.value;
showAllSaints = false;
saintToggle.checked = false;
saintToggleWrapper.style.display = activeMode === 'christian' ? 'flex' : 'none';
rerender();
};
saintToggle.onchange = (e) => {
showAllSaints = e.target.checked;
rerender();
};
document.querySelectorAll('[data-day-index]').forEach(td => {
td.onclick = () => {
const index = +td.dataset.dayIndex;
const dayData = monthGridInstance.generate().days[index];
if (dayData.holidays.length) showHolidayModal(dayData.holidays);
};
});
const allCheckbox = document.querySelector('input[name="holidayTag"][value="all"]');
const otherCheckboxes = document.querySelectorAll('input[name="holidayTag"]:not([value="all"])');
function updateFilter() {
const checked = Array.from(otherCheckboxes).filter(cb => cb.checked).map(cb => cb.value);
holidayFilter = checked.length ? checked : null;
allCheckbox.checked = !holidayFilter;
rerender();
}
allCheckbox.onchange = () => {
if (allCheckbox.checked) {
otherCheckboxes.forEach(cb => cb.checked = false);
holidayFilter = null;
rerender();
}
};
otherCheckboxes.forEach(cb => cb.onchange = updateFilter);
}
function rerender() {
monthGridInstance = new MonthGrid({
year: currentYear,
month: currentMonth,
useGeez, weekdayLang, weekStart,
holidayFilter,
mode: activeMode,
showAllSaints
});
renderCalendar(monthGridInstance.generate());
}
rerender();
</script>
</body>
</html>
## add nextMonth, previousMonth, nextYear, previousYear methods for calendar navigation
## 🔹 Core Features to Add
These are essential for most calendar applications.
1. **Conversion Enhancements**
* [ ] Support conversion both ways: `toEC()` and `toGC()` renate this to better dev friendly names like toGregorian() and toEthiopian(). TODO: rename these methods.
* [ ] Accept JavaScript `Date` object directly (and return one too).
* [ ] Add parsing and formatting helpers for ISO-8601 (`YYYY-MM-DD`).
2. **Date Arithmetic**
* [ ] Add/subtract days, months, years on Ethiopian dates. Add already added, work on subtracting.
* [x] Get difference between two Ethiopian dates in days/months/years.
3. **Validation**
* [ ] Validate Ethiopian dates (e.g. Pagume has 5 or 6 days only).
* [ ] Throw helpful errors for invalid dates.
4. **Leap Year Helpers**
* [x] `.isLeapYear()` method for both Ethiopian and Gregorian dates.
* [ ] `.daysInMonth()` method for any month/year combo.
---
## 🔹 Display & Formatting Features
5. **Localized Formatting**
* [ ] Support `format()` for multiple languages: `amharic`, `english`, `oromo`, etc.
* [ ] Add options for different formats: long (e.g. “15 Meskerem 2017”), short (e.g. “15/01/2017”), etc.
6. **Geez Numerals Everywhere**
* [x] Add option to display full date in Geez: "መስከረም ፲፭ ፳፻፲፯" and also time in Geez (if relevant).
7. **Pretty Today**
* [x] `Kenat.today()` returns a `Kenat` for current date.
* [ ] `.isToday()` to check if the stored Ethiopian date is today.
---
## 🔹 Advanced Calendar Features
8. **Weekday Support**
* [ ] `.getWeekday()` – returns day of the week in Amharic or English.
* [ ] Support for calculating holidays based on weekdays (e.g. Meskel always falls on Wednesday one week after finding the true cross).
9. **Holiday Support**
* [x] Built-in support for major Ethiopian holidays (Fasika, Meskel, Timket, Enkutatash, etc.).
* [ ] Ability to list holidays in a given Ethiopian year. / added a method to list in month will list the year too.
10. **Week Numbers**
* [ ] `.getWeekNumber()` for Ethiopian calendar (ISO-style).
---
## 🔹 Utility / Developer-Friendly Features
11. **Static Utilities**
* [ ] `Kenat.isValidEthiopianDate(y, m, d)`
* [ ] `Kenat.parse(string)` to convert from formatted string.
12. **CLI Tool (Optional)**
* [ ] CLI tool to convert and format dates (`kenat convert 2017/01/15 --to=gregorian`).
13. **Calendar View Generator**
* [x] Function to return an array of days for a given month (e.g., for building UIs).
* [x] Optional metadata (weekday, holiday, isToday, etc.).
---
## Bonus / Fun Features
14. **Date Range Generator**
* [ ] Generate all dates between two Ethiopian dates.
15. **Countdown to Next Holiday**
* [ ] `.daysUntil('meskel')` or `.daysUntilNextHoliday()`
16. **Ethiopian Time Support**
* [x] Format times in Ethiopian 12-hour system (e.g., “3:00 in the morning” = 9:00 AM Gregorian)
export default {
testEnvironment: "node",
transform: {},
};
{
"source": {
"include": ["src", "README.md"],
"includePattern": ".js$",
"excludePattern": "(node_modules|docs)"
},
"opts": {
"destination": "./docs",
"recurse": true,
"readme": "README.md"
}
}
import { validateNumericInputs, getWeekday } from './utils.js';
import { addDays } from './dayArithmetic.js';
import { toGC } from './conversions.js';
import { UnknownHolidayError } from './errors/errorHandler.js';
import {
daysOfWeek,
evangelistNames,
tewsakMap,
movableHolidayTewsak,
keyToTewsakMap,
holidayInfo,
movableHolidays
} from './constants.js';
/**
* Calculates all Bahire Hasab values for a given Ethiopian year, including all movable feasts.
*
* @param {number} ethiopianYear - The Ethiopian year to calculate for.
* @param {Object} [options={}] - Options for language.
* @param {string} [options.lang='amharic'] - The language for names.
* @returns {Object} An object containing all the calculated Bahire Hasab values.
*/
export function getBahireHasab(ethiopianYear, options = {}) {
validateNumericInputs('getBahireHasab', { ethiopianYear });
const { lang = 'amharic' } = options;
const base = _calculateBahireHasabBase(ethiopianYear);
const evangelistRemainder = base.ameteAlem % 4;
const evangelistName = evangelistNames[lang]?.[evangelistRemainder] || evangelistNames.english[evangelistRemainder];
const tinteQemer = (base.ameteAlem + base.meteneRabiet) % 7;
const weekdayIndex = (tinteQemer + 1) % 7;
const newYearWeekday = daysOfWeek[lang]?.[weekdayIndex] || daysOfWeek.english[weekdayIndex];
const movableFeasts = {};
const tewsakToKeyMap = Object.entries(keyToTewsakMap).reduce((acc, [key, val]) => {
acc[val] = key; return acc;
}, {});
Object.keys(movableHolidayTewsak).forEach(tewsakKey => {
const holidayKey = tewsakToKeyMap[tewsakKey];
if (holidayKey) {
const date = addDays(base.ninevehDate, movableHolidayTewsak[tewsakKey]);
const info = holidayInfo[holidayKey];
const rules = movableHolidays[holidayKey];
movableFeasts[holidayKey] = {
key: holidayKey,
tags: rules.tags,
movable: true,
name: info?.name?.[lang] || info?.name?.english,
description: info?.description?.[lang] || info?.description?.english,
ethiopian: date,
gregorian: toGC(date.year, date.month, date.day)
};
}
});
return {
ameteAlem: base.ameteAlem,
meteneRabiet: base.meteneRabiet,
evangelist: { name: evangelistName, remainder: evangelistRemainder },
newYear: { dayName: newYearWeekday, tinteQemer: tinteQemer },
medeb: base.medeb,
wenber: base.wenber,
abektie: base.abektie,
metqi: base.metqi,
bealeMetqi: { date: base.bealeMetqiDate, weekday: base.bealeMetqiWeekday },
mebajaHamer: base.mebajaHamer,
nineveh: base.ninevehDate,
movableFeasts
};
}
/**
* Calculates the date of a movable holiday for a given year.
* This is now a pure date calculator that returns a simple date object,
* ensuring backward compatibility with existing tests.
*
* @param {'ABIY_TSOME'|'TINSAYE'|'ERGET'|...} holidayKey - The key of the holiday from movableHolidayTewsak.
* @param {number} ethiopianYear - The Ethiopian year.
* @returns {Object} An Ethiopian date object { year, month, day }.
*/
export function getMovableHoliday(holidayKey, ethiopianYear) {
validateNumericInputs('getMovableHoliday', { ethiopianYear });
const tewsak = movableHolidayTewsak[holidayKey];
if (tewsak === undefined) {
throw new UnknownHolidayError(holidayKey);
}
const { ninevehDate } = _calculateBahireHasabBase(ethiopianYear);
return addDays(ninevehDate, tewsak);
}
/**
* Calculates and returns all base values for the Bahire Hasab system for a given Ethiopian year.
* This helper is the single source of truth for the core computational logic.
*
* @param {number} ethiopianYear - The Ethiopian year for which to perform the calculations.
* @returns {{
* ameteAlem: number,
* meteneRabiet: number,
* medeb: number,
* wenber: number,
* abektie: number,
* metqi: number,
* bealeMetqiDate: { year: number, month: number, day: number },
* bealeMetqiWeekday: string,
* mebajaHamer: number,
* ninevehDate: { year: number, month: number, day: number }
* }} An object containing all core calculated values.
*/
function _calculateBahireHasabBase(ethiopianYear) {
const ameteAlem = 5500 + ethiopianYear;
const meteneRabiet = Math.floor(ameteAlem / 4);
const medeb = ameteAlem % 19;
const wenber = medeb === 0 ? 18 : medeb - 1;
const abektie = (wenber * 11) % 30;
const metqi = (wenber * 19) % 30;
const bealeMetqiMonth = metqi > 14 ? 1 : 2;
const bealeMetqiDay = metqi;
const bealeMetqiDate = { year: ethiopianYear, month: bealeMetqiMonth, day: bealeMetqiDay };
const bealeMetqiWeekday = daysOfWeek.english[getWeekday(bealeMetqiDate)];
const tewsak = tewsakMap[bealeMetqiWeekday];
const mebajaHamerSum = bealeMetqiDay + tewsak;
const mebajaHamer = mebajaHamerSum > 30 ? mebajaHamerSum % 30 : mebajaHamerSum;
let ninevehMonth = metqi > 14 ? 5 : 6;
if (mebajaHamerSum > 30) ninevehMonth++;
const ninevehDate = { year: ethiopianYear, month: ninevehMonth, day: mebajaHamer };
return {
ameteAlem,
meteneRabiet,
medeb,
wenber,
abektie,
metqi,
bealeMetqiDate,
bealeMetqiWeekday,
mebajaHamer,
ninevehDate,
};
}
export const monthNames = {
english: [
"Meskerem",
"Tikimt",
"Hidar",
"Tahsas",
"Tir",
"Yekatit",
"Megabit",
"Miazia",
"Ginbot",
"Sene",
"Hamle",
"Nehase",
"Pagume",
],
amharic: [
"መስከረም",
"ጥቅምት",
"ህዳር",
"ታህሳስ",
"ጥር",
"የካቲት",
"መጋቢት",
"ሚያዝያ",
"ግንቦት",
"ሰኔ",
"ሀምሌ",
"ነሐሴ",
"ጳጉሜ",
],
gregorian: [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
],
};
export const daysOfWeek = {
english: [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
],
amharic: ["እሑድ", "ሰኞ", "ማክሰኞ", "ረቡዕ", "ሐሙስ", "ዓርብ", "ቅዳሜ"],
};
export const HolidayTags = {
PUBLIC: "public",
RELIGIOUS: "religious",
CHRISTIAN: "christian",
MUSLIM: "muslim",
STATE: "state",
CULTURAL: "cultural",
OTHER: "other",
};
export const keyToTewsakMap = {
nineveh: "NINEVEH",
abiyTsome: "ABIY_TSOME",
debreZeit: "DEBRE_ZEIT",
hosanna: "HOSANNA",
siklet: "SIKLET",
fasika: "TINSAYE",
rikbeKahnat: "RIKBE_KAHNAT",
erget: "ERGET",
paraclete: "PARACLETE",
tsomeHawaryat: "TSOME_HAWARYAT",
tsomeDihnet: "TSOME_DIHENET",
};
export const PERIOD_LABELS = {
day: "ጠዋት",
night: "ማታ",
};
// Renamed from holidayNames and restructured for i18n
export const holidayInfo = {
enkutatash: {
name: { amharic: "እንቁጣጣሽ", english: "Ethiopian New Year (Enkutatash)" },
description: {
amharic: "የኢትዮጵያ አዲስ ዓመት መጀመሪያ፤ የዝናብ ወቅት ማብቃቱን እና ዳግም መታደስን ያመለክታል።",
english:
"Marks the start of the Ethiopian year; symbolizes renewal and the end of the rainy season.",
},
},
meskel: {
name: { amharic: "መስቀል", english: "Finding of the True Cross (Meskel)" },
description: {
amharic: "በ4ኛው መቶ ክፍለ ዘመን በንግሥት እሌኒ አማካኝነት የጌታችን መስቀል መገኘቱን ያከብራል።",
english:
"Commemorates the discovery of the True Cross by Empress Helena in the 4th century.",
},
},
beherbehereseb: {
name: {
amharic: "የብሔር ብሔረሰቦች ቀን",
english: "Nations, Nationalities, and Peoples' Day",
},
description: {
amharic:
"የኢትዮጵያ ብሔር ብሔረሰቦችን ልዩነት የሚያከብር፣ እኩል መብታቸውን የሚያረጋግጥ እና በባህልና ቋንቋ አንድነትን የሚያጠናክር በዓል ነው።",
english:
"Acknowledges and celebrates the diversity of Ethiopia's ethnic groups, affirming their equal rights and fostering unity.",
},
},
gena: {
name: { amharic: "ገና", english: "Ethiopian Christmas (Genna)" },
description: {
amharic: "የኢየሱስ ክርስቶስን ልደት የሚያከብር የኢትዮጵያ ኦርቶዶክስ ተዋሕዶ ቤተ ክርስቲያን በዓል።",
english:
"Ethiopian Orthodox Christmas celebrating the birth of Jesus Christ.",
},
},
timket: {
name: { amharic: "ጥምቀት", english: "Ethiopian Epiphany (Timket)" },
description: {
amharic: "የኢየሱስ ክርስቶስን በዮርዳኖስ ወንዝ መጠመቁን ያከብራል።",
english: "Commemorates the baptism of Jesus in the Jordan River.",
},
},
martyrsDay: {
name: { amharic: "የሰማዕታት ቀን", english: "Martyrs' Day" },
description: {
amharic: "ለኢትዮጵያ ነፃነትና ክብር ሕይወታቸውን የሠዉ ሰማዕታትን ያስባል።",
english:
"Honors those who sacrificed their lives for Ethiopia’s freedom and independence.",
},
},
adwa: {
name: { amharic: "የአድዋ ድል በዓል", english: "Victory of Adwa" },
description: {
amharic: "በ1896 ዓ.ም. ኢትዮጵያ በጣሊያን ቅኝ ገዥዎች ላይ የተቀዳጀችውን ድል ያከብራል።",
english: "Celebrates Ethiopia’s victory over Italian colonizers in 1896.",
},
},
labour: {
name: { amharic: "የሰራተኞች ቀን", english: "International Labour Day" },
description: {
amharic: "ዓለም አቀፍ የሠራተኞችና የሥራ መብቶች ቀን ነው።",
english: "A global celebration of workers and labor rights.",
},
},
patriots: {
name: { amharic: "የአርበኞች ቀን", english: "Patriots' Victory Day" },
description: {
amharic: "የጣሊያን ወረራን የተቋቋሙ ኢትዮጵያውያን አርበኞችን ድል ያስባል።",
english:
"Honors Ethiopian resistance fighters who defeated Italian occupation.",
},
},
nineveh: {
name: { amharic: "ጾመ ነነዌ", english: "Fast of Nineveh" },
description: {
amharic: "የነነዌ ሰዎች ንስሐ መግባታቸውን የሚያስታውስ የሦስት ቀን ጾም ነው።",
english:
"A three-day fast commemorating the repentance of the people of Nineveh.",
},
},
abiyTsome: {
name: { amharic: "ዐቢይ ጾም", english: "Great Lent" },
description: {
amharic: "ከፋሲካ በፊት የሚጾም የ55 ቀናት የጾም ወቅት ነው።",
english: "The Great Lent, a 55-day fasting period before Easter.",
},
},
debreZeit: {
name: { amharic: "ደብረ ዘይት", english: "Mid-Lent Sunday" },
description: {
amharic: "ኢየሱስ በደብረ ዘይት ተራራ ያስተማረውን ትምህርት የሚያስታውስ የዐቢይ ጾም አጋማሽ እሑድ።",
english:
"Mid-Lent Sunday, commemorating Jesus's sermon on the Mount of Olives.",
},
},
hosanna: {
name: { amharic: "ሆሳዕና", english: "Palm Sunday" },
description: {
amharic: "ኢየሱስ በክብር ወደ ኢየሩሳሌም መግባቱን የሚያስታውስ በዓል።",
english:
"Palm Sunday, commemorating Jesus's triumphal entry into Jerusalem.",
},
},
siklet: {
name: { amharic: "ስቅለት", english: "Good Friday" },
description: {
amharic: "የኢየሱስ ክርስቶስን ስቅለት የሚያስታውስ ነው።",
english: "Marks the crucifixion of Jesus Christ.",
},
},
fasika: {
name: { amharic: "ፋሲካ", english: "Ethiopian Easter" },
description: {
amharic:
"የኢየሱስ ክርስቶስን ከሙታን መነሣት ያከብራል። በኢትዮጵያ ውስጥ ካሉ ክርስቲያናዊ በዓላት አንዱና ዋነኛው ነው።",
english:
"Celebrates the resurrection of Jesus Christ. One of the most important Christian holidays in Ethiopia.",
},
},
rikbeKahnat: {
name: { amharic: "ርክበ ካህናት", english: "Meeting of the Priests" },
description: {
amharic: "ከፋሲካ 24 ቀናት በኋላ የሚከበር የካህናት መሰባሰብ በዓል ነው።",
english: "The Meeting of the Priests, 24 days after Easter.",
},
},
erget: {
name: { amharic: "ዕርገት", english: "Ascension" },
description: {
amharic: "ከፋሲካ 40 ቀናት በኋላ ኢየሱስ ወደ ሰማይ ማረጉን ያከብራል።",
english: "The Ascension of Jesus into heaven, 40 days after Easter.",
},
},
paraclete: {
name: { amharic: "ጰራቅሊጦስ", english: "Pentecost" },
description: {
amharic: "መንፈስ ቅዱስ በሐዋርያት ላይ መውረዱን የሚያከብር በዓል፣ ከፋሲካ 50 ቀናት በኋላ።",
english:
"Pentecost, celebrating the descent of the Holy Spirit upon the Apostles, 50 days after Easter.",
},
},
tsomeHawaryat: {
name: { amharic: "ጾመ ሐዋርያት", english: "Apostles' Fast" },
description: {
amharic: "ከጰራቅሊጦስ ማግስት የሚጀምር የሐዋርያት ጾም ነው።",
english:
"The Fast of the Apostles, which begins the day after Pentecost.",
},
},
tsomeDihnet: {
name: { amharic: "ጾመ ድኅነት", english: "Fast of Salvation" },
description: {
amharic: "በየሳምንቱ ረቡዕ እና ዓርብ የሚጾም የድኅነት ጾም ነው።",
english: "The Fast of Salvation, observed on Wednesdays and Fridays.",
},
},
eidFitr: {
name: { amharic: "ዒድ አል ፈጥር", english: "Eid al-Fitr" },
description: {
amharic: "የረመዳን ጾም ወር መገባደድን የሚያመለክት በዓል።",
english: "Marks the end of Ramadan, the month of fasting for Muslims.",
},
},
eidAdha: {
name: { amharic: "ዒድ አል አድሐ", english: "Eid al-Adha" },
description: {
amharic: "አብርሃም ለእግዚአብሔር በመታዘዝ ልጁን ለመሠዋት ፈቃደኝነቱን የሚያስታውስ በዓል።",
english:
"Commemorates Abraham’s willingness to sacrifice his son as an act of obedience to God.",
},
},
moulid: {
name: { amharic: "መውሊድ", english: "Birth of the Prophet" },
description: {
amharic: "የነቢዩ ሙሐመድን የልደት በዓል ያከብራል።",
english: "Celebrates the birthday of the Prophet Mohammed.",
},
},
jummah: {
name: { amharic: "ጁምዓ", english: "Jummah" },
description: {
amharic: "ሳምንታዊ የሙስሊሞች የጋራ ስግደት።",
english: "The weekly Muslim congregational prayer."
}
}
};
// Stable keys for holidays to avoid hardcoded strings in user code
export const HolidayNames = Object.freeze(
Object.keys(holidayInfo).reduce((acc, key) => {
acc[key] = key;
return acc;
}, {})
);
// Bahire Hasab related constants
export const evangelistNames = {
english: { 1: "Matthew", 2: "Mark", 3: "Luke", 0: "John" },
amharic: { 1: "ማቴዎስ", 2: "ማርቆስ", 3: "ሉቃስ", 0: "ዮሐንስ" },
};
export const tewsakMap = {
Sunday: 7,
Monday: 6,
Tuesday: 5,
Wednesday: 4,
Thursday: 3,
Friday: 2,
Saturday: 8,
};
export const movableHolidayTewsak = {
NINEVEH: 0,
ABIY_TSOME: 14,
DEBRE_ZEIT: 41,
HOSANNA: 62,
SIKLET: 67,
TINSAYE: 69,
RIKBE_KAHNAT: 93,
ERGET: 108,
PARACLETE: 118,
TSOME_HAWARYAT: 119,
TSOME_DIHENET: 121,
};
export const movableHolidays = {
nineveh: { tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN] },
abiyTsome: { tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN] },
debreZeit: { tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN] },
hosanna: { tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN] },
siklet: { tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN, HolidayTags.PUBLIC] },
fasika: {
tags: [HolidayTags.PUBLIC, HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN],
},
rikbeKahnat: { tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN] },
erget: { tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN] },
paraclete: { tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN] },
tsomeHawaryat: { tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN] },
tsomeDihnet: { tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN] },
eidFitr: {
tags: [HolidayTags.PUBLIC, HolidayTags.RELIGIOUS, HolidayTags.MUSLIM],
},
eidAdha: {
tags: [HolidayTags.PUBLIC, HolidayTags.RELIGIOUS, HolidayTags.MUSLIM],
},
moulid: {
tags: [HolidayTags.PUBLIC, HolidayTags.RELIGIOUS, HolidayTags.MUSLIM],
},
};
// Canonical fasting keys and list to avoid hardcoded strings
export const FastingKeys = {
ABIY_TSOME: 'ABIY_TSOME',
TSOME_HAWARYAT: 'TSOME_HAWARYAT',
TSOME_NEBIYAT: 'TSOME_NEBIYAT',
NINEVEH: 'NINEVEH',
FILSETA: 'FILSETA',
RAMADAN: 'RAMADAN',
TSOME_DIHENET: 'TSOME_DIHENET',
};
// Fasting metadata similar to holidayInfo, with i18n-friendly names and descriptions
export const fastingInfo = {
ABIY_TSOME: {
name: { amharic: "ዐቢይ ጾም (ሁዳዴ)", english: "Great Lent (Abiy Tsome)" },
description: {
amharic: "ከፋሲካ በፊት የሚጾም 55 ቀናት የጾም ወቅት ነው።",
english: "A 55-day fasting season observed before Easter in the Ethiopian Orthodox Church.",
},
tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN],
},
NINEVEH: {
name: { amharic: "ጾመ ነነዌ", english: "Fast of Nineveh" },
description: {
amharic: "የነነዌ ሰዎች ለሰሩት ሐጢያት የእግዚአብሔርን ምህረትና ይቅርታ ለመጠየቅ የፆሙትን ለማስታወስ ነው።",
english: "A three-day fast commemorating the repentance of the people of Nineveh.",
},
tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN],
},
TSOME_HAWARYAT: {
name: { amharic: "ጾመ ሐዋርያት", english: "Apostles' Fast (Tsome Hawaryat)" },
description: {
amharic: "ክርስቶስ ከሙታን ተለይቶ ከተነሳ በሃላ ቅዱሳን ሀዋርያት ሰማያዊ ተልእኮ ስለተሰማቸው ከ50ቀን በሃላ በበአለ ጰራቅሊጦስ ማግስት መፆም ጀመሩ።",
english: "Begins after Pentecost and lasts until Hamle 4 in the Ethiopian calendar.",
},
tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN],
},
TSOME_NEBIYAT: {
name: { amharic: "የገና-ፆም (ጾመ ነቢያት)", english: "Fast of the Prophets (Tsome Nebiyat)" },
description: {
amharic: "ከህዳር 15 እስከ ታሕሳስ 28 ድረስ የሚጠብቅ የቋሚ ጾም ነው።",
english: "A fixed fast observed from Hidar 15 to Tahsas 28.",
},
tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN],
},
FILSETA: {
name: { amharic: "ፍልሰታ", english: "Filseta" },
description: {
amharic: "ፆመ ፍልሰታ /ከነሐሴ 1 እስከ ነሐሴ 14 ቀን/ የእመቤታችን ዕረፍት፣ ትንሣኤና ዕርገቷ የሚታሰብበት ወቅት ነው፡፡",
english: "A fixed 14-day fast (Nehase 1–14) commemorating the Dormition, Resurrection, and Assumption of the Virgin Mary; a season of prayer and intercession.",
},
tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN],
},
RAMADAN: {
name: { amharic: "ረመዳን", english: "Ramadan" },
description: {
amharic: "ረመዳን በሂጅራ አቆጣጠር ዘጠነኛው ወር እና በሙስሊሞች ዘንድ የጾም፣ ጸሎት ወር ነው",
english: "The Islamic month of fasting from dawn to sunset; a time of prayer, reflection, and charity.",
},
tags: [HolidayTags.RELIGIOUS, HolidayTags.MUSLIM],
},
TSOME_DIHENET: {
name: { amharic: "ጾመ ድኅነት (ረቡእ·ዓርብ)", english: "Fast of Salvation (Wed & Fri)" },
description: {
amharic: "ከበአለ ሀምሳ ውጭ በሳምንት ረቡእ እና አርብ የምንፆመው ነው።",
english: "A fast observed on Wednesdays and Fridays outside of the 50 days after Easter.",
},
tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN],
},
};
import { InvalidEthiopianDateError, InvalidGregorianDateError, InvalidInputTypeError } from './errors/errorHandler.js'
import { getGregorianDateOfEthiopianNewYear } from './utils.js';
import { dayOfYear, monthDayFromDayOfYear, isGregorianLeapYear, isEthiopianLeapYear } from './utils.js';
/**
* Validates that all provided date parts are numbers.
* @param {string} funcName - The name of the function being validated.
* @param {Object} dateParts - An object where keys are param names and values are the inputs.
* @throws {InvalidInputTypeError} if any value is not a number.
*/
function validateNumericInputs(funcName, dateParts) {
for (const [name, value] of Object.entries(dateParts)) {
if (typeof value !== 'number') {
throw new InvalidInputTypeError(funcName, name, 'number', value);
}
}
}
/**
* Converts an Ethiopian date to its corresponding Gregorian date.
*
* @param {number} ethYear - The Ethiopian year.
* @param {number} ethMonth - The Ethiopian month (1-13).
* @param {number} ethDay - The Ethiopian day of the month.
* @returns {{ year: number, month: number, day: number }} The equivalent Gregorian date.
* @throws {InvalidInputTypeError} If any input is not a number.
* @throws {InvalidEthiopianDateError} If the provided Ethiopian date is invalid.
*/
export function toGC(ethYear, ethMonth, ethDay) {
// 1. Validate input types first
validateNumericInputs('toGC', { ethYear, ethMonth, ethDay });
// 2. Validate date range
if (ethMonth < 1 || ethMonth > 13) {
throw new InvalidEthiopianDateError(ethYear, ethMonth, ethDay)
}
const maxDay = ethMonth === 13 ? (isEthiopianLeapYear(ethYear) ? 6 : 5) : 30
if (ethDay < 1 || ethDay > maxDay) {
throw new InvalidEthiopianDateError(ethYear, ethMonth, ethDay)
}
// 3. Perform conversion
const newYear = getGregorianDateOfEthiopianNewYear(ethYear)
const daysSinceNewYear = (ethMonth - 1) * 30 + ethDay - 1
const newYearDOY = dayOfYear(newYear.gregorianYear, newYear.month, newYear.day)
let gregorianDOY = newYearDOY + daysSinceNewYear
let gregorianYear = newYear.gregorianYear
const yearLength = isGregorianLeapYear(gregorianYear) ? 366 : 365
if (gregorianDOY > yearLength) {
gregorianDOY -= yearLength
gregorianYear += 1
}
const { month, day } = monthDayFromDayOfYear(gregorianYear, gregorianDOY)
return { year: gregorianYear, month, day }
}
/**
* Converts a Gregorian date to the Ethiopian calendar (EC) date.
*
* @param {number} gYear - The Gregorian year (e.g., 2024).
* @param {number} gMonth - The Gregorian month (1-12).
* @param {number} gDay - The Gregorian day of the month (1-31).
* @returns {{ year: number, month: number, day: number }} The corresponding Ethiopian calendar date.
* @throws {InvalidInputTypeError} If any input is not a number.
* @throws {InvalidGregorianDateError} If the input date is invalid or out of supported range.
*/
export function toEC(gYear, gMonth, gDay) {
// 1. Validate input types first
validateNumericInputs('toEC', { gYear, gMonth, gDay });
// 2. Validate date range and validity
const isValidDate = (y, m, d) => {
const date = new Date(Date.UTC(y, m - 1, d))
return (
date.getUTCFullYear() === y &&
date.getUTCMonth() === m - 1 &&
date.getUTCDate() === d
)
}
const inputDate = new Date(Date.UTC(gYear, gMonth - 1, gDay))
const minDate = new Date(Date.UTC(1900, 0, 1))
const maxDate = new Date(Date.UTC(2100, 11, 31))
if (!isValidDate(gYear, gMonth, gDay) || inputDate < minDate || inputDate > maxDate) {
throw new InvalidGregorianDateError(gYear, gMonth, gDay)
}
// 3. Perform conversion
const oneDay = 86400000
const oneYear = 365 * oneDay
const fourYears = 1461 * oneDay
const baseDate = new Date(Date.UTC(1971, 8, 12))
const diff = inputDate.getTime() - baseDate.getTime()
const fourYearCycles = Math.floor(diff / fourYears)
let remainingYears = Math.floor((diff - fourYearCycles * fourYears) / oneYear)
if (remainingYears === 4) remainingYears = 3
const remainingMonths = Math.floor(
(diff - fourYearCycles * fourYears - remainingYears * oneYear) / (30 * oneDay)
)
const remainingDays = Math.floor(
(diff - fourYearCycles * fourYears - remainingYears * oneYear - remainingMonths * 30 * oneDay) / oneDay
)
const ethYear = 1964 + fourYearCycles * 4 + remainingYears
const month = remainingMonths + 1
const day = remainingDays + 1
return { year: ethYear, month, day }
}
/**
* Converts an Ethiopian date to a Gregorian Calendar JavaScript Date object (UTC).
*
* @param {number} ethYear - The Ethiopian year.
* @param {number} ethMonth - The Ethiopian month (1-based).
* @param {number} ethDay - The Ethiopian day.
* @returns {Date} A JavaScript Date object representing the equivalent Gregorian date in UTC.
*/
export function toGCDate(ethYear, ethMonth, ethDay) {
const { year, month, day } = toGC(ethYear, ethMonth, ethDay);
return new Date(Date.UTC(year, month - 1, day));
}
/**
* Converts a JavaScript Date object to the Ethiopian Calendar (EC) date representation.
*
* @param {Date} dateObj - The JavaScript Date object to convert.
* @returns {*} The Ethiopian Calendar date, as returned by the `toEC` function.
*/
export function fromDateToEC(dateObj) {
return toEC(
dateObj.getFullYear(),
dateObj.getMonth() + 1,
dateObj.getDate()
);
}
// muslim conversions
export const islamicFormatter = new Intl.DateTimeFormat('en-TN-u-ca-islamic', {
day: 'numeric',
month: 'numeric',
year: 'numeric',
});
/**
* Get Hijri year from a Gregorian date
* @param {Date} date
* @returns {number} hijri year
*/
export function getHijriYear(date) {
const parts = islamicFormatter.formatToParts(date);
let hYear = null;
parts.forEach(({ type, value }) => {
if (type === 'year') hYear = parseInt(value, 10);
});
return hYear;
}
const hijriToGregorianCache = new Map();
/**
* Converts a Hijri date to the corresponding Gregorian date within a given Gregorian year.
*
* @param {number} hYear - Hijri year (e.g., 1445)
* @param {number} hMonth - Hijri month (1–12)
* @param {number} hDay - Hijri day (1–30)
* @param {number} gregorianYear - Target Gregorian year to restrict the search range
* @returns {Date|null} Gregorian Date object or null if not found
*/
export function hijriToGregorian(hYear, hMonth, hDay, gregorianYear) {
const cacheKey = `${hYear}-${hMonth}-${hDay}-${gregorianYear}`;
if (hijriToGregorianCache.has(cacheKey)) {
return hijriToGregorianCache.get(cacheKey);
}
const baseDate = new Date(gregorianYear - 1, 0, 1);
for (let offset = 0; offset <= 730; offset++) {
const testDate = new Date(baseDate);
testDate.setDate(testDate.getDate() + offset);
const parts = islamicFormatter.formatToParts(testDate);
const hijriParts = {};
parts.forEach(({ type, value }) => {
if (type !== 'literal') hijriParts[type] = parseInt(value, 10);
});
if (
hijriParts.year === hYear &&
hijriParts.month === hMonth &&
hijriParts.day === hDay &&
testDate.getFullYear() === gregorianYear
) {
hijriToGregorianCache.set(cacheKey, testDate);
return testDate;
}
}
hijriToGregorianCache.set(cacheKey, null);
return null;
}
import {
getEthiopianDaysInMonth,
isEthiopianLeapYear,
validateNumericInputs,
validateEthiopianDateObject
} from './utils.js';
/**
* Adds a specified number of days to an Ethiopian date.
*
* @param {Object} ethiopian - The Ethiopian date object { year, month, day }.
* @param {number} days - The number of days to add.
* @returns {Object} The resulting Ethiopian date.
* @throws {InvalidInputTypeError} If inputs are not of the correct type.
*/
export function addDays(ethiopian, days) {
validateEthiopianDateObject(ethiopian, 'addDays', 'ethiopian');
validateNumericInputs('addDays', { days });
let { year, month, day } = ethiopian;
day += days;
// Handle positive days (moving forward)
while (day > getEthiopianDaysInMonth(year, month)) {
day -= getEthiopianDaysInMonth(year, month);
month += 1;
if (month > 13) {
month = 1;
year += 1;
}
}
// Handle negative days (moving backward)
while (day <= 0) {
month -= 1;
if (month < 1) {
month = 13;
year -= 1;
}
day += getEthiopianDaysInMonth(year, month);
}
return { year, month, day };
}
/**
* Adds a specified number of months to an Ethiopian date.
*
* @param {Object} ethiopian - The Ethiopian date object { year, month, day }.
* @param {number} months - The number of months to add.
* @returns {Object} The resulting Ethiopian date.
* @throws {InvalidInputTypeError} If inputs are not of the correct type.
*/
export function addMonths(ethiopian, months) {
validateEthiopianDateObject(ethiopian, 'addMonths', 'ethiopian');
validateNumericInputs('addMonths', { months });
let { year, month, day } = ethiopian;
let totalMonths = month + months;
year += Math.floor((totalMonths - 1) / 13);
month = ((totalMonths - 1) % 13 + 13) % 13 + 1;
const daysInTargetMonth = getEthiopianDaysInMonth(year, month);
if (day > daysInTargetMonth) {
day = daysInTargetMonth;
}
return { year, month, day };
}
/**
* Adds a specified number of years to an Ethiopian date.
*
* @param {Object} ethiopian - The Ethiopian date object { year, month, day }.
* @param {number} years - The number of years to add.
* @returns {Object} The resulting Ethiopian date.
* @throws {InvalidInputTypeError} If inputs are not of the correct type.
*/
export function addYears(ethiopian, years) {
validateEthiopianDateObject(ethiopian, 'addYears', 'ethiopian');
validateNumericInputs('addYears', { years });
let { year, month, day } = ethiopian;
year += years;
if (month === 13 && day === 6 && !isEthiopianLeapYear(year)) {
day = 5;
}
return { year, month, day };
}
/**
* Calculates the difference in days between two Ethiopian dates.
*
* @param {Object} a - The first Ethiopian date object.
* @param {Object} b - The second Ethiopian date object.
* @returns {number} The difference in days.
* @throws {InvalidInputTypeError} If inputs are not valid date objects.
*/
export function diffInDays(a, b) {
validateEthiopianDateObject(a, 'diffInDays', 'a');
validateEthiopianDateObject(b, 'diffInDays', 'b');
const totalDays = (eth) => {
let days = 0;
for (let y = 1; y < eth.year; y++) {
days += isEthiopianLeapYear(y) ? 366 : 365;
}
for (let m = 1; m < eth.month; m++) {
days += getEthiopianDaysInMonth(eth.year, m);
}
days += eth.day;
return days;
};
return totalDays(a) - totalDays(b);
}
/**
* Calculates the difference in months between two Ethiopian dates.
*
* @param {Object} a - The first Ethiopian date object.
* @param {Object} b - The second Ethiopian date object.
* @returns {number} The difference in months.
* @throws {InvalidInputTypeError} If inputs are not valid date objects.
*/
export function diffInMonths(a, b) {
validateEthiopianDateObject(a, 'diffInMonths', 'a');
validateEthiopianDateObject(b, 'diffInMonths', 'b');
const totalMonthsA = a.year * 13 + (a.month - 1);
const totalMonthsB = b.year * 13 + (b.month - 1);
let diff = totalMonthsA - totalMonthsB;
if (a.day < b.day) {
diff -= 1;
}
return diff;
}
/**
* Calculates the difference in years between two Ethiopian dates.
*
* @param {Object} a - The first Ethiopian date object.
* @param {Object} b - The second Ethiopian date object.
* @returns {number} The difference in years.
* @throws {InvalidInputTypeError} If inputs are not valid date objects.
*/
export function diffInYears(a, b) {
validateEthiopianDateObject(a, 'diffInYears', 'a');
validateEthiopianDateObject(b, 'diffInYears', 'b');
const isAfter = (a.year > b.year) ||
(a.year === b.year && a.month > b.month) ||
(a.year === b.year && a.month === b.month && a.day >= b.day);
const [later, earlier] = isAfter ? [a, b] : [b, a];
let diff = later.year - earlier.year;
if (later.month < earlier.month || (later.month === earlier.month && later.day < earlier.day)) {
diff--;
}
const finalDiff = isAfter ? diff : -diff;
// Coerce -0 to 0 to ensure strict equality passes in tests.
if (finalDiff === 0) {
return 0;
}
return finalDiff;
}
/**
* Calculates a human-friendly breakdown between two Ethiopian dates.
* Iteratively accumulates years, then months, then days to avoid off-by-one issues.
*
* @param {Object} a - First Ethiopian date { year, month, day }.
* @param {Object} b - Second Ethiopian date { year, month, day }.
* @param {Object} [options]
* @param {Array<'years'|'months'|'days'>} [options.units=['years','months','days']] - Units to include, in order.
* @returns {{ sign: 1|-1, years?: number, months?: number, days?: number, totalDays: number }}
*/
export function diffBreakdown(a, b, options = {}) {
validateEthiopianDateObject(a, 'diffBreakdown', 'a');
validateEthiopianDateObject(b, 'diffBreakdown', 'b');
const { units = ['years', 'months', 'days'] } = options;
const totalDaysDiff = diffInDays(a, b); // positive if a after b
const sign = totalDaysDiff === 0 ? 1 : (totalDaysDiff > 0 ? 1 : -1);
const later = sign >= 0 ? a : b;
const earlier = sign >= 0 ? b : a;
let cursor = { ...earlier };
const result = { sign, totalDays: Math.abs(totalDaysDiff) };
if (units.includes('years')) {
let years = 0;
while (true) {
const next = addYears(cursor, 1);
if (diffInDays(later, next) >= 0) {
years += 1;
cursor = next;
} else {
break;
}
}
result.years = years;
}
if (units.includes('months')) {
let months = 0;
while (true) {
const next = addMonths(cursor, 1);
if (diffInDays(later, next) >= 0) {
months += 1;
cursor = next;
} else {
break;
}
}
result.months = months;
}
if (units.includes('days')) {
result.days = Math.abs(diffInDays(later, cursor));
}
return result;
}
/* /src/errors/index.js */
/**
* Base class for all custom errors in the Kenat library.
*/
export class KenatError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
/**
* Provides a serializable representation of the error.
* @returns {Object} A plain object with error details.
*/
toJSON() {
return {
type: this.name,
message: this.message,
};
}
}
/**
* Thrown when an Ethiopian date is numerically invalid (e.g., month 14).
*/
export class InvalidEthiopianDateError extends KenatError {
constructor(year, month, day) {
super(`Invalid Ethiopian date: ${year}/${month}/${day}`);
this.date = { year, month, day };
}
toJSON() {
return {
...super.toJSON(),
date: this.date,
validRange: {
month: "1–13",
day: "1–30 (or 5/6 for the 13th month)",
},
};
}
}
/**
* Thrown when a Gregorian date is numerically invalid.
*/
export class InvalidGregorianDateError extends KenatError {
constructor(year, month, day) {
super(`Invalid Gregorian date: ${year}/${month}/${day}`);
this.date = { year, month, day };
}
toJSON() {
return {
...super.toJSON(),
date: this.date,
validRange: {
month: "1–12",
day: "1–31 (depending on month)",
},
};
}
}
/**
* Thrown when a date string provided to the constructor has an invalid format.
*/
export class InvalidDateFormatError extends KenatError {
constructor(inputString) {
super(`Invalid date string format: "${inputString}". Expected 'yyyy/mm/dd' or 'yyyy-mm-dd'.`);
this.inputString = inputString;
}
toJSON() {
return {
...super.toJSON(),
inputString: this.inputString,
};
}
}
/**
* Thrown when the Kenat constructor receives an input type it cannot handle.
*/
export class UnrecognizedInputError extends KenatError {
constructor(input) {
const inputType = typeof input;
super(`Unrecognized input type for Kenat constructor: ${inputType}`);
this.input = input;
}
toJSON() {
return {
...super.toJSON(),
inputType: typeof this.input,
};
}
}
/**
* Thrown for errors occurring during Ge'ez numeral conversion.
*/
export class GeezConverterError extends KenatError {
constructor(message) {
super(message);
}
}
/**
* Thrown when a function receives an argument of an incorrect type.
*/
export class InvalidInputTypeError extends KenatError {
constructor(functionName, parameterName, expectedType, receivedValue) {
const receivedType = typeof receivedValue;
super(`Invalid type for parameter '${parameterName}' in function '${functionName}'. Expected '${expectedType}' but got '${receivedType}'.`);
this.functionName = functionName;
this.parameterName = parameterName;
this.expectedType = expectedType;
this.receivedValue = receivedValue;
}
toJSON() {
return {
...super.toJSON(),
functionName: this.functionName,
parameterName: this.parameterName,
expectedType: this.expectedType,
receivedType: typeof this.receivedValue,
};
}
}
/**
* Thrown for errors related to invalid time components.
*/
export class InvalidTimeError extends KenatError {
constructor(message) {
super(message);
}
}
/**
* Thrown for invalid configuration options passed to MonthGrid.
*/
export class InvalidGridConfigError extends KenatError {
constructor(message) {
super(message);
}
}
/**
* Thrown when an unknown holiday key is used.
*/
export class UnknownHolidayError extends KenatError {
constructor(holidayKey) {
super(`Unknown movable holiday key: "${holidayKey}"`);
this.holidayKey = holidayKey;
}
toJSON() {
return {
...super.toJSON(),
holidayKey: this.holidayKey,
};
}
}
import { getBahireHasab } from './bahireHasab.js';
import { findHijriMonthRanges } from './holidays.js';
import { addDays, diffInDays } from './dayArithmetic.js';
import { fastingInfo, FastingKeys } from './constants.js';
import { getWeekday, getEthiopianDaysInMonth, validateNumericInputs, validateEthiopianDateObject } from './utils.js';
/**
* Calculates the start and end dates of a specific fasting period for a given year.
* @param {'ABIY_TSOME' | 'TSOME_HAWARYAT' | 'TSOME_NEBIYAT' | 'NINEVEH' | 'RAMADAN'} fastKey - The key for the fast.
* @param {number} ethiopianYear - The Ethiopian year.
* @returns {{start: object, end: object}|null} An object with start and end PLAIN date objects.
*/
export function getFastingPeriod(fastKey, ethiopianYear) {
const bh = getBahireHasab(ethiopianYear);
switch (fastKey) {
case FastingKeys.ABIY_TSOME: {
const start = bh.movableFeasts.abiyTsome?.ethiopian;
const end = bh.movableFeasts.siklet?.ethiopian;
if (start && end) {
return { start, end };
}
return null;
}
case FastingKeys.TSOME_HAWARYAT: {
const start = bh.movableFeasts.tsomeHawaryat?.ethiopian;
const end = { year: ethiopianYear, month: 11, day: 4 };
if (start) {
return { start, end };
}
return null;
}
case FastingKeys.NINEVEH: {
const start = bh.movableFeasts.nineveh?.ethiopian;
if (start) {
const end = addDays(start, 2);
return { start, end };
}
return null;
}
case FastingKeys.TSOME_NEBIYAT: {
const start = { year: ethiopianYear, month: 3, day: 15 };
const end = { year: ethiopianYear, month: 4, day: 28 };
return { start, end };
}
case FastingKeys.FILSETA: {
// Nehase 1 to Nehase 14
const start = { year: ethiopianYear, month: 12, day: 1 };
const end = { year: ethiopianYear, month: 12, day: 14 };
return { start, end };
}
case FastingKeys.RAMADAN: {
const ranges = findHijriMonthRanges(ethiopianYear, 9);
return ranges.length > 0 ? ranges[0] : null;
}
default:
return null;
}
}
/**
* Returns fasting information (names, descriptions, period) for a given fast and year.
* @param {'ABIY_TSOME'|'TSOME_HAWARYAT'|'TSOME_NEBIYAT'|'NINEVEH'|'RAMADAN'} fastKey
* @param {number} ethiopianYear
* @param {{lang?: 'amharic'|'english'}} options
* @returns {{ key: string, name: string, description: string, period: { start: object, end: object } } | null}
*/
export function getFastingInfo(fastKey, ethiopianYear, options = {}) {
validateNumericInputs('getFastingInfo', { ethiopianYear });
const { lang = 'amharic' } = options;
const info = fastingInfo[fastKey];
if (!info) return null;
const name = info?.name?.[lang] || info?.name?.english;
const description = info?.description?.[lang] || info?.description?.english;
// TSOME_DIHENET is a weekly fast (Wed/Fri) with an exception; it doesn't have a single contiguous period.
if (fastKey === FastingKeys.TSOME_DIHENET) {
return {
key: fastKey,
name,
description,
tags: info.tags,
period: null,
};
}
const period = getFastingPeriod(fastKey, ethiopianYear);
if (!period) return null;
return {
key: fastKey,
name,
description,
tags: info.tags,
period,
};
}
/**
* Checks if a given Ethiopian date is an Orthodox weekly fasting day (Tsome Dihnet).
* Rules:
* - Fasting occurs every Wednesday and Friday.
* - Exception: for the 50 days after Easter (Fasika) up to and including Pentecost (Paraclete),
* Wednesdays and Fridays are NOT considered fasting days.
*
* @param {{year:number, month:number, day:number}} etDate - Ethiopian date object.
* @returns {boolean} true if it's a fasting day, false otherwise.
*/
function isTsomeDihnetFastDay(etDate) {
validateEthiopianDateObject(etDate, 'isTsomeDihnetFastDay', 'etDate');
const weekday = getWeekday(etDate); // 0=Sun ... 6=Sat
const isWedOrFri = (weekday === 3 || weekday === 5);
if (!isWedOrFri) return false;
// Get Easter (Fasika) for the year and apply 50-day exception window
const bh = getBahireHasab(etDate.year);
const fasika = bh?.movableFeasts?.fasika?.ethiopian;
const paraclete = bh?.movableFeasts?.paraclete?.ethiopian;
if (!fasika || !paraclete) {
// If for some reason we cannot compute the window, default to standard Wed/Fri fasting.
return true;
}
const daysFromEaster = diffInDays(etDate, fasika);
const inPentecostSeason = daysFromEaster >= 1 && diffInDays(etDate, paraclete) <= 0;
return !inPentecostSeason;
}
// (no export) private helper only
/**
* Return an array of day numbers in the given Ethiopian month that belong to a fasting period.
* For TSOME_DIHENET, it returns all Wednesdays and Fridays excluding the 50-day period after Easter (through Pentecost).
* For fixed/range fasts, it returns the days intersecting the fast period.
*
* @param {string} fastKey - One of FastingKeys
* @param {number} year - Ethiopian year
* @param {number} month - Ethiopian month (1-13)
* @returns {number[]}
*/
export function getFastingDays(fastKey, year, month) {
validateNumericInputs('getFastingDays', { year, month });
const daysInMonth = getEthiopianDaysInMonth(year, month);
if (fastKey === FastingKeys.TSOME_DIHENET) {
const out = [];
for (let d = 1; d <= daysInMonth; d++) {
if (isTsomeDihnetFastDay({ year, month, day: d })) out.push(d);
}
return out;
}
// For other fasts: compute period and intersect with the month
const period = getFastingPeriod(fastKey, year);
if (!period) return [];
const startYearMonth = { y: period.start.year, m: period.start.month };
const endYearMonth = { y: period.end.year, m: period.end.month };
// If the month is completely outside, return []
const before = (year < startYearMonth.y) || (year === startYearMonth.y && month < startYearMonth.m);
const after = (year > endYearMonth.y) || (year === endYearMonth.y && month > endYearMonth.m);
if (before || after) return [];
const startDay = (year === period.start.year && month === period.start.month) ? period.start.day : 1;
const endDay = (year === period.end.year && month === period.end.month) ? period.end.day : daysInMonth;
const result = [];
for (let d = startDay; d <= endDay; d++) result.push(d);
return result;
}
import { toGeez } from './geezConverter.js';
import { monthNames } from './constants.js';
import { getWeekday } from './utils.js';
import { daysOfWeek } from './constants.js';
/**
* Formats an Ethiopian date using language-specific month name and Arabic numerals.
*
* @param {{year: number, month: number, day: number}} etDate - Ethiopian date object
* @param {'amharic'|'english'} [lang='amharic'] - Language for month name
* @returns {string} Formatted string like "መስከረም 10 2016"
*/
export function formatStandard(etDate, lang = 'amharic') {
const names = monthNames[lang] || monthNames.amharic;
const monthName = names[etDate.month - 1] || `Month${etDate.month}`;
return `${monthName} ${etDate.day} ${etDate.year}`;
}
/**
* Formats an Ethiopian date in Geez numerals with Amharic month name.
*
* @param {{year: number, month: number, day: number}} etDate - Ethiopian date
* @returns {string} Example: "መስከረም ፲፩ ፳፻፲፮"
*/
export function formatInGeezAmharic(etDate) {
const monthName = monthNames.amharic[etDate.month - 1] || `Month${etDate.month}`;
return `${monthName} ${toGeez(etDate.day)} ${toGeez(etDate.year)}`;
}
/**
* Formats an Ethiopian date and time as a string.
*
* @param {{year: number, month: number, day: number}} etDate - Ethiopian date
* @param {import('../Time.js').Time} time - An instance of the Time class
* @param {'amharic'|'english'} [lang='amharic'] - Language for suffix
* @returns {string} Example: "መስከረም 10 2016 08:30 ጠዋት"
*/
export function formatWithTime(etDate, time, lang = 'amharic') {
const base = formatStandard(etDate, lang);
// THIS IS THE FIX: Ensure zeroAsDash is false for this specific format.
const timeString = time.format({
lang,
useGeez: false,
zeroAsDash: false
});
return `${base} ${timeString}`;
}
/**
* Formats an Ethiopian date object with the weekday name, month name, day, and year.
*
* @param {Object} etDate - The Ethiopian date object to format.
* @param {number} etDate.day - The day of the month.
* @param {number} etDate.month - The month number (1-based).
* @param {number} etDate.year - The year.
* @param {string} [lang='amharic'] - The language to use for weekday and month names ('amharic', 'english', etc.).
* @param {boolean} [useGeez=false] - Whether to format the day and year in Geez numerals.
* @returns {string} The formatted date string, e.g., "ማክሰኞ, መስከረም 1 2016".
*/
export function formatWithWeekday(etDate, lang = 'amharic', useGeez = false) {
const weekdayIndex = getWeekday(etDate);
const weekdayName = daysOfWeek[lang]?.[weekdayIndex] || daysOfWeek.amharic[weekdayIndex];
const monthName = monthNames[lang]?.[etDate.month - 1] || `Month${etDate.month}`;
const day = useGeez ? toGeez(etDate.day) : etDate.day;
const year = useGeez ? toGeez(etDate.year) : etDate.year;
return `${weekdayName}, ${monthName} ${day} ${year}`;
}
/**
* Returns Ethiopian date in short "yyyy/mm/dd" format.
* @param {{year: number, month: number, day: number}} etDate
* @returns {string} e.g., "2017/10/25"
*/
export function formatShort(etDate) {
const y = etDate.year;
const m = etDate.month.toString().padStart(2, '0');
const d = etDate.day.toString().padStart(2, '0');
return `${y}/${m}/${d}`;
}
/**
* Returns an ISO-like string: "YYYY-MM-DD" or "YYYY-MM-DDTHH:mm".
* @param {{year: number, month: number, day: number}} etDate
* @param {{hour: number, minute: number, period: 'day'|'night'}|null} time
* @returns {string}
*/
export function toISODateString(etDate, time = null) {
const y = etDate.year;
const m = etDate.month.toString().padStart(2, '0');
const d = etDate.day.toString().padStart(2, '0');
if (!time) return `${y}-${m}-${d}`;
const hr = time.hour.toString().padStart(2, '0');
const min = time.minute.toString().padStart(2, '0');
const suffix = time.period === 'night' ? '+12h' : '';
return `${y}-${m}-${d}T${hr}:${min}${suffix}`;
}
/**
* ethiopianNumberConverter.js
*
* Converts Arabic numerals (natural numbers) to their equivalent Ethiopic numerals.
* Supports numbers from 1 up to 99999999.
*
* Example:
* toGeez(1); // '፩'
* toGeez(30); // '፴'
* toGeez(123); // '፻፳፫'
* toGeez(10000); // '፼'
*
* @author Melaku Demeke
* @license MIT
*/
/* /src/geezConverter.js (Updated) */
/* /src/geezConverter.js (Updated) */
import { GeezConverterError } from './errors/errorHandler.js';
const symbols = {
ones: ['', '፩', '፪', '፫', '፬', '፭', '፮', '፯', '፰', '፱'],
tens: ['', '፲', '፳', '፴', '፵', '፶', '፷', '፸', '፹', '፺'],
hundred: '፻',
tenThousand: '፼'
};
/**
* Converts a natural number to Ethiopic numeral string.
*
* @param {number|string} input - The number to convert (positive integer only).
* @returns {string} Ethiopic numeral string.
* @throws {GeezConverterError} If input is not a valid positive integer.
*/
export function toGeez(input) {
if (typeof input !== 'number' && typeof input !== 'string') {
throw new GeezConverterError("Input must be a number or a string.");
}
const num = Number(input);
if (isNaN(num) || !Number.isInteger(num) || num < 0) {
throw new GeezConverterError("Input must be a non-negative integer.");
}
if (num === 0) return '0'; // Often Ge'ez doesn't have a zero, but useful for modern contexts.
// Helper for numbers 1-99
function convertBelow100(n) {
if (n <= 0) return '';
const tensDigit = Math.floor(n / 10);
const onesDigit = n % 10;
return symbols.tens[tensDigit] + symbols.ones[onesDigit];
}
if (num < 100) {
return convertBelow100(num);
}
if (num === 100) return symbols.hundred;
if (num < 10000) {
const hundreds = Math.floor(num / 100);
const remainder = num % 100;
// For numbers like 101, it's ፻፩, not ፩፻፩. If the hundred part is 1, don't add a prefix.
const hundredPart = (hundreds > 1 ? convertBelow100(hundreds) : '') + symbols.hundred;
return hundredPart + convertBelow100(remainder);
}
// For numbers >= 10000, use recursion
const tenThousandPart = Math.floor(num / 10000);
const remainder = num % 10000;
// If the ten-thousand part is 1, no prefix is needed (e.g., ፼, not ፩፼)
const tenThousandGeez = (tenThousandPart > 1 ? toGeez(tenThousandPart) : '') + symbols.tenThousand;
return tenThousandGeez + (remainder > 0 ? toGeez(remainder) : '');
}
/**
* Converts a Ge'ez numeral string to its Arabic numeral equivalent.
*
* @param {string} geezStr - The Ge'ez numeral string to convert.
* @returns {number} The Arabic numeral representation of the input string.
* @throws {GeezConverterError} If the input is not a valid Ge'ez numeral string.
*/
export function toArabic(geezStr) {
if (typeof geezStr !== 'string') {
throw new GeezConverterError('Input must be a non-empty string.');
}
if (geezStr.trim() === '') {
return 0; // Or throw error, depending on desired behavior for empty string
}
const reverseMap = {};
symbols.ones.forEach((char, i) => { if (char) reverseMap[char] = i; });
symbols.tens.forEach((char, i) => { if (char) reverseMap[char] = i * 10; });
reverseMap[symbols.hundred] = 100;
reverseMap[symbols.tenThousand] = 10000;
let total = 0;
let currentNumber = 0;
for (const char of geezStr) {
const value = reverseMap[char];
if (value === undefined) {
throw new GeezConverterError(`Unknown Ge'ez numeral: ${char}`);
}
if (value === 100 || value === 10000) {
// If currentNumber is 0, it implies a standalone ፻ or ፼, so treat it as 1 * multiplier.
currentNumber = (currentNumber || 1) * value;
// ፼ acts as a separator for large numbers. Add the completed segment to the total.
if (value === 10000) {
total += currentNumber;
currentNumber = 0;
}
} else {
// Add simple digit values (1-99)
currentNumber += value;
}
}
// Add any remaining part (for numbers that don't end in ፼)
total += currentNumber;
return total;
}
import { toEC, toGC, hijriToGregorian, getHijriYear } from "./conversions.js";
import {
holidayInfo,
HolidayTags,
keyToTewsakMap,
movableHolidays,
} from "./constants.js";
import { validateNumericInputs } from "./utils.js";
import {
InvalidInputTypeError,
UnknownHolidayError,
} from "./errors/errorHandler.js";
import { getMovableHoliday } from "./bahireHasab.js";
const fixedHolidays = {
enkutatash: {
month: 1,
day: 1,
tags: [HolidayTags.PUBLIC, HolidayTags.CULTURAL],
},
meskel: {
month: 1,
day: 17,
tags: [HolidayTags.PUBLIC, HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN],
},
beherbehereseb: {
month: 3,
day: 29,
tags: [HolidayTags.PUBLIC, HolidayTags.STATE],
},
gena: {
month: 4,
day: 29,
tags: [HolidayTags.PUBLIC, HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN],
},
timket: {
month: 5,
day: 11,
tags: [HolidayTags.PUBLIC, HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN],
},
martyrsDay: {
month: 6,
day: 12,
tags: [HolidayTags.PUBLIC, HolidayTags.STATE],
},
adwa: { month: 6, day: 23, tags: [HolidayTags.PUBLIC, HolidayTags.STATE] },
labour: { month: 8, day: 23, tags: [HolidayTags.PUBLIC, HolidayTags.STATE] },
patriots: {
month: 8,
day: 27,
tags: [HolidayTags.PUBLIC, HolidayTags.STATE],
},
};
function findAllIslamicOccurrences(ethiopianYear, hijriMonth, hijriDay) {
const startGregorianYear = toGC(ethiopianYear, 1, 1).year;
const endGregorianYear = toGC(ethiopianYear, 13, 5).year;
const occurrences = [];
const checkGregorianYear = (gYear) => {
const hijriYearAtStart = getHijriYear(new Date(gYear, 0, 1));
const hijriYearsToCheck = [hijriYearAtStart, hijriYearAtStart + 1];
hijriYearsToCheck.forEach((hYear) => {
const gregorianDate = hijriToGregorian(
hYear,
hijriMonth,
hijriDay,
gYear
);
if (gregorianDate && gregorianDate.getFullYear() === gYear) {
const ecDate = toEC(
gregorianDate.getFullYear(),
gregorianDate.getMonth() + 1,
gregorianDate.getDate()
);
if (ecDate.year === ethiopianYear) {
occurrences.push({
gregorian: {
year: gregorianDate.getFullYear(),
month: gregorianDate.getMonth() + 1,
day: gregorianDate.getDate(),
},
ethiopian: ecDate,
});
}
}
});
};
checkGregorianYear(startGregorianYear);
if (startGregorianYear !== endGregorianYear) {
checkGregorianYear(endGregorianYear);
}
return Array.from(
new Map(
occurrences.map((item) => [JSON.stringify(item.ethiopian), item])
).values()
);
}
/**
* Finds the start and end dates of a specific Hijri month that falls within an Ethiopian year.
* @param {number} ethiopianYear - The Ethiopian year.
* @param {number} hijriMonth - The Hijri month to find (e.g., 9 for Ramadan).
* @returns {Array<{start: Kenat, end: Kenat}>} An array of start/end date ranges.
*/
export function findHijriMonthRanges(ethiopianYear, hijriMonth) {
const startGregorianYear = toGC(ethiopianYear, 1, 1).year;
const endGregorianYear = toGC(ethiopianYear, 13, 5).year;
const ranges = [];
const findRangeInGregorianYear = (gYear) => {
const hijriYearAtStart = getHijriYear(new Date(gYear, 0, 1));
const hijriYearsToCheck = [
hijriYearAtStart - 1,
hijriYearAtStart,
hijriYearAtStart + 1,
];
for (const hYear of hijriYearsToCheck) {
const startDateGregorian = hijriToGregorian(hYear, hijriMonth, 1, gYear);
if (!startDateGregorian) continue;
const nextMonth = hijriMonth === 12 ? 1 : hijriMonth + 1;
const nextYear = hijriMonth === 12 ? hYear + 1 : hYear;
let endDateGregorian;
const endDateCandidate =
hijriToGregorian(nextYear, nextMonth, 1, gYear) ||
hijriToGregorian(nextYear, nextMonth, 1, gYear + 1);
if (endDateCandidate) {
endDateGregorian = new Date(endDateCandidate.getTime() - 86400000);
} else {
continue;
}
const startEC = toEC(
startDateGregorian.getFullYear(),
startDateGregorian.getMonth() + 1,
startDateGregorian.getDate()
);
const endEC = toEC(
endDateGregorian.getFullYear(),
endDateGregorian.getMonth() + 1,
endDateGregorian.getDate()
);
if (startEC.year === ethiopianYear || endEC.year === ethiopianYear) {
ranges.push({
start: startEC,
end: endEC,
});
}
}
};
findRangeInGregorianYear(startGregorianYear);
if (startGregorianYear !== endGregorianYear) {
findRangeInGregorianYear(endGregorianYear);
}
const uniqueRanges = Array.from(new Map(ranges.map(item => [`${item.start.year}/${item.start.month}/${item.start.day}`, item])).values());
return uniqueRanges;
}
const getAllMoulidDates = (year) => findAllIslamicOccurrences(year, 3, 12);
const getAllEidFitrDates = (year) => findAllIslamicOccurrences(year, 10, 1);
const getAllEidAdhaDates = (year) => findAllIslamicOccurrences(year, 12, 10);
export function getHoliday(holidayKey, ethYear, options = {}) {
validateNumericInputs("getHoliday", { ethYear });
const { lang = "amharic" } = options;
const info = holidayInfo[holidayKey];
if (!info) return null;
const name = info?.name?.[lang] || info?.name?.english;
const description = info?.description?.[lang] || info?.description?.english;
if (fixedHolidays[holidayKey]) {
const rules = fixedHolidays[holidayKey];
return {
key: holidayKey,
tags: rules.tags,
movable: false,
name,
description,
ethiopian: { year: ethYear, month: rules.month, day: rules.day },
};
}
const tewsakKey = keyToTewsakMap[holidayKey];
if (tewsakKey) {
const date = getMovableHoliday(tewsakKey, ethYear);
return {
key: holidayKey,
tags: movableHolidays[holidayKey].tags,
movable: true,
name,
description,
ethiopian: date,
gregorian: toGC(date.year, date.month, date.day),
};
}
let muslimDateData;
if (holidayKey === "eidFitr") muslimDateData = getAllEidFitrDates(ethYear)[0];
else if (holidayKey === "eidAdha")
muslimDateData = getAllEidAdhaDates(ethYear)[0];
else if (holidayKey === "moulid")
muslimDateData = getAllMoulidDates(ethYear)[0];
if (muslimDateData) {
return {
key: holidayKey,
tags: movableHolidays[holidayKey].tags,
movable: true,
name,
description,
ethiopian: muslimDateData.ethiopian,
gregorian: muslimDateData.gregorian,
};
}
return null;
}
export function getHolidaysInMonth(ethYear, ethMonth, options = {}) {
validateNumericInputs("getHolidaysInMonth", { ethYear, ethMonth });
if (ethMonth < 1 || ethMonth > 13) {
throw new InvalidInputTypeError(
"getHolidaysInMonth",
"ethMonth",
"number between 1 and 13",
ethMonth
);
}
const { lang = "amharic", filter = null } = options;
const allHolidaysForMonth = [];
const allHolidayKeys = Object.keys(holidayInfo);
allHolidayKeys.forEach((key) => {
const holiday = getHoliday(key, ethYear, { lang });
if (holiday && holiday.ethiopian.month === ethMonth) {
allHolidaysForMonth.push(holiday);
}
});
// Handle cases where Islamic holidays occur twice
const muslimHolidays = [
...getAllMoulidDates(ethYear).map((d) => ({ ...d, key: "moulid" })),
...getAllEidFitrDates(ethYear).map((d) => ({ ...d, key: "eidFitr" })),
...getAllEidAdhaDates(ethYear).map((d) => ({ ...d, key: "eidAdha" })),
];
muslimHolidays.forEach((data) => {
if (data.ethiopian.month === ethMonth) {
const info = holidayInfo[data.key];
const holidayObj = {
key: data.key,
tags: movableHolidays[data.key].tags,
movable: true,
name: info?.name?.[lang] || info?.name?.english,
description: info?.description?.[lang] || info?.description?.english,
ethiopian: data.ethiopian,
gregorian: data.gregorian,
};
// Avoid duplicates from the getHoliday call
if (
!allHolidaysForMonth.some(
(h) =>
JSON.stringify(h.ethiopian) === JSON.stringify(holidayObj.ethiopian)
)
) {
allHolidaysForMonth.push(holidayObj);
}
}
});
const filterTags = filter
? Array.isArray(filter)
? filter
: [filter]
: null;
const finalHolidays = filterTags
? allHolidaysForMonth.filter((holiday) =>
holiday.tags.some((tag) => filterTags.includes(tag))
)
: allHolidaysForMonth;
finalHolidays.sort((a, b) => a.ethiopian.day - b.ethiopian.day);
return finalHolidays;
}
export function getHolidaysForYear(ethYear, options = {}) {
validateNumericInputs("getHolidaysForYear", { ethYear });
const { lang = "amharic", filter = null } = options;
const allHolidaysForYear = [];
// Process all fixed and Christian movable holidays
const singleOccurrenceKeys = Object.keys(fixedHolidays).concat(
Object.keys(keyToTewsakMap)
);
singleOccurrenceKeys.forEach((key) => {
const holiday = getHoliday(key, ethYear, { lang });
if (holiday) {
allHolidaysForYear.push(holiday);
}
});
// Process all occurrences of Islamic holidays
const addMuslimHolidays = (key, dateArray) => {
dateArray.forEach((data) => {
const info = holidayInfo[key];
allHolidaysForYear.push({
key,
tags: movableHolidays[key].tags,
movable: true,
name: info?.name?.[lang] || info?.name?.english,
description: info?.description?.[lang] || info?.description?.english,
ethiopian: data.ethiopian,
gregorian: data.gregorian,
});
});
};
addMuslimHolidays("moulid", getAllMoulidDates(ethYear));
addMuslimHolidays("eidFitr", getAllEidFitrDates(ethYear));
addMuslimHolidays("eidAdha", getAllEidAdhaDates(ethYear));
const filterTags = filter
? Array.isArray(filter)
? filter
: [filter]
: null;
const finalHolidays = filterTags
? allHolidaysForYear.filter((holiday) =>
holiday.tags.some((tag) => filterTags.includes(tag))
)
: allHolidaysForYear;
finalHolidays.sort((a, b) => {
if (a.ethiopian.month !== b.ethiopian.month) {
return a.ethiopian.month - b.ethiopian.month;
}
return a.ethiopian.day - b.ethiopian.day;
});
return finalHolidays;
}
import { Kenat } from './Kenat.js';
import { diffBreakdown } from './dayArithmetic.js';
import { MonthGrid } from './MonthGrid.js';
import { toEC, toGC } from './conversions.js';
import { toArabic, toGeez } from './geezConverter.js';
import { getHolidaysInMonth, getHoliday, getHolidaysForYear } from './holidays.js';
import { Time } from './Time.js';
import { HolidayTags, HolidayNames } from './constants.js';
import { getBahireHasab } from './bahireHasab.js';
import { monthNames } from './constants.js';
import { getFastingPeriod, getFastingInfo, getFastingDays } from './fasting.js';
// Default export is the Kenat class directly
export default Kenat;
// Named exports for the conversion functions
export {
toEC as toEC,
toGC,
toArabic,
toGeez,
getHolidaysInMonth,
getHolidaysForYear,
getBahireHasab,
getFastingPeriod,
getFastingInfo,
getFastingDays,
MonthGrid,
Time,
getHoliday,
HolidayTags,
HolidayNames,
monthNames,
diffBreakdown,
};
import { toGC, toEC } from './conversions.js';
import { printMonthCalendarGrid } from './render/printMonthCalendarGrid.js';
import { monthNames, daysOfWeek } from './constants.js';
import { Time } from './Time.js';
import { toGeez } from './geezConverter.js';
import { getBahireHasab } from './bahireHasab.js';
import { MonthGrid } from './MonthGrid.js';
import { getHolidaysInMonth, getHoliday } from './holidays.js';
import { getEthiopianDaysInMonth, isValidEthiopianDate, isEthiopianLeapYear, getWeekday } from './utils.js';
import {
InvalidEthiopianDateError,
InvalidDateFormatError,
UnrecognizedInputError,
InvalidInputTypeError
} from './errors/errorHandler.js';
import {
formatStandard,
formatInGeezAmharic,
formatWithTime,
formatWithWeekday,
formatShort,
toISODateString
} from './formatting.js';
import {
addDays,
addMonths,
addYears,
diffInDays,
diffInMonths,
diffInYears,
diffBreakdown
} from './dayArithmetic.js';
/**
* Kenat - Ethiopian Calendar Date Wrapper
*
* A lightweight class to work with both Gregorian and Ethiopian calendars.
* It wraps JavaScript's built-in `Date` object and converts Gregorian dates to Ethiopian equivalents.
*
*/
export class Kenat {
/**
* Constructs a Kenat instance.
* Can be initialized with:
* - An Ethiopian date string (e.g., '2016/1/1', '2016-1-1').
* - An object with { year, month, day }.
* - A native JavaScript Date object (will be converted from Gregorian).
* - No arguments, for the current date.
*
* @param {string|Object|Date} [input] - The date input.
* @param {Object} [timeObj] - An optional time object.
* @throws {InvalidEthiopianDateError} If the provided Ethiopian date is invalid.
* @throws {InvalidDateFormatError} If the provided date string format is invalid.
* @throws {UnrecognizedInputError} If the input format is unrecognized.
*/
constructor(input, timeObj = null) {
let year, month, day;
if (!input) {
// Default to current Gregorian date -> Ethiopian
const today = new Date();
const ethiopianToday = toEC(
today.getFullYear(),
today.getMonth() + 1,
today.getDate()
);
year = ethiopianToday.year;
month = ethiopianToday.month;
day = ethiopianToday.day;
// MODIFICATION: Use the Time class
this.time = Time.fromGregorian(today.getHours(), today.getMinutes());
} else if (input instanceof Date) {
// Input is a JS Date object
const ethiopianDate = toEC(
input.getFullYear(),
input.getMonth() + 1,
input.getDate()
);
year = ethiopianDate.year;
month = ethiopianDate.month;
day = ethiopianDate.day;
// MODIFICATION: Use the Time class
this.time = Time.fromGregorian(input.getHours(), input.getMinutes());
} else if (typeof input === 'object' && input !== null && 'year' in input && 'month' in input && 'day' in input) {
// Input is an object { year, month, day }
year = input.year;
month = input.month;
day = input.day;
// MODIFICATION: Create a Time instance
const t = timeObj || { hour: 12, minute: 0, period: 'day' };
this.time = new Time(t.hour, t.minute, t.period);
} else if (typeof input === 'string') {
const parts = input.split(/[-/]/).map(Number);
if (parts.length === 3 && !parts.some(isNaN)) {
[year, month, day] = parts;
} else {
throw new InvalidDateFormatError(input);
}
// MODIFICATION: Create a Time instance
const t = timeObj || { hour: 12, minute: 0, period: 'day' };
this.time = new Time(t.hour, t.minute, t.period);
} else {
throw new UnrecognizedInputError(input);
}
if (!isValidEthiopianDate(year, month, day)) {
throw new InvalidEthiopianDateError(year, month, day);
}
this.ethiopian = { year, month, day };
}
/**
* Creates and returns a new instance of the Kenat class representing the current moment.
*
* @returns {Kenat} A new Kenat instance set to the current date and time.
*/
static now() {
return new Kenat();
}
/**
* Converts the current Ethiopian date stored in this.ethiopian to its Gregorian equivalent.
*
* @returns {{ year: number, month: number, day: number }} The Gregorian date corresponding to the Ethiopian date.
*/
getGregorian() {
const { year, month, day } = this.ethiopian;
return toGC(year, month, day);
}
/**
* Returns the Ethiopian equivalent of the stored Gregorian date.
*
* @returns {{ year: number, month: number, day: number }} An object representing the Ethiopian date.
*/
getEthiopian() {
return this.ethiopian;
}
/**
* Sets the time and returns a new Kenat instance.
* Supports method chaining.
*
* @param {number} hour - The hour value to set.
* @param {number} minute - The minute value to set.
* @param {string} period - The period of the day (e.g., 'day' or 'night').
* @returns {Kenat} A new Kenat instance with the updated time.
*/
setTime(hour, minute, period) {
const newKenat = new Kenat(this.ethiopian);
newKenat.time = new Time(hour, minute, period);
return newKenat;
}
/**
* Calculates and returns the Bahire Hasab values for the current instance's year.
*
* @returns {Object} An object containing all the calculated Bahire Hasab values
* (ameteAlem, evangelist, wenber, metqi, nineveh, etc.).
*/
getBahireHasab() {
return getBahireHasab(this.ethiopian.year);
}
// Format Methods
/**
* Returns a string representation of the Ethiopian date and time.
*
* The format is: "Ethiopian: {year}-{month}-{day} {hh:mm period}".
* If the time is not available, hour and minute are replaced with '??'.
*
* @returns {string} The formatted Ethiopian date and time string.
*/
toString() {
return formatWithTime(this.ethiopian, this.time);
}
/**
* Formats the Ethiopian date according to the specified options.
*
* @param {Object} [options={}] - Formatting options.
* @param {string} [options.lang='amharic'] - Language to use for formatting ('amharic', 'english', etc.).
* @param {boolean} [options.showWeekday=false] - Whether to include the weekday in the formatted string.
* @param {boolean} [options.useGeez=false] - Whether to use Geez numerals (only applies if lang is 'amharic').
* @param {boolean} [options.includeTime=false] - Whether to include the time in the formatted string.
* @returns {string} The formatted Ethiopian date string.
*/
format(options = {}) {
const {
lang = 'amharic',
showWeekday = false,
useGeez = false,
includeTime = false
} = options;
if (showWeekday && includeTime) {
return `${formatWithWeekday(this.ethiopian, lang, useGeez)} ${this.time.format({ lang, useGeez })}`;
}
if (showWeekday) {
return formatWithWeekday(this.ethiopian, lang, useGeez);
}
if (includeTime) {
return formatWithTime(this.ethiopian, this.time, lang);
}
return useGeez && lang === 'amharic'
? formatInGeezAmharic(this.ethiopian)
: formatStandard(this.ethiopian, lang);
}
/**
* Formats the Ethiopian date in Geez numerals and Amharic month name.
*
* @returns {string} The formatted date string in the format: "{Amharic Month Name} {Geez Day} {Geez Year}".
*
* formatInGeezAmharic(); // "የካቲት ፲ ፳፻፲፭"
*/
formatInGeezAmharic() {
return formatInGeezAmharic(this.ethiopian);
}
/**
* Formats the Ethiopian date with weekday name.
*
* @param {'amharic'|'english'} [lang='amharic'] - Language for month and weekday names.
* @param {boolean} [useGeez=false] - Whether to show numerals in Geez.
* @returns {string} Formatted string with weekday, e.g. "ማክሰኞ, መስከረም ፳፩ ፳፻፲፯"
*/
formatWithWeekday(lang = 'amharic', useGeez = false) {
return formatWithWeekday(this.ethiopian, lang, useGeez);
}
/**
* Returns the Ethiopian date in "yyyy/mm/dd" short format.
* @returns {string}
*/
formatShort() {
return formatShort(this.ethiopian);
}
/**
* Returns an ISO-style date string: "YYYY-MM-DD" or "YYYY-MM-DDTHH:mm".
* @returns {string}
*/
toISOString() {
return toISODateString(this.ethiopian, this.time);
}
/**
* Checks if the current date is a holiday.
* @param {Object} [options={}] - Options for language.
* @param {string} [options.lang='amharic'] - The language for the holiday names and descriptions.
* @returns {Array<Object>} An array of holiday objects for the current date, or an empty array if it's not a holiday.
*/
isHoliday(options = {}) {
const { lang = 'amharic' } = options;
const { year, month, day } = this.ethiopian;
// Get all holidays for the current month
const holidaysInMonth = getHolidaysInMonth(year, month, lang);
// Filter to find holidays that fall on the current day
const todaysHolidays = holidaysInMonth.filter(holiday => holiday.ethiopian.day === day);
return todaysHolidays;
}
// format ends
/**
* Generates a calendar for a given Ethiopian month and year, mapping each Ethiopian date
* to its corresponding Gregorian date and providing formatted display strings.
*
* @param {number} [year=this.ethiopian.year] - The Ethiopian year for the calendar.
* @param {number} [month=this.ethiopian.month] - The Ethiopian month (1-13).
* @param {boolean} [useGeez=false] - Whether to display dates in Geez numerals.
* @returns {Array<Object>} An array of objects, each representing a day in the month with
* Ethiopian and Gregorian date information and display strings.
*/
getMonthCalendar(year = this.ethiopian.year, month = this.ethiopian.month, useGeez = false) {
const daysInMonth = getEthiopianDaysInMonth(year, month);
const calendar = [];
for (let day = 1; day <= daysInMonth; day++) {
const ethDate = { year, month, day };
const gregDate = toGC(year, month, day);
calendar.push({
ethiopian: {
...ethDate,
display: useGeez
? `${monthNames.amharic[month - 1]} ${toGeez(day)} ${toGeez(year)}`
: `${monthNames.amharic[month - 1]} ${day} ${year}`
},
gregorian: {
...gregDate,
display: `${gregDate.year}-${gregDate.month.toString().padStart(2, '0')}-${gregDate.day.toString().padStart(2, '0')}`
}
});
}
return calendar;
}
/**
* Prints the calendar grid for the current Ethiopian month.
*
* @param {boolean} [useGeez=false] - If true, displays the calendar using Geez numerals.
* @returns {void}
*/
printThisMonth(useGeez = false) {
const { year, month } = this.getEthiopian();
const calendar = this.getMonthCalendar(year, month, useGeez);
printMonthCalendarGrid(year, month, calendar, useGeez);
}
static getMonthCalendar(year, month, options = {}) {
const { useGeez = false, weekdayLang = 'amharic', weekStart = 0, holidayFilter = null, mode = "public" } = options;
const monthGrid = MonthGrid.create({
year,
month,
useGeez,
weekdayLang,
weekStart,
holidayFilter,
mode
});
return {
month,
monthName: monthGrid.monthName,
year: monthGrid.year,
headers: monthGrid.headers,
days: monthGrid.days
};
}
/**
* Generates a full-year calendar as an array of month objects for the specified year.
*
* @param {number} year - The year for which to generate the calendar.
* @param {Object} [options={}] - Optional configuration for calendar generation.
* @param {boolean} [options.useGeez=false] - Whether to use the Geez calendar system.
* @param {string} [options.weekdayLang='amharic'] - Language for weekday names (e.g., 'amharic').
* @param {number} [options.weekStart=0] - The starting day of the week (0 = Sunday, 1 = Monday, etc.).
* @param {function|null} [options.holidayFilter=null] - Optional filter function for holidays.
* @returns {Array<Object>} An array of 13 month objects, each containing:
* - {number} month: The month number (1-13).
* - {string} monthName: The name of the month.
* - {number} year: The year of the month.
* - {Array<string>} headers: The headers for the days of the week.
* - {Array<Array<Object>>} days: The grid of day objects for the month.
*/
static getYearCalendar(year, options = {}) {
const { useGeez = false, weekdayLang = 'amharic', weekStart = 0, holidayFilter = null } = options;
const fullYear = [];
for (let month = 1; month <= 13; month++) {
const monthGrid = MonthGrid.create({
year,
month,
useGeez,
weekdayLang,
weekStart,
holidayFilter // Pass filter to MonthGrid
});
fullYear.push({
month,
monthName: monthGrid.monthName,
year: monthGrid.year,
headers: monthGrid.headers,
days: monthGrid.days
});
}
return fullYear;
}
/**
* Generates an array of Kenat instances for a given date range.
* @param {Kenat} startDate - The start of the range.
* @param {Kenat} endDate - The end of the range.
* @returns {Kenat[]} An array of Kenat objects.
* @throws {InvalidInputTypeError} If start or end dates are not Kenat instances.
*/
static generateDateRange(startDate, endDate) {
if (!(startDate instanceof Kenat)) {
throw new InvalidInputTypeError('generateDateRange', 'startDate', 'Kenat instance', startDate);
}
if (!(endDate instanceof Kenat)) {
throw new InvalidInputTypeError('generateDateRange', 'endDate', 'Kenat instance', endDate);
}
const range = [];
let currentDate = startDate;
if (startDate.isAfter(endDate)) {
return [];
}
while (currentDate.isBefore(endDate) || currentDate.isSameDay(endDate)) {
range.push(currentDate);
currentDate = currentDate.addDays(1);
}
return range;
}
// Arithmetic methods start here
/**
* Adds a specified amount of time to the current date.
* Supports method chaining for fluent API.
*
* @param {number} amount - The amount to add.
* @param {string} unit - The unit of time ('days', 'months', 'years').
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
add(amount, unit) {
if (typeof amount !== 'number') {
throw new Error('Amount must be a number');
}
let newDate;
switch (unit) {
case 'days':
newDate = addDays(this.ethiopian, amount);
break;
case 'months':
newDate = addMonths(this.ethiopian, amount);
break;
case 'years':
newDate = addYears(this.ethiopian, amount);
break;
default:
throw new Error(`Invalid unit: ${unit}. Use 'days', 'months', or 'years'`);
}
// Create new Kenat instance with preserved time
const newKenat = new Kenat(newDate);
if (this.time) {
newKenat.time = this.time;
}
return newKenat;
}
/**
* Subtracts a specified amount of time from the current date.
* Supports method chaining for fluent API.
*
* @param {number} amount - The amount to subtract.
* @param {string} unit - The unit of time ('days', 'months', 'years').
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
subtract(amount, unit) {
return this.add(-amount, unit);
}
/**
* Adds a specified number of days to the current Ethiopian date.
* Maintains backward compatibility.
*
* @param {number} days - The number of days to add.
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
addDays(days) {
return this.add(days, 'days');
}
/**
* Returns a new Kenat instance with the date advanced by the specified number of months.
* Maintains backward compatibility.
*
* @param {number} months - The number of months to add to the current date.
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
addMonths(months) {
return this.add(months, 'months');
}
/**
* Returns a new Kenat instance with the year increased by the specified number of years.
* Maintains backward compatibility.
*
* @param {number} years - The number of years to add to the current date.
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
addYears(years) {
return this.add(years, 'years');
}
/**
* Calculates the difference in days between this object's Ethiopian date and another object's Ethiopian date.
*
* @param {Object} other - An object with a `getEthiopian` method that returns an Ethiopian date.
* @returns {number} The number of days difference between the two Ethiopian dates.
*/
diffInDays(other) {
return diffInDays(this.ethiopian, other.getEthiopian());
}
/**
* Calculates the difference in months between this instance's Ethiopian date and another Ethiopian date.
*
* @param {Object} other - An object with a `getEthiopian` method that returns an Ethiopian date.
* @returns {number} The number of months difference between the two Ethiopian dates.
*/
diffInMonths(other) {
return diffInMonths(this.ethiopian, other.getEthiopian());
}
/**
* Calculates the difference in years between this instance's Ethiopian date and another.
*
* @param {Object} other - An object with a getEthiopian() method returning an Ethiopian date.
* @returns {number} The number of years difference between the two Ethiopian dates.
*/
diffInYears(other) {
return diffInYears(this.ethiopian, other.getEthiopian());
}
// Arithmetic methods end here
// Time Methods
getCurrentTime() {
const now = new Date();
const hour = now.getHours();
const minute = now.getMinutes();
return Time.fromGregorian(hour, minute);
}
/**
* Checks if the current Kenat instance's date is before another Kenat instance's date.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the current date is before the other date.
*/
isBefore(other) {
return this.diffInDays(other) < 0;
}
/**
* Checks if the current Kenat instance's date is after another Kenat instance's date.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the current date is after the other date.
*/
isAfter(other) {
return this.diffInDays(other) > 0;
}
/**
* Checks if the current Kenat instance's date is the same as another Kenat instance's date.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the dates are the same.
*/
isSameDay(other) {
const otherEth = other.getEthiopian();
return this.ethiopian.year === otherEth.year &&
this.ethiopian.month === otherEth.month &&
this.ethiopian.day === otherEth.day;
}
/**
* Returns a new Kenat instance set to the start of the specified unit.
* Supports method chaining.
*
* @param {string} unit - The unit ('day', 'month', 'year').
* @returns {Kenat} A new Kenat instance.
*/
startOf(unit) {
switch (unit) {
case 'day':
// Start of day is the same date with time reset to 00:00
const startOfDay = new Kenat(`${this.ethiopian.year}/${this.ethiopian.month}/${this.ethiopian.day}`);
startOfDay.time = new Time(12, 0, 'day'); // 6:00 AM Gregorian
return startOfDay;
case 'month':
const startOfMonth = new Kenat(`${this.ethiopian.year}/${this.ethiopian.month}/1`);
if (this.time) {
startOfMonth.time = this.time;
}
return startOfMonth;
case 'year':
const startOfYear = new Kenat(`${this.ethiopian.year}/1/1`);
if (this.time) {
startOfYear.time = this.time;
}
return startOfYear;
default:
throw new Error(`Invalid unit: ${unit}. Use 'day', 'month', or 'year'`);
}
}
/**
* Returns a new Kenat instance set to the end of the specified unit.
* Supports method chaining.
*
* @param {string} unit - The unit ('day', 'month', 'year').
* @returns {Kenat} A new Kenat instance.
*/
endOf(unit) {
switch (unit) {
case 'day':
// End of day is the same date with time set to 23:59
const endOfDay = new Kenat(`${this.ethiopian.year}/${this.ethiopian.month}/${this.ethiopian.day}`);
endOfDay.time = new Time(12, 0, 'night'); // 6:00 PM Gregorian
return endOfDay;
case 'month':
const lastDay = getEthiopianDaysInMonth(this.ethiopian.year, this.ethiopian.month);
const endOfMonth = new Kenat(`${this.ethiopian.year}/${this.ethiopian.month}/${lastDay}`);
if (this.time) {
endOfMonth.time = this.time;
}
return endOfMonth;
case 'year':
const endOfYear = new Kenat(`${this.ethiopian.year}/13/5`); // Last day of Pagume
if (this.time) {
endOfYear.time = this.time;
}
return endOfYear;
default:
throw new Error(`Invalid unit: ${unit}. Use 'day', 'month', or 'year'`);
}
}
/**
* Returns a new Kenat instance set to the first day of the current month.
* Maintains backward compatibility.
* @returns {Kenat} A new Kenat instance.
*/
startOfMonth() {
return this.startOf('month');
}
/**
* Returns a new Kenat instance set to the last day of the current month.
* Maintains backward compatibility.
* @returns {Kenat} A new Kenat instance.
*/
endOfMonth() {
return this.endOf('month');
}
/**
* Checks if the current Ethiopian year is a leap year.
* @returns {boolean} True if it is a leap year.
*/
isLeapYear() {
return isEthiopianLeapYear(this.ethiopian.year);
}
/**
* Returns the weekday number for the current date.
* @returns {number} The day of the week (0 for Sunday, 6 for Saturday).
*/
weekday() {
return getWeekday(this.ethiopian);
}
// --- Distance helpers ---
/**
* Returns a breakdown of distance from this date to another date.
* @param {Kenat|{year:number,month:number,day:number}|string|Date} other - target date
* @param {{units?: ('years'|'months'|'days')[], output?: 'object'|'string', lang?: 'english'|'amharic'}} [options]
* @returns {Object|string}
*/
distanceTo(other, options = {}) {
const target = Kenat._coerceToKenat(other);
const breakdown = diffBreakdown(target.getEthiopian(), this.getEthiopian(), { units: options.units || ['years', 'months', 'days'] });
if ((options.output || 'object') === 'string') {
return Kenat.formatDistance(breakdown, options);
}
return breakdown;
}
/**
* Returns a breakdown of distance from today to this date.
*/
distanceFromToday(options = {}) {
const today = new Kenat();
return today.distanceTo(this, options);
}
/**
* Returns distance from today to the specified holiday occurrence.
* @param {string} holidayKey
* @param {{direction?: 'auto'|'future'|'past', units?: ('years'|'months'|'days')[], output?: 'object'|'string', lang?: 'english'|'amharic'}} [options]
*/
static distanceToHoliday(holidayKey, options = {}) {
const today = new Kenat();
const next = Kenat._getHolidayOccurrence(holidayKey, today.getEthiopian(), 'next');
const prev = Kenat._getHolidayOccurrence(holidayKey, today.getEthiopian(), 'prev');
const direction = options.direction || 'auto';
let target = next;
if (direction === 'past') target = prev;
if (direction === 'auto') {
const dNext = Math.abs(diffInDays(next.getEthiopian(), today.getEthiopian()));
const dPrev = Math.abs(diffInDays(prev.getEthiopian(), today.getEthiopian()));
target = dNext <= dPrev ? next : prev;
}
return today.distanceTo(target, options);
}
/**
* Formats a breakdown result to human string like "1 year 2 months 5 days".
*/
static formatDistance(breakdown, options = {}) {
const lang = options.lang || 'english';
const parts = [];
const units = options.units || ['years', 'months', 'days'];
const labelsEn = { years: 'year', months: 'month', days: 'day' };
const labelsAm = { years: 'ዓመት', months: 'ወር', days: 'ቀን' };
const labels = lang === 'amharic' ? labelsAm : labelsEn;
const plural = (n, k) => lang === 'amharic' ? labels[k] : (n === 1 ? labels[k] : labels[k] + 's');
if (units.includes('years') && typeof breakdown.years === 'number') parts.push(`${breakdown.years} ${plural(breakdown.years, 'years')}`);
if (units.includes('months') && typeof breakdown.months === 'number') parts.push(`${breakdown.months} ${plural(breakdown.months, 'months')}`);
if (units.includes('days') && typeof breakdown.days === 'number') parts.push(`${breakdown.days} ${plural(breakdown.days, 'days')}`);
const base = parts.length ? parts.join(' ') : `0 ${plural(0, units[units.length - 1])}`;
if (lang === 'amharic') {
return `${base}`;
}
return base;
}
// --- internal helpers ---
static _coerceToKenat(input) {
if (input instanceof Kenat) return input;
if (input instanceof Date) return new Kenat(input);
if (typeof input === 'string') return new Kenat(input);
if (input && typeof input === 'object' && 'year' in input && 'month' in input && 'day' in input) return new Kenat(input);
throw new Error('Unsupported date input');
}
static _getHolidayOccurrence(holidayKey, refEth, which) {
const thisYear = getHoliday(holidayKey, refEth.year);
const prevYear = getHoliday(holidayKey, refEth.year - 1);
const nextYear = getHoliday(holidayKey, refEth.year + 1);
const candidates = [thisYear, prevYear, nextYear].filter(Boolean).map(h => new Kenat(h.ethiopian));
const ref = new Kenat(refEth);
const sorted = candidates.sort((a, b) => diffInDays(a.getEthiopian(), ref.getEthiopian()) - diffInDays(b.getEthiopian(), ref.getEthiopian()));
if (which === 'prev') {
const past = candidates.filter(c => diffInDays(c.getEthiopian(), ref.getEthiopian()) <= 0).sort((a, b) => Math.abs(diffInDays(b.getEthiopian(), ref.getEthiopian())) - Math.abs(diffInDays(a.getEthiopian(), ref.getEthiopian())));
return past[0] || sorted[0];
}
const future = candidates.filter(c => diffInDays(c.getEthiopian(), ref.getEthiopian()) >= 0).sort((a, b) => Math.abs(diffInDays(a.getEthiopian(), ref.getEthiopian())) - Math.abs(diffInDays(b.getEthiopian(), ref.getEthiopian())));
return future[0] || sorted[0];
}
}
import { Kenat } from './Kenat.js';
import { getHolidaysInMonth } from './holidays.js';
import { toGeez } from './geezConverter.js';
import { orthodoxMonthlydays } from './nigs.js';
import { daysOfWeek, monthNames, HolidayTags, holidayInfo } from './constants.js';
import { getWeekday, validateNumericInputs } from './utils.js';
import { InvalidGridConfigError } from './errors/errorHandler.js';
export class MonthGrid {
constructor(config = {}) {
this._validateConfig(config);
const current = Kenat.now().getEthiopian();
this.year = config.year ?? current.year;
this.month = config.month ?? current.month;
this.weekStart = config.weekStart ?? 1;
this.useGeez = config.useGeez ?? false;
this.weekdayLang = config.weekdayLang ?? 'amharic';
this.holidayFilter = config.holidayFilter ?? null;
this.mode = config.mode ?? null;
this.showAllSaints = config.showAllSaints ?? false;
}
_validateConfig(config) {
const { year, month, weekStart, weekdayLang } = config;
if ((year !== undefined && month === undefined) || (year === undefined && month !== undefined)) {
throw new InvalidGridConfigError('If providing year or month, both must be provided.');
}
if (year !== undefined) validateNumericInputs('MonthGrid.constructor', { year });
if (month !== undefined) validateNumericInputs('MonthGrid.constructor', { month });
if (weekStart !== undefined) {
validateNumericInputs('MonthGrid.constructor', { weekStart });
if (weekStart < 0 || weekStart > 6) {
throw new InvalidGridConfigError(`Invalid weekStart value: ${weekStart}. Must be between 0 and 6.`);
}
}
if (weekdayLang !== undefined) {
if (typeof weekdayLang !== 'string' || !Object.keys(daysOfWeek).includes(weekdayLang)) {
throw new InvalidGridConfigError(`Invalid weekdayLang: "${weekdayLang}". Must be one of [${Object.keys(daysOfWeek).join(', ')}].`);
}
}
}
static create(config = {}) {
const instance = new MonthGrid(config);
return instance.generate();
}
generate() {
const rawDays = this._getRawDays();
const holidays = this._getFilteredHolidays();
const saints = this._getSaintsMap();
const paddedDays = this._mergeDays(rawDays, holidays, saints);
const headers = this._getWeekdayHeaders();
const monthName = this._getLocalizedMonthName();
const yearLabel = this._getLocalizedYear();
return {
headers,
days: paddedDays,
year: yearLabel,
month: this.month,
monthName,
up: () => this.up().generate(),
down: () => this.down().generate()
};
}
_getRawDays() {
const base = new Kenat(`${this.year}/${this.month}/1`);
return base.getMonthCalendar(this.year, this.month, this.useGeez);
}
_getFilteredHolidays() {
let filter = this.holidayFilter;
if (this.mode === 'christian') filter = [HolidayTags.CHRISTIAN];
if (this.mode === 'muslim') filter = [HolidayTags.MUSLIM];
if (this.mode === 'public') filter = [HolidayTags.PUBLIC];
return getHolidaysInMonth(this.year, this.month, {
lang: this.weekdayLang,
filter
});
}
_getSaintsMap() {
if (this.mode !== 'christian') return {};
const map = {};
Object.entries(orthodoxMonthlydays).forEach(([saintKey, saint]) => {
if (saint.events) {
// This is a nested saint object with multiple events
const nigsEvent = saint.events.find(event =>
Array.isArray(event.negs) ? event.negs.includes(this.month) : event.negs === this.month
);
if (nigsEvent) {
// It's a major feast ("Nigs") month, so show the specific event
const day = saint.recuringDate;
if (!map[day]) map[day] = [];
map[day].push({
key: nigsEvent.key,
name: saint.name[this.weekdayLang] || saint.name.english,
description: nigsEvent.description[this.weekdayLang] || nigsEvent.description.english,
isNigs: true,
tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN, 'NIGS']
});
} else if (this.showAllSaints && saint.defaultDescription) {
// It's NOT a major feast month, but the user wants to see all saints. Show the generic commemoration.
const day = saint.recuringDate;
if (!map[day]) map[day] = [];
map[day].push({
key: saintKey, // Use the parent key for the generic event
name: saint.name[this.weekdayLang] || saint.name.english,
description: saint.defaultDescription[this.weekdayLang] || saint.defaultDescription.english,
isNigs: false,
tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN, 'SAINT_DAY']
});
}
} else {
// This is a flat (single-event) saint object
const isNigs = Array.isArray(saint.negs) ? saint.negs.includes(this.month) : saint.negs === this.month;
if (isNigs || this.showAllSaints) {
const day = saint.recuringDate;
if (!map[day]) map[day] = [];
map[day].push({
key: saint.key,
name: saint.name[this.weekdayLang] || saint.name.english,
description: saint.description[this.weekdayLang] || saint.description.english,
isNigs,
tags: [HolidayTags.RELIGIOUS, HolidayTags.CHRISTIAN, isNigs ? 'NIGS' : 'SAINT_DAY']
});
}
}
});
return map;
}
_mergeDays(rawDays, holidaysList, saintsMap) {
const today = Kenat.now().getEthiopian();
const labels = daysOfWeek[this.weekdayLang] || daysOfWeek.amharic;
const monthLabels = monthNames[this.weekdayLang] || monthNames.amharic;
const holidayMap = {};
holidaysList.forEach(h => {
const key = `${h.ethiopian.year}-${h.ethiopian.month}-${h.ethiopian.day}`;
if (!holidayMap[key]) holidayMap[key] = [];
holidayMap[key].push(h);
});
const mapped = rawDays.map(day => {
const eth = day.ethiopian;
const greg = day.gregorian;
const weekday = getWeekday(eth);
const key = `${eth.year}-${eth.month}-${eth.day}`;
let holidays = holidayMap[key] || [];
if (this.mode === 'christian') {
holidays = holidays.concat(saintsMap[eth.day] || []);
}
if (this.mode === 'muslim' && weekday === 5) {
const j = holidayInfo.jummah;
holidays.push({
key: 'jummah',
name: j.name[this.weekdayLang] || j.name.english,
description: j.description[this.weekdayLang] || j.description.english,
tags: [HolidayTags.RELIGIOUS, HolidayTags.MUSLIM]
});
}
return {
ethiopian: {
year: this.useGeez ? toGeez(eth.year) : eth.year,
month: this.useGeez ? monthLabels[eth.month - 1] : eth.month,
day: this.useGeez ? toGeez(eth.day) : eth.day
},
gregorian: greg,
weekday,
weekdayName: labels[weekday],
isToday: eth.year === today.year && eth.month === today.month && eth.day === today.day,
holidays
};
});
const offset = ((mapped.length > 0 ? mapped[0].weekday : (new Date(this.year, this.month - 1, 1).getDay())) - this.weekStart + 7) % 7;
return Array(offset).fill(null).concat(mapped);
}
_getWeekdayHeaders() {
const labels = daysOfWeek[this.weekdayLang] || daysOfWeek.amharic;
return labels.slice(this.weekStart).concat(labels.slice(0, this.weekStart));
}
_getLocalizedMonthName() {
return (monthNames[this.weekdayLang] || monthNames.amharic)[this.month - 1];
}
_getLocalizedYear() {
return this.useGeez ? toGeez(this.year) : this.year;
}
up() {
if (this.month === 13) {
this.month = 1;
this.year++;
} else {
this.month++;
}
return this;
}
down() {
if (this.month === 1) {
this.month = 13;
this.year--;
} else {
this.month--;
}
return this;
}
}
export const orthodoxMonthlydays = {
ledeta_mariam: {
key: "ledeta_mariam",
name: {
english: "Ldata Mariam, The nativity of Our Holy Mother Virgin Mariam",
amharic: "ልደታ ማርያም",
},
description: {
english: "Commemoration of the birth of St. Mary.",
amharic: "የቅድስት ድንግል ማርያም ልደት መታሰቢያ።",
},
recuringDate: 1,
major: true,
negs: 9,
},
archangel_raguel: {
key: "archangel_raguel",
name: {
english: "Archangel Raguel",
amharic: "ራጉኤል",
},
description: {
english: "Commemoration of Archangel Raguel.",
amharic: "የመላኩ ራጉኤል መታሰቢያ።",
},
recuringDate: 1,
major: true,
negs: null,
},
bartholomew_the_apostle: {
key: "bartholomew_the_apostle",
name: {
english: "Bartholomew the Apostle",
amharic: "ሐዋርያው በርተሎሜዎስ",
},
description: {
english: "Commemoration of Bartholomew the Apostle.",
amharic: "የሐዋርያው በርተሎሜዎስ መታሰቢያ።",
},
recuringDate: 1,
major: true,
negs: null,
},
prophet_elijah_ginbot: {
key: "prophet_elijah_ginbot",
name: {
english: "Prophet Elijah",
amharic: "ነብዩ ኤልያስ",
},
description: {
english: "Commemoration of Prophet Elijah.",
amharic: "የነብዩ ኤልያስ መታሰቢያ።",
},
recuringDate: 1,
major: true,
negs: 4,
},
kidus_yohannes: {
key: "kidus_yohannes",
name: {
english: "Kidus Yohannes",
amharic: "ቅዱስ ዮሐንስ",
},
description: {
english: "Commemoration of Kidus Yohannes.",
amharic: "የቅዱስ ዮሐንስ መታሰቢያ።",
},
recuringDate: 1,
major: false,
negs: 1,
},
abba_guba: {
key: "abba_guba",
name: {
english: "Abba Guba, one of the Nine Saints",
amharic: "አባ ጉባ",
},
description: {
english: "Commemoration of Abba Guba, one of the Nine Saints.",
amharic: "ከዘጠኙ ቅዱሳን አንዱ የአባ ጉባ መታሰቢያ።",
},
recuringDate: 2,
major: true,
negs: null,
},
thaddeus_the_apostle: {
name: {
english: "Thaddeus the Apostle",
amharic: "ሐዋርያው ታድዮስ",
},
recuringDate: 2,
defaultDescription: {
english: "Commemoration of Thaddeus the Apostle.",
amharic: "ሐዋርያው ታድዮስ መታሰቢያ",
},
events: [
{
key: "thaddeus_the_apostle_major",
description: { english: "Commemoration of Thaddeus the Apostle.", amharic: "የሐዋርያው ታድዮስ መታሰቢያ።" },
major: true,
negs: 11,
},
{
key: "thaddeus_apostle_minor",
description: { english: "Commemoration of Thaddeus the Apostle.", amharic: "የሐዋርያው ታድዮስ መታሰቢያ።" },
major: false,
negs: 13,
}
]
},
st_john_the_baptist_martyrdom: {
key: "st_john_the_baptist_martyrdom",
name: {
english: "The Martyrdom (Beheading) of St. John the Baptist",
amharic: "የቅዱስ ዮሐንስ መጥምቅ አንገት መቁረጥ",
},
description: {
english:
"Commemoration of the Martyrdom (Beheading) of St. John the Baptist.",
amharic: "የቅዱስ ዮሐንስ መጥምቅ አንገት መቁረጥ መታሰቢያ።",
},
recuringDate: 2,
major: false,
negs: 1,
},
beata: {
key: "beata",
name: {
english:
"Beata: The Entrance of the three years old Virgin Mariam into the Temple",
amharic: "በአታለማርያም",
},
description: {
english: "Commemoration of St. Mary's entrance into the Temple.",
amharic: "የቅድስት ድንግል ማርያም ወደ ቤተመቅደስ መግባት መታሰቢያ።",
},
recuringDate: 3,
major: true,
negs: 4,
},
abune_zena_markos: {
key: "abune_zena_markos",
name: {
english: "Abune Zena Markos",
amharic: "አቡነ ዜና ማርቆስ",
},
description: {
english: "Commemoration of Abune Zena Markos.",
amharic: "የአቡነ ዜና ማርቆስ መታሰቢያ።",
},
recuringDate: 3,
major: true,
negs: null,
},
archangel_phanuel: {
key: "archangel_phanuel",
name: {
english: "Archangel Phanuel",
amharic: "ቅዱስ ፋኑኤል",
},
description: {
english: "Commemoration of Archangel Phanuel.",
amharic: "የመላኩ ፋኑኤል መታሰቢያ።",
},
recuringDate: 3,
major: true,
negs: 13,
},
saint_neakuto_leab: {
key: "saint_neakuto_leab",
name: {
english: "Saint Neakuto Leab",
amharic: "ቅዱስ ነአኩቶ ለአብ",
},
description: {
english: "Commemoration of Saint Neakuto Leab.",
amharic: "የቅዱስ ነአኩቶ ለአብ መታሰቢያ።",
},
recuringDate: 3,
major: true,
negs: null,
},
johannis_welde_negedguad: {
key: "johannis_welde_negedguad",
name: {
english: "John the son of the Thunder (Yohannis Welde Negedguad)",
amharic: "ዮሐንስ ወልደ ነጎድጓድ",
},
description: {
english: "Commemoration of John the son of the Thunder.",
amharic: "የዮሐንስ ወልደ ነጎድጓድ መታሰቢያ።",
},
recuringDate: 4,
major: true,
negs: 5,
},
andrew_the_apostle: {
key: "andrew_the_apostle",
name: {
english: "Andrew the Apostle",
amharic: "ሐዋርያው እንድርያስ",
},
description: {
english: "Commemoration of Andrew the Apostle.",
amharic: "የሐዋርያው እንድርያስ መታሰቢያ።",
},
recuringDate: 4,
major: true,
negs: 4,
},
petros_we_paulos: {
key: "petros_we_paulos",
name: {
english: "Petros we Paulos (Peter and Paul)",
amharic: "ጴጥሮስወ ጳውሎስ",
},
description: {
english: "Commemoration of Saints Peter and Paul.",
amharic: "የቅዱሳን ጴጥሮስና ጳውሎስ መታሰቢያ።",
},
recuringDate: 5,
major: true,
negs: 11,
},
abuna_gebre_menfes_kidus: {
key: "abuna_gebre_menfes_kidus",
name: {
english: "Abuna Gebre Menfes Kidus",
amharic: "ገብረ መንፈስ ቅዱስ",
},
description: {
english: "Commemoration of Abuna Gebre Menfes Kidus.",
amharic: "የአቡነ ገብረ መንፈስ ቅዱስ መታሰቢያ።",
},
recuringDate: 5,
major: true,
negs: 2,
},
abune_arone: {
key: "abune_arone",
name: {
english: "Abune Arone",
amharic: "አቡነ አሮን",
},
description: {
english: "Commemoration of Abune Arone.",
amharic: "የአቡነ አሮን መታሰቢያ።",
},
recuringDate: 5,
major: true,
negs: null,
},
iyyasu: {
key: "iyyasu",
name: {
english: "Iyyasus",
amharic: "ኢየሱስ",
},
description: {
english: "Commemoration of Iyyasus.",
amharic: "የኢየሱስ መታሰቢያ።",
},
recuringDate: 6,
major: true,
negs: 5,
},
dabra_quesqam_mariam: {
key: "dabra_quesqam_mariam",
name: {
english: "Dabra Quesqam Mariam",
amharic: "ቁስቋም ማርያም",
},
description: {
english: "Commemoration of Dabra Quesqam Mariam.",
amharic: "የቁስቋም ማርያም መታሰቢያ።",
},
recuringDate: 6,
major: true,
negs: 3,
},
saint_arsema: {
key: "saint_arsema",
name: {
english: "Saint Arsema",
amharic: "አርሴማ",
},
description: {
english: "Commemoration of Saint Arsema.",
amharic: "የቅድስት አርሴማ መታሰቢያ።",
},
recuringDate: 6,
major: true,
negs: 4,
},
holy_trinity: {
key: "holy_trinity",
name: {
english: "Holy Trinity",
amharic: "አጋዕዝተ አለም ስላሴ",
},
description: {
english: "Commemoration of the Holy Trinity.",
amharic: "የአጋዕዝተ ዓለም ስላሴ መታሰቢያ።",
},
recuringDate: 7,
major: true,
negs: 5,
},
cherubim: {
key: "cherubim",
name: {
english: "Cherubim (Four Living Creatures)",
amharic: "ኪሩቤል አርባእቱ እንስሳ",
},
description: {
english: "Commemoration of the Cherubim, the four living creatures.",
amharic: "የኪሩቤል አርባእቱ እንስሳ መታሰቢያ።",
},
recuringDate: 8,
major: true,
negs: 3,
},
saint_matthias_apostle: {
key: "saint_matthias_apostle",
name: {
english: "Saint Matthias the Apostle",
amharic: "ሐዋርያው ማትያስ",
},
description: {
english: "Commemoration of Saint Matthias the Apostle.",
amharic: "የሐዋርያው ማትያስ መታሰቢያ።",
},
recuringDate: 8,
major: true,
negs: 7,
},
abune_kiros: {
key: "abune_kiros",
name: {
english: "Abune Kiros",
amharic: "አቡነ ኪሮስ",
},
description: {
english: "Commemoration of Abune Kiros.",
amharic: "የአቡነ ኪሮስ መታሰቢያ።",
},
recuringDate: 8,
major: true,
negs: 3,
},
abba_banuda: {
key: "abba_banuda",
name: {
english: "Abba Banuda",
amharic: "አባ ባኑዳ",
},
description: {
english: "Commemoration of Abba Banuda.",
amharic: "የአባ ባኑዳ መታሰቢያ።",
},
recuringDate: 8,
major: false,
negs: null,
},
saint_thomas_apostle: {
key: "saint_thomas_apostle",
name: {
english: "Saint Thomas the Apostle",
amharic: "ሐዋርያው ቶማስ",
},
description: {
english: "Commemoration of Saint Thomas the Apostle.",
amharic: "የሐዋርያው ቶማስ መታሰቢያ።",
},
recuringDate: 9,
major: true,
negs: 2,
},
abune_isitinifase_kirisitosi: {
key: "abune_isitinifase_kirisitosi",
name: {
english: "Abune Isitinifase Kirisitosi",
amharic: "አቡነ እስትንፋሰ ክርስቶስ",
},
description: {
english: "Commemoration of Abune Isitinifase Kirisitosi.",
amharic: "የአቡነ እስትንፋሰ ክርስቶስ መታሰቢያ።",
},
recuringDate: 9,
major: true,
negs: null,
},
three_eighteen_holy_fathers_nice: {
key: "three_eighteen_holy_fathers_nice",
name: {
english: "The 318 holy fathers assembled in the city of Nice",
amharic: "ሰልስቱ ምዕት",
},
description: {
english:
"Commemoration of the 318 holy fathers assembled in the city of Nice.",
amharic: "በኒቂያ ከተማ የተሰበሰቡት 318 ቅዱሳን አባቶች መታሰቢያ።",
},
recuringDate: 9,
major: true,
negs: null,
},
holy_cross: {
key: "holy_cross",
name: {
english: "Holy Cross (Masqal)",
amharic: "መስቀለ ክርስቶስ",
},
description: {
english:
"Commemoration of the Holy Cross (Masqal)/the finding of true cross.",
amharic: "የመስቀለ ክርስቶስ መታሰቢያ።",
},
recuringDate: 10,
major: true,
negs: 7,
},
tsedeniya_mariyami: {
key: "tsedeniya_mariyami",
name: {
english: "Ts'edeniya Mariyami",
amharic: "ፀደንያ ማርያም",
},
description: {
english: "Commemoration of Ts'edeniya Mariyami.",
amharic: "የፀደንያ ማርያም መታሰቢያ።",
},
recuringDate: 10,
major: true,
negs: 1,
},
simon_the_zealot: {
key: "simon_the_zealot",
name: {
english: "Simon the Zealot",
amharic: "ስምኦን ቀኖናዊ ሐዋርያ",
},
description: {
english: "Commemoration of Simon the Zealot.",
amharic: "የስምኦን ቀኖናዊ ሐዋርያ መታሰቢያ።",
},
recuringDate: 10,
major: true,
negs: 11,
},
saint_hanna: {
key: "saint_hanna",
name: {
english: "Saint Hanna, the mother of our holy Mother",
amharic: "ሐና",
},
description: {
english: "Commemoration of Saint Hanna, the mother of Our Holy Mother.",
amharic: "የቅድስት ሐና (የእመቤታችን እናት) መታሰቢያ።",
},
recuringDate: 11,
major: true,
negs: 11,
},
saint_joachim: {
key: "saint_joachim",
name: {
english: "Saint Joachim, the father of our holy Mother",
amharic: "ኢያቄም",
},
description: {
english: "Commemoration of Saint Joachim, the father of Our Holy Mother.",
amharic: "የቅዱስ ኢያቄም (የእመቤታችን አባት) መታሰቢያ።",
},
recuringDate: 11,
major: false,
negs: null,
},
saint_yared: {
key: "saint_yared",
name: {
english: "Saint Yared",
amharic: "ቅዱስ ያሬድ",
},
description: {
english: "Commemoration of Saint Yared.",
amharic: "የቅዱስ ያሬድ መታሰቢያ።",
},
recuringDate: 11,
major: true,
negs: 9,
},
abune_hara: {
key: "abune_hara",
name: {
english: "Abune Hara",
amharic: "አቡነ ሐራ",
},
description: {
english: "Commemoration of Abune Hara.",
amharic: "የአቡነ ሐራ መታሰቢያ።",
},
recuringDate: 11,
major: false,
negs: null,
},
archangel_michael: {
key: "archangel_michael",
name: {
english: "Archangel Michael",
amharic: "ቅዱስ ሚካኤል",
},
description: {
english: "Commemoration of Archangel Michael.",
amharic: "የቅዱስ ሚካኤል መታሰቢያ።",
},
recuringDate: 12,
major: true,
negs: [3, 12],
},
matthew_apostle: {
key: "matthew_apostle",
name: {
english: "Matthew the Apostle",
amharic: "ሐዋርያ ማቴዎስ",
},
description: {
english: "Commemoration of Matthew the Apostle.",
amharic: "የሐዋርያ ማቴዎስ መታሰቢያ።",
},
recuringDate: 12,
major: true,
negs: null,
},
abba_samuel_waldeba: {
key: "abba_samuel_waldeba",
name: {
english: "Abba Samuel from Waldeba",
amharic: "አባ ሳሙኤል",
},
description: {
english: "Commemoration of Abba Samuel from Waldeba.",
amharic: "ከዋልድባ የአባ ሳሙኤል መታሰቢያ።",
},
recuringDate: 12,
major: true,
negs: 4,
},
god_the_father: {
key: "god_the_father",
name: {
english: "Egziabher Abe (God the Father)",
amharic: "እግዚአብሔር አብ",
},
description: {
english: "Commemoration of God the Father.",
amharic: "የእግዚአብሔር አብ መታሰቢያ።",
},
recuringDate: 13,
major: true,
negs: 3,
},
archangel_raphael: {
key: "archangel_raphael",
name: {
english: "Archangel Raphael",
amharic: "ቅዱስ ሩፋኤል",
},
description: {
english: "Commemoration of Archangel Raphael.",
amharic: "የቅዱስ ሩፋኤል መታሰቢያ።",
},
recuringDate: 13,
major: true,
negs: 4,
},
saint_abba_zara_buruk: {
key: "saint_abba_zara_buruk",
name: {
english: "Saint Abba Zar'a-Buruk",
amharic: "አቡነ ዘርአ ብሩክ",
},
description: {
english:
"Commemoration of Saint Abba Zar'a-Buruk-departed from this world.",
amharic: "የቅዱስ አባ ዘርአ ብሩክ እረፍታቸው መታሰቢያ።",
},
recuringDate: 13,
major: true,
negs: 5,
},
// 14. Abuna Aragwi
abuna_aragwi: {
key: "abuna_aragwi",
name: {
english: "Abuna Aragwi-one of the Nine Saints",
amharic: "አቡነ አረጋዊ ",
},
description: {
english: "Commemoration of Abuna Aragwi, one of the Nine Saints",
amharic: "ከዘጠኙ ቅዱሳን አንዱ የአቡነ አረጋዊ መታሰቢያ።",
},
recuringDate: 14,
major: true,
negs: 2,
},
gabra_krestos_the_hermit: {
key: "gabra_krestos_the_hermit",
name: {
english: "Gabra Krestos the hermit",
amharic: "ገብረ ክርስቶስ/ገብረ መርአዊ",
},
description: {
english: "Commemoration of Gabra Krestos the hermit.",
amharic: "የገብረ ክርስቶስ ገዳማዊ መታሰቢያ።",
},
recuringDate: 14,
major: true,
negs: 9,
},
cyriacus_julietta: {
key: "cyriacus_julietta",
name: {
english: "St. Cyriacus and St. Julietta",
amharic: "ቂርቆስና እየሉጣ",
},
description: {
english: "Commemoration of St. Cyriacus and St. Julietta.",
amharic: "የቅዱስ ቂርቆስና የቅድስት እየሉጣ መታሰቢያ።",
},
recuringDate: 15,
major: true,
negs: [11, 5],
},
saint_stefanos_migration_of_his_body: {
key: "saint_stefanos_migration_of_his_body",
name: {
english: "Saint Stefanos",
amharic: "ቅዱስ እስጢፋኖስ ",
},
description: {
english:
"Commemoration of Saint Estifanos the First Martyr migration of his body",
amharic: "የቅዱስ እስጢፋኖስ ሰማዕት (ቀዳሚ ሰማዕትና ሊቀ ዲያቆናት) አካል ሽግግራቸው መታሰቢያ",
},
recuringDate: 15,
major: false,
negs: null,
},
kidane_meheret: {
key: "kidane_meheret",
name: {
english: "Kidane Meheret - Our Lady, Covenant of Mercy",
amharic: "ኪዳነ ምህረት",
},
description: {
english: "Commemoration of Kidane Meheret (Covenant of Mercy).",
amharic: "የኪዳነ ምህረት መታሰቢያ።",
},
recuringDate: 16,
major: true,
negs: [12, 6],
},
johannis_welde_negedguad_erget: {
key: "johannis_welde_negedguad_16",
name: {
english: "John the son of the Thunder (Yohannis Welde Negedguad)",
amharic: "ዮሐንስ ወልደ ነጎድጓድ",
},
description: {
english:
"Commemoration of John, the Son of Thunder his taken away to heaven.",
amharic: "የዮሐንስ ወልደ ነጎድጓድ ወደ ሰማይ መውሰዳቸው መታሰቢያ።",
},
recuringDate: 16,
major: false,
negs: 11,
},
saint_stefanos: {
key: "saint_stefanos",
name: {
english:
"Saint Stefanos (Stephen the Martyr) - the First Martyr and Archdeacon)",
amharic: "ቅዱስ እስጢፋኖስ ፣ ሐዋርያውያዕቆብ ወልደ ዘብዲዮስ ፣ አባ ገሪማ",
},
description: {
english:
"Commemoration of Saint Stephen the Martyr (First Martyr and Archdeacon) ordination date",
amharic: "የቅዱስ እስጢፋኖስ ሰማዕት (ቀዳሚ ሰማዕትና ሊቀ ዲያቆናት) የክህነት ዘመቻቸው መታሰቢያ",
},
recuringDate: 17,
major: true,
negs: 2,
},
james_the_apostle_17: {
key: "james_the_apostle_17",
name: {
english: " James the apostle",
amharic: " ሐዋርያው ያዕቆብ ወልደ ዘብዲዮስ",
},
description: {
english: "Commemoration of James the Apostle.",
amharic: "የሐዋርያው ያዕቆብ ወልደ ዘብዲዮስ መታሰቢያ።",
},
recuringDate: 17,
major: true,
negs: 8,
},
abba_gerima: {
key: "abba_gerima",
name: {
english: "Abba Gerima (one of the Nine saints)",
amharic: "አባ ገሪማ",
},
description: {
english: "Commemoration of Abba Gerima (one of the Nine Saints).",
amharic: "የአባ ገሪማ (ከዘጠኙ ቅዱሳን አንዱ) መታሰቢያ።",
},
recuringDate: 17,
major: true,
negs: null,
},
philip_the_apostle: {
key: "philip_the_apostle",
name: {
english: "Philip the Apostle /Abune Ewostatewos",
amharic: "ሐዋርያው ፊሊጶስ ",
},
description: {
english: "Commemoration of Philip the Apostle.",
amharic: "የሐዋርያው ፊሊጶስ መታሰቢያ።",
},
recuringDate: 18,
major: true,
negs: 3,
},
abune_ewostatewos: {
key: "abune_ewostatewos",
name: {
english: "Abune_Ewostatewos",
amharic: "አቡነ ኢዩስጣቲዮስ",
},
description: {
english: "Commemoration of Abune Ewostatewos.",
amharic: "የአቡነ ኢዩስጣቲዮስ መታሰቢያ።",
},
recuringDate: 18,
major: true,
negs: 1,
},
saint_georgis_scattering_of_his_bone: {
key: "saint_georgis_scattering_of_his_bone",
name: {
english: "Saint Georgis",
amharic: "ቅዱስ ጊዮርጊስ",
},
description: {
english:
" Feast of the Great Martyr Saint George, the scattering of his bone",
amharic: "ቅዱስ ጊዮርጊስ የአጥንቱ ፍልሰት መታሰቢያ",
},
recuringDate: 24,
major: false,
negs: 5,
},
james_the_apostle_18: {
key: "james_the_apostle_18",
name: {
english: " James the apostle",
amharic: " ሐዋርያው ያዕቆብ ወልደ ዘብዲዮስ",
},
description: {
english: "Commemoration of James the Apostle.",
amharic: "የሐዋርያው ያዕቆብ ወልደ ዘብዲዮስ መታሰቢያ።",
},
recuringDate: 18,
major: false,
negs: 11,
},
// 19. Gabriel the Archangel
gabriel_archangel_rescue_of_the_three_youths: {
key: "gabriel_archangel_rescue_of_the_three_youths",
name: {
english: "Gabriel the Archangel",
amharic: "ቅዱስ ገብርኤል",
},
description: {
english:
"Commemoration of Archangel Gabriel is dedicated to the rescue of the three youths (Sidraq, Misaq, and Abdenago).",
amharic: "ሊቀ መላእክት ገብርኤል ሦስቱን ወጣቶች (ሲድራቅ፣ ሚሳቅና አብደናጎን) ማዳን መታሰቢያ",
},
recuringDate: 19,
major: true,
negs: 4,
},
gabriel_archangel_rescue_of_rescue_of_quirqos_and_his_mother: {
key: "gabriel_archangel_rescue_of_rescue_of_quirqos_and_his_mother",
name: {
english: "Gabriel the Archangel",
amharic: "ቅዱስ ገብርኤል",
},
description: {
english:
"Commemoration of Archangel Gabriel is dedicated to the rescue of rescue of Quirqos and his mother, Saint Iyeluta (Julitta). .",
amharic: "የእግዚአብሔር መልአክ ቅዱስ ገብርኤል ቂርቆስን እና እናቱን ቅድስት ኢየሉጣን ማዳን መታሰቢያ",
},
recuringDate: 19,
major: false,
negs: 11,
},
hnstata_betkerestyan: {
key: "hnstata_betkerestyan",
name: {
english: "Hnstata Betkerestyan",
amharic: "ሕንፀተ ቤተ ክርስቲያን",
},
description: {
english:
"Commemoration of the building of the church in the name of Our Holy Virgin Mary.",
amharic:
"ሄነሳተ ቤተ ክርስቲያን - ለቅድስት ድንግል ማርያም የተሰጠች የመጀመሪያዋ ቤተ ክርስቲያን በፊልጵስዩስ መመሥረት",
},
recuringDate: 20,
major: true,
negs: 10,
},
egzeet_na_maryam_departure: {
key: "egzeet_na_maryam_departure",
name: {
english: "Egze'et-na Maryam",
amharic: "ቅድስት ድንግል ማርያም",
},
description: {
english: "The departure of Our Holy Mother.",
amharic: "የቅድስት ድንግል ማርያም ዕርገት",
},
recuringDate: 21,
major: false,
negs: 5,
},
egzeet_na_maryam_debre_mitmak: {
key: "egzeet_na_maryam_debre_mitmak",
name: {
english: "Egze'et-na Maryam",
amharic: "ቅድስት ድንግል ማርያም",
},
description: {
english:
"The appearance of Our Holy Mother Virgin Mariam in the church of Debre Mitmak",
amharic: "የቅድስት ድንግል ማርያም በደብረ ምጥማቅ ቤተ ክርስቲያን መገለጥ።",
},
recuringDate: 21,
major: false,
negs: 9,
},
egzeet_na_maryam_prayed_at_golgota: {
key: "egzeet_na_maryam_prayed_at_golgota",
name: {
english: "Egze'et-na Maryam",
amharic: "ቅድስት ድንግል ማርያም",
},
description: {
english: "Sene Golgota - Our Holy Mother Virgin Mariam prayed at Golgota",
amharic: "ሰኔ ጎልጎታ - ቅድስት ድንግል ማርያም በጎልጎታ ጸለየች",
},
recuringDate: 21,
major: false,
negs: 10,
},
// 22. Archangel Uriel
archangel_uriel: {
key: "archangel_uriel",
name: {
english: "Archangel Uriel",
amharic: "ቅዱስ ኡራኤል",
},
description: {
english: "Archangel Uriel received covenant and ordained as an Archangel",
amharic: "ሊቀ መላእክት ዑራኤል ቃል ኪዳን ተቀብሎ ሊቀ መልአክ ሆኖ ተሾመ።",
},
recuringDate: 22,
major: false,
negs: 11,
},
saint_lukas: {
key: "saint_lukas",
name: {
english: "Saint Lukas",
amharic: "ቅዱስ ሉቃስ",
},
description: {
english: "Saint Mark departed from this world",
amharic: "ቅዱስ ማርቆስ ከዚህ ዓለም በሞት ተለየ",
},
recuringDate: 22,
major: true,
negs: 2,
},
saint_daqsyos: {
key: "saint_daqsyos",
name: {
english: "Saint Daqsyos",
amharic: "ቅዱስ ደቅስዮስ",
},
description: {
english: "Saint_Daqsyos",
amharic: "ቅዱስ ደቅስዮስ",
},
recuringDate: 22,
major: true,
negs: 4,
},
bisrate_gebriel: {
key: "bisrate_gebriel",
name: {
english: "bisrate gebriel ",
amharic: "ብስራተ ገብርኤል",
},
description: {
english: "Commemoration of the Annunciation",
amharic: "ብስራተ ገብርኤል",
},
recuringDate: 22,
major: true,
negs: 4,
},
saint_georgis_of_Lydda: {
key: "saint_georgis_of_Lydda",
name: {
english: "Saint Georgis - George of Lydda",
amharic: "ቅዱስ ጊዮርጊስ",
},
description: {
english: " Saint Georgis - George of Lydda",
amharic: "ቅዱስ ጊዮርጊስ ",
},
recuringDate: 23,
major: false,
negs: 8,
},
saint_georgis_the_battle_of_adwa: {
key: "saint_georgis_the_battle_of_adwa",
name: {
english: "Saint Giorgis (George)-The Battle of Adwa",
amharic: "ቅዱስ ጊዮርጊስ - የአድዋ ጦርነት",
},
description: {
english: "Saint Giorgis (George)-The Battle of Adwa",
amharic: "ቅዱስ ጊዮርጊስ - የአድዋ ጦርነት",
},
recuringDate: 23,
major: true,
negs: 6,
},
takla_haymanot: {
// Information about the saint is defined ONCE
name: {
english: "Abuna Takla Haymanot",
amharic: "አቡነ ተክለ ሐይማኖት",
},
recuringDate: 24,
defaultDescription: {
english: "Commemoration of Abuna Takla Haymanot.",
amharic: "የአቡነ ተክለ ሐይማኖት መታሰቢያ።",
},
events: [
{
key: "takla_haymanot_diparted_from_this_world",
description: {
english: "Abuna Takla Haymanot departed from this world",
amharic: "አቡነ ተክለ ሃይማኖት ከዚህ ዓለም በሞት ተለዩ",
},
major: true,
negs: 12, // The major annual feast for his passing is in the 12th month
},
{
key: "takla_haymanot_legs_broke",
description: {
english: "Having stood for 7 years, one of his legs broke",
amharic: "ለሰባት ዓመታት ከቆሙ በኋላ አንደኛው እግራቸው ተሰበረ።",
},
major: true, // `major` is now tied to the specific event
negs: 5, // The major feast for this event is in the 5th month
},
{
key: "takla_haymanot_birth_date",
description: {
english: "Abuna Takla Haymanot's birth date",
amharic: "የአቡነ ተክለ ሃይማኖት የልደት ቀን",
},
major: false,
negs: 4, // The major feast for his birth is in the 4th month
},
],
},
saint_kirstos_semera: {
key: "saint_kirstos_semera",
name: {
english: "Saint Kirstos Semera",
amharic: "ቅድስት ክርስረቶስ ሰምራ ",
},
description: {
english: "Saint Kirstos Semera",
amharic: "ቅድስት ክርስረቶስ ሰምራ ",
},
recuringDate: 24,
major: true,
negs: 12,
},
heavenly_priests: {
key: "24_heavenly_priests",
name: {
english: "24 Heavenly Priests",
amharic: "24ቱ ካህናተ ሰማይ",
},
description: {
english: "24 Heavenly Priests",
amharic: "24ቱ ካህናተ ሰማይ",
},
recuringDate: 24,
major: true,
negs: 3,
},
saint_marqorewos: {
key: "saint_marqorewos",
name: {
english: "Saint Marqorewos (Merkorios)/ Abune Habibe",
amharic: "ቅዱስ መርቆሬዎስ ፣አቡነ ሀቢብ (አባ ቡላ)",
},
description: {
english: "Saint Joseph",
amharic: "አረጋዊው ዮሴፍ ",
},
recuringDate: 27,
major: true,
negs: 3,
},
abune_habibe: {
key: "abune_habibe",
name: {
english: "Abune Habibe",
amharic: "አቡነ ሀቢብ (አባ ቡላ)",
},
description: {
english: "Abune_Habibe",
amharic: "አረጋዊው ዮሴፍ ",
},
recuringDate: 25,
major: true,
negs: 2,
},
saint_jude_son_of_alpheus: {
key: "saint_jude_son_of_alpheus",
name: {
english: "Saint Jude, son of Alpheus, the Apostle",
amharic: "ቅዱስ ይሁዳ ዘአልፍዮስ ሐዋርያው",
},
description: {
english: "Saint Jude, son of Alpheus, the Apostle his martyrdom",
amharic: "ቅዱስ ይሁዳ ዘአልፍዮስ ሐዋርያውሰማዕትነቱ",
},
recuringDate: 25,
major: false,
negs: 10,
},
saint_joseph: {
key: "saint_joseph",
name: {
english: "Saint Joseph ",
amharic: "አረጋዊው ዮሴፍ",
},
description: {
english: "Saint Joseph",
amharic: "አረጋዊው ዮሴፍ ",
},
recuringDate: 26,
major: true,
negs: 11,
},
abba_salama_frumentius: {
key: "abba_salama_frumentius",
name: {
english: " Abba_Salama_Frumentius, the Enlightener of Ethiopia",
amharic: "አባ ሰላማ ከሳቴ ብርሀን",
},
description: {
english: "Abba_Salama_Frumentius, the Enlightener of Ethiopia",
amharic: "አባ ሰላማ ከሳቴ ብርሀን ",
},
recuringDate: 26,
major: true,
negs: 11,
},
thomas_the_apostle: {
key: "thomas_the_apostle",
name: {
english: "Thomas the apostle",
amharic: " ቶማስ ዘህንደኬ",
},
description: {
english: "Thomas the apostle",
amharic: "ቶማስ ዘህንደኬ ",
},
recuringDate: 26,
major: true,
negs: 9,
},
abune_habte_mariam: {
key: "abune_habte_mariam",
name: {
english: "Abune Habte Mariam",
amharic: "አቡነ ሀብተ ማርያም",
},
description: {
english: "Abune Habte Mariam",
amharic: "አቡነ ሀብተ ማርያም",
},
recuringDate: 26,
major: true,
negs: 3,
},
aba_eyesus_moea: {
key: "aba_eyesus_moea",
name: {
english: "Aba Eyesus Moe`a",
amharic: "አባ ኢየሱስ ሞአ",
},
description: {
english: "Saint Eyesus Moe`a",
amharic: "አባ ኢየሱስ ሞአ ",
},
recuringDate: 26,
major: true,
negs: null,
},
medhane_alem: {
key: "medhane_alem",
name: {
english: "Medhane Alem -The Saviour of the world/ Abune Mebea Zion",
amharic: "መድሐኔአለም ",
},
description: {
english: "Medhane Alem -The Saviour of the world",
amharic: "የዓለም መድኃኒት",
},
recuringDate: 27,
major: true,
negs: 7,
},
abune_mebea_zion: {
key: "abune_mebea_zion",
name: {
english: " Abune Mebea Zion",
amharic: "አቡነ መባዓ ፅዮን",
},
description: {
english: "Abune Mebea Zion",
amharic: "አቡነ መባዓ ፅዮን",
},
recuringDate: 27,
negs: 2,
},
ammanuel: {
key: "ammanuel",
name: {
english: "ammanuel",
amharic: "አማኑኤል",
},
description: {
english: "ammanuel",
amharic: "አማኑኤል",
},
recuringDate: 28,
major: true,
negs: [2, 5, 9],
},
baale_wold: {
key: "baale_wold",
name: {
english: "Ba`ale Wold",
amharic: "በዓለወልድ",
},
description: {
english: "Ba`ale Wold (Feast of God The Son)",
amharic: "ዮሐንስ መጥምቅ",
},
recuringDate: 29,
major: true,
negs: [4, 5],
},
saint_lalibela: {
key: "saint_lalibela",
name: {
english: "Saint Lalibela",
amharic: "ቅዱስ ላሊበላ",
},
description: {
english: "Saint Lalibela",
amharic: "ቅዱስ ላሊበላ",
},
recuringDate: 29,
major: true,
negs: 4,
},
saint_arsema_Beheading: {
key: "saint_arsema_29",
name: {
english: "Saint Arsema",
amharic: "አርሴማ",
},
description: {
english: "Commemoration of The Martyrdom (Beheading) of Kidist Arsema",
amharic: "የቅድስት አርሴማ ሰማዕትነት (ራስ መቁረጥ)",
},
recuringDate: 29,
major: false,
negs: 1,
},
john_baptist: {
key: "john_baptist",
name: {
english: "John the baptist",
amharic: "ዮሐንስ መጥምቅ ",
},
description: {
english: "John the baptist",
amharic: "ዮሐንስ መጥምቅ",
},
recuringDate: 30,
major: true,
negs: 10,
},
saint_markos: {
key: "saint_markos",
name: {
english: "Saint_Markos",
amharic: "ቅዱስ ማርቆስ",
},
description: {
english: "Saint Markos /St. Mark the Evangelist/",
amharic: "የቅዱስ ማርቆስ የልደት",
},
recuringDate: 30,
major: true,
negs: [2, 8],
},
};
import { monthNames } from '../constants.js';
import { toGeez } from '../geezConverter.js';
import { validateNumericInputs } from '../utils.js';
import { InvalidInputTypeError } from '../errors/errorHandler.js';
export function printMonthCalendarGrid(ethiopianYear, ethiopianMonth, calendarData, useGeez = false) {
validateNumericInputs('printMonthCalendarGrid', { ethiopianYear, ethiopianMonth });
if (ethiopianMonth < 1 || ethiopianMonth > 13) {
throw new InvalidInputTypeError('printMonthCalendarGrid', 'ethiopianMonth', 'number between 1 and 13', ethiopianMonth);
}
if (!Array.isArray(calendarData) || calendarData.length === 0) {
// This function would crash if calendarData is empty or not an array, so we check it.
console.error("Calendar data is empty or invalid. Cannot print grid.");
return;
}
const daysOfWeek = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
console.log(`\n ${monthNames.amharic[ethiopianMonth - 1]} ${useGeez ? toGeez(ethiopianYear) : ethiopianYear}`);
console.log(daysOfWeek.join(' '));
const firstDayGregorian = calendarData[0].gregorian;
const jsDate = new Date(firstDayGregorian.year, firstDayGregorian.month - 1, firstDayGregorian.day);
const weekDay = (jsDate.getDay() + 6) % 7; // Monday is 0
let row = Array(weekDay).fill(' ');
for (const day of calendarData) {
const ethDay = useGeez ? toGeez(day.ethiopian.day).padStart(2, '፩') : String(day.ethiopian.day).padStart(2, ' ');
const gregDay = String(day.gregorian.day).padStart(2, ' ');
row.push(`${ethDay}/${gregDay}`);
if (row.length === 7) {
console.log(row.join(' '));
row = [];
}
}
if (row.length > 0) {
console.log(row.join(' ').padEnd(27, ' ')); // Pad the last row
}
}
import { toGeez, toArabic } from './geezConverter.js';
import { PERIOD_LABELS } from './constants.js';
import { InvalidTimeError } from './errors/errorHandler.js';
import { validateNumericInputs } from './utils.js';
export class Time {
/**
* Constructs a Time instance representing an Ethiopian time.
* @param {number} hour - The Ethiopian hour (1-12).
* @param {number} [minute=0] - The minute (0-59).
* @param {string} [period='day'] - The period ('day' or 'night').
* @throws {InvalidTimeError} If any time component is invalid.
*/
constructor(hour, minute = 0, period = 'day') {
validateNumericInputs('Time.constructor', { hour, minute });
if (hour < 1 || hour > 12) {
throw new InvalidTimeError(`Invalid Ethiopian hour: ${hour}. Must be between 1 and 12.`);
}
if (minute < 0 || minute > 59) {
throw new InvalidTimeError(`Invalid minute: ${minute}. Must be between 0 and 59.`);
}
if (period !== 'day' && period !== 'night') {
throw new InvalidTimeError(`Invalid period: "${period}". Must be 'day' or 'night'.`);
}
this.hour = hour;
this.minute = minute;
this.period = period;
}
/**
* Creates a Time instance from a Gregorian 24-hour time.
* @param {number} hour - The Gregorian hour (0-23).
* @param {number} [minute=0] - The minute (0-59).
* @returns {Time} A new Time instance.
* @throws {InvalidTimeError} If the Gregorian time is invalid.
*/
static fromGregorian(hour, minute = 0) {
validateNumericInputs('Time.fromGregorian', { hour, minute });
if (hour < 0 || hour > 23) {
throw new InvalidTimeError(`Invalid Gregorian hour: ${hour}. Must be between 0 and 23.`);
}
if (minute < 0 || minute > 59) {
throw new InvalidTimeError(`Invalid minute: ${minute}. Must be between 0 and 59.`);
}
// Normalize Gregorian hour to an Ethiopian base (where 6 AM is 0)
let tempHour = hour - 6;
if (tempHour < 0) {
tempHour += 24;
}
const period = (tempHour < 12) ? 'day' : 'night';
let ethHour = tempHour % 12;
ethHour = (ethHour === 0) ? 12 : ethHour;
return new Time(ethHour, minute, period);
}
/**
* Converts the Ethiopian time to Gregorian 24-hour format.
* @returns {{hour: number, minute: number}}
*/
toGregorian() {
// Convert Ethiopian 1-12 hour to a 0-11 offset, where 12 o'clock is 0.
let gregHour = this.hour % 12;
if (this.period === 'day') {
gregHour += 6;
} else { // 'night'
gregHour += 18;
}
// Handle the 24-hour wrap-around (e.g., 6 night becomes 24, which should be 0)
gregHour = gregHour % 24;
return { hour: gregHour, minute: this.minute };
}
/**
* Creates a `Time` object from a string representation.
*
* This static method parses a time string, which can include hours, minutes, and an optional period (day/night).
* It supports both Arabic numerals (e.g., "1", "30") and Ethiopic numerals (e.g., "፩", "፴") for hours and minutes,
* assuming a `toArabic` utility function is available to convert Ethiopic numerals to Arabic numbers.
*
* The time string must contain a colon (`:`) separating the hour and minute.
*
* @static
* @param {string} timeString - The string representation of the time.
* Expected formats:
* - "HH:MM" (e.g., "6:30", "፮:፴")
* - "HH:MM period" (e.g., "6:30 night", "፮:፴ ማታ")
* Where:
* - HH: Hour (Arabic or Ethiopic numeral).
* - MM: Minute (Arabic or Ethiopic numeral).
* - period: Optional. Case-insensitive. Recognized values are "night" or "ማታ".
* If the period is omitted, or if a third part is present but not recognized as "night" or "ማታ",
* the time is assumed to be in the 'day' period.
*
* @returns {Time} A new `Time` object representing the parsed time.
*
* @throws {InvalidTimeError} If the `timeString` is:
* - Not a string or an empty string.
* - Missing the colon (`:`) separator.
* - Formatted incorrectly (e.g., not enough parts after splitting).
* - Contains non-numeric values for hour or minute that cannot be parsed into numbers
* (neither as Arabic nor as Ethiopic numerals via `toArabic`).
*
*/
static fromString(timeString) {
if (typeof timeString !== 'string' || timeString.trim() === '') {
throw new InvalidTimeError(`Input must be a non-empty string, but received "${timeString}".`);
}
if (!timeString.includes(':')) {
throw new InvalidTimeError(`Invalid time string format: "${timeString}". Time must include a colon ':' separator.`);
}
const parseNumber = (str) => {
const arabicNum = parseInt(str, 10);
if (!isNaN(arabicNum)) {
return arabicNum;
}
try {
return toArabic(str);
} catch (e) {
return NaN;
}
};
const parts = timeString.split(/[:\s]+/).filter(p => p);
if (parts.length < 2) {
throw new InvalidTimeError(`Invalid time string format: "${timeString}".`);
}
const hour = parseNumber(parts[0]);
const minute = parseNumber(parts[1]);
if (isNaN(hour) || isNaN(minute)) {
throw new InvalidTimeError(`Invalid number in time string: "${timeString}"`);
}
let period = 'day';
if (parts.length > 2) {
const periodStr = parts[2].toLowerCase();
if (periodStr === 'night' || periodStr === 'ማታ') {
period = 'night';
}
}
return new Time(hour, minute, period);
}
// Time Artimatic
/**
* Adds a duration to the current time.
* @param {{hours?: number, minutes?: number}} duration - Object with hours and/or minutes to add.
* @returns {Time} A new Time instance with the added duration.
*/
add(duration) {
if (typeof duration !== 'object' || duration === null) {
throw new InvalidTimeError('Duration must be an object.');
}
const { hours = 0, minutes = 0 } = duration;
validateNumericInputs('Time.add', { hours, minutes });
const greg = this.toGregorian();
let totalMinutes = greg.hour * 60 + greg.minute + hours * 60 + minutes;
totalMinutes = ((totalMinutes % 1440) + 1440) % 1440; // Normalize to a 24-hour cycle
const newHour = Math.floor(totalMinutes / 60);
const newMinute = totalMinutes % 60;
return Time.fromGregorian(newHour, newMinute);
}
/**
* Subtracts a duration from the current time.
* @param {{hours?: number, minutes?: number}} duration - Object with hours and/or minutes to subtract.
* @returns {Time} A new Time instance with the subtracted duration.
*/
subtract(duration) {
if (typeof duration !== 'object' || duration === null) {
throw new InvalidTimeError('Duration must be an object.');
}
const { hours = 0, minutes = 0 } = duration;
return this.add({ hours: -hours, minutes: -minutes });
}
/**
* Calculates the difference between this time and another.
* @param {Time} otherTime - Another Time instance to compare against.
* @returns {{hours: number, minutes: number}} An object with the absolute difference.
*/
diff(otherTime) {
if (!(otherTime instanceof Time)) {
throw new InvalidTimeError('Can only compare with another Time instance.');
}
const t1 = this.toGregorian();
const t2 = otherTime.toGregorian();
const totalMinutes1 = t1.hour * 60 + t1.minute;
const totalMinutes2 = t2.hour * 60 + t2.minute;
let diff = Math.abs(totalMinutes1 - totalMinutes2);
// Time wraps in a 24h cycle, so find the shortest path
if (diff > 720) diff = 1440 - diff;
return {
hours: Math.floor(diff / 60),
minutes: diff % 60,
};
}
/**
* Formats the time as a string.
* @param {Object} [options] - Formatting options.
* @param {string} [options.lang] - The language for the period label. Defaults to 'english' if useGeez is false, otherwise 'amharic'.
* @param {boolean} [options.useGeez=true] - Whether to use Ge'ez numerals.
* @param {boolean} [options.showPeriodLabel=true] - Whether to show the period label.
* @param {boolean} [options.zeroAsDash=true] - Whether to represent zero minutes as a dash.
* @returns {string} The formatted time string.
*/
format(options = {}) {
// If useGeez is explicitly false, the default language should be English.
const defaultLang = options.useGeez === false ? 'english' : 'amharic';
const { lang = defaultLang, useGeez = true, showPeriodLabel = true, zeroAsDash = true } = options;
const formatNum = (num) => {
if (useGeez) return toGeez(num);
return num.toString().padStart(2, '0');
};
const hourStr = formatNum(this.hour);
let minuteStr;
if (zeroAsDash && this.minute === 0) {
minuteStr = '_';
} else {
minuteStr = useGeez ? toGeez(this.minute) : this.minute.toString().padStart(2, '0');
}
let periodLabel = '';
if (showPeriodLabel) {
if (lang === 'english') {
// Use English labels for the period
periodLabel = this.period; // 'day' or 'night'
} else {
// Default to Amharic labels from constants
const amharicLabels = { day: 'ጠዋት', night: 'ማታ' };
periodLabel = amharicLabels[this.period];
}
}
const label = periodLabel ? ` ${periodLabel}` : '';
return `${hourStr}:${minuteStr}${label}`;
}
}
import { toGC, toEC } from './conversions.js';
import { monthNames } from './constants.js';
import { InvalidInputTypeError } from './errors/errorHandler.js';
// --- Validation Helpers ---
/**
* Validates that all provided date parts are numbers.
* @param {string} funcName - The name of the function being validated.
* @param {Object} dateParts - An object where keys are param names and values are the inputs.
* @throws {InvalidInputTypeError} if any value is not a number.
*/
export function validateNumericInputs(funcName, dateParts) {
for (const [name, value] of Object.entries(dateParts)) {
if (typeof value !== 'number' || isNaN(value)) {
throw new InvalidInputTypeError(funcName, name, 'number', value);
}
}
}
/**
* Validates that the input is a valid Ethiopian date object.
* @param {Object} dateObj - The object to validate.
* @param {string} funcName - The name of the function being validated.
* @param {string} paramName - The name of the parameter being validated.
* @throws {InvalidInputTypeError} if the object is invalid.
*/
export function validateEthiopianDateObject(dateObj, funcName, paramName) {
if (typeof dateObj !== 'object' || dateObj === null) {
throw new InvalidInputTypeError(funcName, paramName, 'object', dateObj);
}
validateNumericInputs(funcName, {
[`${paramName}.year`]: dateObj.year,
[`${paramName}.month`]: dateObj.month,
[`${paramName}.day`]: dateObj.day,
});
}
/**
* Validates that the input is a valid Ethiopian time object.
* @param {Object} timeObj - The object to validate.
* @param {string} funcName - The name of the function being validated.
* @param {string} paramName - The name of the parameter being validated.
* @throws {InvalidInputTypeError} if the object is invalid.
*/
export function validateEthiopianTimeObject(timeObj, funcName, paramName) {
if (typeof timeObj !== 'object' || timeObj === null) {
throw new InvalidInputTypeError(funcName, paramName, 'object', timeObj);
}
if (typeof timeObj.period !== 'string' || (timeObj.period !== 'day' && timeObj.period !== 'night')) {
throw new InvalidInputTypeError(funcName, `${paramName}.period`, "'day' or 'night'", timeObj.period);
}
validateNumericInputs(funcName, {
[`${paramName}.hour`]: timeObj.hour,
[`${paramName}.minute`]: timeObj.minute,
});
}
/**
* Calculates the day of the year for a given date.
*
* @param {number} year - The full year (e.g., 2024).
* @param {number} month - The month (1-based, January is 1, December is 12).
* @param {number} day - The day of the month.
* @returns {number} The day of the year (1-based).
*/
export function dayOfYear(year, month, day) {
const monthLengths = [31, isGregorianLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let doy = 0;
for (let i = 0; i < month - 1; i++) {
doy += monthLengths[i];
}
doy += day;
return doy;
}
/**
* Convert a day of year to Gregorian month and day.
*/
export function monthDayFromDayOfYear(year, dayOfYear) {
const monthLengths = [31, isGregorianLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let month = 1;
while (dayOfYear > monthLengths[month - 1]) {
dayOfYear -= monthLengths[month - 1];
month++;
}
return { month, day: dayOfYear };
}
/**
* Checks if the given Gregorian year is a leap year.
*
* Gregorian leap years occur every 4 years, except centuries not divisible by 400.
* For example: 2000 is a leap year, 1900 is not.
*
* @param {number} year - Gregorian calendar year (e.g., 2025)
* @returns {boolean} - True if the year is a leap year, otherwise false.
*/
export function isGregorianLeapYear(year) {
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}
/**
* Checks if the given Ethiopian year is a leap year.
*
* Ethiopian leap years occur every 4 years, when the year modulo 4 equals 3.
* This means years like 2011, 2015, 2019 (in Ethiopian calendar) are leap years.
*
* @param {number} year - Ethiopian calendar year (e.g., 2011)
* @returns {boolean} - True if the year is a leap year, otherwise false.
*/
export function isEthiopianLeapYear(year) {
return year % 4 === 3;
}
/**
* Returns the number of days in the given Ethiopian month and year.
* @param {number} year - Ethiopian year
* @param {number} month - Ethiopian month (1-13)
* @returns {number} Number of days in the month
*/
export function getEthiopianDaysInMonth(year, month) {
if (month === 13) {
return isEthiopianLeapYear(year) ? 6 : 5;
}
return 30;
}
/**
* Returns the weekday (0-6) for a given Ethiopian date.
*
* @param {Object} param0 - The Ethiopian date.
* @param {number} param0.year - The Ethiopian year.
* @param {number} param0.month - The Ethiopian month (1-13).
* @param {number} param0.day - The Ethiopian day (1-30).
* @returns {number} The day of the week (0 for Sunday, 6 for Saturday).
*/
export function getWeekday({ year, month, day }) {
const g = toGC(year, month, day);
return new Date(g.year, g.month - 1, g.day).getDay();
}
/**
* Checks if a given Ethiopian date is valid.
* @param {number} year - Ethiopian year
* @param {number} month - Ethiopian month (1-13)
* @param {number} day - Ethiopian day (1-30 or 1-5/6)
* @returns {boolean} - True if the date is valid, otherwise false.
*/
export function isValidEthiopianDate(year, month, day) {
if (month < 1 || month > 13) {
return false;
}
if (day < 1 || day > getEthiopianDaysInMonth(year, month)) {
return false;
}
return true;
}
/**
* Helper: Get Ethiopian New Year for a Gregorian year.
* @param {number} gYear - The Gregorian year.
* @returns {{gregorianYear: number, month: number, day: number}}
* @throws {InvalidInputTypeError} If gYear is not a number.
*/
export function getEthiopianNewYearForGregorian(gYear) {
validateNumericInputs('getEthiopianNewYearForGregorian', { gYear });
const prevGYear = gYear - 1;
const newYearDay = isGregorianLeapYear(prevGYear) ? 12 : 11;
return {
gregorianYear: gYear,
month: 9,
day: newYearDay
};
}
/**
* Returns the Gregorian date of the Ethiopian New Year for the given Ethiopian year.
*
* @param {number} ethiopianYear - Ethiopian calendar year.
* @returns {{gregorianYear: number, month: number, day: number}}
* @throws {InvalidInputTypeError} If ethiopianYear is not a number.
*/
export function getGregorianDateOfEthiopianNewYear(ethiopianYear) {
validateNumericInputs('getGregorianDateOfEthiopianNewYear', { ethiopianYear });
const gregorianYear = ethiopianYear + 7;
const newYearDay = isGregorianLeapYear(gregorianYear + 1) ? 12 : 11;
return { gregorianYear, month: 9, day: newYearDay };
}
import { getBahireHasab, getMovableHoliday } from '../src/bahireHasab.js';
import { InvalidInputTypeError } from '../src/errors/errorHandler.js';
describe('Bahire Hasab Calculation', () => {
describe('getBahireHasab for 2016 E.C.', () => {
const bahireHasab2016 = getBahireHasab(2016);
test('should calculate Amete Alem and Metene Rabiet correctly', () => {
expect(bahireHasab2016.ameteAlem).toBe(7516);
expect(bahireHasab2016.meteneRabiet).toBe(1879);
});
test('should identify the correct Evangelist', () => {
expect(bahireHasab2016.evangelist.name).toBe('ዮሐንስ'); // John in Amharic (default)
expect(bahireHasab2016.evangelist.remainder).toBe(0);
});
test('should determine the correct New Year day', () => {
expect(bahireHasab2016.newYear.dayName).toBe('ማክሰኞ'); // Tuesday in Amharic (default)
});
test('should calculate Medeb, Wenber, Abektie, and Metqi correctly', () => {
expect(bahireHasab2016.medeb).toBe(11);
expect(bahireHasab2016.wenber).toBe(10);
expect(bahireHasab2016.abektie).toBe(20);
expect(bahireHasab2016.metqi).toBe(10);
});
test('should calculate the correct date for Nineveh', () => {
expect(bahireHasab2016.nineveh).toEqual({ year: 2016, month: 6, day: 18 });
});
});
describe('Internationalization (i18n)', () => {
test('should return names in English when specified', () => {
const bahireHasabEnglish = getBahireHasab(2016, { lang: 'english' });
expect(bahireHasabEnglish.evangelist.name).toBe('John');
expect(bahireHasabEnglish.newYear.dayName).toBe('Tuesday');
});
});
describe('Movable Feasts Calculation', () => {
const { movableFeasts } = getBahireHasab(2016, { lang: 'english' });
test('should return a complete movableFeasts object', () => {
expect(movableFeasts).toBeDefined();
expect(Object.keys(movableFeasts).length).toBeGreaterThan(5);
});
test('should correctly calculate the date for Fasika (Tinsaye)', () => {
const fasika = movableFeasts.fasika;
expect(fasika).toBeDefined();
expect(fasika.ethiopian).toEqual({ year: 2016, month: 8, day: 27 });
expect(fasika.name).toBe('Ethiopian Easter');
expect(fasika.tags).toContain('public');
});
test('should correctly calculate the date for Abiy Tsome', () => {
const abiyTsome = movableFeasts.abiyTsome;
expect(abiyTsome).toBeDefined();
expect(abiyTsome.ethiopian).toEqual({ year: 2016, month: 7, day: 2 });
expect(abiyTsome.name).toBe('Great Lent');
});
});
describe('Error Handling', () => {
test('should throw InvalidInputTypeError for non-numeric input', () => {
expect(() => getBahireHasab('2016')).toThrow(InvalidInputTypeError);
expect(() => getMovableHoliday('TINSAYE', '2016')).toThrow(InvalidInputTypeError);
});
});
});
import { toEC, toGC } from "../src/conversions";
import { InvalidGregorianDateError } from "../src/errors/errorHandler.js";
describe('Ethiopian to Gregorian conversion', () => {
test('Ethiopian to Gregorian: 2017-9-14 -> May 22, 2025', () => {
const result = toGC(2017, 9, 14);
expect(result).toEqual({ year: 2025, month: 5, day: 22 });
});
test('Ethiopian to Gregorian: Pagumē 5, 2016 (2016-13-5) -> September 10, 2024', () => {
const result = toGC(2016, 13, 5);
expect(result).toEqual({ year: 2024, month: 9, day: 10 });
});
test('Ethiopian to Gregorian Leap Year: 2011-13-6 (Pagumē 6, 2011) -> September 11, 2019', () => {
const result = toGC(2011, 13, 6);
expect(result).toEqual({ year: 2019, month: 9, day: 11 });
});
test('Ethiopian to Gregorian Leap Year: Pagumē 6, 2019 (2019-13-6) -> September 11, 2027', () => {
const result = toGC(2019, 13, 6);
expect(result).toEqual({ year: 2027, month: 9, day: 11 });
});
test('Ethiopian to Gregorian Leap Year: May 5, 2024 -> Miazia 27, 2016', () => {
const result = toGC(2016, 8, 27);
expect(result).toEqual({ year: 2024, month: 5, day: 5 });
});
test('Ethiopian to Gregorian: Meskerem 1, 2016 (2016-1-1) -> September 11, 2023', () => {
const result = toGC(2016, 1, 1);
expect(result).toEqual({ year: 2023, month: 9, day: 12 });
});
test('Ethiopian to Gregorian: Tahsas 30, 2015 (2015-4-30) -> January 8, 2023', () => {
const result = toGC(2015, 4, 30);
expect(result).toEqual({ year: 2023, month: 1, day: 8 });
});
test('Ethiopian to Gregorian: Pagume 1, 2011 (2011-13-1) -> September 6, 2019', () => {
const result = toGC(2011, 13, 1);
expect(result).toEqual({ year: 2019, month: 9, day: 6 });
});
test('Ethiopian to Gregorian: Meskerem 1, 1964 (1964-1-1) -> September 12, 1971', () => {
const result = toGC(1964, 1, 1);
expect(result).toEqual({ year: 1971, month: 9, day: 12 });
});
test('Ethiopian to Gregorian: Pagume 6, 2007 (leap Pagume) -> September 11, 2015', () => {
const result = toGC(2007, 13, 6);
expect(result).toEqual({ year: 2015, month: 9, day: 11 });
});
test('Ethiopian to Gregorian: Pagume 5, 2006 (non-leap Pagume) -> September 10, 2014', () => {
const result = toGC(2006, 13, 5);
expect(result).toEqual({ year: 2014, month: 9, day: 10 });
});
test('Ethiopian to Gregorian: End of Ethiopian year (Pagume 5, 2015) -> September 10, 2023', () => {
const result = toGC(2015, 13, 5);
expect(result).toEqual({ year: 2023, month: 9, day: 10 });
});
test('Ethiopian to Gregorian: Start of Ethiopian year (Meskerem 1, 2015) -> September 11, 2022', () => {
const result = toGC(2015, 1, 1);
expect(result).toEqual({ year: 2022, month: 9, day: 11 });
});
});
describe('Gregorian to Ethiopian conversion', () => {
test('Gregorian to Ethiopian: May 22, 2025 -> 2017-9-14', () => {
const result = toEC(2025, 5, 22);
expect(result).toEqual({ year: 2017, month: 9, day: 14 });
});
test('Gregorian to Ethiopian Leap Year: February 29, 2020 -> Yekatit 22, 2012', () => {
const result = toEC(2020, 2, 29);
expect(result).toEqual({ year: 2012, month: 6, day: 21 });
});
test('Gregorian to Ethiopian Leap Year: May 5, 2024 -> Miazia 27, 2016', () => {
const result = toEC(2024, 5, 5);
expect(result).toEqual({ year: 2016, month: 8, day: 27 });
});
test('Gregorian to Ethiopian: September 10, 2024 -> 2016-13-5 (Pagumē 5, 2016)', () => {
const result = toEC(2024, 9, 10);
expect(result).toEqual({ year: 2016, month: 13, day: 5 });
});
test('Gregorian to Ethiopian Leap Year: September 11, 2019 -> 2011-13-6 (Pagumē 6, 2011)', () => {
const result = toEC(2019, 9, 11);
expect(result).toEqual({ year: 2011, month: 13, day: 6 });
});
test('Gregorian to Ethiopian Leap Year: September 11, 2027 -> 2019-13-6 (Pagumē 6, 2019)', () => {
const result = toEC(2027, 9, 11);
expect(result).toEqual({ year: 2019, month: 13, day: 6 });
});
test('Gregorian to Ethiopian: January 1, 2000 -> 1992-4-23', () => {
const result = toEC(2000, 1, 1);
expect(result).toEqual({ year: 1992, month: 4, day: 22 });
});
test('Gregorian to Ethiopian: Out of range year throws error', () => {
expect(() => toEC(1800, 1, 1)).toThrow(InvalidGregorianDateError);
expect(() => toEC(2200, 1, 1)).toThrow(InvalidGregorianDateError);
});
test('Gregorian to Ethiopian: September 12, 1971 (base date) -> 1964-1-1', () => {
const result = toEC(1971, 9, 12);
expect(result).toEqual({ year: 1964, month: 1, day: 1 });
});
test('Gregorian to Ethiopian: December 31, 2100 (upper bound) -> valid Ethiopian date', () => {
const result = toEC(2100, 12, 31);
expect(result.year).toBeGreaterThanOrEqual(2092);
expect(result.month).toBeGreaterThanOrEqual(4);
expect(result.day).toBeGreaterThanOrEqual(20);
});
test('Gregorian to Ethiopian: January 1, 1900 (lower bound) -> valid Ethiopian date', () => {
const result = toEC(1900, 1, 1);
expect(result.year).toBeLessThanOrEqual(1892);
expect(result.month).toBeGreaterThanOrEqual(4);
expect(result.day).toBeGreaterThanOrEqual(22);
});
test('Gregorian to Ethiopian: End of Gregorian leap year (December 31, 2020)', () => {
const result = toEC(2020, 12, 31);
expect(result).toEqual({ year: 2013, month: 4, day: 22 });
});
test('Gregorian to Ethiopian: Start of Gregorian leap year (January 1, 2020)', () => {
const result = toEC(2020, 1, 1);
expect(result).toEqual({ year: 2012, month: 4, day: 22 });
});
test('Gregorian to Ethiopian: Last day of Ethiopian year (September 10, 2023)', () => {
const result = toEC(2023, 9, 10);
expect(result).toEqual({ year: 2015, month: 13, day: 5 });
});
test('Gregorian to Ethiopian: Leap Pagume (September 11, 2015)', () => {
const result = toEC(2023, 9, 11);
expect(result).toEqual({ year: 2015, month: 13, day: 6 });
});
});
import { Kenat } from '../src/Kenat.js';
describe('KenatAddDaysTests', () => {
test('Add days within same month', () => {
const k = new Kenat('2016/01/10');
const result = k.addDays(5);
expect(result.getEthiopian()).toEqual({ year: 2016, month: 1, day: 15 });
});
test('Add days crossing month boundary', () => {
const k = new Kenat('2016/01/28');
const result = k.addDays(5); // Month 1 has 30 days
expect(result.getEthiopian()).toEqual({ year: 2016, month: 2, day: 3 });
});
test('Add days crossing year boundary', () => {
const k = new Kenat('2016/13/4'); // Pagume month with 5 days (non-leap)
const result = k.addDays(3);
expect(result.getEthiopian()).toEqual({ year: 2017, month: 1, day: 2 });
});
test('Add days exactly at month end', () => {
const k = new Kenat('2016/02/25');
const result = k.addDays(5);
expect(result.getEthiopian()).toEqual({ year: 2016, month: 2, day: 30 });
});
test('Add zero days returns same date', () => {
const k = new Kenat('2016/05/15');
const result = k.addDays(0);
expect(result.getEthiopian()).toEqual({ year: 2016, month: 5, day: 15 });
});
test('Add zero days returns same date', () => {
const k = new Kenat('2016/13/1');
const result = k.addDays(6);
expect(result.getEthiopian()).toEqual({ year: 2017, month: 1, day: 2 });
});
});
describe('KenatAddMonthsTests', () => {
test('Add months within same year', () => {
const k = new Kenat('2016/03/10');
const result = k.addMonths(5);
expect(result.getEthiopian()).toEqual({ year: 2016, month: 8, day: 10 });
});
test('Add months with year rollover', () => {
const k = new Kenat('2016/11/10');
const result = k.addMonths(3);
expect(result.getEthiopian()).toEqual({ year: 2017, month: 1, day: 10 });
});
test('Add months resulting in Pagume with day clamp', () => {
const k = new Kenat('2015/12/30'); // 2015 is not a leap year (Pagume = 5 days)
const result = k.addMonths(1); // 30th goes to Pagume but 30 > 5, so clamp
expect(result.getEthiopian()).toEqual({ year: 2015, month: 13, day: 6 }); // ✅ real converted result
});
test('Add months resulting in Pagume in leap year', () => {
const k = new Kenat('2011/12/30'); // 2011 is a leap year (Pagume = 6 days)
const result = k.addMonths(1);
expect(result.getEthiopian()).toEqual({ year: 2011, month: 13, day: 6 });
});
test('Add zero months returns same date', () => {
const k = new Kenat('2016/06/20');
const result = k.addMonths(0);
expect(result.getEthiopian()).toEqual({ year: 2016, month: 6, day: 20 });
});
test('Add negative months across year boundary', () => {
const k = new Kenat('2016/03/10');
const result = k.addMonths(-4); // Goes to 2015/12 (not 11)
expect(result.getEthiopian()).toEqual({ year: 2015, month: 12, day: 10 }); // ✅ fixed
});
test('Subtract into Pagume with clamping', () => {
const k = new Kenat('2016/01/06'); // Meskerem 6
const result = k.addMonths(-1); // Should go to Pagume
expect(result.getEthiopian()).toEqual({ year: 2015, month: 13, day: 6 }); // leap year
});
});
describe('KenatAddYearsTests', () => {
test('Add years within leap-safe month', () => {
const k = new Kenat('2010/05/15');
const result = k.addYears(3);
expect(result.getEthiopian()).toEqual({ year: 2013, month: 5, day: 15 });
});
test('Add years from leap Pagume 6 to non-leap year', () => {
const k = new Kenat('2011/13/6'); // 2011 is leap
const result = k.addYears(1); // 2012 is not leap
expect(result.getEthiopian()).toEqual({ year: 2012, month: 13, day: 5 }); // day clamped
});
test('Add years to another leap year, keeping Pagume 6', () => {
const k = new Kenat('2011/13/6'); // 2011 is leap
const result = k.addYears(4); // 2015 is also leap
expect(result.getEthiopian()).toEqual({ year: 2015, month: 13, day: 6 });
});
test('Add zero years returns same date', () => {
const k = new Kenat('2016/03/10');
const result = k.addYears(0);
expect(result.getEthiopian()).toEqual({ year: 2016, month: 3, day: 10 });
});
test('Subtract years across leap/non-leap transition', () => {
const k = new Kenat('2015/13/6'); // 2015 is leap
const result = k.addYears(-1); // 2014 is not leap
expect(result.getEthiopian()).toEqual({ year: 2014, month: 13, day: 5 });
});
});
describe('KenatDiffInDaysTests', () => {
test('Same date returns zero', () => {
const a = new Kenat('2016/05/15');
const b = new Kenat('2016/05/15');
expect(a.diffInDays(b)).toBe(0);
});
test('Later date minus earlier date returns positive', () => {
const a = new Kenat('2016/06/10');
const b = new Kenat('2016/06/05');
expect(a.diffInDays(b)).toBe(5);
});
test('Earlier date minus later date returns negative', () => {
const a = new Kenat('2016/06/01');
const b = new Kenat('2016/06/06');
expect(a.diffInDays(b)).toBe(-5);
});
test('Crossing year boundary', () => {
const a = new Kenat('2017/01/03');
const b = new Kenat('2016/13/04');
expect(a.diffInDays(b)).toBe(4); // Pagume 5 to Meskerem 3
});
test('Crossing multiple years', () => {
const a = new Kenat('2018/01/01');
const b = new Kenat('2016/01/01');
expect(a.diffInDays(b)).toBe(730); // 2 Ethiopian years = 365 * 2
});
});
describe('KenatDiffInMonthsTests', () => {
test('Same date returns zero', () => {
const a = new Kenat('2016/05/15');
const b = new Kenat('2016/05/15');
expect(a.diffInMonths(b)).toBe(0);
});
test('Later date minus earlier date within same year returns positive', () => {
const a = new Kenat('2016/06/10');
const b = new Kenat('2016/05/05');
expect(a.diffInMonths(b)).toBe(1);
});
test('Earlier date minus later date within same year returns negative', () => {
const a = new Kenat('2016/05/01');
const b = new Kenat('2016/06/06');
expect(a.diffInMonths(b)).toBe(-2);
// Explanation: totalMonths difference is -1, but day 1 < 6 subtracts 1 more month = -2
});
test('Crossing year boundary', () => {
const a = new Kenat('2017/01/03');
const b = new Kenat('2016/13/04');
expect(a.diffInMonths(b)).toBe(0);
});
test('Crossing year boundary with day adjustment', () => {
const a = new Kenat('2017/01/05'); // day 5
const b = new Kenat('2016/13/04'); // day 4
expect(a.diffInMonths(b)).toBe(1);
// Because 5 >= 4 no decrement
});
test('Crossing multiple years', () => {
const a = new Kenat('2018/01/01');
const b = new Kenat('2016/01/01');
expect(a.diffInMonths(b)).toBe(26); // 2 Ethiopian years * 13 months
});
});
describe('KenatDiffInYearsTests', () => {
test('Same date returns zero', () => {
const a = new Kenat('2016/05/15');
const b = new Kenat('2016/05/15');
expect(a.diffInYears(b)).toBe(0);
});
test('Later date minus earlier date within same year returns zero', () => {
const a = new Kenat('2016/06/10');
const b = new Kenat('2016/05/05');
expect(a.diffInYears(b)).toBe(0); // Same year, difference less than full year
});
test('Earlier date minus later date within same year returns zero', () => {
const a = new Kenat('2016/05/01');
const b = new Kenat('2016/06/06');
expect(a.diffInYears(b)).toBe(0); // Same year, difference less than full year
});
test('Later date minus earlier date crossing year boundary returns positive', () => {
const a = new Kenat('2017/01/03');
const b = new Kenat('2016/13/04');
expect(a.diffInYears(b)).toBe(0); // Full year difference, day/month adjustment done
});
test('Later date minus earlier date crossing year boundary but day adjustment subtracts one', () => {
const a = new Kenat('2017/01/03'); // day 3 < day 4
const b = new Kenat('2016/13/04');
// Same as above test, but test again to confirm day < day subtracts 1
expect(a.diffInYears(b)).toBe(0);
});
test('Crossing multiple years returns correct positive value', () => {
const a = new Kenat('2018/01/01');
const b = new Kenat('2016/01/01');
expect(a.diffInYears(b)).toBe(2);
});
test('Earlier date minus later date returns negative years', () => {
const a = new Kenat('2016/01/01');
const b = new Kenat('2018/01/01');
expect(a.diffInYears(b)).toBe(-2);
});
});
import { toGeez, toArabic } from '../src/geezConverter.js';
import { GeezConverterError } from '../src/errors/errorHandler.js';
describe('toGeez', () => {
test('converts single digits correctly', () => {
expect(toGeez(0)).toBe('0');
expect(toGeez(1)).toBe('፩');
expect(toGeez(5)).toBe('፭');
expect(toGeez(9)).toBe('፱');
});
test('converts tens correctly', () => {
expect(toGeez(10)).toBe('፲');
expect(toGeez(30)).toBe('፴');
expect(toGeez(99)).toBe('፺፱');
});
test('converts hundreds correctly', () => {
expect(toGeez(100)).toBe('፻');
expect(toGeez(123)).toBe('፻፳፫');
expect(toGeez(999)).toBe('፱፻፺፱');
});
test('converts thousands and ten-thousands correctly', () => {
expect(toGeez(10000)).toBe('፼');
expect(toGeez(12345)).toBe('፼፳፫፻፵፭');
expect(toGeez(99999)).toBe('፱፼፺፱፻፺፱');
});
test('throws error for invalid inputs', () => {
expect(() => toGeez(-1)).toThrow();
expect(() => toGeez('abc')).toThrow();
expect(() => toGeez(null)).toThrow();
});
});
describe('toArabic (reverse of toGeez)', () => {
test('reverses single digits correctly', () => {
expect(toArabic('፩')).toBe(1);
expect(toArabic('፭')).toBe(5);
expect(toArabic('፱')).toBe(9);
});
test('reverses tens correctly', () => {
expect(toArabic('፲')).toBe(10);
expect(toArabic('፴')).toBe(30);
expect(toArabic('፺፱')).toBe(99);
});
test('reverses hundreds correctly', () => {
expect(toArabic('፻')).toBe(100);
expect(toArabic('፻፳፫')).toBe(123);
expect(toArabic('፱፻፺፱')).toBe(999);
});
test('reverses thousands and ten-thousands correctly', () => {
expect(toArabic('፼')).toBe(10000);
expect(toArabic('፼፳፫፻፵፭')).toBe(12345);
expect(toArabic('፱፼፺፱፻፺፱')).toBe(99999);
});
test('throws error for invalid geez input', () => {
expect(() => toArabic('xyz')).toThrow(GeezConverterError);
expect(toArabic('')).toBe(0);
expect(() => toArabic('፻፻፻x')).toThrow(GeezConverterError);
});
});
import { getFastingPeriod, getFastingInfo, getFastingDays } from '../src/fasting.js';
import { FastingKeys } from '../src/constants.js';
import { getBahireHasab } from '../src/bahireHasab.js';
import { addDays } from '../src/dayArithmetic.js';
import { getWeekday, getEthiopianDaysInMonth } from '../src/utils.js';
// Mock dependencies if they are not available in the test environment
// For this example, we assume the underlying functions are correct.
describe('getFastingPeriod', () => {
describe('Christian Fasts for 2016 E.C.', () => {
const year = 2016;
test('should return the correct start and end dates for The Great Lent (Abiy Tsome)', () => {
const period = getFastingPeriod(FastingKeys.ABIY_TSOME, year);
// VERIFIED: In 2016, Abiy Tsome starts on Megabit 2 and ends on Siklet (Miazia 25)
expect(period.start).toEqual({ year: 2016, month: 7, day: 2 });
expect(period.end).toEqual({ year: 2016, month: 8, day: 25 });
});
test('should return the correct start and end dates for Fast of the Apostles (Tsome Hawaryat)', () => {
const period = getFastingPeriod(FastingKeys.TSOME_HAWARYAT, year);
// VERIFIED: In 2016, Paraclete is Sene 17, so Tsome Hawaryat starts Sene 18. It ends on Hamle 4.
expect(period.end).toEqual({ year: 2016, month: 11, day: 4 });
});
test('should return the correct start and end dates for Fast of Nineveh', () => {
const period = getFastingPeriod(FastingKeys.NINEVEH, year);
// VERIFIED: In 2016, Nineveh starts on Yekatit 18 and lasts 3 days.
expect(period.start).toEqual({ year: 2016, month: 6, day: 18 });
expect(period.end).toEqual({ year: 2016, month: 6, day: 20 });
});
test('should return the correct start and end dates for Fast of the Prophets (Tsome Nebiyat)', () => {
const period = getFastingPeriod(FastingKeys.TSOME_NEBIYAT, year);
// This is a fixed fast from Hidar 15 to Tahsas 28. This test was already correct.
expect(period.start).toEqual({ year: 2016, month: 3, day: 15 });
expect(period.end).toEqual({ year: 2016, month: 4, day: 28 });
});
});
describe('Muslim Fasts for 2016 E.C.', () => {
const year = 2016;
test('should return the correct start and end dates for Ramadan', () => {
const period = getFastingPeriod(FastingKeys.RAMADAN, year);
// VERIFIED: For 2016 E.C., Ramadan 1445 A.H. runs from Megabit 2 to Miazia 1.
expect(period.start).toEqual({ year: 2016, month: 7, day: 2 });
});
});
describe('Error and Edge Case Handling', () => {
test('should return null for an unknown fast key', () => {
const period = getFastingPeriod('UNKNOWN_FAST_KEY', 2016);
expect(period).toBeNull();
});
test('should return a valid period for a future year', () => {
// This confirms the calculation logic doesn't crash on different inputs.
const period = getFastingPeriod(FastingKeys.ABIY_TSOME, 2020);
expect(period).toBeDefined();
expect(period.start).toBeDefined();
expect(period.end).toBeDefined();
});
});
});
describe('getFastingInfo', () => {
const year = 2016;
test('returns multilingual info and period for Abiy Tsome', () => {
const infoAm = getFastingInfo(FastingKeys.ABIY_TSOME, year, { lang: 'amharic' });
expect(infoAm).toBeTruthy();
expect(infoAm.key).toBe(FastingKeys.ABIY_TSOME);
expect(infoAm.name).toBe('ዐቢይ ጾም (ሁዳዴ)');
expect(infoAm.description).toBeDefined();
expect(infoAm.period.start).toEqual({ year: 2016, month: 7, day: 2 });
const infoEn = getFastingInfo(FastingKeys.ABIY_TSOME, year, { lang: 'english' });
expect(infoEn.name).toMatch(/Great Lent/i);
expect(infoEn.period.end).toEqual({ year: 2016, month: 8, day: 25 });
});
test('returns info for Nineveh with 3-day period', () => {
const info = getFastingInfo(FastingKeys.NINEVEH, year);
expect(info.name).toBe('ጾመ ነነዌ');
expect(info.period.start).toEqual({ year: 2016, month: 6, day: 18 });
expect(info.period.end).toEqual({ year: 2016, month: 6, day: 20 });
});
test('returns info for Ramadan including tags', () => {
const info = getFastingInfo(FastingKeys.RAMADAN, year, { lang: 'english' });
expect(info.name).toBe('Ramadan');
expect(Array.isArray(info.tags)).toBe(true);
expect(info.period.start).toEqual({ year: 2016, month: 7, day: 2 });
});
test('returns info for Filseta (fixed Nehase 1-14)', () => {
const info = getFastingInfo(FastingKeys.FILSETA, year, { lang: 'amharic' });
expect(info).toBeTruthy();
expect(info.key).toBe(FastingKeys.FILSETA);
expect(info.name).toBe('ፍልሰታ');
expect(info.period.start).toEqual({ year: 2016, month: 12, day: 1 });
expect(info.period.end).toEqual({ year: 2016, month: 12, day: 14 });
});
});
describe('Orthodox Weekly Fasting (Tsome Dihnet)', () => {
test('Wednesdays and Fridays are fasting days outside the 50 days after Easter', () => {
const year = 2016;
// Choose a month well before Easter season: Hidar (month 3)
const month = 3;
const daysInMonth = getEthiopianDaysInMonth(year, month);
let foundWed = false;
let foundFri = false;
const days = getFastingDays('TSOME_DIHENET', year, month);
for (let day = 1; day <= daysInMonth; day++) {
const date = { year, month, day };
const wd = getWeekday(date);
if (wd === 3) { // Wednesday
foundWed = true;
expect(days.includes(day)).toBe(true);
}
if (wd === 5) { // Friday
foundFri = true;
expect(days.includes(day)).toBe(true);
}
}
expect(foundWed || foundFri).toBe(true);
});
test('No fasting on Wednesdays/Fridays during the 50 days after Easter (until Pentecost)', () => {
const year = 2016;
const bh = getBahireHasab(year);
const easter = bh.movableFeasts.fasika.ethiopian;
const pentecost = bh.movableFeasts.paraclete.ethiopian;
// Scan the full window starting the day after Easter through Pentecost inclusive
let d = addDays(easter, 1);
while (true) {
const wd = getWeekday(d);
if (wd === 3 || wd === 5) {
const list = getFastingDays('TSOME_DIHENET', d.year, d.month);
expect(list.includes(d.day)).toBe(false);
}
if (d.year === pentecost.year && d.month === pentecost.month && d.day === pentecost.day) break;
d = addDays(d, 1);
}
});
test('Fasting resumes on Wed/Fri after Pentecost in the same year', () => {
const year = 2016;
const bh = getBahireHasab(year);
const pentecost = bh.movableFeasts.paraclete.ethiopian;
// Search within a few weeks after Pentecost for a Wed or Fri marked as fasting
let found = false;
for (let i = 1; i <= 21; i++) {
const d = addDays(pentecost, i);
const wd = getWeekday(d);
if (wd === 3 || wd === 5) {
const list = getFastingDays('TSOME_DIHENET', d.year, d.month);
expect(list.includes(d.day)).toBe(true);
found = true;
break;
}
}
expect(found).toBe(true);
});
});
import { toGeez, toArabic } from '../src/geezConverter';
import { GeezConverterError } from '../src/errors/errorHandler.js';
describe('toGeez', () => {
it('converts single digits correctly', () => {
expect(toGeez(1)).toBe('፩');
expect(toGeez(2)).toBe('፪');
expect(toGeez(9)).toBe('፱');
});
it('converts tens correctly', () => {
expect(toGeez(10)).toBe('፲');
expect(toGeez(20)).toBe('፳');
expect(toGeez(99)).toBe('፺፱');
});
it('converts hundreds correctly', () => {
expect(toGeez(100)).toBe('፻');
expect(toGeez(101)).toBe('፻፩');
expect(toGeez(110)).toBe('፻፲');
expect(toGeez(123)).toBe('፻፳፫');
expect(toGeez(999)).toBe('፱፻፺፱');
});
it('converts thousands and ten thousands correctly', () => {
expect(toGeez(1000)).toBe('፲፻');
expect(toGeez(10000)).toBe('፼');
});
it('returns "0" for input 0', () => {
expect(toGeez(0)).toBe('0');
});
it('accepts string input', () => {
expect(toGeez('123')).toBe('፻፳፫');
expect(toGeez('10000')).toBe('፼');
});
it('throws error for invalid input', () => {
expect(() => toGeez(-1)).toThrow(GeezConverterError);
expect(() => toGeez('abc')).toThrow(GeezConverterError);
expect(() => toGeez(null)).toThrow();
expect(() => toGeez(undefined)).toThrow();
expect(() => toGeez(1.5)).toThrow(GeezConverterError);
});
});
describe('toArabic', () => {
it('converts single Ge\'ez numerals to Arabic', () => {
expect(toArabic('፩')).toBe(1);
expect(toArabic('፪')).toBe(2);
expect(toArabic('፱')).toBe(9);
});
it('converts Ge\'ez tens to Arabic', () => {
expect(toArabic('፲')).toBe(10);
expect(toArabic('፳')).toBe(20);
expect(toArabic('፺፱')).toBe(99);
});
it('converts Ge\'ez hundreds to Arabic', () => {
expect(toArabic('፻')).toBe(100);
expect(toArabic('፻፩')).toBe(101);
expect(toArabic('፻፲')).toBe(110);
expect(toArabic('፻፳፫')).toBe(123);
expect(toArabic('፱፻፺፱')).toBe(999);
});
it('converts Ge\'ez thousands and ten thousands to Arabic', () => {
expect(toArabic('፲፻')).toBe(1000);
expect(toArabic('፼')).toBe(10000);
expect(toArabic('፲፼')).toBe(100000);
});
it('handles complex numbers', () => {
expect(toArabic('፲፻፺፱')).toBe(1099);
expect(toArabic('፬፻')).toBe(300 + 100);
});
it('throws error for unknown Ge\'ez numerals', () => {
expect(() => toArabic('A')).toThrow('Unknown Ge\'ez numeral: A');
expect(() => toArabic('፩X')).toThrow('Unknown Ge\'ez numeral: X');
});
it('throws error for non-string input', () => {
expect(() => toArabic(null)).toThrow(GeezConverterError);
expect(() => toArabic(undefined)).toThrow(GeezConverterError);
expect(() => toArabic(123)).toThrow(GeezConverterError);
});
it('converts round-trip toGeez -> toArabic', () => {
for (let n of [1, 10, 99, 100, 123, 999, 1000, 10000, 12345, 999999]) {
expect(toArabic(toGeez(n))).toBe(n);
}
});
});
import { getHolidaysInMonth, getHoliday } from '../src/holidays.js';
import { getMovableHoliday } from '../src/bahireHasab.js';
import { InvalidInputTypeError } from '../src/errors/errorHandler.js';
import { UnknownHolidayError } from '../src/errors/errorHandler.js';
describe('Holiday Calculation', () => {
describe('getHolidaysInMonth', () => {
test('should return fixed and movable holidays for a given month', () => {
// Meskerem 2016 has Enkutatash (day 1) and Meskel (day 17)
const holidays = getHolidaysInMonth(2016, 1);
const holidayKeys = holidays.map(h => h.key);
expect(holidayKeys).toContain('enkutatash');
expect(holidayKeys).toContain('meskel');
});
test('should correctly calculate and include movable Christian holidays', () => {
// In 2016 E.C., Fasika is on Miazia 27 and Siklet is on Miazia 25
const holidays = getHolidaysInMonth(2016, 8);
const fasika = holidays.find(h => h.key === 'fasika');
const siklet = holidays.find(h => h.key === 'siklet');
expect(fasika).toBeDefined();
expect(fasika.ethiopian.day).toBe(27);
expect(siklet).toBeDefined();
expect(siklet.ethiopian.day).toBe(25);
});
});
describe('getMovableHoliday (Bahire Hasab)', () => {
test('should return correct Fasika (Tinsaye) date for Ethiopian years 2012 to 2016', () => {
expect(getMovableHoliday('TINSAYE', 2012)).toEqual({ year: 2012, month: 8, day: 11 });
expect(getMovableHoliday('TINSAYE', 2013)).toEqual({ year: 2013, month: 8, day: 24 });
expect(getMovableHoliday('TINSAYE', 2014)).toEqual({ year: 2014, month: 8, day: 16 });
expect(getMovableHoliday('TINSAYE', 2015)).toEqual({ year: 2015, month: 8, day: 8 });
expect(getMovableHoliday('TINSAYE', 2016)).toEqual({ year: 2016, month: 8, day: 27 });
});
test('should return correct Siklet (Good Friday) date for Ethiopian years 2012 to 2016', () => {
expect(getMovableHoliday('SIKLET', 2012)).toEqual({ year: 2012, month: 8, day: 9 });
expect(getMovableHoliday('SIKLET', 2013)).toEqual({ year: 2013, month: 8, day: 22 });
expect(getMovableHoliday('SIKLET', 2014)).toEqual({ year: 2014, month: 8, day: 14 });
expect(getMovableHoliday('SIKLET', 2015)).toEqual({ year: 2015, month: 8, day: 6 });
expect(getMovableHoliday('SIKLET', 2016)).toEqual({ year: 2016, month: 8, day: 25 });
});
});
describe('Error Handling', () => {
test('getHolidaysInMonth should throw for invalid input types', () => {
expect(() => getHolidaysInMonth('2016', 1)).toThrow(InvalidInputTypeError);
expect(() => getHolidaysInMonth(2016, 'one')).toThrow(InvalidInputTypeError);
});
test('getHolidaysInMonth should throw for out-of-range month', () => {
expect(() => getHolidaysInMonth(2016, 0)).toThrow(InvalidInputTypeError);
expect(() => getHolidaysInMonth(2016, 14)).toThrow(InvalidInputTypeError);
});
test('getMovableHoliday should throw for invalid input type', () => {
expect(() => getMovableHoliday('TINSAYE', null)).toThrow(InvalidInputTypeError);
expect(() => getMovableHoliday('TINSAYE', '2016')).toThrow(InvalidInputTypeError);
});
test('getMovableHoliday should throw for unknown holiday key', () => {
expect(() => getMovableHoliday('UNKNOWN_HOLIDAY', 2016)).toThrow(UnknownHolidayError);
});
});
describe('Movable Muslim Holidays', () => {
test('should return correct date for Moulid in 2016', () => {
// Moulid in 2016 E.C. is on Meskerem 17
const holiday = getHoliday('moulid', 2016);
expect(holiday).toBeDefined();
expect(holiday.ethiopian).toEqual({ year: 2016, month: 1, day: 16 });
});
test('should return correct date for Eid al-Fitr in 2016', () => {
// Eid al-Fitr in 2016 E.C. is on Miazia 2
const holiday = getHoliday('eidFitr', 2016);
expect(holiday).toBeDefined();
expect(holiday.ethiopian).toEqual({ year: 2016, month: 8, day: 1 });
});
test('should return correct date for Eid al-Adha in 2016', () => {
// Eid al-Adha in 2016 E.C. is on Sene 9
const holiday = getHoliday('eidAdha', 2016);
expect(holiday).toBeDefined();
expect(holiday.ethiopian).toEqual({ year: 2016, month: 10, day: 9 });
});
});
});
import { Kenat } from '../src/Kenat.js';
describe('Kenat class', () => {
test('should create an instance with current date', () => {
const now = new Date();
const kenat = new Kenat();
const gregorian = kenat.getGregorian();
expect(gregorian).toHaveProperty('year');
expect(gregorian).toHaveProperty('month');
expect(gregorian).toHaveProperty('day');
// Compare only the date portion
expect(`${gregorian.year}-${gregorian.month}-${gregorian.day}`).toBe(
`${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`
);
const ethiopian = kenat.getEthiopian();
expect(ethiopian).toHaveProperty('year');
expect(ethiopian).toHaveProperty('month');
expect(ethiopian).toHaveProperty('day');
});
test('should convert a specific Ethiopian date correctly', () => {
const kenat = new Kenat("2016/9/15");
const gregorian = kenat.getGregorian();
expect(gregorian).toEqual({ year: 2024, month: 5, day: 23 });
const ethiopian = kenat.getEthiopian();
expect(ethiopian).toEqual({ year: 2016, month: 9, day: 15 });
});
test('toString should return Ethiopian date string', () => {
const kenat = new Kenat("2016/9/15");
const str = kenat.toString();
expect(str).toBe("ግንቦት 15 2016 12:00 ጠዋት");
});
test('format returns Ethiopian date string with month name in English and Amharic', () => {
const kenat = new Kenat("2017/1/15"); // Meskerem 15, 2017
const englishFormat = kenat.format({ lang: 'english' });
expect(englishFormat).toBe("Meskerem 15 2017");
const amharicFormat = kenat.format({ lang: 'amharic' });
expect(amharicFormat).toBe("መስከረም 15 2017");
});
test('formatInGeezAmharic returns Ethiopian date string with Amharic month and Geez numerals', () => {
const kenat = new Kenat("2017/1/15"); // Meskerem 15, 2017
const formatted = kenat.formatInGeezAmharic();
// Expected: "መስከረም ፲፭ ፳፻፲፯"
expect(formatted).toBe("መስከረም ፲፭ ፳፻፲፯");
});
});
describe('Kenat API Helper Methods', () => {
const date1 = new Kenat("2016/8/15");
const date2 = new Kenat("2016/8/20");
const date3 = new Kenat("2016/8/15");
const leapYearDate = new Kenat("2015/1/1");
const nonLeapYearDate = new Kenat("2016/1/1");
test('isBefore() should correctly compare dates', () => {
expect(date1.isBefore(date2)).toBe(true);
expect(date2.isBefore(date1)).toBe(false);
expect(date1.isBefore(date3)).toBe(false);
});
test('isAfter() should correctly compare dates', () => {
expect(date2.isAfter(date1)).toBe(true);
expect(date1.isAfter(date2)).toBe(false);
expect(date1.isAfter(date3)).toBe(false);
});
test('isSameDay() should correctly compare dates', () => {
expect(date1.isSameDay(date3)).toBe(true);
expect(date1.isSameDay(date2)).toBe(false);
});
test('startOfMonth() should return the first day of the month', () => {
const start = date1.startOfMonth();
expect(start.getEthiopian()).toEqual({ year: 2016, month: 8, day: 1 });
});
test('endOfMonth() should return the last day of a standard month', () => {
const end = date1.endOfMonth();
expect(end.getEthiopian()).toEqual({ year: 2016, month: 8, day: 30 });
});
test('endOfMonth() should return the last day of Pagume in a leap year', () => {
const pagume = new Kenat("2015/13/1"); // 2015 is a leap year
const end = pagume.endOfMonth();
expect(end.getEthiopian()).toEqual({ year: 2015, month: 13, day: 6 });
});
test('isLeapYear() should correctly identify leap years', () => {
expect(leapYearDate.isLeapYear()).toBe(true);
expect(nonLeapYearDate.isLeapYear()).toBe(false);
});
test('weekday() should return the correct day of the week', () => {
// May 23, 2024 is a Thursday, which is index 4
const specificDate = new Kenat("2016/9/15");
expect(specificDate.weekday()).toBe(4);
});
});
import { Kenat } from '../src/Kenat.js';
describe('Method Chaining Tests', () => {
test('should support chaining add operations', () => {
const date = new Kenat('2017/1/1');
const future = date.add(7, 'days').add(1, 'months').add(1, 'years');
expect(future.getEthiopian()).toEqual({ year: 2018, month: 2, day: 8 });
});
test('should support chaining subtract operations', () => {
const date = new Kenat('2017/1/1');
const past = date.subtract(7, 'days').subtract(1, 'months').subtract(1, 'years');
expect(past.getEthiopian()).toEqual({ year: 2015, month: 11, day: 29 });
});
test('should support mixed add and subtract operations', () => {
const date = new Kenat('2017/1/1');
const result = date.add(7, 'days').subtract(1, 'months').add(1, 'years');
expect(result.getEthiopian()).toEqual({ year: 2017, month: 13, day: 5 });
});
test('should preserve time during arithmetic operations', () => {
const date = new Kenat('2017/1/1', { hour: 3, minute: 30, period: 'day' });
const future = date.add(7, 'days').add(1, 'months');
expect(future.getEthiopian()).toEqual({ year: 2017, month: 2, day: 8 });
expect(future.time.hour).toBe(3);
expect(future.time.minute).toBe(30);
expect(future.time.period).toBe('day');
});
test('should support chaining with startOf and endOf', () => {
const date = new Kenat('2017/6/15');
const result = date.startOf('month').add(7, 'days').endOf('day');
expect(result.getEthiopian()).toEqual({ year: 2017, month: 6, day: 8 });
expect(result.time.hour).toBe(12);
expect(result.time.period).toBe('night');
});
test('should support chaining with setTime', () => {
const date = new Kenat('2017/1/1');
const result = date.add(7, 'days').setTime(6, 30, 'night').add(1, 'months');
expect(result.getEthiopian()).toEqual({ year: 2017, month: 2, day: 8 });
expect(result.time.hour).toBe(6);
expect(result.time.minute).toBe(30);
expect(result.time.period).toBe('night');
});
test('should maintain immutability - original date unchanged', () => {
const original = new Kenat('2017/1/1');
const modified = original.add(7, 'days').add(1, 'months');
expect(original.getEthiopian()).toEqual({ year: 2017, month: 1, day: 1 });
expect(modified.getEthiopian()).toEqual({ year: 2017, month: 2, day: 8 });
});
test('should handle leap year correctly in chaining', () => {
const date = new Kenat('2015/13/6'); // Leap year Pagume
const result = date.add(1, 'days').add(1, 'months');
expect(result.getEthiopian()).toEqual({ year: 2016, month: 2, day: 1 });
});
test('should handle month boundaries correctly in chaining', () => {
const date = new Kenat('2017/1/30'); // Last day of Meskerem
const result = date.add(1, 'days').add(1, 'months');
expect(result.getEthiopian()).toEqual({ year: 2017, month: 3, day: 1 });
});
test('should support complex chaining operations', () => {
const date = new Kenat('2017/1/1');
const result = date
.startOf('month')
.add(14, 'days')
.setTime(12, 0, 'day')
.add(1, 'months')
.endOf('day')
.add(1, 'years');
expect(result.getEthiopian()).toEqual({ year: 2018, month: 2, day: 15 });
expect(result.time.hour).toBe(12);
expect(result.time.period).toBe('night');
});
test('should throw error for invalid unit in add()', () => {
const date = new Kenat('2017/1/1');
expect(() => date.add(1, 'weeks')).toThrow('Invalid unit: weeks');
});
test('should throw error for invalid unit in startOf()', () => {
const date = new Kenat('2017/1/1');
expect(() => date.startOf('week')).toThrow('Invalid unit: week');
});
test('should throw error for invalid unit in endOf()', () => {
const date = new Kenat('2017/1/1');
expect(() => date.endOf('week')).toThrow('Invalid unit: week');
});
test('should throw error for non-numeric amount in add()', () => {
const date = new Kenat('2017/1/1');
expect(() => date.add('seven', 'days')).toThrow('Amount must be a number');
});
});
import { Kenat } from '../src/Kenat.js';
// Mock daysOfWeek and getWeekday if they are global or imported in your module
const daysOfWeek = {
amharic: ['ሰኞ', 'ማክሰኞ', 'እሮብ', 'ሐሙስ', 'ዓርብ', 'ቅዳሜ', 'እሑድ'],
english: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
// add others if needed
};
const getWeekday = (ethDate) => {
// Simplified mock: returns 0..6 cyclic by day for test purpose
return (ethDate.day - 1) % 7;
};
// Mock Kenat.now().getEthiopian() and Kenat constructor for testing
Kenat.now = () => ({
getEthiopian: () => ({ year: 2015, month: 9, day: 10 }),
});
Kenat.prototype.getMonthCalendar = function(year, month, useGeez) {
// Return mock days including the current day 10 for the default test
return [
{ ethiopian: { year, month, day: 8 }, someData: 'day8' },
{ ethiopian: { year, month, day: 9 }, someData: 'day9' },
{ ethiopian: { year, month, day: 10 }, someData: 'day10' }, // current day
{ ethiopian: { year, month, day: 11 }, someData: 'day11' },
{ ethiopian: { year, month, day: 12 }, someData: 'day12' },
];
};
Kenat.prototype.constructor = Kenat;
// Attach dependencies to global or module scope as needed
global.daysOfWeek = daysOfWeek;
global.getWeekday = getWeekday;
// The method under test
Kenat.getMonthGrid = function(input = {}) {
let year, month, weekStart = 0, useGeez = false, weekdayLang = 'amharic';
if (typeof input === 'string') {
const match = input.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})$/);
if (!match) throw new Error("Invalid Ethiopian date format. Use 'yyyy/mm/dd'");
year = parseInt(match[1]);
month = parseInt(match[2]);
} else if (typeof input === 'object') {
({ year, month, weekStart = 0, useGeez = false, weekdayLang = 'amharic' } = input);
}
const current = Kenat.now().getEthiopian();
const y = year || current.year;
const m = month || current.month;
const todayEth = Kenat.now().getEthiopian();
const temp = new Kenat(`${y}/${m}/1`);
const days = temp.getMonthCalendar(y, m, useGeez);
const labels = daysOfWeek[weekdayLang] || daysOfWeek.amharic;
const daysWithWeekday = days.map(day => {
const weekday = getWeekday(day.ethiopian);
const isToday =
Number(day.ethiopian.year) === Number(todayEth.year) &&
Number(day.ethiopian.month) === Number(todayEth.month) &&
Number(day.ethiopian.day) === Number(todayEth.day);
return {
...day,
weekday,
weekdayName: labels[weekday],
isToday,
};
});
const firstWeekday = daysWithWeekday[0].weekday;
let offset = firstWeekday - weekStart;
if (offset < 0) offset += 7;
const padded = Array(offset).fill(null).concat(daysWithWeekday);
const headers = labels.slice(weekStart).concat(labels.slice(0, weekStart));
return {
headers,
days: padded,
};
};
describe('Kenat.getMonthGrid', () => {
test('returns month grid with default current Ethiopian date when no input', () => {
const result = Kenat.getMonthGrid();
expect(result).toHaveProperty('headers');
expect(result).toHaveProperty('days');
expect(Array.isArray(result.headers)).toBe(true);
expect(Array.isArray(result.days)).toBe(true);
// Check days contain weekdays and isToday flag
expect(result.days.some(d => d && d.isToday)).toBe(true);
});
test('parses string input correctly and returns valid grid', () => {
const result = Kenat.getMonthGrid('2015/9/1');
expect(result.headers.length).toBe(7);
expect(result.days.filter(d => d !== null).length).toBe(5); // mock 5 days
expect(result.days[0]).toHaveProperty('weekday');
});
test('throws error on invalid string format', () => {
expect(() => Kenat.getMonthGrid('invalid-date')).toThrow(/Invalid Ethiopian date format/);
});
test('accepts object input with year, month and custom weekStart', () => {
const result = Kenat.getMonthGrid({ year: 2015, month: 9, weekStart: 1, weekdayLang: 'english' });
expect(result.headers[0]).toBe('Tuesday'); // weekStart=1 shifts headers
expect(result.days.filter(d => d !== null).length).toBe(5);
// Check weekdayName matches English labels
expect(result.days.find(d => d !== null).weekdayName).toBe('Monday');
});
test('uses default amharic weekday names if invalid weekdayLang', () => {
const result = Kenat.getMonthGrid({ year: 2015, month: 9, weekdayLang: 'invalid' });
expect(result.headers).toEqual(daysOfWeek.amharic);
});
test('pads days array correctly based on weekStart', () => {
const resultDefault = Kenat.getMonthGrid({ year: 2015, month: 9, weekStart: 0 });
const offsetDefault = resultDefault.days.findIndex(day => day !== null);
expect(offsetDefault).toBeGreaterThanOrEqual(0);
const resultShift = Kenat.getMonthGrid({ year: 2015, month: 9, weekStart: 3 });
const offsetShift = resultShift.days.findIndex(day => day !== null);
expect(offsetShift).toBeGreaterThanOrEqual(0);
expect(offsetShift).not.toBe(offsetDefault); // Should differ if weekStart changed
});
});
import Kenat from '../src/index.js';
describe('Kenat - getMonthCalendar', () => {
test('should return 30 days for a standard Ethiopian month', () => {
const k = new Kenat('2015/1/1');
const calendar = k.getMonthCalendar(2015, 1);
expect(calendar.length).toBe(30);
});
test('should return 6 days for Pagumē in a leap year (year % 4 === 3)', () => {
const k = new Kenat('2011/13/1'); // 2011 is a leap year in Ethiopian calendar
const calendar = k.getMonthCalendar(2011, 13);
expect(calendar.length).toBe(6);
});
test('should return 5 days for Pagumē in a non-leap year', () => {
const k = new Kenat('2012/13/1');
const calendar = k.getMonthCalendar(2012, 13);
expect(calendar.length).toBe(5);
});
test('each day should include properly formatted Ethiopian and Gregorian fields', () => {
const k = new Kenat('2015/2/1');
const calendar = k.getMonthCalendar(2015, 2, false);
const day = calendar[0];
expect(day.ethiopian).toHaveProperty('display');
expect(day.gregorian).toHaveProperty('display');
expect(typeof day.ethiopian.display).toBe('string');
expect(typeof day.gregorian.display).toBe('string');
});
test('should correctly format Geez numerals when useGeez = true', () => {
const k = new Kenat('2015/1/1');
const calendar = k.getMonthCalendar(2015, 1, true);
const day = calendar[0];
expect(day.ethiopian.display).toMatch(/[፩-፻]/); // Regex to match at least one Geez numeral
});
});
/* /test/Time.test.js */
import { Kenat } from '../src/Kenat.js';
import { Time } from '../src/Time.js';
import {
InvalidTimeError,
InvalidInputTypeError,
} from '../src/errors/errorHandler.js';
describe('Time Class and Related Logic', () => {
//----------------------------------------------------------------
// 1. Tests for the Time Class Constructor
//----------------------------------------------------------------
describe('Constructor and Validation', () => {
test('should create a valid Time object', () => {
const time = new Time(3, 30, 'day');
expect(time.hour).toBe(3);
expect(time.minute).toBe(30);
expect(time.period).toBe('day');
});
test('should default minute and period correctly', () => {
const time = new Time(5);
expect(time.minute).toBe(0);
expect(time.period).toBe('day');
});
test('should throw InvalidTimeError for out-of-range values', () => {
expect(() => new Time(0, 0, 'day')).toThrow(InvalidTimeError); // Hour 0 is invalid
expect(() => new Time(13, 0, 'day')).toThrow(InvalidTimeError); // Hour 13 is invalid
expect(() => new Time(5, -1, 'day')).toThrow(InvalidTimeError); // Negative minute
expect(() => new Time(5, 60, 'day')).toThrow(InvalidTimeError); // Minute >= 60
expect(() => new Time(5, 0, 'morning')).toThrow(InvalidTimeError); // Invalid period
});
test('should throw InvalidInputTypeError for non-numeric inputs', () => {
expect(() => new Time('three', 30)).toThrow(InvalidInputTypeError);
expect(() => new Time(3, 'thirty')).toThrow(InvalidInputTypeError);
});
});
//----------------------------------------------------------------
// 2. Tests for Time.fromString() - Addressing the PR Comment
//----------------------------------------------------------------
describe('Time.fromString()', () => {
test('should parse valid strings with Arabic numerals', () => {
expect(Time.fromString('10:30 day')).toEqual(new Time(10, 30, 'day'));
expect(Time.fromString('5:00 night')).toEqual(new Time(5, 0, 'night'));
});
test('should parse valid strings with Geez numerals', () => {
expect(Time.fromString('፫:፲፭ ማታ')).toEqual(new Time(3, 15, 'night'));
expect(Time.fromString('፲፪:፴ day')).toEqual(new Time(12, 30, 'day'));
});
test('should default to "day" period when missing', () => {
expect(Time.fromString('11:45')).toEqual(new Time(11, 45, 'day'));
});
test('should handle inconsistent spacing', () => {
expect(Time.fromString(' 4 : 20 night ')).toEqual(new Time(4, 20, 'night'));
});
test('should throw InvalidTimeError for malformed strings', () => {
// NOTE: We test for the general InvalidTimeError, as InvalidTimeFormatError has been removed.
expect(() => Time.fromString('10')).toThrow(InvalidTimeError);
expect(() => Time.fromString('10:')).toThrow(InvalidTimeError);
expect(() => Time.fromString(':30')).toThrow(InvalidTimeError);
expect(() => Time.fromString('10 30 day')).toThrow(InvalidTimeError); // No colon
expect(() => Time.fromString('abc:def period')).toThrow(InvalidTimeError); // Throws from constructor
});
test('should throw InvalidTimeError for empty or whitespace strings', () => {
// NOTE: We test for the general InvalidTimeError, as InvalidTimeFormatError has been removed.
expect(() => Time.fromString('')).toThrow(InvalidTimeError);
expect(() => Time.fromString(' ')).toThrow(InvalidTimeError);
});
test('should throw InvalidTimeError for valid format but out-of-range values', () => {
expect(() => Time.fromString('13:00 day')).toThrow(InvalidTimeError);
expect(() => Time.fromString('5:60 night')).toThrow(InvalidTimeError);
});
});
//----------------------------------------------------------------
// 3. Tests for Gregorian/Ethiopian Conversions
//----------------------------------------------------------------
describe('Gregorian-Ethiopian Conversions', () => {
test.each([
[7, 30, new Time(1, 30, 'day')],
[18, 0, new Time(12, 0, 'night')],
[0, 0, new Time(6, 0, 'night')],
[6, 0, new Time(12, 0, 'day')],
])('fromGregorian: should convert %i:%i correctly', (gHour, gMinute, expected) => {
expect(Time.fromGregorian(gHour, gMinute)).toEqual(expected);
});
test.each([
[new Time(1, 30, 'day'), { hour: 7, minute: 30 }],
[new Time(12, 0, 'night'), { hour: 18, minute: 0 }],
[new Time(6, 0, 'night'), { hour: 0, minute: 0 }],
[new Time(12, 0, 'day'), { hour: 6, minute: 0 }],
])('toGregorian: should convert %s correctly', (ethTime, expected) => {
expect(ethTime.toGregorian()).toEqual(expected);
});
test('fromGregorian should throw for invalid Gregorian time', () => {
expect(() => Time.fromGregorian(24, 0)).toThrow(InvalidTimeError);
expect(() => Time.fromGregorian(-1, 0)).toThrow(InvalidTimeError);
});
});
//----------------------------------------------------------------
// 4. Tests for Time Arithmetic
//----------------------------------------------------------------
describe('Time Arithmetic', () => {
const startTime = new Time(3, 15, 'day'); // 9:15 AM
test('add: should add hours and minutes correctly within the same period', () => {
const newTime = startTime.add({ hours: 2, minutes: 10 });
expect(newTime).toEqual(new Time(5, 25, 'day')); // 11:25 AM
});
test('add: should handle rolling over to the next period (day to night)', () => {
const newTime = startTime.add({ hours: 9 }); // 9:15 AM + 9 hours = 6:15 PM
expect(newTime).toEqual(new Time(12, 15, 'night'));
});
test('subtract: should subtract time correctly', () => {
const newTime = startTime.subtract({ hours: 1, minutes: 15 });
expect(newTime).toEqual(new Time(2, 0, 'day')); // 8:00 AM
});
test('subtract: should handle rolling back to the previous period (day to night)', () => {
const newTime = new Time(1, 0, 'day').subtract({ hours: 2 }); // 7:00 AM - 2 hours = 5:00 AM
expect(newTime).toEqual(new Time(11, 0, 'night'));
});
test('diff: should calculate the difference between two times', () => {
const endTime = new Time(5, 45, 'day');
const difference = startTime.diff(endTime);
expect(difference).toEqual({ hours: 2, minutes: 30 });
});
test('diff: should calculate the shortest difference across the 24h wrap', () => {
const t1 = new Time(2, 0, 'night'); // 8 PM
const t2 = new Time(10, 0, 'night'); // 4 AM
const difference = t1.diff(t2);
expect(difference).toEqual({ hours: 8, minutes: 0 }); // 8 hours difference
});
test('add/subtract should throw on invalid duration', () => {
expect(() => startTime.add({ hours: 'two' })).toThrow(InvalidInputTypeError);
expect(() => startTime.subtract('one hour')).toThrow(InvalidTimeError);
});
});
//----------------------------------------------------------------
// 5. Tests for Formatting
//----------------------------------------------------------------
describe('Formatting', () => {
test('format: should format with default options (Geez)', () => {
const time = new Time(5, 30, 'day');
expect(time.format()).toBe('፭:፴ ጠዋት');
});
test('format: should format with Arabic numerals', () => {
const time = new Time(5, 30, 'day');
expect(time.format({ useGeez: false })).toBe('05:30 day');
});
test('format: should format without period label', () => {
const time = new Time(8, 15, 'night');
expect(time.format({ useGeez: false, showPeriodLabel: false })).toBe('08:15');
});
test('format: should use a dash for zero minutes', () => {
const time = new Time(12, 0, 'day');
expect(time.format({ useGeez: false, zeroAsDash: true })).toBe('12:_ day');
});
});
//----------------------------------------------------------------
// 6. Tests for Kenat Class Time-Related Methods
//----------------------------------------------------------------
describe('Kenat Time Methods', () => {
test('getCurrentTime returns a valid Time instance', () => {
const now = new Kenat();
const ethTime = now.getCurrentTime();
expect(ethTime).toBeInstanceOf(Time);
expect(ethTime).toHaveProperty('hour');
expect(ethTime).toHaveProperty('minute');
expect(ethTime).toHaveProperty('period');
});
});
});
import { dayOfYear } from "../src/utils";
import {
monthDayFromDayOfYear,
isEthiopianLeapYear,
isGregorianLeapYear,
getEthiopianDaysInMonth
} from "../src/utils";
describe('dayOfYear', () => {
it('returns 1 for January 1st of a non-leap year', () => {
expect(dayOfYear(2023, 1, 1)).toBe(1);
});
it('returns 32 for February 1st of a non-leap year', () => {
expect(dayOfYear(2023, 2, 1)).toBe(32);
});
it('returns 59 for February 28th of a non-leap year', () => {
expect(dayOfYear(2023, 2, 28)).toBe(59);
});
it('returns 60 for March 1st of a non-leap year', () => {
expect(dayOfYear(2023, 3, 1)).toBe(60);
});
it('returns 60 for February 29th of a leap year', () => {
expect(dayOfYear(2024, 2, 29)).toBe(60);
});
it('returns 61 for March 1st of a leap year', () => {
expect(dayOfYear(2024, 3, 1)).toBe(61);
});
it('returns 365 for December 31st of a non-leap year', () => {
expect(dayOfYear(2023, 12, 31)).toBe(365);
});
it('returns 366 for December 31st of a leap year', () => {
expect(dayOfYear(2024, 12, 31)).toBe(366);
});
});
describe('monthDayFromDayOfYear', () => {
it('returns January 1 for day 1 of a non-leap year', () => {
expect(monthDayFromDayOfYear(2023, 1)).toEqual({ month: 1, day: 1 });
});
it('returns January 31 for day 31 of a non-leap year', () => {
expect(monthDayFromDayOfYear(2023, 31)).toEqual({ month: 1, day: 31 });
});
it('returns February 1 for day 32 of a non-leap year', () => {
expect(monthDayFromDayOfYear(2023, 32)).toEqual({ month: 2, day: 1 });
});
it('returns February 28 for day 59 of a non-leap year', () => {
expect(monthDayFromDayOfYear(2023, 59)).toEqual({ month: 2, day: 28 });
});
it('returns March 1 for day 60 of a non-leap year', () => {
expect(monthDayFromDayOfYear(2023, 60)).toEqual({ month: 3, day: 1 });
});
it('returns February 29 for day 60 of a leap year', () => {
expect(monthDayFromDayOfYear(2024, 60)).toEqual({ month: 2, day: 29 });
});
it('returns March 1 for day 61 of a leap year', () => {
expect(monthDayFromDayOfYear(2024, 61)).toEqual({ month: 3, day: 1 });
});
it('returns December 31 for day 365 of a non-leap year', () => {
expect(monthDayFromDayOfYear(2023, 365)).toEqual({ month: 12, day: 31 });
});
it('returns December 31 for day 366 of a leap year', () => {
expect(monthDayFromDayOfYear(2024, 366)).toEqual({ month: 12, day: 31 });
});
});
describe('isGregorianLeapYear', () => {
it('returns true for years divisible by 4 but not by 100', () => {
expect(isGregorianLeapYear(2024)).toBe(true);
expect(isGregorianLeapYear(1996)).toBe(true);
expect(isGregorianLeapYear(2008)).toBe(true);
});
it('returns false for years not divisible by 4', () => {
expect(isGregorianLeapYear(2023)).toBe(false);
expect(isGregorianLeapYear(2019)).toBe(false);
expect(isGregorianLeapYear(2101)).toBe(false);
});
it('returns false for years divisible by 100 but not by 400', () => {
expect(isGregorianLeapYear(1900)).toBe(false);
expect(isGregorianLeapYear(2100)).toBe(false);
expect(isGregorianLeapYear(1800)).toBe(false);
});
it('returns true for years divisible by 400', () => {
expect(isGregorianLeapYear(2000)).toBe(true);
expect(isGregorianLeapYear(1600)).toBe(true);
expect(isGregorianLeapYear(2400)).toBe(true);
});
});
describe('isEthiopianLeapYear', () => {
it('returns true for Ethiopian years where year % 4 === 3', () => {
expect(isEthiopianLeapYear(2011)).toBe(true);
expect(isEthiopianLeapYear(2015)).toBe(true);
expect(isEthiopianLeapYear(2019)).toBe(true);
expect(isEthiopianLeapYear(2003)).toBe(true);
});
it('returns false for Ethiopian years where year % 4 !== 3', () => {
expect(isEthiopianLeapYear(2010)).toBe(false);
expect(isEthiopianLeapYear(2012)).toBe(false);
expect(isEthiopianLeapYear(2013)).toBe(false);
expect(isEthiopianLeapYear(2014)).toBe(false);
expect(isEthiopianLeapYear(2020)).toBe(false);
});
});
describe('getEthiopianDaysInMonth', () => {
it('returns 30 for any month from 1 to 12', () => {
for (let month = 1; month <= 12; month++) {
expect(getEthiopianDaysInMonth(2010, month)).toBe(30);
expect(getEthiopianDaysInMonth(2011, month)).toBe(30);
expect(getEthiopianDaysInMonth(2012, month)).toBe(30);
expect(getEthiopianDaysInMonth(2013, month)).toBe(30);
}
});
it('returns 6 for month 13 in a leap year', () => {
// 2011, 2015, 2019 are leap years (year % 4 === 3)
expect(getEthiopianDaysInMonth(2011, 13)).toBe(6);
expect(getEthiopianDaysInMonth(2015, 13)).toBe(6);
expect(getEthiopianDaysInMonth(2019, 13)).toBe(6);
});
it('returns 5 for month 13 in a non-leap year', () => {
// 2010, 2012, 2013, 2014, 2020 are not leap years
expect(getEthiopianDaysInMonth(2010, 13)).toBe(5);
expect(getEthiopianDaysInMonth(2012, 13)).toBe(5);
expect(getEthiopianDaysInMonth(2013, 13)).toBe(5);
expect(getEthiopianDaysInMonth(2014, 13)).toBe(5);
expect(getEthiopianDaysInMonth(2020, 13)).toBe(5);
});
});
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"declaration": true,
"emitDeclarationOnly": true,
"declarationDir": "./types",
"outDir": "./dist",
"rootDir": "./src",
"target": "ES2020",
"module": "ESNext",
"lib": ["es2022", "dom", "es2020.intl"],
"skipLibCheck": true,
"noEmitOnError": true
},
"include": [
"src/**/*.js"
]
}
/**
* Calculates all Bahire Hasab values for a given Ethiopian year, including all movable feasts.
*
* @param {number} ethiopianYear - The Ethiopian year to calculate for.
* @param {Object} [options={}] - Options for language.
* @param {string} [options.lang='amharic'] - The language for names.
* @returns {Object} An object containing all the calculated Bahire Hasab values.
*/
export function getBahireHasab(ethiopianYear: number, options?: {
lang?: string;
}): any;
/**
* Calculates the date of a movable holiday for a given year.
* This is now a pure date calculator that returns a simple date object,
* ensuring backward compatibility with existing tests.
*
* @param {'ABIY_TSOME'|'TINSAYE'|'ERGET'|...} holidayKey - The key of the holiday from movableHolidayTewsak.
* @param {number} ethiopianYear - The Ethiopian year.
* @returns {Object} An Ethiopian date object { year, month, day }.
*/
export function getMovableHoliday(holidayKey: any, ethiopianYear: number): any;
export namespace monthNames {
let english: string[];
let amharic: string[];
let gregorian: string[];
}
export namespace daysOfWeek {
let english_1: string[];
export { english_1 as english };
let amharic_1: string[];
export { amharic_1 as amharic };
}
export namespace HolidayTags {
let PUBLIC: string;
let RELIGIOUS: string;
let CHRISTIAN: string;
let MUSLIM: string;
let STATE: string;
let CULTURAL: string;
let OTHER: string;
}
export namespace keyToTewsakMap {
let nineveh: string;
let abiyTsome: string;
let debreZeit: string;
let hosanna: string;
let siklet: string;
let fasika: string;
let rikbeKahnat: string;
let erget: string;
let paraclete: string;
let tsomeHawaryat: string;
let tsomeDihnet: string;
}
export namespace PERIOD_LABELS {
let day: string;
let night: string;
}
export namespace holidayInfo {
export namespace enkutatash {
namespace name {
let amharic_2: string;
export { amharic_2 as amharic };
let english_2: string;
export { english_2 as english };
}
namespace description {
let amharic_3: string;
export { amharic_3 as amharic };
let english_3: string;
export { english_3 as english };
}
}
export namespace meskel {
export namespace name_1 {
let amharic_4: string;
export { amharic_4 as amharic };
let english_4: string;
export { english_4 as english };
}
export { name_1 as name };
export namespace description_1 {
let amharic_5: string;
export { amharic_5 as amharic };
let english_5: string;
export { english_5 as english };
}
export { description_1 as description };
}
export namespace beherbehereseb {
export namespace name_2 {
let amharic_6: string;
export { amharic_6 as amharic };
let english_6: string;
export { english_6 as english };
}
export { name_2 as name };
export namespace description_2 {
let amharic_7: string;
export { amharic_7 as amharic };
let english_7: string;
export { english_7 as english };
}
export { description_2 as description };
}
export namespace gena {
export namespace name_3 {
let amharic_8: string;
export { amharic_8 as amharic };
let english_8: string;
export { english_8 as english };
}
export { name_3 as name };
export namespace description_3 {
let amharic_9: string;
export { amharic_9 as amharic };
let english_9: string;
export { english_9 as english };
}
export { description_3 as description };
}
export namespace timket {
export namespace name_4 {
let amharic_10: string;
export { amharic_10 as amharic };
let english_10: string;
export { english_10 as english };
}
export { name_4 as name };
export namespace description_4 {
let amharic_11: string;
export { amharic_11 as amharic };
let english_11: string;
export { english_11 as english };
}
export { description_4 as description };
}
export namespace martyrsDay {
export namespace name_5 {
let amharic_12: string;
export { amharic_12 as amharic };
let english_12: string;
export { english_12 as english };
}
export { name_5 as name };
export namespace description_5 {
let amharic_13: string;
export { amharic_13 as amharic };
let english_13: string;
export { english_13 as english };
}
export { description_5 as description };
}
export namespace adwa {
export namespace name_6 {
let amharic_14: string;
export { amharic_14 as amharic };
let english_14: string;
export { english_14 as english };
}
export { name_6 as name };
export namespace description_6 {
let amharic_15: string;
export { amharic_15 as amharic };
let english_15: string;
export { english_15 as english };
}
export { description_6 as description };
}
export namespace labour {
export namespace name_7 {
let amharic_16: string;
export { amharic_16 as amharic };
let english_16: string;
export { english_16 as english };
}
export { name_7 as name };
export namespace description_7 {
let amharic_17: string;
export { amharic_17 as amharic };
let english_17: string;
export { english_17 as english };
}
export { description_7 as description };
}
export namespace patriots {
export namespace name_8 {
let amharic_18: string;
export { amharic_18 as amharic };
let english_18: string;
export { english_18 as english };
}
export { name_8 as name };
export namespace description_8 {
let amharic_19: string;
export { amharic_19 as amharic };
let english_19: string;
export { english_19 as english };
}
export { description_8 as description };
}
export namespace nineveh_1 {
export namespace name_9 {
let amharic_20: string;
export { amharic_20 as amharic };
let english_20: string;
export { english_20 as english };
}
export { name_9 as name };
export namespace description_9 {
let amharic_21: string;
export { amharic_21 as amharic };
let english_21: string;
export { english_21 as english };
}
export { description_9 as description };
}
export { nineveh_1 as nineveh };
export namespace abiyTsome_1 {
export namespace name_10 {
let amharic_22: string;
export { amharic_22 as amharic };
let english_22: string;
export { english_22 as english };
}
export { name_10 as name };
export namespace description_10 {
let amharic_23: string;
export { amharic_23 as amharic };
let english_23: string;
export { english_23 as english };
}
export { description_10 as description };
}
export { abiyTsome_1 as abiyTsome };
export namespace debreZeit_1 {
export namespace name_11 {
let amharic_24: string;
export { amharic_24 as amharic };
let english_24: string;
export { english_24 as english };
}
export { name_11 as name };
export namespace description_11 {
let amharic_25: string;
export { amharic_25 as amharic };
let english_25: string;
export { english_25 as english };
}
export { description_11 as description };
}
export { debreZeit_1 as debreZeit };
export namespace hosanna_1 {
export namespace name_12 {
let amharic_26: string;
export { amharic_26 as amharic };
let english_26: string;
export { english_26 as english };
}
export { name_12 as name };
export namespace description_12 {
let amharic_27: string;
export { amharic_27 as amharic };
let english_27: string;
export { english_27 as english };
}
export { description_12 as description };
}
export { hosanna_1 as hosanna };
export namespace siklet_1 {
export namespace name_13 {
let amharic_28: string;
export { amharic_28 as amharic };
let english_28: string;
export { english_28 as english };
}
export { name_13 as name };
export namespace description_13 {
let amharic_29: string;
export { amharic_29 as amharic };
let english_29: string;
export { english_29 as english };
}
export { description_13 as description };
}
export { siklet_1 as siklet };
export namespace fasika_1 {
export namespace name_14 {
let amharic_30: string;
export { amharic_30 as amharic };
let english_30: string;
export { english_30 as english };
}
export { name_14 as name };
export namespace description_14 {
let amharic_31: string;
export { amharic_31 as amharic };
let english_31: string;
export { english_31 as english };
}
export { description_14 as description };
}
export { fasika_1 as fasika };
export namespace rikbeKahnat_1 {
export namespace name_15 {
let amharic_32: string;
export { amharic_32 as amharic };
let english_32: string;
export { english_32 as english };
}
export { name_15 as name };
export namespace description_15 {
let amharic_33: string;
export { amharic_33 as amharic };
let english_33: string;
export { english_33 as english };
}
export { description_15 as description };
}
export { rikbeKahnat_1 as rikbeKahnat };
export namespace erget_1 {
export namespace name_16 {
let amharic_34: string;
export { amharic_34 as amharic };
let english_34: string;
export { english_34 as english };
}
export { name_16 as name };
export namespace description_16 {
let amharic_35: string;
export { amharic_35 as amharic };
let english_35: string;
export { english_35 as english };
}
export { description_16 as description };
}
export { erget_1 as erget };
export namespace paraclete_1 {
export namespace name_17 {
let amharic_36: string;
export { amharic_36 as amharic };
let english_36: string;
export { english_36 as english };
}
export { name_17 as name };
export namespace description_17 {
let amharic_37: string;
export { amharic_37 as amharic };
let english_37: string;
export { english_37 as english };
}
export { description_17 as description };
}
export { paraclete_1 as paraclete };
export namespace tsomeHawaryat_1 {
export namespace name_18 {
let amharic_38: string;
export { amharic_38 as amharic };
let english_38: string;
export { english_38 as english };
}
export { name_18 as name };
export namespace description_18 {
let amharic_39: string;
export { amharic_39 as amharic };
let english_39: string;
export { english_39 as english };
}
export { description_18 as description };
}
export { tsomeHawaryat_1 as tsomeHawaryat };
export namespace tsomeDihnet_1 {
export namespace name_19 {
let amharic_40: string;
export { amharic_40 as amharic };
let english_40: string;
export { english_40 as english };
}
export { name_19 as name };
export namespace description_19 {
let amharic_41: string;
export { amharic_41 as amharic };
let english_41: string;
export { english_41 as english };
}
export { description_19 as description };
}
export { tsomeDihnet_1 as tsomeDihnet };
export namespace eidFitr {
export namespace name_20 {
let amharic_42: string;
export { amharic_42 as amharic };
let english_42: string;
export { english_42 as english };
}
export { name_20 as name };
export namespace description_20 {
let amharic_43: string;
export { amharic_43 as amharic };
let english_43: string;
export { english_43 as english };
}
export { description_20 as description };
}
export namespace eidAdha {
export namespace name_21 {
let amharic_44: string;
export { amharic_44 as amharic };
let english_44: string;
export { english_44 as english };
}
export { name_21 as name };
export namespace description_21 {
let amharic_45: string;
export { amharic_45 as amharic };
let english_45: string;
export { english_45 as english };
}
export { description_21 as description };
}
export namespace moulid {
export namespace name_22 {
let amharic_46: string;
export { amharic_46 as amharic };
let english_46: string;
export { english_46 as english };
}
export { name_22 as name };
export namespace description_22 {
let amharic_47: string;
export { amharic_47 as amharic };
let english_47: string;
export { english_47 as english };
}
export { description_22 as description };
}
export namespace jummah {
export namespace name_23 {
let amharic_48: string;
export { amharic_48 as amharic };
let english_48: string;
export { english_48 as english };
}
export { name_23 as name };
export namespace description_23 {
let amharic_49: string;
export { amharic_49 as amharic };
let english_49: string;
export { english_49 as english };
}
export { description_23 as description };
}
}
export const HolidayNames: Readonly<{}>;
export namespace evangelistNames {
let english_50: {
1: string;
2: string;
3: string;
0: string;
};
export { english_50 as english };
let amharic_50: {
1: string;
2: string;
3: string;
0: string;
};
export { amharic_50 as amharic };
}
export namespace tewsakMap {
let Sunday: number;
let Monday: number;
let Tuesday: number;
let Wednesday: number;
let Thursday: number;
let Friday: number;
let Saturday: number;
}
export namespace movableHolidayTewsak {
let NINEVEH: number;
let ABIY_TSOME: number;
let DEBRE_ZEIT: number;
let HOSANNA: number;
let SIKLET: number;
let TINSAYE: number;
let RIKBE_KAHNAT: number;
let ERGET: number;
let PARACLETE: number;
let TSOME_HAWARYAT: number;
let TSOME_DIHENET: number;
}
export namespace movableHolidays {
export namespace nineveh_2 {
let tags: string[];
}
export { nineveh_2 as nineveh };
export namespace abiyTsome_2 {
let tags_1: string[];
export { tags_1 as tags };
}
export { abiyTsome_2 as abiyTsome };
export namespace debreZeit_2 {
let tags_2: string[];
export { tags_2 as tags };
}
export { debreZeit_2 as debreZeit };
export namespace hosanna_2 {
let tags_3: string[];
export { tags_3 as tags };
}
export { hosanna_2 as hosanna };
export namespace siklet_2 {
let tags_4: string[];
export { tags_4 as tags };
}
export { siklet_2 as siklet };
export namespace fasika_2 {
let tags_5: string[];
export { tags_5 as tags };
}
export { fasika_2 as fasika };
export namespace rikbeKahnat_2 {
let tags_6: string[];
export { tags_6 as tags };
}
export { rikbeKahnat_2 as rikbeKahnat };
export namespace erget_2 {
let tags_7: string[];
export { tags_7 as tags };
}
export { erget_2 as erget };
export namespace paraclete_2 {
let tags_8: string[];
export { tags_8 as tags };
}
export { paraclete_2 as paraclete };
export namespace tsomeHawaryat_2 {
let tags_9: string[];
export { tags_9 as tags };
}
export { tsomeHawaryat_2 as tsomeHawaryat };
export namespace tsomeDihnet_2 {
let tags_10: string[];
export { tags_10 as tags };
}
export { tsomeDihnet_2 as tsomeDihnet };
export namespace eidFitr_1 {
let tags_11: string[];
export { tags_11 as tags };
}
export { eidFitr_1 as eidFitr };
export namespace eidAdha_1 {
let tags_12: string[];
export { tags_12 as tags };
}
export { eidAdha_1 as eidAdha };
export namespace moulid_1 {
let tags_13: string[];
export { tags_13 as tags };
}
export { moulid_1 as moulid };
}
export namespace FastingKeys {
let ABIY_TSOME_1: string;
export { ABIY_TSOME_1 as ABIY_TSOME };
let TSOME_HAWARYAT_1: string;
export { TSOME_HAWARYAT_1 as TSOME_HAWARYAT };
export let TSOME_NEBIYAT: string;
let NINEVEH_1: string;
export { NINEVEH_1 as NINEVEH };
export let FILSETA: string;
export let RAMADAN: string;
let TSOME_DIHENET_1: string;
export { TSOME_DIHENET_1 as TSOME_DIHENET };
}
export namespace fastingInfo {
export namespace ABIY_TSOME_2 {
export namespace name_24 {
let amharic_51: string;
export { amharic_51 as amharic };
let english_51: string;
export { english_51 as english };
}
export { name_24 as name };
export namespace description_24 {
let amharic_52: string;
export { amharic_52 as amharic };
let english_52: string;
export { english_52 as english };
}
export { description_24 as description };
let tags_14: string[];
export { tags_14 as tags };
}
export { ABIY_TSOME_2 as ABIY_TSOME };
export namespace NINEVEH_2 {
export namespace name_25 {
let amharic_53: string;
export { amharic_53 as amharic };
let english_53: string;
export { english_53 as english };
}
export { name_25 as name };
export namespace description_25 {
let amharic_54: string;
export { amharic_54 as amharic };
let english_54: string;
export { english_54 as english };
}
export { description_25 as description };
let tags_15: string[];
export { tags_15 as tags };
}
export { NINEVEH_2 as NINEVEH };
export namespace TSOME_HAWARYAT_2 {
export namespace name_26 {
let amharic_55: string;
export { amharic_55 as amharic };
let english_55: string;
export { english_55 as english };
}
export { name_26 as name };
export namespace description_26 {
let amharic_56: string;
export { amharic_56 as amharic };
let english_56: string;
export { english_56 as english };
}
export { description_26 as description };
let tags_16: string[];
export { tags_16 as tags };
}
export { TSOME_HAWARYAT_2 as TSOME_HAWARYAT };
export namespace TSOME_NEBIYAT_1 {
export namespace name_27 {
let amharic_57: string;
export { amharic_57 as amharic };
let english_57: string;
export { english_57 as english };
}
export { name_27 as name };
export namespace description_27 {
let amharic_58: string;
export { amharic_58 as amharic };
let english_58: string;
export { english_58 as english };
}
export { description_27 as description };
let tags_17: string[];
export { tags_17 as tags };
}
export { TSOME_NEBIYAT_1 as TSOME_NEBIYAT };
export namespace FILSETA_1 {
export namespace name_28 {
let amharic_59: string;
export { amharic_59 as amharic };
let english_59: string;
export { english_59 as english };
}
export { name_28 as name };
export namespace description_28 {
let amharic_60: string;
export { amharic_60 as amharic };
let english_60: string;
export { english_60 as english };
}
export { description_28 as description };
let tags_18: string[];
export { tags_18 as tags };
}
export { FILSETA_1 as FILSETA };
export namespace RAMADAN_1 {
export namespace name_29 {
let amharic_61: string;
export { amharic_61 as amharic };
let english_61: string;
export { english_61 as english };
}
export { name_29 as name };
export namespace description_29 {
let amharic_62: string;
export { amharic_62 as amharic };
let english_62: string;
export { english_62 as english };
}
export { description_29 as description };
let tags_19: string[];
export { tags_19 as tags };
}
export { RAMADAN_1 as RAMADAN };
export namespace TSOME_DIHENET_2 {
export namespace name_30 {
let amharic_63: string;
export { amharic_63 as amharic };
let english_63: string;
export { english_63 as english };
}
export { name_30 as name };
export namespace description_30 {
let amharic_64: string;
export { amharic_64 as amharic };
let english_64: string;
export { english_64 as english };
}
export { description_30 as description };
let tags_20: string[];
export { tags_20 as tags };
}
export { TSOME_DIHENET_2 as TSOME_DIHENET };
}
/**
* Converts an Ethiopian date to its corresponding Gregorian date.
*
* @param {number} ethYear - The Ethiopian year.
* @param {number} ethMonth - The Ethiopian month (1-13).
* @param {number} ethDay - The Ethiopian day of the month.
* @returns {{ year: number, month: number, day: number }} The equivalent Gregorian date.
* @throws {InvalidInputTypeError} If any input is not a number.
* @throws {InvalidEthiopianDateError} If the provided Ethiopian date is invalid.
*/
export function toGC(ethYear: number, ethMonth: number, ethDay: number): {
year: number;
month: number;
day: number;
};
/**
* Converts a Gregorian date to the Ethiopian calendar (EC) date.
*
* @param {number} gYear - The Gregorian year (e.g., 2024).
* @param {number} gMonth - The Gregorian month (1-12).
* @param {number} gDay - The Gregorian day of the month (1-31).
* @returns {{ year: number, month: number, day: number }} The corresponding Ethiopian calendar date.
* @throws {InvalidInputTypeError} If any input is not a number.
* @throws {InvalidGregorianDateError} If the input date is invalid or out of supported range.
*/
export function toEC(gYear: number, gMonth: number, gDay: number): {
year: number;
month: number;
day: number;
};
/**
* Converts an Ethiopian date to a Gregorian Calendar JavaScript Date object (UTC).
*
* @param {number} ethYear - The Ethiopian year.
* @param {number} ethMonth - The Ethiopian month (1-based).
* @param {number} ethDay - The Ethiopian day.
* @returns {Date} A JavaScript Date object representing the equivalent Gregorian date in UTC.
*/
export function toGCDate(ethYear: number, ethMonth: number, ethDay: number): Date;
/**
* Converts a JavaScript Date object to the Ethiopian Calendar (EC) date representation.
*
* @param {Date} dateObj - The JavaScript Date object to convert.
* @returns {*} The Ethiopian Calendar date, as returned by the `toEC` function.
*/
export function fromDateToEC(dateObj: Date): any;
/**
* Get Hijri year from a Gregorian date
* @param {Date} date
* @returns {number} hijri year
*/
export function getHijriYear(date: Date): number;
/**
* Converts a Hijri date to the corresponding Gregorian date within a given Gregorian year.
*
* @param {number} hYear - Hijri year (e.g., 1445)
* @param {number} hMonth - Hijri month (1–12)
* @param {number} hDay - Hijri day (1–30)
* @param {number} gregorianYear - Target Gregorian year to restrict the search range
* @returns {Date|null} Gregorian Date object or null if not found
*/
export function hijriToGregorian(hYear: number, hMonth: number, hDay: number, gregorianYear: number): Date | null;
export const islamicFormatter: Intl.DateTimeFormat;
/**
* Adds a specified number of days to an Ethiopian date.
*
* @param {Object} ethiopian - The Ethiopian date object { year, month, day }.
* @param {number} days - The number of days to add.
* @returns {Object} The resulting Ethiopian date.
* @throws {InvalidInputTypeError} If inputs are not of the correct type.
*/
export function addDays(ethiopian: any, days: number): any;
/**
* Adds a specified number of months to an Ethiopian date.
*
* @param {Object} ethiopian - The Ethiopian date object { year, month, day }.
* @param {number} months - The number of months to add.
* @returns {Object} The resulting Ethiopian date.
* @throws {InvalidInputTypeError} If inputs are not of the correct type.
*/
export function addMonths(ethiopian: any, months: number): any;
/**
* Adds a specified number of years to an Ethiopian date.
*
* @param {Object} ethiopian - The Ethiopian date object { year, month, day }.
* @param {number} years - The number of years to add.
* @returns {Object} The resulting Ethiopian date.
* @throws {InvalidInputTypeError} If inputs are not of the correct type.
*/
export function addYears(ethiopian: any, years: number): any;
/**
* Calculates the difference in days between two Ethiopian dates.
*
* @param {Object} a - The first Ethiopian date object.
* @param {Object} b - The second Ethiopian date object.
* @returns {number} The difference in days.
* @throws {InvalidInputTypeError} If inputs are not valid date objects.
*/
export function diffInDays(a: any, b: any): number;
/**
* Calculates the difference in months between two Ethiopian dates.
*
* @param {Object} a - The first Ethiopian date object.
* @param {Object} b - The second Ethiopian date object.
* @returns {number} The difference in months.
* @throws {InvalidInputTypeError} If inputs are not valid date objects.
*/
export function diffInMonths(a: any, b: any): number;
/**
* Calculates the difference in years between two Ethiopian dates.
*
* @param {Object} a - The first Ethiopian date object.
* @param {Object} b - The second Ethiopian date object.
* @returns {number} The difference in years.
* @throws {InvalidInputTypeError} If inputs are not valid date objects.
*/
export function diffInYears(a: any, b: any): number;
/**
* Calculates a human-friendly breakdown between two Ethiopian dates.
* Iteratively accumulates years, then months, then days to avoid off-by-one issues.
*
* @param {Object} a - First Ethiopian date { year, month, day }.
* @param {Object} b - Second Ethiopian date { year, month, day }.
* @param {Object} [options]
* @param {Array<'years'|'months'|'days'>} [options.units=['years','months','days']] - Units to include, in order.
* @returns {{ sign: 1|-1, years?: number, months?: number, days?: number, totalDays: number }}
*/
export function diffBreakdown(a: any, b: any, options?: {
units?: Array<"years" | "months" | "days">;
}): {
sign: 1 | -1;
years?: number;
months?: number;
days?: number;
totalDays: number;
};
/**
* Base class for all custom errors in the Kenat library.
*/
export class KenatError extends Error {
constructor(message: any);
/**
* Provides a serializable representation of the error.
* @returns {Object} A plain object with error details.
*/
toJSON(): any;
}
/**
* Thrown when an Ethiopian date is numerically invalid (e.g., month 14).
*/
export class InvalidEthiopianDateError extends KenatError {
constructor(year: any, month: any, day: any);
date: {
year: any;
month: any;
day: any;
};
}
/**
* Thrown when a Gregorian date is numerically invalid.
*/
export class InvalidGregorianDateError extends KenatError {
constructor(year: any, month: any, day: any);
date: {
year: any;
month: any;
day: any;
};
}
/**
* Thrown when a date string provided to the constructor has an invalid format.
*/
export class InvalidDateFormatError extends KenatError {
inputString: any;
}
/**
* Thrown when the Kenat constructor receives an input type it cannot handle.
*/
export class UnrecognizedInputError extends KenatError {
input: any;
}
/**
* Thrown for errors occurring during Ge'ez numeral conversion.
*/
export class GeezConverterError extends KenatError {
}
/**
* Thrown when a function receives an argument of an incorrect type.
*/
export class InvalidInputTypeError extends KenatError {
constructor(functionName: any, parameterName: any, expectedType: any, receivedValue: any);
functionName: any;
parameterName: any;
expectedType: any;
receivedValue: any;
}
/**
* Thrown for errors related to invalid time components.
*/
export class InvalidTimeError extends KenatError {
}
/**
* Thrown for invalid configuration options passed to MonthGrid.
*/
export class InvalidGridConfigError extends KenatError {
}
/**
* Thrown when an unknown holiday key is used.
*/
export class UnknownHolidayError extends KenatError {
holidayKey: any;
}
/**
* Calculates the start and end dates of a specific fasting period for a given year.
* @param {'ABIY_TSOME' | 'TSOME_HAWARYAT' | 'TSOME_NEBIYAT' | 'NINEVEH' | 'RAMADAN'} fastKey - The key for the fast.
* @param {number} ethiopianYear - The Ethiopian year.
* @returns {{start: object, end: object}|null} An object with start and end PLAIN date objects.
*/
export function getFastingPeriod(fastKey: "ABIY_TSOME" | "TSOME_HAWARYAT" | "TSOME_NEBIYAT" | "NINEVEH" | "RAMADAN", ethiopianYear: number): {
start: object;
end: object;
} | null;
/**
* Returns fasting information (names, descriptions, period) for a given fast and year.
* @param {'ABIY_TSOME'|'TSOME_HAWARYAT'|'TSOME_NEBIYAT'|'NINEVEH'|'RAMADAN'} fastKey
* @param {number} ethiopianYear
* @param {{lang?: 'amharic'|'english'}} options
* @returns {{ key: string, name: string, description: string, period: { start: object, end: object } } | null}
*/
export function getFastingInfo(fastKey: "ABIY_TSOME" | "TSOME_HAWARYAT" | "TSOME_NEBIYAT" | "NINEVEH" | "RAMADAN", ethiopianYear: number, options?: {
lang?: "amharic" | "english";
}): {
key: string;
name: string;
description: string;
period: {
start: object;
end: object;
};
} | null;
/**
* Return an array of day numbers in the given Ethiopian month that belong to a fasting period.
* For TSOME_DIHENET, it returns all Wednesdays and Fridays excluding the 50-day period after Easter (through Pentecost).
* For fixed/range fasts, it returns the days intersecting the fast period.
*
* @param {string} fastKey - One of FastingKeys
* @param {number} year - Ethiopian year
* @param {number} month - Ethiopian month (1-13)
* @returns {number[]}
*/
export function getFastingDays(fastKey: string, year: number, month: number): number[];
/**
* Formats an Ethiopian date using language-specific month name and Arabic numerals.
*
* @param {{year: number, month: number, day: number}} etDate - Ethiopian date object
* @param {'amharic'|'english'} [lang='amharic'] - Language for month name
* @returns {string} Formatted string like "መስከረም 10 2016"
*/
export function formatStandard(etDate: {
year: number;
month: number;
day: number;
}, lang?: "amharic" | "english"): string;
/**
* Formats an Ethiopian date in Geez numerals with Amharic month name.
*
* @param {{year: number, month: number, day: number}} etDate - Ethiopian date
* @returns {string} Example: "መስከረም ፲፩ ፳፻፲፮"
*/
export function formatInGeezAmharic(etDate: {
year: number;
month: number;
day: number;
}): string;
/**
* Formats an Ethiopian date and time as a string.
*
* @param {{year: number, month: number, day: number}} etDate - Ethiopian date
* @param {import('../Time.js').Time} time - An instance of the Time class
* @param {'amharic'|'english'} [lang='amharic'] - Language for suffix
* @returns {string} Example: "መስከረም 10 2016 08:30 ጠዋት"
*/
export function formatWithTime(etDate: {
year: number;
month: number;
day: number;
}, time: any, lang?: "amharic" | "english"): string;
/**
* Formats an Ethiopian date object with the weekday name, month name, day, and year.
*
* @param {Object} etDate - The Ethiopian date object to format.
* @param {number} etDate.day - The day of the month.
* @param {number} etDate.month - The month number (1-based).
* @param {number} etDate.year - The year.
* @param {string} [lang='amharic'] - The language to use for weekday and month names ('amharic', 'english', etc.).
* @param {boolean} [useGeez=false] - Whether to format the day and year in Geez numerals.
* @returns {string} The formatted date string, e.g., "ማክሰኞ, መስከረም 1 2016".
*/
export function formatWithWeekday(etDate: {
day: number;
month: number;
year: number;
}, lang?: string, useGeez?: boolean): string;
/**
* Returns Ethiopian date in short "yyyy/mm/dd" format.
* @param {{year: number, month: number, day: number}} etDate
* @returns {string} e.g., "2017/10/25"
*/
export function formatShort(etDate: {
year: number;
month: number;
day: number;
}): string;
/**
* Returns an ISO-like string: "YYYY-MM-DD" or "YYYY-MM-DDTHH:mm".
* @param {{year: number, month: number, day: number}} etDate
* @param {{hour: number, minute: number, period: 'day'|'night'}|null} time
* @returns {string}
*/
export function toISODateString(etDate: {
year: number;
month: number;
day: number;
}, time?: {
hour: number;
minute: number;
period: "day" | "night";
} | null): string;
/**
* Converts a natural number to Ethiopic numeral string.
*
* @param {number|string} input - The number to convert (positive integer only).
* @returns {string} Ethiopic numeral string.
* @throws {GeezConverterError} If input is not a valid positive integer.
*/
export function toGeez(input: number | string): string;
/**
* Converts a Ge'ez numeral string to its Arabic numeral equivalent.
*
* @param {string} geezStr - The Ge'ez numeral string to convert.
* @returns {number} The Arabic numeral representation of the input string.
* @throws {GeezConverterError} If the input is not a valid Ge'ez numeral string.
*/
export function toArabic(geezStr: string): number;
/**
* Finds the start and end dates of a specific Hijri month that falls within an Ethiopian year.
* @param {number} ethiopianYear - The Ethiopian year.
* @param {number} hijriMonth - The Hijri month to find (e.g., 9 for Ramadan).
* @returns {Array<{start: Kenat, end: Kenat}>} An array of start/end date ranges.
*/
export function findHijriMonthRanges(ethiopianYear: number, hijriMonth: number): Array<{
start: Kenat;
end: Kenat;
}>;
export function getHoliday(holidayKey: any, ethYear: any, options?: {}): {
key: any;
tags: any;
movable: boolean;
name: any;
description: any;
ethiopian: {
year: any;
month: any;
day: any;
};
gregorian?: undefined;
} | {
key: any;
tags: any;
movable: boolean;
name: any;
description: any;
ethiopian: any;
gregorian: any;
};
export function getHolidaysInMonth(ethYear: any, ethMonth: any, options?: {}): any[];
export function getHolidaysForYear(ethYear: any, options?: {}): any[];
export default Kenat;
import { Kenat } from './Kenat.js';
import { toEC } from './conversions.js';
import { toGC } from './conversions.js';
import { toArabic } from './geezConverter.js';
import { toGeez } from './geezConverter.js';
import { getHolidaysInMonth } from './holidays.js';
import { getHolidaysForYear } from './holidays.js';
import { getBahireHasab } from './bahireHasab.js';
import { getFastingPeriod } from './fasting.js';
import { getFastingInfo } from './fasting.js';
import { getFastingDays } from './fasting.js';
import { MonthGrid } from './MonthGrid.js';
import { Time } from './Time.js';
import { getHoliday } from './holidays.js';
import { HolidayTags } from './constants.js';
import { HolidayNames } from './constants.js';
import { monthNames } from './constants.js';
import { diffBreakdown } from './dayArithmetic.js';
export { toEC, toGC, toArabic, toGeez, getHolidaysInMonth, getHolidaysForYear, getBahireHasab, getFastingPeriod, getFastingInfo, getFastingDays, MonthGrid, Time, getHoliday, HolidayTags, HolidayNames, monthNames, diffBreakdown };
/**
* Kenat - Ethiopian Calendar Date Wrapper
*
* A lightweight class to work with both Gregorian and Ethiopian calendars.
* It wraps JavaScript's built-in `Date` object and converts Gregorian dates to Ethiopian equivalents.
*
*/
export class Kenat {
/**
* Creates and returns a new instance of the Kenat class representing the current moment.
*
* @returns {Kenat} A new Kenat instance set to the current date and time.
*/
static now(): Kenat;
static getMonthCalendar(year: any, month: any, options?: {}): {
month: any;
monthName: any;
year: any;
headers: any;
days: any[];
};
/**
* Generates a full-year calendar as an array of month objects for the specified year.
*
* @param {number} year - The year for which to generate the calendar.
* @param {Object} [options={}] - Optional configuration for calendar generation.
* @param {boolean} [options.useGeez=false] - Whether to use the Geez calendar system.
* @param {string} [options.weekdayLang='amharic'] - Language for weekday names (e.g., 'amharic').
* @param {number} [options.weekStart=0] - The starting day of the week (0 = Sunday, 1 = Monday, etc.).
* @param {function|null} [options.holidayFilter=null] - Optional filter function for holidays.
* @returns {Array<Object>} An array of 13 month objects, each containing:
* - {number} month: The month number (1-13).
* - {string} monthName: The name of the month.
* - {number} year: The year of the month.
* - {Array<string>} headers: The headers for the days of the week.
* - {Array<Array<Object>>} days: The grid of day objects for the month.
*/
static getYearCalendar(year: number, options?: {
useGeez?: boolean;
weekdayLang?: string;
weekStart?: number;
holidayFilter?: Function | null;
}): Array<any>;
/**
* Generates an array of Kenat instances for a given date range.
* @param {Kenat} startDate - The start of the range.
* @param {Kenat} endDate - The end of the range.
* @returns {Kenat[]} An array of Kenat objects.
* @throws {InvalidInputTypeError} If start or end dates are not Kenat instances.
*/
static generateDateRange(startDate: Kenat, endDate: Kenat): Kenat[];
/**
* Returns distance from today to the specified holiday occurrence.
* @param {string} holidayKey
* @param {{direction?: 'auto'|'future'|'past', units?: ('years'|'months'|'days')[], output?: 'object'|'string', lang?: 'english'|'amharic'}} [options]
*/
static distanceToHoliday(holidayKey: string, options?: {
direction?: "auto" | "future" | "past";
units?: ("years" | "months" | "days")[];
output?: "object" | "string";
lang?: "english" | "amharic";
}): any;
/**
* Formats a breakdown result to human string like "1 year 2 months 5 days".
*/
static formatDistance(breakdown: any, options?: {}): string;
static _coerceToKenat(input: any): Kenat;
static _getHolidayOccurrence(holidayKey: any, refEth: any, which: any): Kenat;
/**
* Constructs a Kenat instance.
* Can be initialized with:
* - An Ethiopian date string (e.g., '2016/1/1', '2016-1-1').
* - An object with { year, month, day }.
* - A native JavaScript Date object (will be converted from Gregorian).
* - No arguments, for the current date.
*
* @param {string|Object|Date} [input] - The date input.
* @param {Object} [timeObj] - An optional time object.
* @throws {InvalidEthiopianDateError} If the provided Ethiopian date is invalid.
* @throws {InvalidDateFormatError} If the provided date string format is invalid.
* @throws {UnrecognizedInputError} If the input format is unrecognized.
*/
constructor(input?: string | any | Date, timeObj?: any);
time: Time;
ethiopian: {
year: any;
month: any;
day: any;
};
/**
* Converts the current Ethiopian date stored in this.ethiopian to its Gregorian equivalent.
*
* @returns {{ year: number, month: number, day: number }} The Gregorian date corresponding to the Ethiopian date.
*/
getGregorian(): {
year: number;
month: number;
day: number;
};
/**
* Returns the Ethiopian equivalent of the stored Gregorian date.
*
* @returns {{ year: number, month: number, day: number }} An object representing the Ethiopian date.
*/
getEthiopian(): {
year: number;
month: number;
day: number;
};
/**
* Sets the time and returns a new Kenat instance.
* Supports method chaining.
*
* @param {number} hour - The hour value to set.
* @param {number} minute - The minute value to set.
* @param {string} period - The period of the day (e.g., 'day' or 'night').
* @returns {Kenat} A new Kenat instance with the updated time.
*/
setTime(hour: number, minute: number, period: string): Kenat;
/**
* Calculates and returns the Bahire Hasab values for the current instance's year.
*
* @returns {Object} An object containing all the calculated Bahire Hasab values
* (ameteAlem, evangelist, wenber, metqi, nineveh, etc.).
*/
getBahireHasab(): any;
/**
* Returns a string representation of the Ethiopian date and time.
*
* The format is: "Ethiopian: {year}-{month}-{day} {hh:mm period}".
* If the time is not available, hour and minute are replaced with '??'.
*
* @returns {string} The formatted Ethiopian date and time string.
*/
toString(): string;
/**
* Formats the Ethiopian date according to the specified options.
*
* @param {Object} [options={}] - Formatting options.
* @param {string} [options.lang='amharic'] - Language to use for formatting ('amharic', 'english', etc.).
* @param {boolean} [options.showWeekday=false] - Whether to include the weekday in the formatted string.
* @param {boolean} [options.useGeez=false] - Whether to use Geez numerals (only applies if lang is 'amharic').
* @param {boolean} [options.includeTime=false] - Whether to include the time in the formatted string.
* @returns {string} The formatted Ethiopian date string.
*/
format(options?: {
lang?: string;
showWeekday?: boolean;
useGeez?: boolean;
includeTime?: boolean;
}): string;
/**
* Formats the Ethiopian date in Geez numerals and Amharic month name.
*
* @returns {string} The formatted date string in the format: "{Amharic Month Name} {Geez Day} {Geez Year}".
*
* formatInGeezAmharic(); // "የካቲት ፲ ፳፻፲፭"
*/
formatInGeezAmharic(): string;
/**
* Formats the Ethiopian date with weekday name.
*
* @param {'amharic'|'english'} [lang='amharic'] - Language for month and weekday names.
* @param {boolean} [useGeez=false] - Whether to show numerals in Geez.
* @returns {string} Formatted string with weekday, e.g. "ማክሰኞ, መስከረም ፳፩ ፳፻፲፯"
*/
formatWithWeekday(lang?: "amharic" | "english", useGeez?: boolean): string;
/**
* Returns the Ethiopian date in "yyyy/mm/dd" short format.
* @returns {string}
*/
formatShort(): string;
/**
* Returns an ISO-style date string: "YYYY-MM-DD" or "YYYY-MM-DDTHH:mm".
* @returns {string}
*/
toISOString(): string;
/**
* Checks if the current date is a holiday.
* @param {Object} [options={}] - Options for language.
* @param {string} [options.lang='amharic'] - The language for the holiday names and descriptions.
* @returns {Array<Object>} An array of holiday objects for the current date, or an empty array if it's not a holiday.
*/
isHoliday(options?: {
lang?: string;
}): Array<any>;
/**
* Generates a calendar for a given Ethiopian month and year, mapping each Ethiopian date
* to its corresponding Gregorian date and providing formatted display strings.
*
* @param {number} [year=this.ethiopian.year] - The Ethiopian year for the calendar.
* @param {number} [month=this.ethiopian.month] - The Ethiopian month (1-13).
* @param {boolean} [useGeez=false] - Whether to display dates in Geez numerals.
* @returns {Array<Object>} An array of objects, each representing a day in the month with
* Ethiopian and Gregorian date information and display strings.
*/
getMonthCalendar(year?: number, month?: number, useGeez?: boolean): Array<any>;
/**
* Prints the calendar grid for the current Ethiopian month.
*
* @param {boolean} [useGeez=false] - If true, displays the calendar using Geez numerals.
* @returns {void}
*/
printThisMonth(useGeez?: boolean): void;
/**
* Adds a specified amount of time to the current date.
* Supports method chaining for fluent API.
*
* @param {number} amount - The amount to add.
* @param {string} unit - The unit of time ('days', 'months', 'years').
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
add(amount: number, unit: string): Kenat;
/**
* Subtracts a specified amount of time from the current date.
* Supports method chaining for fluent API.
*
* @param {number} amount - The amount to subtract.
* @param {string} unit - The unit of time ('days', 'months', 'years').
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
subtract(amount: number, unit: string): Kenat;
/**
* Adds a specified number of days to the current Ethiopian date.
* Maintains backward compatibility.
*
* @param {number} days - The number of days to add.
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
addDays(days: number): Kenat;
/**
* Returns a new Kenat instance with the date advanced by the specified number of months.
* Maintains backward compatibility.
*
* @param {number} months - The number of months to add to the current date.
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
addMonths(months: number): Kenat;
/**
* Returns a new Kenat instance with the year increased by the specified number of years.
* Maintains backward compatibility.
*
* @param {number} years - The number of years to add to the current date.
* @returns {Kenat} A new Kenat instance representing the updated date.
*/
addYears(years: number): Kenat;
/**
* Calculates the difference in days between this object's Ethiopian date and another object's Ethiopian date.
*
* @param {Object} other - An object with a `getEthiopian` method that returns an Ethiopian date.
* @returns {number} The number of days difference between the two Ethiopian dates.
*/
diffInDays(other: any): number;
/**
* Calculates the difference in months between this instance's Ethiopian date and another Ethiopian date.
*
* @param {Object} other - An object with a `getEthiopian` method that returns an Ethiopian date.
* @returns {number} The number of months difference between the two Ethiopian dates.
*/
diffInMonths(other: any): number;
/**
* Calculates the difference in years between this instance's Ethiopian date and another.
*
* @param {Object} other - An object with a getEthiopian() method returning an Ethiopian date.
* @returns {number} The number of years difference between the two Ethiopian dates.
*/
diffInYears(other: any): number;
getCurrentTime(): Time;
/**
* Checks if the current Kenat instance's date is before another Kenat instance's date.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the current date is before the other date.
*/
isBefore(other: Kenat): boolean;
/**
* Checks if the current Kenat instance's date is after another Kenat instance's date.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the current date is after the other date.
*/
isAfter(other: Kenat): boolean;
/**
* Checks if the current Kenat instance's date is the same as another Kenat instance's date.
* @param {Kenat} other - The other Kenat instance to compare against.
* @returns {boolean} True if the dates are the same.
*/
isSameDay(other: Kenat): boolean;
/**
* Returns a new Kenat instance set to the start of the specified unit.
* Supports method chaining.
*
* @param {string} unit - The unit ('day', 'month', 'year').
* @returns {Kenat} A new Kenat instance.
*/
startOf(unit: string): Kenat;
/**
* Returns a new Kenat instance set to the end of the specified unit.
* Supports method chaining.
*
* @param {string} unit - The unit ('day', 'month', 'year').
* @returns {Kenat} A new Kenat instance.
*/
endOf(unit: string): Kenat;
/**
* Returns a new Kenat instance set to the first day of the current month.
* Maintains backward compatibility.
* @returns {Kenat} A new Kenat instance.
*/
startOfMonth(): Kenat;
/**
* Returns a new Kenat instance set to the last day of the current month.
* Maintains backward compatibility.
* @returns {Kenat} A new Kenat instance.
*/
endOfMonth(): Kenat;
/**
* Checks if the current Ethiopian year is a leap year.
* @returns {boolean} True if it is a leap year.
*/
isLeapYear(): boolean;
/**
* Returns the weekday number for the current date.
* @returns {number} The day of the week (0 for Sunday, 6 for Saturday).
*/
weekday(): number;
/**
* Returns a breakdown of distance from this date to another date.
* @param {Kenat|{year:number,month:number,day:number}|string|Date} other - target date
* @param {{units?: ('years'|'months'|'days')[], output?: 'object'|'string', lang?: 'english'|'amharic'}} [options]
* @returns {Object|string}
*/
distanceTo(other: Kenat | {
year: number;
month: number;
day: number;
} | string | Date, options?: {
units?: ("years" | "months" | "days")[];
output?: "object" | "string";
lang?: "english" | "amharic";
}): any | string;
/**
* Returns a breakdown of distance from today to this date.
*/
distanceFromToday(options?: {}): any;
}
import { Time } from './Time.js';
export class MonthGrid {
static create(config?: {}): {
headers: any;
days: any[];
year: any;
month: any;
monthName: any;
up: () => /*elided*/ any;
down: () => /*elided*/ any;
};
constructor(config?: {});
year: any;
month: any;
weekStart: any;
useGeez: any;
weekdayLang: any;
holidayFilter: any;
mode: any;
showAllSaints: any;
_validateConfig(config: any): void;
generate(): {
headers: any;
days: any[];
year: any;
month: any;
monthName: any;
up: () => {
headers: any;
days: any[];
year: any;
month: any;
monthName: any;
up: /*elided*/ any;
down: () => /*elided*/ any;
};
down: () => {
headers: any;
days: any[];
year: any;
month: any;
monthName: any;
up: () => /*elided*/ any;
down: /*elided*/ any;
};
};
_getRawDays(): any[];
_getFilteredHolidays(): any[];
_getSaintsMap(): {};
_mergeDays(rawDays: any, holidaysList: any, saintsMap: any): any[];
_getWeekdayHeaders(): any;
_getLocalizedMonthName(): any;
_getLocalizedYear(): any;
up(): this;
down(): this;
}

Sorry, the diff of this file is too big to display

export function printMonthCalendarGrid(ethiopianYear: any, ethiopianMonth: any, calendarData: any, useGeez?: boolean): void;
export class Time {
/**
* Creates a Time instance from a Gregorian 24-hour time.
* @param {number} hour - The Gregorian hour (0-23).
* @param {number} [minute=0] - The minute (0-59).
* @returns {Time} A new Time instance.
* @throws {InvalidTimeError} If the Gregorian time is invalid.
*/
static fromGregorian(hour: number, minute?: number): Time;
/**
* Creates a `Time` object from a string representation.
*
* This static method parses a time string, which can include hours, minutes, and an optional period (day/night).
* It supports both Arabic numerals (e.g., "1", "30") and Ethiopic numerals (e.g., "፩", "፴") for hours and minutes,
* assuming a `toArabic` utility function is available to convert Ethiopic numerals to Arabic numbers.
*
* The time string must contain a colon (`:`) separating the hour and minute.
*
* @static
* @param {string} timeString - The string representation of the time.
* Expected formats:
* - "HH:MM" (e.g., "6:30", "፮:፴")
* - "HH:MM period" (e.g., "6:30 night", "፮:፴ ማታ")
* Where:
* - HH: Hour (Arabic or Ethiopic numeral).
* - MM: Minute (Arabic or Ethiopic numeral).
* - period: Optional. Case-insensitive. Recognized values are "night" or "ማታ".
* If the period is omitted, or if a third part is present but not recognized as "night" or "ማታ",
* the time is assumed to be in the 'day' period.
*
* @returns {Time} A new `Time` object representing the parsed time.
*
* @throws {InvalidTimeError} If the `timeString` is:
* - Not a string or an empty string.
* - Missing the colon (`:`) separator.
* - Formatted incorrectly (e.g., not enough parts after splitting).
* - Contains non-numeric values for hour or minute that cannot be parsed into numbers
* (neither as Arabic nor as Ethiopic numerals via `toArabic`).
*
*/
static fromString(timeString: string): Time;
/**
* Constructs a Time instance representing an Ethiopian time.
* @param {number} hour - The Ethiopian hour (1-12).
* @param {number} [minute=0] - The minute (0-59).
* @param {string} [period='day'] - The period ('day' or 'night').
* @throws {InvalidTimeError} If any time component is invalid.
*/
constructor(hour: number, minute?: number, period?: string);
hour: number;
minute: number;
period: string;
/**
* Converts the Ethiopian time to Gregorian 24-hour format.
* @returns {{hour: number, minute: number}}
*/
toGregorian(): {
hour: number;
minute: number;
};
/**
* Adds a duration to the current time.
* @param {{hours?: number, minutes?: number}} duration - Object with hours and/or minutes to add.
* @returns {Time} A new Time instance with the added duration.
*/
add(duration: {
hours?: number;
minutes?: number;
}): Time;
/**
* Subtracts a duration from the current time.
* @param {{hours?: number, minutes?: number}} duration - Object with hours and/or minutes to subtract.
* @returns {Time} A new Time instance with the subtracted duration.
*/
subtract(duration: {
hours?: number;
minutes?: number;
}): Time;
/**
* Calculates the difference between this time and another.
* @param {Time} otherTime - Another Time instance to compare against.
* @returns {{hours: number, minutes: number}} An object with the absolute difference.
*/
diff(otherTime: Time): {
hours: number;
minutes: number;
};
/**
* Formats the time as a string.
* @param {Object} [options] - Formatting options.
* @param {string} [options.lang] - The language for the period label. Defaults to 'english' if useGeez is false, otherwise 'amharic'.
* @param {boolean} [options.useGeez=true] - Whether to use Ge'ez numerals.
* @param {boolean} [options.showPeriodLabel=true] - Whether to show the period label.
* @param {boolean} [options.zeroAsDash=true] - Whether to represent zero minutes as a dash.
* @returns {string} The formatted time string.
*/
format(options?: {
lang?: string;
useGeez?: boolean;
showPeriodLabel?: boolean;
zeroAsDash?: boolean;
}): string;
}
/**
* Validates that all provided date parts are numbers.
* @param {string} funcName - The name of the function being validated.
* @param {Object} dateParts - An object where keys are param names and values are the inputs.
* @throws {InvalidInputTypeError} if any value is not a number.
*/
export function validateNumericInputs(funcName: string, dateParts: any): void;
/**
* Validates that the input is a valid Ethiopian date object.
* @param {Object} dateObj - The object to validate.
* @param {string} funcName - The name of the function being validated.
* @param {string} paramName - The name of the parameter being validated.
* @throws {InvalidInputTypeError} if the object is invalid.
*/
export function validateEthiopianDateObject(dateObj: any, funcName: string, paramName: string): void;
/**
* Validates that the input is a valid Ethiopian time object.
* @param {Object} timeObj - The object to validate.
* @param {string} funcName - The name of the function being validated.
* @param {string} paramName - The name of the parameter being validated.
* @throws {InvalidInputTypeError} if the object is invalid.
*/
export function validateEthiopianTimeObject(timeObj: any, funcName: string, paramName: string): void;
/**
* Calculates the day of the year for a given date.
*
* @param {number} year - The full year (e.g., 2024).
* @param {number} month - The month (1-based, January is 1, December is 12).
* @param {number} day - The day of the month.
* @returns {number} The day of the year (1-based).
*/
export function dayOfYear(year: number, month: number, day: number): number;
/**
* Convert a day of year to Gregorian month and day.
*/
export function monthDayFromDayOfYear(year: any, dayOfYear: any): {
month: number;
day: any;
};
/**
* Checks if the given Gregorian year is a leap year.
*
* Gregorian leap years occur every 4 years, except centuries not divisible by 400.
* For example: 2000 is a leap year, 1900 is not.
*
* @param {number} year - Gregorian calendar year (e.g., 2025)
* @returns {boolean} - True if the year is a leap year, otherwise false.
*/
export function isGregorianLeapYear(year: number): boolean;
/**
* Checks if the given Ethiopian year is a leap year.
*
* Ethiopian leap years occur every 4 years, when the year modulo 4 equals 3.
* This means years like 2011, 2015, 2019 (in Ethiopian calendar) are leap years.
*
* @param {number} year - Ethiopian calendar year (e.g., 2011)
* @returns {boolean} - True if the year is a leap year, otherwise false.
*/
export function isEthiopianLeapYear(year: number): boolean;
/**
* Returns the number of days in the given Ethiopian month and year.
* @param {number} year - Ethiopian year
* @param {number} month - Ethiopian month (1-13)
* @returns {number} Number of days in the month
*/
export function getEthiopianDaysInMonth(year: number, month: number): number;
/**
* Returns the weekday (0-6) for a given Ethiopian date.
*
* @param {Object} param0 - The Ethiopian date.
* @param {number} param0.year - The Ethiopian year.
* @param {number} param0.month - The Ethiopian month (1-13).
* @param {number} param0.day - The Ethiopian day (1-30).
* @returns {number} The day of the week (0 for Sunday, 6 for Saturday).
*/
export function getWeekday({ year, month, day }: {
year: number;
month: number;
day: number;
}): number;
/**
* Checks if a given Ethiopian date is valid.
* @param {number} year - Ethiopian year
* @param {number} month - Ethiopian month (1-13)
* @param {number} day - Ethiopian day (1-30 or 1-5/6)
* @returns {boolean} - True if the date is valid, otherwise false.
*/
export function isValidEthiopianDate(year: number, month: number, day: number): boolean;
/**
* Helper: Get Ethiopian New Year for a Gregorian year.
* @param {number} gYear - The Gregorian year.
* @returns {{gregorianYear: number, month: number, day: number}}
* @throws {InvalidInputTypeError} If gYear is not a number.
*/
export function getEthiopianNewYearForGregorian(gYear: number): {
gregorianYear: number;
month: number;
day: number;
};
/**
* Returns the Gregorian date of the Ethiopian New Year for the given Ethiopian year.
*
* @param {number} ethiopianYear - Ethiopian calendar year.
* @returns {{gregorianYear: number, month: number, day: number}}
* @throws {InvalidInputTypeError} If ethiopianYear is not a number.
*/
export function getGregorianDateOfEthiopianNewYear(ethiopianYear: number): {
gregorianYear: number;
month: number;
day: number;
};