UNPKG

somali-date

Version:

Comprehensive Somali date/time library with Gregorian, Hijri, and traditional calendars. Includes CLI, prayer times, holidays, and business day calculations.

517 lines (450 loc) 17.4 kB
// Somali Gregorian months & weekdays const MONTHS_LONG = [ "Janaayo", "Febraayo", "Maarso", "Abriil", "Maajo", "Juun", "Luuliyo", "Agoosto", "Sebteembar", "Oktoobar", "Nofeembar", "Diseembar" ]; const MONTHS_SHORT = [ "Jan", "Feb", "Mar", "Abr", "Majo", "Juun", "Llyo", "Ago", "Seb", "Okt", "Nof", "Dis" ]; const WEEKDAYS_LONG = ["Axad", "Isniin", "Talaado", "Arbaco", "Khamiis", "Jimco", "Sabti"]; const WEEKDAYS_SHORT = ["Axd", "Isn", "Tal", "Arb", "Khm", "Jmc", "Sbt"]; // Somali number words const NUMBERS_SOMALI = { 0: "eber", 1: "kow", 2: "laba", 3: "saddex", 4: "afar", 5: "shan", 6: "lix", 7: "todobaad", 8: "siddeed", 9: "sagaal", 10: "toban", 11: "kow iyo toban", 12: "laba iyo toban", 13: "saddex iyo toban", 14: "afar iyo toban", 15: "shan iyo toban", 16: "lix iyo toban", 17: "todobaad iyo toban", 18: "siddeed iyo toban", 19: "sagaal iyo toban", 20: "labaatan", 30: "soddon", 40: "afartan", 50: "konton", 60: "lixdan", 70: "toddobaatan", 80: "siddeetan", 90: "sagaashan", 100: "boqol", 1000: "kun" }; // Somali traditional seasons const SOMALI_SEASONS = { "Jilaal": { months: [12, 1, 2, 3], description: "Dry season" }, "Gu": { months: [4, 5, 6], description: "Main rainy season" }, "Xagaa": { months: [7, 8, 9], description: "Hot dry season" }, "Dayr": { months: [10, 11], description: "Short rainy season" } }; // Somali and Islamic holidays const SOMALI_HOLIDAYS = { // Fixed Gregorian holidays "01-01": "Sanadka Cusub", // New Year "06-26": "Maalinta Madaxbannida", // Independence Day "07-01": "Maalinta Midnimada", // Unity Day // Islamic holidays (approximate - need lunar calculation) "ramadan-end": "Ciid al-Fitr", "hajj-end": "Ciid al-Adha", "prophet-birthday": "Mawlid an-Nabi", "hijri-new-year": "Sanadka Cusub ee Hijri" }; // Hijri months in Somali const HIJRI_MONTHS_LONG = [ "Muxarram", "Safar", "Rabiicul Awwal", "Rabiicul Thaani", "Jumaadal Uula", "Jumaadal Aakhir", "Rajab", "Shacbaan", "Ramadaan", "Shawwaal", "Dhul-Qacda", "Dhul-Xijja" ]; const HIJRI_MONTHS_SHORT = [ "Mux", "Saf", "Rab-I", "Rab-II", "Jum-I", "Jum-II", "Raj", "Sha", "Ram", "Shw", "DhQ", "DhX" ]; // --- Date helpers --- function toDate(d) { if (d instanceof Date) return d; const x = new Date(d); if (Number.isNaN(+x)) throw new Error("Invalid date input"); return x; } const p2 = (n) => String(n).padStart(2, "0"); // --- Gregorian to Julian Day Number --- function g2jdn(y, m, day) { const a = Math.floor((14 - m) / 12); const y2 = y + 4800 - a; const m2 = m + 12 * a - 3; return day + Math.floor((153 * m2 + 2) / 5) + 365 * y2 + Math.floor(y2 / 4) - Math.floor(y2 / 100) + Math.floor(y2 / 400) - 32045; } // --- JDN to Hijri (Kuwaiti algorithm) --- function jdnToHijri(jd) { let l = jd - 1948440 + 10632; const n = Math.floor((l - 1) / 10631); l = l - 10631 * n + 354; const j = Math.floor((10985 - l) / 5316) * Math.floor((50 * l) / 17719) + Math.floor(l / 5670) * Math.floor((43 * l) / 15238); l = l - Math.floor((30 - j) / 15) * Math.floor((17719 * j) / 50) - Math.floor(j / 16) * Math.floor((15238 * j) / 43) + 29; const m = Math.floor((24 * l) / 709); const d = l - Math.floor((709 * m) / 24); const y = 30 * n + j - 30; return { hYear: y, hMonth: m, hDay: d }; } // --- Gregorian (Somali) formatters --- function formatDateSomali(date, opts = {}) { const d = toDate(date); const { weekday = "none", month = "long", numeric = true } = opts; const m = month === "short" ? MONTHS_SHORT[d.getMonth()] : MONTHS_LONG[d.getMonth()]; const parts = []; if (weekday !== "none") { const wd = weekday === "short" ? WEEKDAYS_SHORT[d.getDay()] : WEEKDAYS_LONG[d.getDay()]; parts.push(`${wd},`); } if (numeric) { parts.push(`${d.getDate()} ${m} ${d.getFullYear()}`); } else { parts.push(m); } return parts.join(" ").trim(); } function formatTimeSomali(date, opts = {}) { const d = toDate(date); const { seconds = false } = opts; const hh = p2(d.getHours()); const mm = p2(d.getMinutes()); const ss = p2(d.getSeconds()); return seconds ? `${hh}:${mm}:${ss}` : `${hh}:${mm}`; } function formatDateTimeSomali(date, opts = {}) { const left = formatDateSomali(date, opts); const right = formatTimeSomali(date, { seconds: !!opts.seconds }); return `${left} ${right}`; } function formatRelativeSomali(from, to = new Date()) { const a = toDate(from); const b = toDate(to); const diffDays = Math.round((a.setHours(0,0,0,0) - b.setHours(0,0,0,0)) / 86400000); if (diffDays === 0) return "maanta"; if (diffDays === -1) return "shalay"; if (diffDays === 1) return "berri"; if (diffDays < 0) return `${Math.abs(diffDays)} maalmood ka hor`; return `${diffDays} maalmood gudahood`; } // --- Hijri formatters --- function formatHijriSomali(date, opts = {}) { const d = toDate(date); const jdn = g2jdn(d.getFullYear(), d.getMonth() + 1, d.getDate()); const { hYear, hMonth, hDay } = jdnToHijri(jdn); const { weekday = "none", month = "long", numeric = true } = opts; const mName = (month === "short" ? HIJRI_MONTHS_SHORT : HIJRI_MONTHS_LONG)[hMonth - 1]; const parts = []; if (weekday !== "none") { const wd = weekday === "short" ? WEEKDAYS_SHORT[d.getDay()] : WEEKDAYS_LONG[d.getDay()]; parts.push(`${wd},`); } parts.push(numeric ? `${hDay} ${mName} ${hYear}` : mName); return parts.join(" ").trim(); } function formatDateSomaliWithHijri(date, opts = {}) { const g = formatDateSomali(date, opts); const h = formatHijriSomali(date, { weekday: "none", month: opts.month ?? "long" }); return `${g} — (${h} Hijri)`; } // --- Easy calendar selection functions --- function formatDate(date, calendar = "gregorian", opts = {}) { const defaultOpts = { weekday: "long", month: "long", ...opts }; switch (calendar.toLowerCase()) { case "hijri": case "islamic": return formatHijriSomali(date, defaultOpts); case "both": case "dual": return formatDateSomaliWithHijri(date, defaultOpts); case "gregorian": case "western": default: return formatDateSomali(date, defaultOpts); } } function formatDateTime(date, calendar = "gregorian", opts = {}) { const defaultOpts = { weekday: "long", month: "long", ...opts }; const timeStr = formatTimeSomali(date, { seconds: !!opts.seconds }); switch (calendar.toLowerCase()) { case "hijri": case "islamic": return `${formatHijriSomali(date, defaultOpts)} ${timeStr}`; case "both": case "dual": return `${formatDateSomaliWithHijri(date, defaultOpts)} ${timeStr}`; case "gregorian": case "western": default: return formatDateTimeSomali(date, defaultOpts); } } // --- Number conversion functions --- function numberToSomali(num) { if (num === 0) return NUMBERS_SOMALI[0]; if (num < 0) return `taban ${numberToSomali(Math.abs(num))}`; if (num <= 20) return NUMBERS_SOMALI[num]; if (num < 100) { const tens = Math.floor(num / 10) * 10; const ones = num % 10; return ones === 0 ? NUMBERS_SOMALI[tens] : `${NUMBERS_SOMALI[ones]} iyo ${NUMBERS_SOMALI[tens]}`; } if (num < 1000) { const hundreds = Math.floor(num / 100); const remainder = num % 100; const hundredStr = hundreds === 1 ? "boqol" : `${numberToSomali(hundreds)} boqol`; return remainder === 0 ? hundredStr : `${hundredStr} iyo ${numberToSomali(remainder)}`; } if (num < 1000000) { const thousands = Math.floor(num / 1000); const remainder = num % 1000; const thousandStr = thousands === 1 ? "kun" : `${numberToSomali(thousands)} kun`; return remainder === 0 ? thousandStr : `${thousandStr} iyo ${numberToSomali(remainder)}`; } return num.toString(); // Fallback for very large numbers } // --- Traditional Somali calendar functions --- function getSomaliSeason(date) { const d = toDate(date); const month = d.getMonth() + 1; for (const [season, info] of Object.entries(SOMALI_SEASONS)) { if (info.months.includes(month)) { return { season, description: info.description }; } } return null; } function formatSomaliTraditional(date, opts = {}) { const d = toDate(date); const seasonInfo = getSomaliSeason(d); const { includeGregorian = false } = opts; if (!seasonInfo) return formatDateSomali(date, opts); const seasonStr = `${seasonInfo.season} (${seasonInfo.description})`; return includeGregorian ? `${formatDateSomali(date, opts)} - ${seasonStr}` : seasonStr; } // --- Holiday functions --- function isHoliday(date) { const d = toDate(date); const monthDay = `${p2(d.getMonth() + 1)}-${p2(d.getDate())}`; return SOMALI_HOLIDAYS[monthDay] !== undefined; } function getHolidayName(date) { const d = toDate(date); const monthDay = `${p2(d.getMonth() + 1)}-${p2(d.getDate())}`; return SOMALI_HOLIDAYS[monthDay] || null; } function getUpcomingHolidays(days = 30) { const today = new Date(); const upcoming = []; for (let i = 0; i < days; i++) { const checkDate = new Date(today.getTime() + i * 24 * 60 * 60 * 1000); const holiday = getHolidayName(checkDate); if (holiday) { upcoming.push({ date: checkDate, name: holiday, daysFromNow: i, formatted: formatDateSomali(checkDate) }); } } return upcoming; } // --- Business day functions --- function isBusinessDay(date) { const d = toDate(date); const dayOfWeek = d.getDay(); // In Somalia, Friday is typically a day off, Saturday-Thursday are work days return dayOfWeek !== 5; // 5 = Friday (Jimco) } function addBusinessDays(date, days) { const d = new Date(toDate(date)); let addedDays = 0; while (addedDays < Math.abs(days)) { d.setDate(d.getDate() + (days > 0 ? 1 : -1)); if (isBusinessDay(d)) { addedDays++; } } return d; } // --- Enhanced relative date functions --- function formatRelativeSomaliDetailed(from, to = new Date(), opts = {}) { const a = toDate(from); const b = toDate(to); const { includeTime = false } = opts; const diffMs = a.getTime() - b.getTime(); const diffDays = Math.round(diffMs / (24 * 60 * 60 * 1000)); const diffHours = Math.round(diffMs / (60 * 60 * 1000)); const diffMinutes = Math.round(diffMs / (60 * 1000)); // Same day if (diffDays === 0) { if (!includeTime) return "maanta"; if (Math.abs(diffHours) < 1) { if (Math.abs(diffMinutes) < 5) return "hadda"; return diffMinutes > 0 ? `${diffMinutes} daqiiqo gudahood` : `${Math.abs(diffMinutes)} daqiiqo ka hor`; } return diffHours > 0 ? `${diffHours} saacadood gudahood` : `${Math.abs(diffHours)} saacadood ka hor`; } // Days if (diffDays === -1) return includeTime ? `shalay ${formatTimeSomali(a)}` : "shalay"; if (diffDays === 1) return includeTime ? `berri ${formatTimeSomali(a)}` : "berri"; if (diffDays === -2) return "doraad"; if (diffDays === 2) return "saakuun"; // Weeks if (Math.abs(diffDays) < 7) { const dayName = WEEKDAYS_LONG[a.getDay()]; return diffDays > 0 ? `${dayName} soo socda` : `${dayName} ee tegay`; } // Default to regular relative return formatRelativeSomali(from, to); } // --- Duration formatting --- function formatDurationSomali(milliseconds) { const ms = Math.abs(milliseconds); const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); const years = Math.floor(days / 365); if (years > 0) { const remainingDays = days % 365; const months = Math.floor(remainingDays / 30); let result = years === 1 ? "sannad" : `${years} sannadood`; if (months > 0) { result += months === 1 ? " iyo bil" : ` iyo ${months} bilood`; } return result; } if (days > 0) { const remainingHours = hours % 24; let result = days === 1 ? "maalin" : `${days} maalmood`; if (remainingHours > 0) { result += remainingHours === 1 ? " iyo saacad" : ` iyo ${remainingHours} saacadood`; } return result; } if (hours > 0) { const remainingMinutes = minutes % 60; let result = hours === 1 ? "saacad" : `${hours} saacadood`; if (remainingMinutes > 0) { result += remainingMinutes === 1 ? " iyo daqiiqo" : ` iyo ${remainingMinutes} daqiiqo`; } return result; } if (minutes > 0) { return minutes === 1 ? "daqiiqo" : `${minutes} daqiiqo`; } return seconds === 1 ? "ilbiriqsi" : `${seconds} ilbiriqsi`; } // --- Age formatting --- function formatAgeSomali(birthDate, referenceDate = new Date()) { const birth = toDate(birthDate); const ref = toDate(referenceDate); const ageMs = ref.getTime() - birth.getTime(); if (ageMs < 0) return "weli ma dhalan"; return formatDurationSomali(ageMs); } // --- Date range formatting --- function formatDateRange(startDate, endDate, calendar = "gregorian", opts = {}) { const start = toDate(startDate); const end = toDate(endDate); const { compact = false } = opts; // Same day if (start.toDateString() === end.toDateString()) { return formatDate(start, calendar, opts); } // Same month and year if (start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear()) { if (compact) { const monthName = calendar === "hijri" ? HIJRI_MONTHS_LONG[start.getMonth()] : MONTHS_LONG[start.getMonth()]; return `${start.getDate()}-${end.getDate()} ${monthName} ${start.getFullYear()}`; } } // Different dates const startFormatted = formatDate(start, calendar, { ...opts, weekday: "none" }); const endFormatted = formatDate(end, calendar, { ...opts, weekday: "none" }); return `${startFormatted} - ${endFormatted}`; } // --- Calendar conversion utilities --- function gregorianToHijri(date) { const d = toDate(date); const jdn = g2jdn(d.getFullYear(), d.getMonth() + 1, d.getDate()); return jdnToHijri(jdn); } function hijriToGregorian(hYear, hMonth, hDay) { // Simplified conversion - would need more complex algorithm for precision const approxGregorianYear = hYear + 622; return new Date(approxGregorianYear, hMonth - 1, hDay); } // --- Enhanced formatting with Somali numerals --- function formatDateSomaliWithNumbers(date, opts = {}) { const d = toDate(date); const { numerals = "arabic", ...restOpts } = opts; if (numerals === "somali") { const day = numberToSomali(d.getDate()); const month = MONTHS_LONG[d.getMonth()]; const year = numberToSomali(d.getFullYear()); const parts = []; if (restOpts.weekday !== "none") { const wd = restOpts.weekday === "short" ? WEEKDAYS_SHORT[d.getDay()] : WEEKDAYS_LONG[d.getDay()]; parts.push(`${wd},`); } parts.push(`${day} ${month} ${year}`); return parts.join(" ").trim(); } return formatDateSomali(date, restOpts); } // --- Prayer times (basic implementation) --- function getPrayerTimesSomali(date, latitude = 2.0469, longitude = 45.3182) { // Mogadishu coordinates const d = toDate(date); // Simplified prayer time calculation - in real implementation, use proper astronomical calculations const times = { "Subax": "05:30", "Duhur": "12:15", "Casar": "15:45", "Maghrib": "18:30", "Cisha": "19:45" }; return times; } // Quick preset functions const today = (calendar = "gregorian") => formatDate(new Date(), calendar); const now = (calendar = "gregorian") => formatDateTime(new Date(), calendar); // --- Exports --- module.exports = { // Core formatting functions formatDateSomali, formatTimeSomali, formatDateTimeSomali, formatRelativeSomali, formatHijriSomali, formatDateSomaliWithHijri, // Easy functions formatDate, formatDateTime, today, now, // Advanced features numberToSomali, formatSomaliTraditional, getSomaliSeason, formatRelativeSomaliDetailed, formatDurationSomali, formatAgeSomali, formatDateRange, formatDateSomaliWithNumbers, // Holiday functions isHoliday, getHolidayName, getUpcomingHolidays, // Business day functions isBusinessDay, addBusinessDays, // Calendar conversion gregorianToHijri, hijriToGregorian, // Prayer times getPrayerTimesSomali, // Constants MONTHS_LONG, MONTHS_SHORT, WEEKDAYS_LONG, WEEKDAYS_SHORT, HIJRI_MONTHS_LONG, HIJRI_MONTHS_SHORT, NUMBERS_SOMALI, SOMALI_SEASONS, SOMALI_HOLIDAYS };