🎩 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
2.1.1
to
2.2.0
+16
.github/FUNDING.yml
# 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]
<!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;
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;
}
#calendar-container {
flex: 1 1 0%;
padding: 2.5rem 2rem 2rem 2rem;
min-width: 0;
}
#calendar-container h1 {
font-size: 2.1rem;
font-weight: 700;
margin-bottom: 1.2rem;
letter-spacing: 0.02em;
color: #2d3a4a;
}
#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%;
}
#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%;
}
#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: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: 0 0 0 2px #4a2d7b33;
}
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: 95%;
max-width: 600px;
}
th,
td {
border: 1px solid #e0e0e0;
padding: 0.25rem;
width: 48px;
height: 48px;
vertical-align: top;
position: relative;
border-radius: 12px;
}
th {
background-color: #f5f5f5;
font-weight: 600;
border-radius: 12px 12px 0 0;
}
.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>
<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 } from '../../src/constants.js';
import { getFastingPeriod } 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;
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}"`;
}
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;
}
const fastingPeriods = [
{ key: 'ABIY_TSOME', name: 'The Great Lent (Hudade)', color: '#fff59d' },
{ key: 'TSOME_NEBIYAT', name: 'Fast of the Prophets', color: '#b3e5fc' },
{ key: 'TSOME_HAWARYAT', name: 'Fast of the Apostles', color: '#c8e6c9' },
{ key: 'NINEVEH', name: 'Fast of Nineveh', color: '#ffccbc' },
{ key: 'RAMADAN', name: 'Ramadan', color: '#d1c4e9' }
];
function renderFastingSidebar() {
const list = document.getElementById('fasting-list');
list.innerHTML = '';
fastingPeriods.forEach(fast => {
const li = document.createElement('li');
li.textContent = fast.name;
li.style.borderLeftColor = fast.color;
if (activeFastingRange && activeFastingRange.key === fast.key) {
li.classList.add('active');
}
li.onclick = () => {
const yearToSearch = currentYear;
const period = getFastingPeriod(fast.key, yearToSearch);
if (period) {
if (activeFastingRange && activeFastingRange.key === fast.key) {
activeFastingRange = null;
} else {
activeFastingRange = {
key: fast.key,
start: new Kenat(period.start),
end: new Kenat(period.end),
color: fast.color
};
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;
}
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 fastName = fastingPeriods.find(f => f.key === activeFastingRange.key).name;
// 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}`;
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>
import { getBahireHasab } from './bahireHasab.js';
import { findHijriMonthRanges } from './holidays.js';
import { addDays } from './dayArithmetic.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 'ABIY_TSOME': {
const start = bh.movableFeasts.abiyTsome?.ethiopian;
const end = bh.movableFeasts.siklet?.ethiopian;
if (start && end) {
return { start, end };
}
return null;
}
case 'TSOME_HAWARYAT': {
const start = bh.movableFeasts.tsomeHawaryat?.ethiopian;
const end = { year: ethiopianYear, month: 11, day: 4 };
if (start) {
return { start, end };
}
return null;
}
case 'NINEVEH': {
const start = bh.movableFeasts.nineveh?.ethiopian;
if (start) {
const end = addDays(start, 2);
return { start, end };
}
return null;
}
case 'TSOME_NEBIYAT': {
const start = { year: ethiopianYear, month: 3, day: 15 };
const end = { year: ethiopianYear, month: 4, day: 28 };
return { start, end };
}
case 'RAMADAN': {
const ranges = findHijriMonthRanges(ethiopianYear, 9);
return ranges.length > 0 ? ranges[0] : null;
}
default:
return null;
}
}
import { getFastingPeriod } from '../src/fasting.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('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('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('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('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('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('ABIY_TSOME', 2020);
expect(period).toBeDefined();
expect(period.start).toBeDefined();
expect(period.end).toBeDefined();
});
});
});
+1
-1
{
"name": "kenat",
"version": "2.1.1",
"version": "2.2.0",
"description": "A JavaScript library for the Ethiopian calendar with date and time support.",

@@ -5,0 +5,0 @@ "main": "src/index.js",

import { toEC, toGC, hijriToGregorian, getHijriYear } from "./conversions.js";
import { holidayInfo, HolidayTags, keyToTewsakMap,movableHolidays } from "./constants.js";
import {
holidayInfo,
HolidayTags,
keyToTewsakMap,
movableHolidays,
} from "./constants.js";
import { validateNumericInputs } from "./utils.js";
import { InvalidInputTypeError, UnknownHolidayError } from "./errors/errorHandler.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: 20, 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] },
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: 20,
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] },
patriots: {
month: 8,
day: 27,
tags: [HolidayTags.PUBLIC, HolidayTags.STATE],
},
};

@@ -27,11 +63,24 @@

const hijriYearsToCheck = [hijriYearAtStart, hijriYearAtStart + 1];
hijriYearsToCheck.forEach(hYear => {
const gregorianDate = hijriToGregorian(hYear, hijriMonth, hijriDay, gYear);
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());
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
occurrences.push({
gregorian: {
year: gregorianDate.getFullYear(),
month: gregorianDate.getMonth() + 1,
day: gregorianDate.getDate(),
},
ethiopian: ecDate,
});

@@ -47,6 +96,75 @@ }

}
return Array.from(new Map(occurrences.map(item => [JSON.stringify(item.ethiopian), item])).values());
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);

@@ -57,4 +175,4 @@ const getAllEidFitrDates = (year) => findAllIslamicOccurrences(year, 10, 1);

export function getHoliday(holidayKey, ethYear, options = {}) {
validateNumericInputs('getHoliday', { ethYear });
const { lang = 'amharic' } = options;
validateNumericInputs("getHoliday", { ethYear });
const { lang = "amharic" } = options;

@@ -69,3 +187,10 @@ const info = holidayInfo[holidayKey];

const rules = fixedHolidays[holidayKey];
return { key: holidayKey, tags: rules.tags, movable: false, name, description, ethiopian: { year: ethYear, month: rules.month, day: rules.day } };
return {
key: holidayKey,
tags: rules.tags,
movable: false,
name,
description,
ethiopian: { year: ethYear, month: rules.month, day: rules.day },
};
}

@@ -76,12 +201,30 @@

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) };
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 (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 {
key: holidayKey,
tags: movableHolidays[holidayKey].tags,
movable: true,
name,
description,
ethiopian: muslimDateData.ethiopian,
gregorian: muslimDateData.gregorian,
};
}

@@ -95,5 +238,10 @@

if (ethMonth < 1 || ethMonth > 13) {
throw new InvalidInputTypeError("getHolidaysInMonth", "ethMonth", "number between 1 and 13", ethMonth);
throw new InvalidInputTypeError(
"getHolidaysInMonth",
"ethMonth",
"number between 1 and 13",
ethMonth
);
}
const { lang = 'amharic', filter = null } = options;
const { lang = "amharic", filter = null } = options;

@@ -103,3 +251,3 @@ const allHolidaysForMonth = [];

allHolidayKeys.forEach(key => {
allHolidayKeys.forEach((key) => {
const holiday = getHoliday(key, ethYear, { lang });

@@ -110,12 +258,12 @@ if (holiday && holiday.ethiopian.month === ethMonth) {

});
// 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'})),
...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) {
muslimHolidays.forEach((data) => {
if (data.ethiopian.month === ethMonth) {
const info = holidayInfo[data.key];

@@ -132,3 +280,8 @@ const holidayObj = {

// Avoid duplicates from the getHoliday call
if (!allHolidaysForMonth.some(h => JSON.stringify(h.ethiopian) === JSON.stringify(holidayObj.ethiopian))) {
if (
!allHolidaysForMonth.some(
(h) =>
JSON.stringify(h.ethiopian) === JSON.stringify(holidayObj.ethiopian)
)
) {
allHolidaysForMonth.push(holidayObj);

@@ -139,8 +292,14 @@ }

const filterTags = filter ? (Array.isArray(filter) ? filter : [filter]) : null;
const filterTags = filter
? Array.isArray(filter)
? filter
: [filter]
: null;
const finalHolidays = filterTags
? allHolidaysForMonth.filter(holiday => holiday.tags.some(tag => filterTags.includes(tag)))
const finalHolidays = filterTags
? allHolidaysForMonth.filter((holiday) =>
holiday.tags.some((tag) => filterTags.includes(tag))
)
: allHolidaysForMonth;
finalHolidays.sort((a, b) => a.ethiopian.day - b.ethiopian.day);

@@ -150,12 +309,13 @@ return finalHolidays;

export function getHolidaysForYear(ethYear, options = {}) {
validateNumericInputs('getHolidaysForYear', { ethYear });
const { lang = 'amharic', filter = null } = 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 singleOccurrenceKeys = Object.keys(fixedHolidays).concat(
Object.keys(keyToTewsakMap)
);
singleOccurrenceKeys.forEach((key) => {
const holiday = getHoliday(key, ethYear, { lang });

@@ -169,3 +329,3 @@ if (holiday) {

const addMuslimHolidays = (key, dateArray) => {
dateArray.forEach(data => {
dateArray.forEach((data) => {
const info = holidayInfo[key];

@@ -184,10 +344,16 @@ allHolidaysForYear.push({

addMuslimHolidays('moulid', getAllMoulidDates(ethYear));
addMuslimHolidays('eidFitr', getAllEidFitrDates(ethYear));
addMuslimHolidays('eidAdha', getAllEidAdhaDates(ethYear));
const filterTags = filter ? (Array.isArray(filter) ? filter : [filter]) : null;
addMuslimHolidays("moulid", getAllMoulidDates(ethYear));
addMuslimHolidays("eidFitr", getAllEidFitrDates(ethYear));
addMuslimHolidays("eidAdha", getAllEidAdhaDates(ethYear));
const finalHolidays = filterTags
? allHolidaysForYear.filter(holiday => holiday.tags.some(tag => filterTags.includes(tag)))
const filterTags = filter
? Array.isArray(filter)
? filter
: [filter]
: null;
const finalHolidays = filterTags
? allHolidaysForYear.filter((holiday) =>
holiday.tags.some((tag) => filterTags.includes(tag))
)
: allHolidaysForYear;

@@ -194,0 +360,0 @@

@@ -10,2 +10,3 @@ import { Kenat } from './Kenat.js';

import { monthNames } from './constants.js';
import { getFastingPeriod } from './fasting.js';

@@ -24,2 +25,3 @@ // Default export is the Kenat class directly

getBahireHasab,
getFastingPeriod,
MonthGrid,

@@ -26,0 +28,0 @@ Time,

@@ -13,3 +13,4 @@ import { toGC, toEC } from './conversions.js';

InvalidDateFormatError,
UnrecognizedInputError
UnrecognizedInputError,
InvalidInputTypeError
} from './errors/errorHandler.js';

@@ -386,2 +387,33 @@ import {

/**
* 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

@@ -388,0 +420,0 @@