nepali-date-converter-np
Version:
A lightweight TypeScript/JavaScript package for converting dates between Gregorian (AD) and Bikram Sambat (BS) calendar systems with Nepali and English formatting support.
242 lines (218 loc) • 7.94 kB
text/typescript
// Nepali Date Converter - Core API
// Provides conversion between Gregorian (AD) and Bikram Sambat (BS) calendars
// and date formatting utilities.
import { bsMonthData, bsEpoch } from './bs-data';
export type DateFormat = 'nepali-full' | 'nepali-short' | 'english-full' | 'english-short';
export interface BSDate {
year: number;
month: number;
day: number;
}
export interface ADDate {
year: number;
month: number;
day: number;
}
function daysBetweenAD(ad1: ADDate, ad2: ADDate): number {
// Returns the number of days from ad1 to ad2 (ad2 - ad1)
const d1 = new Date(ad1.year, ad1.month - 1, ad1.day);
const d2 = new Date(ad2.year, ad2.month - 1, ad2.day);
return Math.round((d2.getTime() - d1.getTime()) / (1000 * 60 * 60 * 24));
}
function addDaysToBS(bs: BSDate, days: number): BSDate {
let { year, month, day } = bs;
day += days;
while (true) {
const daysInMonth = bsMonthData[year]?.[month - 1];
if (!daysInMonth) break;
if (day > daysInMonth) {
day -= daysInMonth;
month++;
if (month > 12) {
month = 1;
year++;
}
} else {
break;
}
}
return { year, month, day };
}
function subtractDaysFromBS(bs: BSDate, days: number): BSDate {
let { year, month, day } = bs;
day -= days;
while (day < 1) {
month--;
if (month < 1) {
year--;
month = 12;
}
const daysInMonth = bsMonthData[year]?.[month - 1];
if (!daysInMonth) break;
day += daysInMonth;
}
return { year, month, day };
}
function daysBetweenBS(bs1: BSDate, bs2: BSDate): number {
// Returns the number of days from bs1 to bs2 (bs2 - bs1)
let days = 0;
let { year, month, day } = bs1;
while (year < bs2.year || (year === bs2.year && month < bs2.month) || (year === bs2.year && month === bs2.month && day < bs2.day)) {
day++;
days++;
const daysInMonth = bsMonthData[year]?.[month - 1];
if (!daysInMonth) {
throw new Error(`Unsupported BS year: ${year}`);
}
if (day > daysInMonth) {
day = 1;
month++;
if (month > 12) {
month = 1;
year++;
}
}
}
return days;
}
/**
* Converts a Gregorian (AD) date to Bikram Sambat (BS).
* @param adDate - The AD date to convert.
* @returns The corresponding BS date.
*/
export function convertADToBS(adDate: ADDate): BSDate {
// Validate input
if (adDate.month < 1 || adDate.month > 12 || adDate.day < 1 || adDate.day > 31) {
throw new Error('Invalid AD date');
}
// Only supports AD dates between 1943-04-14 and 1945-04-13 (BS 2000-2002)
const days = daysBetweenAD(bsEpoch.ad, adDate);
if (days < 0 || days > 1095) { // ~3 years
throw new Error('AD date outside supported range (1943-1945)');
}
return addDaysToBS(bsEpoch.bs, days);
}
/**
* Converts a Bikram Sambat (BS) date to Gregorian (AD).
* @param bsDate - The BS date to convert.
* @returns The corresponding AD date.
*/
export function convertBSToAD(bsDate: BSDate): ADDate {
// Validate input
if (bsDate.month < 1 || bsDate.month > 12 || bsDate.day < 1 || bsDate.day > 32) {
throw new Error('Invalid BS date');
}
// Only supports BS dates between 2000-01-01 and 2002-12-31
if (bsDate.year < 2000 || bsDate.year > 2002) {
throw new Error('BS date outside supported range (2000-2002)');
}
// Find days between bsEpoch.bs and bsDate
const days = daysBetweenBS(bsEpoch.bs, bsDate);
const d = new Date(bsEpoch.ad.year, bsEpoch.ad.month - 1, bsEpoch.ad.day);
d.setDate(d.getDate() + days);
return { year: d.getFullYear(), month: d.getMonth() + 1, day: d.getDate() };
}
const nepaliMonths = ['बैशाख', 'जेठ', 'असार', 'श्रावण', 'भदौ', 'आश्विन', 'कार्तिक', 'मंसिर', 'पौष', 'माघ', 'फाल्गुण', 'चैत्र'];
const englishMonths = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const nepaliNumerals = ['०','१','२','३','४','५','६','७','८','९'];
function toNepaliNumber(num: number): string {
return num.toString().split('').map(d => nepaliNumerals[+d] ?? d).join('');
}
export function formatDate(date: BSDate | ADDate, format: DateFormat): string {
let isBS = false;
let bs: BSDate;
let ad: ADDate;
// Check if it's a BS date in supported range
if ((date as BSDate).year >= 2000 && (date as BSDate).year <= 2002) {
isBS = true;
bs = date as BSDate;
ad = convertBSToAD(bs);
} else if ((date as ADDate).year >= 1943 && (date as ADDate).year <= 1945) {
// AD date in supported range
ad = date as ADDate;
bs = convertADToBS(ad);
} else {
// For dates outside supported range, treat as AD date for English formatting
// and throw error for Nepali formatting since we can't convert
ad = date as ADDate;
if (format.startsWith('nepali')) {
throw new Error('Cannot format date in Nepali: date outside supported range');
}
// For English formatting, use the AD date as-is
const year = ad.year;
const month = englishMonths[ad.month - 1];
const day = ad.day;
if (format === 'english-full') {
return `${day} ${month} ${year}`;
} else {
return `${year}/${ad.month}/${day}`;
}
}
if (format.startsWith('nepali')) {
// Nepali format
const year = toNepaliNumber(bs.year);
const month = nepaliMonths[bs.month - 1];
const day = toNepaliNumber(bs.day);
if (format === 'nepali-full') {
return `${year} ${month} ${day}`;
} else {
return `${year}/${toNepaliNumber(bs.month)}/${day}`;
}
} else {
// English format
const year = ad.year;
const month = englishMonths[ad.month - 1];
const day = ad.day;
if (format === 'english-full') {
return `${day} ${month} ${year}`;
} else {
return `${year}/${ad.month}/${day}`;
}
}
}
const nepaliWeekdays = ['आइतबार', 'सोमबार', 'मंगलबार', 'बुधबार', 'बिहीबार', 'शुक्रबार', 'शनिबार'];
const englishWeekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
function isBSDate(date: any): date is BSDate {
// Only treat as BSDate if year is in the supported BS range
return (
typeof date.year === 'number' &&
typeof date.month === 'number' &&
typeof date.day === 'number' &&
date.year >= 2000 && date.year <= 2002 &&
date.month >= 1 && date.month <= 12 &&
date.day >= 1 && date.day <= 32
);
}
export function getDayOfWeek(date: BSDate | ADDate, locale: 'ne' | 'en' = 'en'): string {
// Validate locale
if (locale !== 'ne' && locale !== 'en') {
throw new Error('Invalid locale. Use "ne" for Nepali or "en" for English');
}
let ad: ADDate;
if ('year' in date && 'month' in date && 'day' in date) {
// Validate date structure
if (date.month < 1 || date.month > 12 || date.day < 1 || date.day > 32) {
throw new Error('Invalid date object');
}
if (isBSDate(date)) {
// BSDate logic
if (date.year <= 2002) {
try {
ad = convertBSToAD(date);
} catch (error) {
throw new Error('BS date outside supported range');
}
} else {
throw new Error('BS date outside supported range');
}
} else {
// Treat as ADDate
ad = date as ADDate;
}
} else {
throw new Error('Invalid date object');
}
const jsDate = new Date(ad.year, ad.month - 1, ad.day);
const dayIdx = jsDate.getDay();
return locale === 'ne' ? nepaliWeekdays[dayIdx] : englishWeekdays[dayIdx];
}