📌 Overview
Kenat (Amharic: ቀናት) is a comprehensive JavaScript library for the Ethiopian calendar. It provides a complete toolset for developers, handling date conversions, advanced formatting, full date arithmetic, and a powerful, authentic holiday calculation system based on the traditional Bahire Hasab (ባሕረ ሃሳብ).
- Features
- Installation
- Quick Start
- Design Principles
- Bahire Hasab & Holiday System
- More API Examples
- API Reference
- Contribution Guide
- Author
- Contributors
- License
- 🔄 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 — noif/elseneeded 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, 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.
- ⏱️ 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, andisSameYearfor 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
Kenatinstance, so chaining is always safe. - 📦 Zero Dependencies: Pure JavaScript, no runtime dependencies.
npm install kenatNode.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.
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.
import Kenat from 'kenat';
import type { EthiopianDate, Holiday } from 'kenat';
const today: Kenat = new Kenat();
const date: EthiopianDate = today.getEthiopian();// 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';Get today's Ethiopian date:
import Kenat from 'kenat';
const today = new Kenat();
console.log(today.getEthiopian());
// → { year: 2018, month: 1, day: 8 }
console.log(today.format({ lang: 'english', showWeekday: true }));
// → "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"-
Immutable & chainable — every method that "changes" a date (
.add(),.startOf(),.setTime(), ...) returns a newKenatinstance rather than mutating the original. This makes chaining predictable and safe: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.
import { getHolidaysForYear } from 'kenat';
const holidaysIn2017 = getHolidaysForYear(2017);
console.log(holidaysIn2017.find(h => h.key === 'fasika'));// Output for Fasika (Easter) in 2017
{
key: 'fasika',
tags: ['public', 'religious', 'christian'],
movable: true,
name: 'ፋሲካ',
description: 'የኢየሱስ ክርስቶስን ከሙታን መነሣት ያከብራል።...',
ethiopian: { year: 2017, month: 8, day: 12 },
gregorian: { year: 2025, month: 4, day: 20 }
}import { getHolidaysForYear, HolidayTags } from 'kenat';
const publicHolidays = getHolidaysForYear(2017, {
filter: HolidayTags.PUBLIC
});
const religiousHolidays = getHolidaysForYear(2017, {
filter: [HolidayTags.CHRISTIAN, HolidayTags.MUSLIM]
});const meskel = new Kenat('2017/1/17');
console.log(meskel.isHoliday()); // → Returns the Meskel holiday object
const notHoliday = new Kenat('2017/1/18');
console.log(notHoliday.isHoliday()); // → []const bahireHasab = new Kenat('2017/1/1').getBahireHasab();
console.log(bahireHasab.evangelist);
// → { name: 'ማቴዎስ', remainder: 1 }
console.log(bahireHasab.movableFeasts.fasika.ethiopian);
// → { year: 2017, month: 8, day: 21 }// Full output of .getBahireHasab() for 2017
{
ameteAlem: 7517,
meteneRabiet: 1879,
evangelist: { name: 'ማቴዎስ', remainder: 1 },
newYear: { dayName: 'ረቡዕ', tinteQemer: 2 },
medeb: 12,
wenber: 11,
abektie: 1,
metqi: 29,
bealeMetqi: { date: { year: 2017, month: 1, day: 29 }, weekday: 'Wednesday' },
mebajaHamer: 3,
nineveh: { year: 2017, month: 6, day: 3 },
movableFeasts: {
nineveh: { /* ... */ },
abiyTsome: { /* ... */ },
fasika: {
key: 'fasika',
tags: ['public', 'religious', 'christian'],
movable: true,
name: 'ፋሲካ',
description: 'የኢየሱስ ክርስቶስን ከሙታን መነሣት ያከብራል።...',
ethiopian: { year: 2017, month: 8, day: 12 },
gregorian: { year: 2025, month: 4, day: 20 }
},
// ... 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.
const today = new Kenat();
const nextWeek = today.addDays(7);
const lastMonth = today.addMonths(-1);
// Fluent API
const future = today.add(2, 'years').add(3, 'months').add(15, 'days');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)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:
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)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' }));// 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();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);import { HolidayNames } from 'kenat';
// Distance to next holiday
console.log(Kenat.distanceToHoliday(HolidayNames.fasika, {
direction: 'future',
units: ['days'],
output: 'string'
}));
// → "358 days"import { toGeez } from 'kenat';
console.log(toGeez(2017)); // → "፳፻፲፯"
// Format in Geez
const date = new Kenat('2017/1/1');
console.log(date.formatInGeezAmharic()); // → "መስከረም ፩ ፳፻፲፯"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:
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+12hsuffix that the default Ethiopian-calendar ISO string uses for night times — the result is a genuine, parser-safe ISO 8601 string.
// 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"Kenat instances serialize cleanly with JSON.stringify via a built-in toJSON():
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":{...}}}'Main class for Ethiopian date operations.
Constructor:
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 objectAccess & Conversion
getEthiopian()- Returns{year, month, day}getGregorian()- Returns Gregorian equivalentgetDate({ calendar })- Returns the date in'ethiopian'(default) or'gregorian', no if/else neededgetCurrentTime()- Returns the instance'sTimeobjectsetTime(hour, minute, period)- Returns a new instance with the given Ethiopian time
Formatting
format(options)- Format with various options, includinglang,showWeekday,useGeez,includeTime, and{ calendar: 'ethiopian' | 'gregorian' }toString(options)- String representation of date + time; also accepts{ calendar }formatInGeezAmharic()- Amharic month name with Geez numeralsformatWithWeekday(lang, useGeez)- Formatted string including the weekday nameformatShort()- Compact"yyyy/mm/dd"formattoISOString(options)- ISO-style date string;{ calendar: 'gregorian' }returns a standard ISO 8601 datetoJSON()- Plain-object representation forJSON.stringify
Comparison
isBefore(other)/isAfter(other)- Chronological comparisonisSameDay(other)/isSameMonth(other)/isSameYear(other)- Granular equality checksisLeapYear()- Whether the instance's Ethiopian year is a leap yearweekday()- 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 shortcutsstartOf(unit)/endOf(unit)- Snap to the start/end of'day','month', or'year'(leap-year aware)startOfMonth()/endOfMonth()- Shortcuts forstartOf('month')/endOf('month')diffInDays(other)/diffInMonths(other)/diffInYears(other)- Precise numeric differencesdistanceTo(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 holidaygetBahireHasab()- 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 asnew Kenat()Kenat.getMonthCalendar(year, month, options)- Generate a month calendarKenat.getYearCalendar(year, options)- Generate a full year's calendarKenat.generateDateRange(start, end)- Array ofKenatinstances between two datesKenat.distanceToHoliday(holidayKey, options)- Distance to a holiday occurrenceKenat.formatDistance(breakdown, options)- Format a distance breakdown as a human string
toEC(year, month, day)- Gregorian to EthiopiantoGC(year, month, day)- Ethiopian to GregoriantoGeez(number)- Convert to Geez numeralstoArabic(geezString)- Convert from Geez numerals
getHolidaysForYear(year, options)- Get all holidays for a yeargetHolidaysInMonth(year, month, lang)- Get holidays for a monthgetHoliday(key, year)- Get a specific holidayHolidayTags- Constants for filtering holidaysHolidayNames- Constants for holiday keys
getFastingPeriod(fastKey, year)- Get fasting period datesgetFastingInfo(fastKey, year)- Get fasting informationgetFastingDays(fastKey, year)- Get the list of individual fasting daysgetBahireHasab(year)- Get Bahire Hasab calculations
Kenat.getMonthCalendar(year, month, options)- Generate month calendarKenat.getYearCalendar(year, options)- Generate year calendarKenat.generateDateRange(start, end)- Generate date rangeMonthGrid.create(options)- Create calendar grid
diffBreakdown(dateA, dateB, options)- Precise date differenceKenat.distanceToHoliday(holidayKey, options)- Distance to holiday
HolidayTags- Holiday category tagsHolidayNames- Holiday name constantsmonthNames- Month name arrays in multiple languages (includes an Englishgregorianset)FastingKeys- Fasting period keys
For detailed method signatures and advanced usage, refer to the full documentation.
-
Fork the repo & clone it.
-
Create a new branch:
git checkout -b feature/your-feature
-
Write your changes (in
src/*.ts) and add tests in the/testsdirectory. -
Run
npm run typecheckandnpm testto ensure everything passes. -
If you're changing anything in
examples/, runnpm run buildfirst — the examples import fromdist/, notsrc/. -
Open a Pull Request with your improvements or bug fix.
Melaku Demeke
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.
Thanks to all the amazing people who have contributed to this project!
MIT — see LICENSE for details.
