@subrotosaha/bangla-date
Version:
A simple utility package for string manipulation in JavaScript.
899 lines (829 loc) • 38.3 kB
text/typescript
type NumberInWords = {
[key: number]: string;
};
type NumbersInWords = {
en: NumberInWords;
bn: NumberInWords;
hi: NumberInWords;
};
/**
* Converts an integer to its natural-language word equivalent in English,
* Bengali, or Hindi.
*
* - **English** uses the short-scale system: thousand / million / billion.
* - **Bengali & Hindi** use the South-Asian denomination system:
* হাজার/हज़ार (1,000) → লাখ/लाख (1,00,000) → কোটি/करोड़ (1,00,00,000).
* Crore-level values are handled recursively, so very large numbers such
* as 10,000,000,000 correctly render as "এক হাজার কোটি" / "एक हज़ार करोड़".
*
* @param num - An integer (positive, negative, or zero). Throws if `num`
* is not an integer (i.e. `Number.isInteger(num)` is `false`).
* @param language - Target language for word output.
* - `"en"` (default) — English words, e.g. `"twenty-one"`
* - `"bn"` — Bengali words, e.g. `"একাশ"`
* - `"hi"` — Hindi words, e.g. `"इक्कीस"`
* @returns The word representation as a string.
* @throws {Error} When `num` is not an integer.
*
* @example
* numberToWords(21, 'en'); // "twenty-one"
* numberToWords(21, 'bn'); // "একাশ"
* numberToWords(21, 'hi'); // "इक्कीस"
* numberToWords(1000, 'en'); // "one thousand"
* numberToWords(100000, 'bn'); // "এক লাখ"
* numberToWords(-5, 'en'); // "-five"
* numberToWords(0, 'bn'); // "শূন্য"
*/
export const numberToWords = (
num: number,
language: keyof NumbersInWords = "en"
): string => {
if (!Number.isInteger(num)) {
throw new Error(`numberToWords only supports integers, got ${num}`);
}
const numbersInWords: NumbersInWords = {
en: {
0: "zero",
1: "one",
2: "two",
3: "three",
4: "four",
5: "five",
6: "six",
7: "seven",
8: "eight",
9: "nine",
10: "ten",
11: "eleven",
12: "twelve",
13: "thirteen",
14: "fourteen",
15: "fifteen",
16: "sixteen",
17: "seventeen",
18: "eighteen",
19: "nineteen",
20: "twenty",
30: "thirty",
40: "forty",
50: "fifty",
60: "sixty",
70: "seventy",
80: "eighty",
90: "ninety",
100: "hundred",
1000: "thousand",
},
bn: {
0: "শূন্য",
1: "এক",
2: "দুই",
3: "তিন",
4: "চার",
5: "পাঁচ",
6: "ছয়",
7: "সাত",
8: "আট",
9: "নয়",
10: "দশ",
11: "এগারো",
12: "বারো",
13: "তেরো",
14: "চোদ্দো",
15: "পনেরো",
16: "ষোলো",
17: "সতেরো",
18: "আঠারো",
19: "উনিশ",
20: "বিশ",
21: "একুশ",
22: "বাইশ",
23: "তেইশ",
24: "চব্বিশ",
25: "পঁচিশ",
26: "ছাব্বিশ",
27: "সাতাশ",
28: "আটাশ",
29: "উনত্রিশ",
30: "ত্রিশ",
31: "একত্রিশ",
32: "বত্রিশ",
33: "তেত্রিশ",
34: "চৌত্রিশ",
35: "পঁয়ত্রিশ",
36: "ছত্রিশ",
37: "সাতত্রিশ",
38: "আটত্রিশ",
39: "উনচল্লিশ",
40: "চল্লিশ",
41: "একচল্লিশ",
42: "বিয়াল্লিশ",
43: "তেতাল্লিশ",
44: "চৌচল্লিশ",
45: "পঁয়তাল্লিশ",
46: "ছেচল্লিশ",
47: "সাতচল্লিশ",
48: "আটচল্লিশ",
49: "উনপঞ্চাশ",
50: "পঞ্চাশ",
51: "একান্ন",
52: "বায়ান্ন",
53: "তেপান্ন",
54: "চৌপান্ন",
55: "পঞ্চান্ন",
56: "ছাপান্ন",
57: "সাতান্ন",
58: "আটান্ন",
59: "উনষাট",
60: "ষাট",
61: "একষট্টি",
62: "বাষট্টি",
63: "তেষট্টি",
64: "চৌষট্টি",
65: "পঁয়ষট্টি",
66: "ছেষট্টি",
67: "সাতষট্টি",
68: "আটষট্টি",
69: "উনসত্তর",
70: "সত্তর",
71: "একাত্তর",
72: "বাহাত্তর",
73: "তেহাত্তর",
74: "চুয়াত্তর",
75: "পঁচাত্তর",
76: "ছিয়াত্তর",
77: "সাতাত্তর",
78: "আটাত্তর",
79: "উনআশি",
80: "আশি",
81: "একাশি",
82: "বিরাশি",
83: "তিরাশি",
84: "চুরাশি",
85: "পঁচাশি",
86: "ছিয়াশি",
87: "সাতাশি",
88: "আটাশি",
89: "উননব্বই",
90: "নব্বই",
91: "একানব্বই",
92: "বিরানব্বই",
93: "তিরানব্বই",
94: "চুরানব্বই",
95: "পঁচানব্বই",
96: "ছিয়ানব্বই",
97: "সাতানব্বই",
98: "আটানব্বই",
99: "নিরানব্বই",
100: "শত",
1000: "হাজার",
},
hi: {
0: "शून्य",
1: "एक",
2: "दो",
3: "तीन",
4: "चार",
5: "पाँच",
6: "छह",
7: "सात",
8: "आठ",
9: "नौ",
10: "दस",
11: "ग्यारह",
12: "बारह",
13: "तेरह",
14: "चौदह",
15: "पंद्रह",
16: "सोलह",
17: "सत्रह",
18: "अठारह",
19: "उन्नीस",
20: "बीस",
21: "इक्कीस",
22: "बाईस",
23: "तेईस",
24: "चौबीस",
25: "पच्चीस",
26: "छब्बीस",
27: "सत्ताईस",
28: "अट्ठाईस",
29: "उनतीस",
30: "तीस",
31: "इकतीस",
32: "बत्तीस",
33: "तैंतीस",
34: "चौंतीस",
35: "पैंतीस",
36: "छत्तीस",
37: "सैंतीस",
38: "अड़तीस",
39: "उनतालीस",
40: "चालीस",
41: "इकतालीस",
42: "बयालीस",
43: "तैंतालीस",
44: "चवालीस",
45: "पैंतालीस",
46: "छयालीस",
47: "सैंतालीस",
48: "अड़तालीस",
49: "उनचास",
50: "पचास",
51: "इक्यावन",
52: "बावन",
53: "तिरपन",
54: "चौवन",
55: "पचपन",
56: "छप्पन",
57: "सत्तावन",
58: "अट्ठावन",
59: "उनसठ",
60: "साठ",
61: "इकसठ",
62: "बासठ",
63: "तिरसठ",
64: "चौंसठ",
65: "पैंसठ",
66: "छयासठ",
67: "सड़सठ",
68: "अड़सठ",
69: "उनहत्तर",
70: "सत्तर",
71: "इकहत्तर",
72: "बहत्तर",
73: "तिहत्तर",
74: "चौहत्तर",
75: "पचहत्तर",
76: "छिहत्तर",
77: "सतहत्तर",
78: "अठहत्तर",
79: "उन्यासी",
80: "अस्सी",
81: "इक्यासी",
82: "बयासी",
83: "तिरासी",
84: "चौरासी",
85: "पचासी",
86: "छियासी",
87: "सत्तासी",
88: "अट्ठासी",
89: "नवासी",
90: "नब्बे",
91: "इक्यानवे",
92: "बानवे",
93: "तिरानवे",
94: "चौरानवे",
95: "पचानवे",
96: "छानवे",
97: "सत्तानवे",
98: "अट्ठानवे",
99: "निन्यानवे",
100: "सौ",
1000: "हज़ार",
},
};
// Helper function to convert number to words for numbers below 100
const convertBelowHundred = (
num: number,
language: keyof typeof numbersInWords
): string => {
// All values 0-99 have direct entries in the map (bn & hi have each unique
// compound word; en falls back to tens+ones construction for values > 20
// that are not multiples of 10).
if (num in numbersInWords[language]) {
return numbersInWords[language][num];
}
// English only: compose "twenty-one", "thirty-two", etc.
const tens = Math.floor(num / 10) * 10;
const ones = num % 10;
return `${numbersInWords[language][tens]}-${numbersInWords[language][ones]}`;
};
// Helper function to convert number to words for numbers below 1000
const convertBelowThousand = (
num: number,
language: keyof typeof numbersInWords
): string => {
if (num < 100) return convertBelowHundred(num, language);
const hundreds = Math.floor(num / 100);
const remainder = num % 100;
let hundredWord: string;
if (language === "bn") {
// Bengali (colloquial): "একশো", "দুইশো", "তিনশো" …
hundredWord = `${numbersInWords[language][hundreds]}শো`;
} else if (language === "hi") {
// Hindi: "एक सौ", "दो सौ", etc.
hundredWord = `${numbersInWords[language][hundreds]} ${numbersInWords[language][100]}`;
} else {
// English: "one hundred", "two hundred", etc.
hundredWord =
hundreds === 1
? `one ${numbersInWords[language][100]}`
: `${convertBelowHundred(hundreds, language)} ${
numbersInWords[language][100]
}`;
}
return remainder
? `${hundredWord} ${convertBelowHundred(remainder, language)}`
: hundredWord;
};
if (num === 0) return numbersInWords[language][0];
if (num < 0) return `-${numberToWords(-num, language)}`;
if (num < 1000) return convertBelowThousand(num, language);
// Lakh (1,00,000) and Crore (1,00,00,000) for Bengali/Hindi; Million for English
if (language === "en") {
if (num < 1_000_000) {
const thousands = Math.floor(num / 1000);
const remainder = num % 1000;
return remainder
? `${convertBelowThousand(thousands, language)} ${
numbersInWords[language][1000]
} ${convertBelowThousand(remainder, language)}`
: `${convertBelowThousand(thousands, language)} ${
numbersInWords[language][1000]
}`;
}
if (num < 1_000_000_000) {
const millions = Math.floor(num / 1_000_000);
const remainder = num % 1_000_000;
return remainder
? `${convertBelowThousand(millions, language)} million ${numberToWords(
remainder,
language
)}`
: `${convertBelowThousand(millions, language)} million`;
}
const billions = Math.floor(num / 1_000_000_000);
const remainder = num % 1_000_000_000;
return remainder
? `${convertBelowThousand(billions, language)} billion ${numberToWords(
remainder,
language
)}`
: `${convertBelowThousand(billions, language)} billion`;
}
// Bengali / Hindi: use lakh and crore denominations
const lakhWord = language === "bn" ? "লাখ" : "लाख";
const croreWord = language === "bn" ? "কোটি" : "करोड़";
if (num < 100_000) {
const thousands = Math.floor(num / 1000);
const remainder = num % 1000;
return remainder
? `${convertBelowThousand(thousands, language)} ${
numbersInWords[language][1000]
} ${convertBelowThousand(remainder, language)}`
: `${convertBelowThousand(thousands, language)} ${
numbersInWords[language][1000]
}`;
}
if (num < 10_000_000) {
const lakhs = Math.floor(num / 100_000);
const remainder = num % 100_000;
return remainder
? `${convertBelowThousand(lakhs, language)} ${lakhWord} ${numberToWords(
remainder,
language
)}`
: `${convertBelowThousand(lakhs, language)} ${lakhWord}`;
}
const crores = Math.floor(num / 10_000_000);
const remainder = num % 10_000_000;
// Use recursive numberToWords for crores so values >= 1000 crore are
// handled correctly (e.g. 10,000,000,000 → "এক হাজার কোটি").
return remainder
? `${numberToWords(crores, language)} ${croreWord} ${numberToWords(
remainder,
language
)}`
: `${numberToWords(crores, language)} ${croreWord}`;
};
/**
* Replaces every ASCII digit (0–9) in a number or string with the
* equivalent digit character in the target language/script.
*
* This is the low-level utility used by all `BanglaDate` formatting and
* output methods to localise numeric output. Passing `"en"` is a no-op
* (digits remain 0–9).
*
* @param num - The value to localise. Accepts a `number` or a `string`
* that may contain digits anywhere (e.g. formatted date strings, time
* strings, ISO strings). Non-digit characters are passed through unchanged.
* @param language - Target script for digit substitution.
* - `"en"` (default) — ASCII digits (no change)
* - `"bn"` — Bengali digits ০১২৩৪৫৬৭৮৯
* - `"hi"` — Devanagari digits ०१२३४५६७८९
* @returns The input with all ASCII digits replaced by the target script's digits.
*
* @example
* numberToNumber(2025, 'bn'); // "২০২৫"
* numberToNumber('14/4/1432', 'bn'); // "১৪/৪/১৪৩২"
* numberToNumber('08:30:00', 'hi'); // "०८:८०:००"
* numberToNumber(42, 'en'); // "42" (no change)
*/
export const numberToNumber = (
num: number | string,
language: keyof NumbersInWords = "en"
) => {
const numbersInNumber: NumbersInWords = {
en: {
0: "0",
1: "1",
2: "2",
3: "3",
4: "4",
5: "5",
6: "6",
7: "7",
8: "8",
9: "9",
},
bn: {
0: "০",
1: "১",
2: "২",
3: "৩",
4: "৪",
5: "৫",
6: "৬",
7: "৭",
8: "৮",
9: "৯",
},
hi: {
0: "०",
1: "१",
2: "२",
3: "३",
4: "४",
5: "५",
6: "६",
7: "७",
8: "८",
9: "९",
},
};
return num
.toString()
.replace(/\d/g, (digit) => numbersInNumber[language][parseInt(digit)]);
};
/**
* Timezone offsets in fractional hours (e.g. 5.5 = UTC+5:30).
* Ambiguous abbreviations are resolved toward the most common / South-Asian context:
* IST → India Standard Time (+5:30) not Ireland (+1) or Israel (+2)
* AST → Arabia Standard Time (+3) not Atlantic (-4)
* CST → Central Standard Time (-6, US/Canada) — use CST_CN alias for China (+8)
* GST → Gulf Standard Time (+4) not South Georgia (-2) — use GST_SG alias
* AMT → Amazon Time (-4) not Armenia (+4)
*/
export const TIME_ZONE_OFFSETS: Record<string, number> = {
// ── UTC / GMT ──────────────────────────────────────────────────────────
UTC: 0,
GMT: 0,
// ── UTC−12 ─────────────────────────────────────────────────────────────
IDLW: -12, // International Date Line West
BIT: -12, // Baker Island Time
// ── UTC−11 ─────────────────────────────────────────────────────────────
NUT: -11, // Niue Time
SST: -11, // Samoa Standard Time
MIT: -11, // Midway Islands Time
// ── UTC−10 ─────────────────────────────────────────────────────────────
HST: -10, // Hawaii Standard Time
HAST: -10, // Hawaii–Aleutian Standard Time
TAHT: -10, // Tahiti Time
CKT: -10, // Cook Island Time
// ── UTC−9:30 ───────────────────────────────────────────────────────────
MART: -9.5, // Marquesas Islands Time
// ── UTC−9 ──────────────────────────────────────────────────────────────
AKST: -9, // Alaska Standard Time
GAMT: -9, // Gambier Islands Time
HADT: -9, // Hawaii–Aleutian Daylight Time
// ── UTC−8 ──────────────────────────────────────────────────────────────
PST: -8, // Pacific Standard Time (US/Canada)
AKDT: -8, // Alaska Daylight Time
// ── UTC−7 ──────────────────────────────────────────────────────────────
MST: -7, // Mountain Standard Time (US/Canada/Mexico)
PDT: -7, // Pacific Daylight Time
// ── UTC−6 ──────────────────────────────────────────────────────────────
CST: -6, // Central Standard Time (US/Canada) — kept as US default
MDT: -6, // Mountain Daylight Time
// ── UTC−5 ──────────────────────────────────────────────────────────────
EST: -5, // Eastern Standard Time (US/Canada)
CDT: -5, // Central Daylight Time
PET: -5, // Peru Time
COT: -5, // Colombia Time
ECT: -5, // Ecuador Time
COST: -5, // Colombia Summer Time
// ── UTC−4:30 ───────────────────────────────────────────────────────────
VET: -4.5, // Venezuela Time
// ── UTC−4 ──────────────────────────────────────────────────────────────
EDT: -4, // Eastern Daylight Time
ADT: -3, // Atlantic Daylight Time
AST_AR: -4, // Atlantic Standard Time (Americas)
BOT: -4, // Bolivia Time
AMT: -4, // Amazon Time (Brazil)
GYT: -4, // Guyana Time
PYT: -4, // Paraguay Time
// ── UTC−3:30 ───────────────────────────────────────────────────────────
NST: -3.5, // Newfoundland Standard Time
NDT: -2.5, // Newfoundland Daylight Time
// ── UTC−3 ──────────────────────────────────────────────────────────────
BRT: -3, // Brasilia Time
ART: -3, // Argentina Time
UYT: -3, // Uruguay Time
SRT: -3, // Suriname Time
GFT: -3, // French Guiana Time
ROTT: -3, // Rothera Research Station Time
WGT: -3, // West Greenland Time
PMST: -3, // St. Pierre & Miquelon Standard Time
PNST: -8.5, // Pitcairn Standard Time (actually −8:30)
// ── UTC−2 ──────────────────────────────────────────────────────────────
BRST: -2, // Brasilia Summer Time
FNT: -2, // Fernando de Noronha Time
UYST: -2, // Uruguay Summer Time
WGST: -2, // West Greenland Summer Time
PYDT: -3, // Paraguay Daylight Time
// ── UTC−1 ──────────────────────────────────────────────────────────────
AZOT: -1, // Azores Standard Time
CVT: -1, // Cape Verde Time
EGT: -1, // East Greenland Time
AZOST: 0, // Azores Summer Time (≡ UTC)
EGST: 0, // East Greenland Summer Time
// ── UTC+0 ──────────────────────────────────────────────────────────────
WET: 0, // Western European Time (Portugal, UK winter)
WT: 0, // West Africa Time (alternative)
// ── UTC+1 ──────────────────────────────────────────────────────────────
CET: 1, // Central European Time
WAT: 1, // West Africa Time (Nigeria, etc.)
MET: 1, // Middle European Time
IST_IE: 1, // Ireland Standard Time
WEST: 1, // Western European Summer Time
BST_UK: 1, // British Summer Time (not used here; alias only)
// ── UTC+2 ──────────────────────────────────────────────────────────────
EET: 2, // Eastern European Time
CEST: 2, // Central European Summer Time
CAT: 2, // Central Africa Time (Harare, Lusaka)
SAST: 2, // South Africa Standard Time
IST_IL: 2, // Israel Standard Time
FLST: 2, // Falkland Islands Summer Time
// ── UTC+3 ──────────────────────────────────────────────────────────────
EEST: 3, // Eastern European Summer Time
MSK: 3, // Moscow Standard Time (Russia)
EAT: 3, // East Africa Time (Kenya, Tanzania, Uganda)
AST: 3, // Arabia Standard Time (Saudi Arabia, Iraq, Kuwait)
TRT: 3, // Turkey Time
SYOT: 3, // Syowa Station Time (Antarctica)
MSD: 4, // Moscow Daylight (historical; now MSK is permanent)
// ── UTC+3:30 ───────────────────────────────────────────────────────────
IRST: 3.5, // Iran Standard Time
IRDT: 4.5, // Iran Daylight Time
// ── UTC+4 ──────────────────────────────────────────────────────────────
GST: 4, // Gulf Standard Time (UAE, Oman) — primary interpretation
GST_SG: -2, // South Georgia Time (alias to disambiguate)
MUT: 4, // Mauritius Time
RET: 4, // Réunion Time
SCT: 4, // Seychelles Time
SAMT: 4, // Samara Time (Russia)
GET: 4, // Georgia Standard Time
AZT: 4, // Azerbaijan Time
AZST: 5, // Azerbaijan Summer Time
// ── UTC+4:30 ───────────────────────────────────────────────────────────
AFT: 4.5, // Afghanistan Time
// ── UTC+5 ──────────────────────────────────────────────────────────────
PKT: 5, // Pakistan Standard Time
UZT: 5, // Uzbekistan Time
TJT: 5, // Tajikistan Time
TMT: 5, // Turkmenistan Time
MVT: 5, // Maldives Time
YEKT: 5, // Yekaterinburg Time (Russia)
AQTT: 5, // Aqtau/Aktobe Time (Kazakhstan)
// ── UTC+5:30 ───────────────────────────────────────────────────────────
IST: 5.5, // India Standard Time (primary — most used in this library)
SLT: 5.5, // Sri Lanka Time
// ── UTC+5:45 ───────────────────────────────────────────────────────────
NPT: 5.75, // Nepal Time
// ── UTC+6 ──────────────────────────────────────────────────────────────
BST: 6, // Bangladesh Standard Time (UTC+6) ← primary use in this library
BTT: 6, // Bhutan Time
ALMT: 6, // Alma-Ata Time (Kazakhstan)
OMST: 6, // Omsk Time (Russia)
VOST: 6, // Vostok Station Time (Antarctica)
IOT: 6, // Indian Ocean Time (Chagos)
// ── UTC+6:30 ───────────────────────────────────────────────────────────
MMT: 6.5, // Myanmar Time
CCT: 6.5, // Cocos Islands Time
// ── UTC+7 ──────────────────────────────────────────────────────────────
WIB: 7, // Western Indonesian Time
ICT: 7, // Indochina Time (Vietnam, Thailand, Laos, Cambodia)
THA: 7, // Thailand Standard Time
KRAT: 7, // Krasnoyarsk Time (Russia)
HOVT: 7, // Hovd Time (Mongolia)
// ── UTC+8 ──────────────────────────────────────────────────────────────
CST_CN: 8, // China Standard Time (alias to avoid ambiguity)
HKT: 8, // Hong Kong Time
SGT: 8, // Singapore Time
MYT: 8, // Malaysia Time
AWST: 8, // Australian Western Standard Time (Perth)
BNT: 8, // Brunei Darussalam Time
ULAT: 8, // Ulaanbaatar Time (Mongolia)
PHST: 8, // Philippine Standard Time
IRKT: 8, // Irkutsk Time (Russia)
WST: 8, // Western Samoa Time (historical; use WSST for modern)
// ── UTC+8:45 ───────────────────────────────────────────────────────────
CWST: 8.75, // Southeastern Western Australia Standard Time
// ── UTC+9 ──────────────────────────────────────────────────────────────
JST: 9, // Japan Standard Time
KST: 9, // Korea Standard Time
TLT: 9, // East Timor Time
YAKT: 9, // Yakutsk Time (Russia)
WIT: 9, // Eastern Indonesian Time
PWT: 9, // Palau Time
// ── UTC+9:30 ───────────────────────────────────────────────────────────
ACST: 9.5, // Australian Central Standard Time
ACDT: 10.5, // Australian Central Daylight Time
// ── UTC+10 ─────────────────────────────────────────────────────────────
AEST: 10, // Australian Eastern Standard Time
ChST: 10, // Chamorro Standard Time (Guam, CNMI)
PGT: 10, // Papua New Guinea Time
VLAT: 10, // Vladivostok Time (Russia)
TRUT: 10, // Truk Time (Chuuk, Micronesia)
// ── UTC+10:30 ──────────────────────────────────────────────────────────
LHST: 10.5, // Lord Howe Standard Time
// ── UTC+11 ─────────────────────────────────────────────────────────────
AEDT: 11, // Australian Eastern Daylight Time
SBT: 11, // Solomon Islands Time
NCT: 11, // New Caledonia Time
NFT: 11, // Norfolk Island Time
PONT: 11, // Pohnpei Standard Time (Micronesia)
KOST: 11, // Kosrae Time (Micronesia)
LHDT: 11, // Lord Howe Daylight Time
VLAST: 11, // Vladivostok Summer Time (historical)
// ── UTC+12 ─────────────────────────────────────────────────────────────
NZST: 12, // New Zealand Standard Time
FJT: 12, // Fiji Time
TVT: 12, // Tuvalu Time
MHT: 12, // Marshall Islands Time
GILT: 12, // Gilbert Islands Time (Kiribati)
ANAT: 12, // Anadyr Time (Russia)
PETT: 12, // Kamchatka Time (Russia)
WAKT: 12, // Wake Island Time
NRUT: 12, // Nauru Time
// ── UTC+12:45 ──────────────────────────────────────────────────────────
CHAST: 12.75, // Chatham Islands Standard Time
// ── UTC+13 ─────────────────────────────────────────────────────────────
NZDT: 13, // New Zealand Daylight Time
TOT: 13, // Tonga Time
WSST: 13, // Samoa Standard Time (Samoa/American Samoa post-2011)
TKT: 13, // Tokelau Time
PHOT: 13, // Phoenix Islands Time (Kiribati)
// ── UTC+13:45 ──────────────────────────────────────────────────────────
CHADT: 13.75, // Chatham Islands Daylight Time
// ── UTC+14 ─────────────────────────────────────────────────────────────
LINT: 14, // Line Islands Time (Kiribati — easternmost)
YEKST: 6, // Yekaterinburg Summer Time (Russia, historical)
OMSST: 7, // Omsk Summer Time (historical)
IRKST: 9, // Irkutsk Summer Time (historical)
YAKST: 10, // Yakutsk Summer Time (historical)
KRAST: 8, // Krasnoyarsk Summer Time (historical)
ANAST: 12, // Anadyr Summer Time (historical)
PETST: 12, // Kamchatka Summer Time (historical)
FJST: 13, // Fiji Summer Time
AWDT: 9, // Australian Western Daylight Time (historical, not officially observed)
};
/**
* Replaces Gregorian numeric fields inside a pre-formatted locale string
* with the corresponding Bangla calendar values.
*
* This was used by `BanglaDate.toLocaleString()` / `toLocaleDateString()` to
* substitute the year, month, and day in an `Intl.DateTimeFormat` output
* string. Since v1.7 `BanglaDate._applyLocale()` uses
* `Intl.DateTimeFormat.formatToParts()` directly and no longer calls this
* helper, but the function remains exported for any external callers.
*
* @deprecated Since v1.7.0 \u2014 `BanglaDate._applyLocale` now uses
* `Intl.DateTimeFormat.formatToParts()` internally. This function may be
* removed in the next major release. Migrate to `BanglaDate.format()` or
* `BanglaDate.toLocaleString()` instead.
*
* @param banglaDateStr - An ISO-like string in the form
* `\"YYYY-MM-DDThh:mm:ss.mmmZ\"` where `YYYY-MM-DD` are Bangla calendar
* values (as produced by `BanglaDate.toISOString()`).
* @param templateStr - A locale-formatted date string (e.g. from
* `Intl.DateTimeFormat`) that contains the Gregorian values to be replaced.\n * May optionally contain a timezone label (e.g. `\"GMT+6\"`, `\"BST\"`)
* to localise the time component.
* @param gregorianRef - Optional. The original Gregorian `Date` that was
* used to generate `templateStr`. When supplied, replacement uses exact
* field matching to avoid false positives on ambiguous substrings.
* @returns A new string equal to `templateStr` with Gregorian year, month,
* day, and time fields replaced by their Bangla equivalents.
* @throws {Error} If `banglaDateStr` cannot be parsed (missing `YYYY-MM-DD`
* prefix or missing time component).
*
* @example
* // Convert a Gregorian locale string to Bangla calendar values:
* const bd = BanglaDate.fromBanglaDate(1432, 1, 14);
* formatBanglaDateToMatchTemplate(
* bd.toISOString(), // \"1432-01-14T00:00:00.000Z\"
* '4/14/2025, 12:00:00 AM', // Gregorian en-US formatted string
* bd.toGregorian()
* );
* // => '1/14/1432, 12:00:00 AM'
*/
export function formatBanglaDateToMatchTemplate(
banglaDateStr: string,
templateStr: string,
gregorianRef?: Date
): string {
// 1. Parse Bangla date components
const [banglaDatePart, timePartRaw] = banglaDateStr.split(" ");
const [bYear, bMonth, bDay] = banglaDatePart.split("-");
if (!bYear || !bMonth || !bDay) throw new Error("Invalid Bangla date");
// 2. Try to extract timezone (optional)
const timezoneRegex =
/\b(GMT[+-]?\d+(?:\.\d+)?|UTC[+-]?\d+(?:\.\d+)?|UTC|GMT|IDLW|BIT|NUT|SST|MIT|TAHT|CKT|MART|HST|AKST|AKDT|GAMT|HAST|HADT|PST|PDT|MST|MDT|CST|CDT|EST|EDT|PET|COT|ECT|COST|VET|AST|ADT|BOT|AMT|GYT|PYT|PYDT|NST|NDT|BRT|ART|UYT|UYST|SRT|GFT|ROTT|WGT|WGST|PNST|BRST|FNT|GST_SG|AZOT|AZOST|CVT|WET|WEST|WT|EGT|EGST|CET|CEST|WAT|WEST|MET|IST_IE|EET|EEST|CAT|SAST|FLST|MSK|MSD|EAT|AST_AR|TRT|SYOT|IRST|IRDT|GST|MUT|RET|SCT|SAMT|GET|AZT|AZST|AFT|PKT|UZT|TJT|TMT|MVT|YEKT|YEKST|AQTT|IST|SLT|NPT|BST|BTT|ALMT|OMST|OMSST|VOST|IOT|CCT|MMT|WIB|ICT|THA|KRAT|KRAST|HOVT|SGT|HKT|CST_CN|MYT|AWST|AWDT|BNT|ULAT|PHST|IRKT|IRKST|CWST|JST|KST|TLT|YAKT|YAKST|WIT|ACST|ACDT|AEST|AEDT|ChST|PGT|VLAT|VLAST|SBT|NCT|PONT|KOST|NFT|LHST|LHDT|NZST|NZDT|FJT|FJST|TVT|MHT|GILT|ANAT|ANAST|PETT|PETST|WAKT|NRUT|CHAST|CHADT|PHOT|TOT|WSST|TKT|LINT)\b/i;
const tzMatch = templateStr.match(timezoneRegex);
const zoneLabel = tzMatch ? tzMatch[1].toUpperCase() : "UTC"; // fallback to UTC
// 3. Resolve timezone offset in fractional hours
let offsetHours = 0;
if (/^(?:GMT|UTC)[+-]\d/.test(zoneLabel)) {
offsetHours = parseFloat(zoneLabel.replace(/^(?:GMT|UTC)/, ""));
} else {
offsetHours = TIME_ZONE_OFFSETS[zoneLabel] ?? 0;
}
// 4. Parse UTC time components from banglaDateStr
const timePart = timePartRaw?.split(".")[0];
if (!timePart) throw new Error("Missing time in Bangla date string");
const [h, m, s] = timePart.split(":").map(Number);
if ([h, m, s].some(isNaN)) throw new Error("Invalid time format");
// 5. Apply timezone offset to obtain local time
const localDate = new Date(
Date.UTC(2000, 0, 1, h, m, s) + offsetHours * 3_600_000
);
const lH = localDate.getUTCHours();
const lM = localDate.getUTCMinutes();
const lS = localDate.getUTCSeconds();
const lH12 = lH % 12 || 12;
const ampm = lH >= 12 ? "PM" : "AM";
// 6. Template-aware substitution: when we have the Gregorian reference date
// that generated templateStr, replace each numeric field precisely.
if (gregorianRef) {
const gY = gregorianRef.getUTCFullYear();
const gMo = gregorianRef.getUTCMonth() + 1; // 1-indexed
const gD = gregorianRef.getUTCDate();
const gH = gregorianRef.getUTCHours();
const gMi = gregorianRef.getUTCMinutes();
const gS = gregorianRef.getUTCSeconds();
/**
* Replace the *first* occurrence of `from` in `str`.
* Tries the zero-padded form first (avoids false-positives on single-
* digit substrings), then the bare form at a numeric word boundary.
* The replacement preserves the padding style of the matched text.
*/
const replaceFirst = (str: string, from: number, to: number): string => {
const bare = String(from);
const padded = bare.padStart(2, "0");
const toBare = String(to);
const toPadded = toBare.padStart(2, "0");
if (padded !== bare) {
const idx = str.indexOf(padded);
if (idx !== -1)
return str.slice(0, idx) + toPadded + str.slice(idx + padded.length);
}
return str.replace(new RegExp(`(?<!\\d)${bare}(?!\\d)`), toBare);
};
let result = templateStr;
// 4-digit year is unambiguous — replace first
result = result.replace(String(gY), bYear);
// Month then day (month typically comes before day in numeric formats)
result = replaceFirst(result, gMo, parseInt(bMonth, 10));
result = replaceFirst(result, gD, parseInt(bDay, 10));
// Time components
const has12h = /\b(AM|PM)\b/i.test(templateStr);
if (has12h) {
const gH12 = gH % 12 || 12;
result = replaceFirst(result, gH12, lH12);
result = replaceFirst(result, gMi, lM);
result = replaceFirst(result, gS, lS);
const origAmpm = gH >= 12 ? "PM" : "AM";
result = result.replace(new RegExp(`\\b${origAmpm}\\b`, "i"), ampm);
} else {
result = replaceFirst(result, gH, lH);
result = replaceFirst(result, gMi, lM);
result = replaceFirst(result, gS, lS);
}
return result;
}
// 7. Fallback (no gregorianRef): heuristic from template-shape detection
const isTimeOnly = /\d{1,2}:\d{2}:\d{2}\s*(AM|PM)?/i.test(templateStr);
const isDateOnly =
/\d{1,2}\/\d{1,2}\/\d{4}/.test(templateStr) ||
/\d{1,2}-\d{1,2}-\d{4}/.test(templateStr);
const formattedDate = `${bMonth}/${bDay}/${bYear}`;
const formattedTime = `${lH12}:${String(lM).padStart(2, "0")}:${String(
lS
).padStart(2, "0")} ${ampm}`;
if (isDateOnly && isTimeOnly)
return `${formattedDate}, ${formattedTime} ${zoneLabel}`;
if (isDateOnly) return formattedDate;
if (isTimeOnly) return `${formattedTime} ${zoneLabel}`;
return `${formattedDate} ${formattedTime} ${zoneLabel}`;
}