@@ -9,3 +9,8 @@ <!DOCTYPE html> | ||
| <style> | ||
| html, | ||
| body { | ||
| height: 100%; | ||
| } | ||
| body { | ||
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | ||
@@ -26,2 +31,3 @@ background-color: #f9f9f9; | ||
| overflow: hidden; | ||
| height: calc(100vh - 2rem); | ||
| } | ||
@@ -31,4 +37,7 @@ | ||
| flex: 1 1 0%; | ||
| padding: 2.5rem 2rem 2rem 2rem; | ||
| padding: 2rem 2rem; | ||
| min-width: 0; | ||
| display: flex; | ||
| flex-direction: column; | ||
| overflow: hidden; | ||
| } | ||
@@ -44,2 +53,16 @@ | ||
| #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 { | ||
@@ -54,2 +77,3 @@ width: 300px; | ||
| min-height: 100%; | ||
| overflow-y: auto; | ||
| } | ||
@@ -81,2 +105,4 @@ | ||
| width: 100%; | ||
| overflow-y: auto; | ||
| max-height: calc(100% - 120px); | ||
| } | ||
@@ -96,2 +122,14 @@ | ||
| #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 { | ||
@@ -110,3 +148,4 @@ background: #e3eeff; | ||
| background-color: rgba(76, 175, 80, 0.13) !important; | ||
| box-shadow: 0 0 0 2px #4a2d7b33; | ||
| box-shadow: inset 0 0 0 2px #4a2d7b33; | ||
| border-radius: 12px; | ||
| } | ||
@@ -121,4 +160,4 @@ | ||
| overflow: hidden; | ||
| width: 95%; | ||
| max-width: 600px; | ||
| width: 100%; | ||
| max-width: 720px; | ||
| } | ||
@@ -128,6 +167,6 @@ | ||
| td { | ||
| border: 1px solid #e0e0e0; | ||
| padding: 0.25rem; | ||
| width: 48px; | ||
| height: 48px; | ||
| border: 1px solid #eef0f3; | ||
| padding: 0.35rem; | ||
| width: 52px; | ||
| height: 56px; | ||
| vertical-align: top; | ||
@@ -139,13 +178,34 @@ position: relative; | ||
| th { | ||
| background-color: #f5f5f5; | ||
| font-weight: 600; | ||
| 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: #fffde7; | ||
| border: 2px solid #ffc107; | ||
| font-weight: bold; | ||
| 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 { | ||
@@ -156,11 +216,11 @@ cursor: pointer; | ||
| .official-holiday { | ||
| background-color: #e3f2fd; | ||
| background-color: #e8f3ff; | ||
| } | ||
| .christian-holiday { | ||
| background-color: #ebf5ff; | ||
| background-color: #eef7ff; | ||
| } | ||
| .jummah-day { | ||
| background-color: #e8f5e9; | ||
| background-color: #e9f7ef; | ||
| } | ||
@@ -239,4 +299,11 @@ | ||
| 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 { | ||
@@ -316,4 +383,4 @@ display: none; | ||
| import Kenat, { MonthGrid, HolidayTags, toArabic } from '../../src/index.js'; | ||
| import { monthNames } from '../../src/constants.js'; | ||
| import { getFastingPeriod } from '../../src/fasting.js'; | ||
| import { monthNames, FastingKeys } from '../../src/constants.js'; | ||
| import { getFastingPeriod, getFastingInfo, getFastingDays } from '../../src/fasting.js'; | ||
@@ -329,2 +396,3 @@ // State variables | ||
| let activeFastingRange = null; | ||
| let activeFastingKey = null; | ||
@@ -402,2 +470,9 @@ const initialDate = new Kenat().getEthiopian(); | ||
| } | ||
| 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) : ''; | ||
@@ -435,18 +510,44 @@ const eventAttr = hasEvents ? `data-day-index="${i + j}"` : ''; | ||
| 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' } | ||
| ]; | ||
| // 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'); | ||
| li.textContent = fast.name; | ||
| 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) { | ||
| if ((activeFastingRange && activeFastingRange.key === fast.key) || (activeFastingKey === fast.key)) { | ||
| li.classList.add('active'); | ||
@@ -456,20 +557,27 @@ } | ||
| const yearToSearch = currentYear; | ||
| const period = getFastingPeriod(fast.key, yearToSearch); | ||
| if (period) { | ||
| if (activeFastingRange && activeFastingRange.key === fast.key) { | ||
| 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; | ||
| } 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; | ||
| activeFastingKey = null; | ||
| } | ||
| } else { | ||
| alert(`Fasting period for ${fast.name} not found in ${yearToSearch} E.C.`); | ||
| activeFastingRange = null; | ||
| } | ||
@@ -499,3 +607,5 @@ rerender(); | ||
| const days = Kenat.generateDateRange(activeFastingRange.start, activeFastingRange.end).length; | ||
| const fastName = fastingPeriods.find(f => f.key === activeFastingRange.key).name; | ||
| 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 | ||
@@ -505,3 +615,4 @@ 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>End:</strong> ${end.month}/${end.day}/${end.year}<br> | ||
| <strong>Days:</strong> ${days}`; | ||
| <strong>Days:</strong> ${days} | ||
| ${fastDesc ? `<hr><div>${fastDesc}</div>` : ''}`; | ||
| infoBox.style.display = 'block'; | ||
@@ -508,0 +619,0 @@ } else { |
+1
-1
| { | ||
| "name": "kenat", | ||
| "version": "2.2.0", | ||
| "version": "2.3.0", | ||
| "description": "A JavaScript library for the Ethiopian calendar with date and time support.", | ||
@@ -5,0 +5,0 @@ "main": "src/index.js", |
+71
-0
@@ -335,1 +335,72 @@ export const monthNames = { | ||
| }; | ||
| // 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], | ||
| }, | ||
| }; |
+128
-6
| import { getBahireHasab } from './bahireHasab.js'; | ||
| import { findHijriMonthRanges } from './holidays.js'; | ||
| import { addDays } from './dayArithmetic.js'; | ||
| import { addDays, diffInDays } from './dayArithmetic.js'; | ||
| import { fastingInfo, FastingKeys } from './constants.js'; | ||
| import { getWeekday, getEthiopianDaysInMonth, validateNumericInputs, validateEthiopianDateObject } from './utils.js'; | ||
@@ -15,3 +17,3 @@ /** | ||
| switch (fastKey) { | ||
| case 'ABIY_TSOME': { | ||
| case FastingKeys.ABIY_TSOME: { | ||
| const start = bh.movableFeasts.abiyTsome?.ethiopian; | ||
@@ -25,3 +27,3 @@ const end = bh.movableFeasts.siklet?.ethiopian; | ||
| case 'TSOME_HAWARYAT': { | ||
| case FastingKeys.TSOME_HAWARYAT: { | ||
| const start = bh.movableFeasts.tsomeHawaryat?.ethiopian; | ||
@@ -35,3 +37,3 @@ const end = { year: ethiopianYear, month: 11, day: 4 }; | ||
| case 'NINEVEH': { | ||
| case FastingKeys.NINEVEH: { | ||
| const start = bh.movableFeasts.nineveh?.ethiopian; | ||
@@ -45,3 +47,3 @@ if (start) { | ||
| case 'TSOME_NEBIYAT': { | ||
| case FastingKeys.TSOME_NEBIYAT: { | ||
| const start = { year: ethiopianYear, month: 3, day: 15 }; | ||
@@ -52,3 +54,10 @@ const end = { year: ethiopianYear, month: 4, day: 28 }; | ||
| case 'RAMADAN': { | ||
| 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); | ||
@@ -62,1 +71,114 @@ return ranges.length > 0 ? ranges[0] : 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; | ||
| } |
+3
-1
@@ -10,3 +10,3 @@ import { Kenat } from './Kenat.js'; | ||
| import { monthNames } from './constants.js'; | ||
| import { getFastingPeriod } from './fasting.js'; | ||
| import { getFastingPeriod, getFastingInfo, getFastingDays } from './fasting.js'; | ||
@@ -26,2 +26,4 @@ // Default export is the Kenat class directly | ||
| getFastingPeriod, | ||
| getFastingInfo, | ||
| getFastingDays, | ||
| MonthGrid, | ||
@@ -28,0 +30,0 @@ Time, |
+114
-7
@@ -1,2 +0,6 @@ | ||
| import { getFastingPeriod } from '../src/fasting.js'; | ||
| 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'; | ||
@@ -12,3 +16,3 @@ // Mock dependencies if they are not available in the test environment | ||
| test('should return the correct start and end dates for The Great Lent (Abiy Tsome)', () => { | ||
| const period = getFastingPeriod('ABIY_TSOME', year); | ||
| const period = getFastingPeriod(FastingKeys.ABIY_TSOME, year); | ||
| // VERIFIED: In 2016, Abiy Tsome starts on Megabit 2 and ends on Siklet (Miazia 25) | ||
@@ -20,3 +24,3 @@ expect(period.start).toEqual({ year: 2016, month: 7, day: 2 }); | ||
| test('should return the correct start and end dates for Fast of the Apostles (Tsome Hawaryat)', () => { | ||
| const period = getFastingPeriod('TSOME_HAWARYAT', year); | ||
| 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. | ||
@@ -27,3 +31,3 @@ 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); | ||
| const period = getFastingPeriod(FastingKeys.NINEVEH, year); | ||
| // VERIFIED: In 2016, Nineveh starts on Yekatit 18 and lasts 3 days. | ||
@@ -35,3 +39,3 @@ expect(period.start).toEqual({ year: 2016, month: 6, day: 18 }); | ||
| test('should return the correct start and end dates for Fast of the Prophets (Tsome Nebiyat)', () => { | ||
| const period = getFastingPeriod('TSOME_NEBIYAT', year); | ||
| const period = getFastingPeriod(FastingKeys.TSOME_NEBIYAT, year); | ||
| // This is a fixed fast from Hidar 15 to Tahsas 28. This test was already correct. | ||
@@ -47,3 +51,3 @@ expect(period.start).toEqual({ year: 2016, month: 3, day: 15 }); | ||
| test('should return the correct start and end dates for Ramadan', () => { | ||
| const period = getFastingPeriod('RAMADAN', year); | ||
| const period = getFastingPeriod(FastingKeys.RAMADAN, year); | ||
| // VERIFIED: For 2016 E.C., Ramadan 1445 A.H. runs from Megabit 2 to Miazia 1. | ||
@@ -62,3 +66,3 @@ expect(period.start).toEqual({ year: 2016, month: 7, day: 2 }); | ||
| // This confirms the calculation logic doesn't crash on different inputs. | ||
| const period = getFastingPeriod('ABIY_TSOME', 2020); | ||
| const period = getFastingPeriod(FastingKeys.ABIY_TSOME, 2020); | ||
| expect(period).toBeDefined(); | ||
@@ -70,1 +74,104 @@ expect(period.start).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); | ||
| }); | ||
| }); |
545830
3.25%5331
5.4%