@subrotosaha/bangla-date
Version:
A simple utility package for string manipulation in JavaScript.
1,352 lines (1,283 loc) • 87.4 kB
text/typescript
import { DateKit } from "@subrotosaha/datekit";
import { numberToNumber } from "../utils/index.js";
type Language = "en" | "bn" | "hi";
// ── Custom error hierarchy ────────────────────────────────────────────────────
/**
* Base error class for all BanglaDate failures.
*
* Extend this class (or catch it) to handle any error thrown by the library.
* All more-specific error types (`BanglaDateRangeError`, `BanglaDateParseError`)
* inherit from this class, so a single `catch (e)` block can test
* `e instanceof BanglaDateError` to distinguish library errors from unrelated ones.
*
* @example
* try {
* BanglaDate.parse('invalid');
* } catch (e) {
* if (e instanceof BanglaDateError) console.error('Library error:', e.message);
* }
*/
export class BanglaDateError extends Error {
/**
* @param message - Human-readable description of what went wrong.
*/
constructor(message: string) {
super(message);
this.name = "BanglaDateError";
Object.setPrototypeOf(this, new.target.prototype);
}
}
/**
* Thrown when a numeric argument (year, month, or day) falls outside its
* valid range.
*
* Common triggers:
* - Month not in 1–12 in `fromBanglaDate()` or `parse()`.
* - Day exceeding the maximum for the given month (e.g. day 32, or day 31 in
* a 30-day month such as Ashwin).
*
* @example
* try {
* BanglaDate.fromBanglaDate(1432, 13, 1); // month 13 doesn't exist
* } catch (e) {
* if (e instanceof BanglaDateRangeError) console.error(e.message);
* // "Month must be between 1 and 12, got 13."
* }
*/
export class BanglaDateRangeError extends BanglaDateError {
/**
* @param message - Human-readable description of what value was out of range.
*/
constructor(message: string) {
super(message);
this.name = "BanglaDateRangeError";
}
}
/**
* Thrown when a date string passed to `BanglaDate.parse()` cannot be
* interpreted as a valid Bangla date.
*
* Common triggers:
* - The string does not follow the required `"DD MonthName YYYY"` format.
* - The month name is not one of the 12 recognised English Bangla month names.
* - The day or year tokens are not valid integers.
*
* @example
* try {
* BanglaDate.parse('2025-04-14'); // wrong format
* } catch (e) {
* if (e instanceof BanglaDateParseError) console.error(e.message);
* // 'Expected "DD MonthName YYYY" (e.g. "15 Boishakh 1432").'
* }
*/
export class BanglaDateParseError extends BanglaDateError {
/**
* @param message - Human-readable description of why parsing failed.
*/
constructor(message: string) {
super(message);
this.name = "BanglaDateParseError";
}
}
// ─────────────────────────────────────────────────────────────────────────────
class BanglaDate {
private banglaYear: number;
private banglaMonthIndex: number;
private banglaDay: number;
private gregorianDate: Date;
private language: Language;
/**
* Creates a `BanglaDate` instance from a Gregorian `Date` object.
*
* The constructor converts the supplied Gregorian date to the corresponding
* Bangla calendar date following the **revised Bangladesh National Calendar**
* (established by the 1966 Reform Committee).
*
* - Pohela Boishakh (1 Boishakh) is fixed to **April 14 UTC** every year.
* - All date arithmetic is performed in **UTC** to avoid host-timezone drift.
* - Months 1–5 (Boishakh–Bhadra) contain 31 days; months 6–10 (Ashwin–Magh)
* contain 30 days; month 11 (Falgun) has 29 days in a common year and 30
* days in a Bangla leap year (when the following Gregorian year is a leap year);
* month 12 (Chaitra) always contains 30 days.
*
* @param gregorianDate - Any valid JavaScript `Date` object. Time-of-day
* components are preserved so that arithmetic and formatting methods can
* access the full timestamp.
* @param language - Output language for all text-returning methods.
* - `"en"` (default) — English (Latin digits, English month/weekday names)
* - `"bn"` — Bengali (Bengali digits ০–৯, Bengali names)
* - `"hi"` — Hindi (Devanagari digits ०–९, Hindi names)
*
* @example
* // English (default)
* const d = new BanglaDate(new Date('2025-04-14'), 'en');
* d.toString(); // "1 Boishakh 1432 BA"
*
* @example
* // Bengali output
* const d = new BanglaDate(new Date('2025-04-14'), 'bn');
* d.toString(); // "১ বৈশাখ ১৪৩২ বঙ্গাব্দ"
*/
constructor(gregorianDate: Date, language: Language = "en") {
this.language = language;
// Normalise to UTC midnight of the local calendar date so that strings like
// "Feb 24, 2026" or "Tue Feb 24 2026 06:00:00 GMT+0600" always produce
// 2026-02-24T00:00:00.000Z regardless of the host timezone.
const formatted = DateKit.formatFromTimezoneString(
gregorianDate,
"YYYY-MM-DD"
);
this.gregorianDate = new Date(`${formatted}T00:00:00+00:00`);
const gYear = this.gregorianDate.getUTCFullYear();
// Pohela Boishakh is fixed at April 14 UTC in the revised Bangladesh calendar
const pohelaBoishakh = new Date(Date.UTC(gYear, 3, 14));
const isBeforePohelaBoishakh =
this.gregorianDate.getTime() < pohelaBoishakh.getTime();
this.banglaYear = (isBeforePohelaBoishakh ? gYear - 1 : gYear) - 593;
const refYear = isBeforePohelaBoishakh ? gYear - 1 : gYear;
const refDate = new Date(Date.UTC(refYear, 3, 14));
const dayDiff = Math.floor(
(this.gregorianDate.getTime() - refDate.getTime()) / (1000 * 60 * 60 * 24)
);
// FIX: use shared helper so leap-year logic is consistent across constructor and parse()
const monthLengths = BanglaDate.getMonthLengths(refYear);
let remainingDays = dayDiff;
let monthIndex = 0;
// FIX: iterate only up to index 11 inside the loop to prevent overflow;
// the final month absorbs any leftover days (should never exceed 30).
while (monthIndex < 11 && remainingDays >= monthLengths[monthIndex]) {
remainingDays -= monthLengths[monthIndex];
monthIndex++;
}
this.banglaMonthIndex = monthIndex;
this.banglaDay = remainingDays + 1;
}
// ── Static entry-points ─────────────────────────────────────────────────
/**
* Creates a `BanglaDate` for the **current moment**, preserving the
* full time-of-day component of the underlying timestamp.
*
* This is the live equivalent of `new BanglaDate(new Date())`. Use it
* when you need hour/minute/second precision (e.g. in a clock, log
* timestamp, or countdown). If you only need the date portion (no time),
* prefer `today()` which normalises to midnight UTC.
*
* @param language - Output language for all text-returning methods.
* - `"en"` (default) — English
* - `"bn"` — Bengali
* - `"hi"` — Hindi
* @returns A new `BanglaDate` representing the current instant.
*
* @example
* const d = BanglaDate.now('bn');
* d.format('DD MMMM YYYY HH:mm:ss'); // e.g. "২৪ ফাল্গুন ১৪৩২ ১৪:৩০:০৫"
*/
static now(language: Language = "en"): BanglaDate {
return new BanglaDate(new Date(), language);
}
/**
* Parses a Bangla date string in the **`"DD MonthName YYYY"`** format and
* returns a new `BanglaDate` instance set to midnight UTC on that date.
*
* Parsing rules:
* - The three tokens must be separated by one or more whitespace characters.
* - `DD` — a positive integer day of month (e.g. `"1"` or `"15"`).
* - `MonthName` — one of the 12 English Bangla month names, case-insensitive
* (e.g. `"Boishakh"`, `"boishakh"`, `"BOISHAKH"`).
* - `YYYY` — a positive integer Bangla year (e.g. `"1432"`).
*
* Valid month names (case-insensitive):
* `Boishakh`, `Jyoishtho`, `Asharh`, `Shrabon`, `Bhadro`, `Ashwin`,
* `Kartik`, `Ogrohayon`, `Poush`, `Magh`, `Falgun`, `Chaitra`.
*
* @param dateString - A date string in `"DD MonthName YYYY"` format.
* @param language - Language for the returned instance's text output.
* Defaults to `"en"`.
* @returns A new `BanglaDate` at midnight UTC on the parsed Bangla date.
* @throws {BanglaDateParseError} When the string does not have exactly three
* whitespace-separated tokens, the month name is unrecognised, or the day
* or year tokens are not valid positive integers.
* @throws {BanglaDateRangeError} When the day exceeds the maximum for the
* given month (e.g. `"32 Boishakh 1432"` or `"31 Ashwin 1432"`).
*
* @example
* BanglaDate.parse('15 Boishakh 1432').toString();
* // "15 Boishakh 1432 BA"
*
* BanglaDate.parse('1 chaitra 1432', 'bn').toString();
* // "১ চৈত্র ১৪৩২ বঙ্গাব্দ"
*/
static parse(dateString: string, language: Language = "en"): BanglaDate {
const parts = dateString.trim().split(/\s+/);
if (parts.length !== 3) {
throw new BanglaDateParseError(
'Expected "DD MonthName YYYY" (e.g. "15 Boishakh 1432").'
);
}
const [day, monthName, yearWithBS] = parts;
const banglaMonthNames: Record<string, number> = {
Boishakh: 0,
Jyoishtho: 1,
Asharh: 2,
Shrabon: 3,
Bhadro: 4,
Ashwin: 5,
Kartik: 6,
Ogrohayon: 7,
Poush: 8,
Magh: 9,
Falgun: 10,
Chaitra: 11,
};
const normalised =
monthName.charAt(0).toUpperCase() + monthName.slice(1).toLowerCase();
const monthIndex = banglaMonthNames[normalised];
if (monthIndex === undefined) {
throw new BanglaDateParseError(
`Unknown month name: "${monthName}". Valid: ${Object.keys(
banglaMonthNames
).join(", ")}`
);
}
const dayOfMonth = parseInt(day, 10);
if (isNaN(dayOfMonth) || dayOfMonth < 1) {
throw new BanglaDateParseError(`Invalid day: "${day}".`);
}
const yearInBS = parseInt(yearWithBS, 10);
if (isNaN(yearInBS) || yearInBS < 1) {
throw new BanglaDateParseError(`Invalid Bangla year: "${yearWithBS}".`);
}
const gregorianYear = yearInBS + 593;
const monthLengths = BanglaDate.getMonthLengths(gregorianYear);
if (dayOfMonth > monthLengths[monthIndex]) {
throw new BanglaDateRangeError(
`Day ${dayOfMonth} out of range for ${monthName} ${yearInBS} (max ${monthLengths[monthIndex]}).`
);
}
const pohelaBoishakh = new Date(Date.UTC(gregorianYear, 3, 14));
let dayOffset = 0;
for (let i = 0; i < monthIndex; i++) dayOffset += monthLengths[i];
dayOffset += dayOfMonth - 1;
const gDate = new Date(pohelaBoishakh.getTime() + dayOffset * 86_400_000);
return new BanglaDate(gDate, language);
}
/**
* Returns `true` when the given **Gregorian** year is a leap year.
*
* Uses the standard proleptic Gregorian rule:
* - Divisible by 4 → leap, **except** centuries (÷100) which are
* only leap if also divisible by 400.
*
* > **Note:** Pass the Gregorian reference year, *not* the Bangla year.
* > Bangla leap-year determination (see `getMonthLengths`) tests the year
* > *following* the reference year because Falgun (month 11) falls in
* > mid-February to mid-March of that next Gregorian year.
*
* @param year - A full Gregorian year (e.g. `2024`).
* @returns `true` if `year` is a Gregorian leap year, `false` otherwise.
*
* @example
* BanglaDate.isLeapYear(2024); // true
* BanglaDate.isLeapYear(1900); // false (century, not ÷400)
* BanglaDate.isLeapYear(2000); // true (divisible by 400)
*/
static isLeapYear(year: number): boolean {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}
/**
* Returns a 12-element array of day-counts for the Bangla year whose
* **Pohela Boishakh** (1 Boishakh) falls on April 14 of `gregorianRefYear`.
*
* Per the Bangladesh National Calendar:
* - Months 1–5 (Boishakh–Bhadra) → **31 days** each = 155
* - Months 6–10 (Ashwin–Magh) → **30 days** each = 150
* - Month 11 (Falgun) → 29 days (common) or 30 (Bangla leap year)
* - Month 12 (Chaitra) → **30 days** always
*
* **Leap year rule:** The Bangla year straddles two Gregorian years.
* Falgun (month 11) falls in mid-February to mid-March of
* `gregorianRefYear + 1`, so the leap-year test is applied to
* `gregorianRefYear + 1`, not `gregorianRefYear` itself.
*
* Totals: 364 (common) / 365 (leap).
*
* @param gregorianRefYear - The Gregorian year in which Pohela Boishakh
* (April 14) of the target Bangla year falls (e.g. pass `2025` for
* Bangla year 1432).
* @returns An array `[31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29|30, 30]`
* indexed 0 (Boishakh) through 11 (Chaitra).
*
* @example
* BanglaDate.getMonthLengths(2024);
* // [31,31,31,31,31,30,30,30,30,30,30,30] — 2025 is leap → Falgun=30
*
* BanglaDate.getMonthLengths(2025);
* // [31,31,31,31,31,30,30,30,30,30,29,30] — 2026 is not leap → Falgun=29
*/
static getMonthLengths(gregorianRefYear: number): number[] {
const isLeap = BanglaDate.isLeapYear(gregorianRefYear + 1);
return [
31,
31,
31,
31,
31, // Boishakh–Bhadra = 155
30,
30,
30,
30,
30, // Ashwin–Magh = 150
isLeap ? 30 : 29, // Falgun = 29 or 30
30, // Chaitra = 30
];
}
// ── Factory Methods ──────────────────────────────────────────────────────
/**
* Creates a `BanglaDate` at **midnight UTC** for today's date.
*
* Time components are zeroed (`00:00:00.000 UTC`), making this suitable
* for date-only comparisons, calendar rendering, and any use case where
* the time of day is irrelevant. For the current instant with time
* preserved, use `now()` instead.
*
* @param language - Output language for all text-returning methods.
* - `"en"` (default) — English
* - `"bn"` — Bengali
* - `"hi"` — Hindi
* @returns A new `BanglaDate` representing today at `00:00:00.000 UTC`.
*
* @example
* BanglaDate.today('en').toString();
* // e.g. "24 Falgun 1432 BA"
*
* BanglaDate.today('bn').format('WWWW, DD MMMM YYYY');
* // e.g. "রবিবার, ২৪ ফাল্গুন ১৪৩২"
*/
static today(language: Language = "en"): BanglaDate {
const d = new Date();
return new BanglaDate(
new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())),
language
);
}
/**
* Creates a `BanglaDate` from explicit Bangla calendar components, set to
* midnight UTC on that date.
*
* This is the primary factory for constructing a `BanglaDate` when you
* already know the Bangla year, month, and day (e.g. from user input or
* a stored Bangla date string). Internally it converts the components to a
* Gregorian timestamp by offsetting from Pohela Boishakh (April 14 UTC).
*
* @param year - Full Bangla year (e.g. `1432`). Any positive integer.
* @param month - 1-indexed Bangla month number:
* `1`=Boishakh, `2`=Jyoishtho, `3`=Asharh, `4`=Shrabon, `5`=Bhadro,
* `6`=Ashwin, `7`=Kartik, `8`=Ogrohayon, `9`=Poush, `10`=Magh,
* `11`=Falgun, `12`=Chaitra.
* @param day - Day of the month (1-based). Must not exceed the length of
* the given month in the given year (see `getMonthLengths`).
* @param language - Output language for all text-returning methods.
* Defaults to `"en"`.
* @returns A new `BanglaDate` at midnight UTC on the specified Bangla date.
* @throws {BanglaDateRangeError} If `month` is not 1–12, `day` is less than
* 1, or `day` exceeds the maximum day count for the given month.
*
* @example
* BanglaDate.fromBanglaDate(1432, 1, 1, 'en').toString();
* // "1 Boishakh 1432 BA" (= 2025-04-14 UTC)
*
* BanglaDate.fromBanglaDate(1432, 12, 30, 'bn').toString();
* // "৩০ চৈত্র ১৪৩২ বঙ্গাব্দ"
*/
static fromBanglaDate(
year: number,
month: number,
day: number,
language: Language = "en"
): BanglaDate {
if (month < 1 || month > 12) {
throw new BanglaDateRangeError(
`Month must be between 1 and 12, got ${month}.`
);
}
if (day < 1) {
throw new BanglaDateRangeError(`Day must be at least 1, got ${day}.`);
}
const gregorianYear = year + 593;
const monthLengths = BanglaDate.getMonthLengths(gregorianYear);
const maxDay = monthLengths[month - 1];
if (day > maxDay) {
throw new BanglaDateRangeError(
`Day ${day} exceeds the ${maxDay}-day length of month ${month}.`
);
}
const pohelaBoishakh = new Date(Date.UTC(gregorianYear, 3, 14));
let dayOffset = 0;
for (let i = 0; i < month - 1; i++) {
dayOffset += monthLengths[i];
}
dayOffset += day - 1;
const gDate = new Date(pohelaBoishakh.getTime() + dayOffset * 86_400_000);
return new BanglaDate(gDate, language);
}
/**
* Returns `true` when the given Bangla calendar components form a valid date.
*
* Validation checks (all must pass):
* 1. `year`, `month`, and `day` must all be integers
* (`Number.isInteger` is `true`).
* 2. `month` must be in the range 1–12.
* 3. `day` must be at least 1.
* 4. `day` must not exceed the maximum day count for `month` in `year`
* (accounts for Bangla leap years where Chaitra = 31 days).
*
* This is a pure validation helper — it does **not** create a `BanglaDate`
* instance. Use it to guard user input before calling `fromBanglaDate()`.
*
* @param year - Bangla year to validate (e.g. `1432`).
* @param month - 1-indexed Bangla month (1 = Boishakh … 12 = Chaitra).
* @param day - Day of the month.
* @returns `true` if the combination is a valid Bangla date, `false` otherwise.
*
* @example
* BanglaDate.isValidBanglaDate(1432, 1, 31); // true (Boishakh has 31 days)
* BanglaDate.isValidBanglaDate(1432, 6, 31); // false (Ashwin has only 30 days)
* BanglaDate.isValidBanglaDate(1432, 0, 1); // false (month 0 is invalid)
* BanglaDate.isValidBanglaDate(1432, 1, 1.5); // false (day is not an integer)
*/
static isValidBanglaDate(year: number, month: number, day: number): boolean {
if (
!Number.isInteger(year) ||
!Number.isInteger(month) ||
!Number.isInteger(day)
)
return false;
if (month < 1 || month > 12 || day < 1) return false;
const monthLengths = BanglaDate.getMonthLengths(year + 593);
return day <= monthLengths[month - 1];
}
// ── Validity ─────────────────────────────────────────────────────────────
/**
* Returns `true` when this instance represents a valid Bangla date.
*
* An instance can be invalid if:
* - The underlying Gregorian `Date` has an invalid timestamp (`NaN`).
* - The computed Bangla month index is outside 0–11 (should not occur
* under normal construction, but may arise from manual manipulation).
* - The computed Bangla day is less than 1 or greater than the maximum
* day count for the month (e.g. > 30 for Ashwin, or > 30/31 for Chaitra).
*
* @returns `true` if this date is valid, `false` otherwise.
*
* @example
* new BanglaDate(new Date('invalid')).isValid(); // false
* BanglaDate.fromBanglaDate(1432, 1, 15).isValid(); // true
*/
isValid(): boolean {
if (isNaN(this.gregorianDate.getTime())) return false;
if (this.banglaMonthIndex < 0 || this.banglaMonthIndex > 11) return false;
const maxDay = BanglaDate.getMonthLengths(this.banglaYear + 593)[
this.banglaMonthIndex
];
return this.banglaDay >= 1 && this.banglaDay <= maxDay;
}
// ── Comparison Methods ───────────────────────────────────────────────────
/**
* Returns `true` when this date is **strictly before** `other`.
*
* Comparison is performed on the underlying millisecond timestamps
* (UTC), so time-of-day is taken into account. If you want a
* date-only comparison, call `isBefore` on `today()`-normalised
* instances, or use `isSame`/`diff` with `granularity: 'day'`.
*
* @param other - The `BanglaDate` to compare against.
* @returns `true` if `this` is earlier than `other`, `false` otherwise
* (including when they are equal).
*
* @example
* const a = BanglaDate.fromBanglaDate(1432, 1, 1);
* const b = BanglaDate.fromBanglaDate(1432, 6, 1);
* a.isBefore(b); // true
* b.isBefore(a); // false
* a.isBefore(a); // false (equal, not strictly before)
*/
isBefore(other: BanglaDate): boolean {
return this.gregorianDate.getTime() < other.gregorianDate.getTime();
}
/**
* Returns `true` when this date is **strictly after** `other`.
*
* Comparison is performed on the underlying millisecond timestamps
* (UTC), so time-of-day is taken into account.
*
* @param other - The `BanglaDate` to compare against.
* @returns `true` if `this` is later than `other`, `false` otherwise
* (including when they are equal).
*
* @example
* const a = BanglaDate.fromBanglaDate(1432, 1, 1);
* const b = BanglaDate.fromBanglaDate(1432, 6, 1);
* b.isAfter(a); // true
* a.isAfter(b); // false
* a.isAfter(a); // false (equal, not strictly after)
*/
isAfter(other: BanglaDate): boolean {
return this.gregorianDate.getTime() > other.gregorianDate.getTime();
}
/**
* Returns `true` when this date and `other` are equal at the given
* granularity level.
*
* - `"year"` — only the Bangla year must match.
* - `"month"` — both the Bangla year **and** month index must match.
* - `"day"` — year, month, **and** day must all match (default).
*
* Note: the comparison is on Bangla calendar fields, not on raw timestamps.
* Two instances created from different times on the same Bangla calendar day
* will still return `true` for `isSame(other, 'day')`.
*
* @param other - The `BanglaDate` to compare against.
* @param granularity - How precisely to compare. Defaults to `'day'`.
* @returns `true` if the dates are equal at the specified granularity.
*
* @example
* const a = BanglaDate.fromBanglaDate(1432, 3, 10);
* const b = BanglaDate.fromBanglaDate(1432, 3, 25);
* const c = BanglaDate.fromBanglaDate(1433, 3, 10);
*
* a.isSame(b, 'year'); // true (same year 1432)
* a.isSame(b, 'month'); // true (same year + month)
* a.isSame(b, 'day'); // false (different days)
* a.isSame(c, 'year'); // false (different years)
*/
isSame(
other: BanglaDate,
granularity: "day" | "month" | "year" = "day"
): boolean {
if (granularity === "year") return this.banglaYear === other.banglaYear;
if (granularity === "month")
return (
this.banglaYear === other.banglaYear &&
this.banglaMonthIndex === other.banglaMonthIndex
);
return (
this.banglaYear === other.banglaYear &&
this.banglaMonthIndex === other.banglaMonthIndex &&
this.banglaDay === other.banglaDay
);
}
// ── Arithmetic Methods ───────────────────────────────────────────────────
/**
* Returns a **new** `BanglaDate` shifted forward by `amount` of the given
* `unit`. This instance is **not** mutated (immutable operation).
*
* Pass a negative `amount` to shift into the past. Alternatively use
* `subtract()` for a more readable API.
*
* **Day addition** is exact: it simply adds `amount × 86 400 000 ms` to
* the underlying timestamp, so the time-of-day is always preserved.
*
* **Month / year addition** computes the new Bangla calendar position and
* then re-applies the original time-of-day offset. When the current day
* exceeds the maximum day count of the target month, it is **clamped** to
* that month's last day (e.g. 31 Boishakh + 5 months = 30 Ashwin, because
* Ashwin has only 30 days).
*
* @param amount - Number of units to add. Negative values subtract.
* @param unit - Unit of addition. Defaults to `"days"`.
* - `"days"` — calendar days (exact millisecond arithmetic)
* - `"months"` — Bangla calendar months; handles year roll-over automatically
* - `"years"` — Bangla calendar years
* @returns A new `BanglaDate` shifted by the specified amount.
*
* @example
* const d = BanglaDate.fromBanglaDate(1432, 1, 15);
* d.add(10, 'days').toString(); // "25 Boishakh 1432 BA"
* d.add(2, 'months').toString(); // "15 Asharh 1432 BA"
* d.add(1, 'years').toString(); // "15 Boishakh 1433 BA"
* d.add(-3, 'days').toString(); // "12 Boishakh 1432 BA"
*/
add(amount: number, unit: "days" | "months" | "years" = "days"): BanglaDate {
if (unit === "days") {
return new BanglaDate(
new Date(this.gregorianDate.getTime() + amount * 86_400_000),
this.language
);
}
// For months/years: compute the new date-only position via fromBanglaDate,
// then re-apply the original time-of-day so it is preserved.
let newYear: number;
let newMonthIndex: number;
if (unit === "months") {
newMonthIndex = this.banglaMonthIndex + amount;
newYear = this.banglaYear + Math.floor(newMonthIndex / 12);
newMonthIndex = ((newMonthIndex % 12) + 12) % 12;
} else {
newYear = this.banglaYear + amount;
newMonthIndex = this.banglaMonthIndex;
}
const maxDay = BanglaDate.getMonthLengths(newYear + 593)[newMonthIndex];
const dateMidnight = BanglaDate.fromBanglaDate(
newYear,
newMonthIndex + 1,
Math.min(this.banglaDay, maxDay),
this.language
);
// Time-of-day offset in ms from the start of the UTC day
const timeOfDay =
this.gregorianDate.getTime() -
Date.UTC(
this.gregorianDate.getUTCFullYear(),
this.gregorianDate.getUTCMonth(),
this.gregorianDate.getUTCDate()
);
return new BanglaDate(
new Date(dateMidnight.gregorianDate.getTime() + timeOfDay),
this.language
);
}
/**
* Returns a **new** `BanglaDate` shifted **backward** by `amount` of the
* given `unit`. This instance is **not** mutated.
*
* This is a convenience alias for `add(-amount, unit)`. All behaviour
* (day clamping, time-of-day preservation, year roll-over) is identical
* to `add()`.
*
* @param amount - Number of units to subtract (positive number = go back).
* @param unit - Unit of subtraction. Defaults to `"days"`.
* - `"days"` — calendar days
* - `"months"` — Bangla calendar months
* - `"years"` — Bangla calendar years
* @returns A new `BanglaDate` shifted backward by the specified amount.
*
* @example
* const d = BanglaDate.fromBanglaDate(1432, 6, 15);
* d.subtract(1, 'months').toString(); // "15 Bhadro 1432 BA"
* d.subtract(7, 'days').toString(); // "8 Ashwin 1432 BA"
* d.subtract(1, 'years').toString(); // "15 Ashwin 1431 BA"
*/
subtract(
amount: number,
unit: "days" | "months" | "years" = "days"
): BanglaDate {
return this.add(-amount, unit);
}
/**
* Calculates the **signed** difference between this date and `other`.
*
* - A **positive** result means `this` is **after** `other`.
* - A **negative** result means `this` is **before** `other`.
* - Zero means the dates are equal at the requested unit.
*
* **`"days"`** — divides the raw millisecond difference by 86 400 000 and
* floors the result, so partial days are truncated.
*
* **`"months"`** — counts completed Bangla calendar months. The result is
* adjusted so that a partial trailing month is not counted (e.g. Jan 31 →
* Feb 28 = 0 full months, not 1).
*
* **`"years"`** — counts completed Bangla calendar years, using the
* month and day to decide whether the anniversary has been passed.
*
* @param other - The reference `BanglaDate` to subtract from `this`.
* @param unit - Unit for the result. Defaults to `"days"`.
* @returns Signed integer difference in the requested unit.
*
* @example
* const start = BanglaDate.fromBanglaDate(1432, 1, 1);
* const end = BanglaDate.fromBanglaDate(1432, 3, 15);
*
* end.diff(start, 'days'); // 76
* end.diff(start, 'months'); // 2
* start.diff(end, 'days'); // -76 (negative — start is before end)
*
* const a = BanglaDate.fromBanglaDate(1430, 5, 20);
* const b = BanglaDate.fromBanglaDate(1432, 3, 10);
* b.diff(a, 'years'); // 1 (only 1 full year has elapsed)
*/
diff(other: BanglaDate, unit: "days" | "months" | "years" = "days"): number {
if (unit === "days") {
return Math.floor(
(this.gregorianDate.getTime() - other.gregorianDate.getTime()) /
86_400_000
);
}
if (unit === "months") {
let months =
(this.banglaYear - other.banglaYear) * 12 +
(this.banglaMonthIndex - other.banglaMonthIndex);
// Sign-aware day-of-month adjustment: only trim when the incomplete
// month lies in the same direction as the overall sign.
if (months > 0 && this.banglaDay < other.banglaDay) months--;
else if (months < 0 && this.banglaDay > other.banglaDay) months++;
return months;
}
// years — respect month + day offset, sign-aware
let years = this.banglaYear - other.banglaYear;
if (years > 0) {
if (
this.banglaMonthIndex < other.banglaMonthIndex ||
(this.banglaMonthIndex === other.banglaMonthIndex &&
this.banglaDay < other.banglaDay)
)
years--;
} else if (years < 0) {
if (
this.banglaMonthIndex > other.banglaMonthIndex ||
(this.banglaMonthIndex === other.banglaMonthIndex &&
this.banglaDay > other.banglaDay)
)
years++;
}
return years;
}
// ── Start / End Helpers ──────────────────────────────────────────────────
/**
* Returns a **new** `BanglaDate` set to the **first day** of the current
* Bangla month, at midnight UTC. This instance is not mutated.
*
* Equivalent to `BanglaDate.fromBanglaDate(year, month, 1)`.
*
* @returns A new `BanglaDate` at day 1 of this month.
*
* @example
* BanglaDate.fromBanglaDate(1432, 3, 20).startOfMonth().toString();
* // "1 Asharh 1432 BA"
*/
startOfMonth(): BanglaDate {
return BanglaDate.fromBanglaDate(
this.banglaYear,
this.banglaMonthIndex + 1,
1,
this.language
);
}
/**
* Returns a **new** `BanglaDate` set to the **last day** of the current
* Bangla month, at midnight UTC. This instance is not mutated.
*
* The last day is determined by `getMonthLengths()` and correctly handles
* Bangla leap years (Chaitra = 30 or 31 days).
*
* @returns A new `BanglaDate` at the last day of this month.
*
* @example
* BanglaDate.fromBanglaDate(1432, 1, 5).endOfMonth().toString();
* // "31 Boishakh 1432 BA" (Boishakh always has 31 days)
*
* BanglaDate.fromBanglaDate(1432, 6, 1).endOfMonth().toString();
* // "30 Ashwin 1432 BA" (Ashwin has 30 days)
*/
endOfMonth(): BanglaDate {
const monthLengths = BanglaDate.getMonthLengths(this.banglaYear + 593);
const lastDay = monthLengths[this.banglaMonthIndex];
return BanglaDate.fromBanglaDate(
this.banglaYear,
this.banglaMonthIndex + 1,
lastDay,
this.language
);
}
/**
* Returns a **new** `BanglaDate` set to **1 Boishakh** (the first day of
* the Bangla year) at midnight UTC. This instance is not mutated.
*
* @returns A new `BanglaDate` at 1 Boishakh of this Bangla year.
*
* @example
* BanglaDate.fromBanglaDate(1432, 8, 15).startOfYear().toString();
* // "1 Boishakh 1432 BA" (= 2025-04-14 UTC)
*/
startOfYear(): BanglaDate {
return BanglaDate.fromBanglaDate(this.banglaYear, 1, 1, this.language);
}
/**
* Returns a **new** `BanglaDate` set to the **last day of Chaitra** (the
* final day of the Bangla year) at midnight UTC. This instance is not mutated.
*
* Chaitra has 30 days in a common year and 31 days in a Bangla leap year
* (when the following Gregorian year is a leap year).
*
* @returns A new `BanglaDate` at 30 or 31 Chaitra of this Bangla year.
*
* @example
* BanglaDate.fromBanglaDate(1432, 3, 1).endOfYear().toString();
* // "30 Chaitra 1432 BA" (2026 is not a Gregorian leap year)
*/
endOfYear(): BanglaDate {
const monthLengths = BanglaDate.getMonthLengths(this.banglaYear + 593);
return BanglaDate.fromBanglaDate(
this.banglaYear,
12,
monthLengths[11],
this.language
);
}
// ── Calendar Info ────────────────────────────────────────────────────────
/**
* Returns the **1-based day number** within the Bangla year.
*
* Day 1 = 1 Boishakh. The maximum value is 365 in a common year and
* 366 in a Bangla leap year (when Chaitra has 31 days).
*
* @returns Integer in the range 1–366 indicating the ordinal day of the year.
*
* @example
* BanglaDate.fromBanglaDate(1432, 1, 1).getDayOfYear(); // 1
* BanglaDate.fromBanglaDate(1432, 2, 1).getDayOfYear(); // 32 (after 31-day Boishakh)
* BanglaDate.fromBanglaDate(1432, 6, 1).getDayOfYear(); // 156 (after 5×31 days)
*/
getDayOfYear(): number {
const monthLengths = BanglaDate.getMonthLengths(this.banglaYear + 593);
let doy = 0;
for (let i = 0; i < this.banglaMonthIndex; i++) {
doy += monthLengths[i];
}
return doy + this.banglaDay;
}
/**
* Returns the **Bengali season (Ritu)** name for the current month in
* the instance's language.
*
* The Bengali calendar divides the year into six two-month seasons:
*
* | Months | en | bn | hi |
* |-------------------------|----------|--------|----------|
* | Boishakh – Jyoishtho | Grishmo | গ্রীষ্ম | ग्रीष्म |
* | Asharh – Shrabon | Borsha | বর্ষা | वर्षा |
* | Bhadro – Ashwin | Shorot | শরৎ | शरद |
* | Kartik – Ogrohayon | Hemonto | হেমন্ত | हेमन्त |
* | Poush – Magh | Sheet | শীত | शीत |
* | Falgun – Chaitra | Boshonto | বসন্ত | वसन्त |
*
* @returns The season name in the instance's language.
*
* @example
* BanglaDate.fromBanglaDate(1432, 1, 1, 'en').getRitu(); // "Grishmo"
* BanglaDate.fromBanglaDate(1432, 1, 1, 'bn').getRitu(); // "গ্রীষ্ম"
* BanglaDate.fromBanglaDate(1432, 9, 1, 'en').getRitu(); // "Sheet"
*/
getRitu(): string {
const ritu: Record<Language, string[]> = {
en: ["Grishmo", "Borsha", "Shorot", "Hemonto", "Sheet", "Boshonto"],
bn: ["গ্রীষ্ম", "বর্ষা", "শরৎ", "হেমন্ত", "শীত", "বসন্ত"],
hi: ["ग्रीष्म", "वर्षा", "शरद", "हेमन्त", "शीत", "वसन्त"],
};
return ritu[this.language][Math.floor(this.banglaMonthIndex / 2)];
}
/**
* Returns the **era label** for the Bangla calendar in the instance's language.
*
* The Bangla era is called *Bangabda* (বঙ্গাব্দ). Its epoch is traditionally
* placed at 594 CE. This label should not be confused with the Gregorian
* BC/AD era used by `Intl.DateTimeFormat`.
*
* | Language | Era label |
* |----------|-------------|
* | `en` | `"BA"` |
* | `bn` | `"বঙ্গাব্দ"` |
* | `hi` | `"बंगाब्द"` |
*
* @returns The era label string in the instance's language.
*
* @example
* BanglaDate.today('en').getEra(); // "BA"
* BanglaDate.today('bn').getEra(); // "বঙ্গাব্দ"
* BanglaDate.today('hi').getEra(); // "बंगाब्द"
*/
getEra(): string {
switch (this.language) {
case "bn":
return "বঙ্গাব্দ";
case "hi":
return "बंगाब्द";
default:
return "BA"; // Bangabda
}
}
/**
* Returns the name of a **major Bengali festival** that falls on this date,
* or an empty string if no recognised festival occurs today.
*
* Festival dates follow the **revised Bangladesh National Calendar**:
*
* | Bangla date | en |
* |---------------------------|---------------------------------|
* | 1 Boishakh | Pohela Boishakh (Bengali New Year) |
* | 25 Boishakh | Rabindra Jayanti |
* | 11 Jyoishtho | Nazrul Jayanti |
* | Last day of Chaitra | Chaitra Sangkranti (Year end) |
*
* The returned string is in the instance's language (`en` / `bn` / `hi`).
* Chaitra Sangkranti is computed dynamically (30th or 31st Chaitra
* depending on whether it is a Bangla leap year).
*
* @returns The festival name in the instance's language, or `""` if none.
*
* @example
* BanglaDate.fromBanglaDate(1432, 1, 1, 'en').getFestival();
* // "Pohela Boishakh (Bengali New Year)"
*
* BanglaDate.fromBanglaDate(1432, 1, 1, 'bn').getFestival();
* // "পহেলা বৈশাখ (নববর্ষ)"
*
* BanglaDate.fromBanglaDate(1432, 3, 1, 'en').getFestival();
* // "" (no festival on 1 Asharh)
*/
getFestival(): string {
// Key: "monthIndex-day" (0-indexed month)
const festivals: Record<string, Record<Language, string>> = {
"0-1": {
en: "Pohela Boishakh (Bengali New Year)",
bn: "পহেলা বৈশাখ (নববর্ষ)",
hi: "पहला बैशाख (नव वर्ष)",
},
"0-25": {
en: "Rabindra Jayanti",
bn: "রবীন্দ্র জয়ন্তী",
hi: "रवींद्र जयंति",
},
"1-11": {
en: "Nazrul Jayanti",
bn: "নজরুল জয়ন্তী",
hi: "नजरुल जयंति",
},
};
// Chaitra Sangkranti is always the LAST day of Chaitra (30 in regular
// years, 31 in Bangla leap years), so compute it dynamically.
const chaitraLastDay = BanglaDate.getMonthLengths(
this.banglaYear + 593
)[11];
festivals[`11-${chaitraLastDay}`] = {
en: "Chaitra Sangkranti (Year end)",
bn: "চৈত্র সংক্রান্তি",
hi: "चैत्र संक्रांति",
};
const key = `${this.banglaMonthIndex}-${this.banglaDay}`;
return festivals[key]?.[this.language] ?? "";
}
/**
* Returns a **human-readable relative time string** comparing this date to
* today (midnight UTC), in the instance's language.
*
* **Thresholds:**
* - 0 days difference → `"today"` / `"আজ"` / `"आज"`
* - 1 day difference → `"yesterday"`/`"tomorrow"` (language-aware)
* - < 30 days → `"N days ago"` / `"in N days"`
* - 30+ days but < 12 months → `"N months ago"` / `"in N months"`
* (falls back to days if no full Bangla month boundary has been crossed)
* - 12+ months → `"N years ago"` / `"in N years"`
*
* @returns A localised relative time string in the instance's language.
*
* @example
* // Assuming today is 24 Falgun 1432
* BanglaDate.fromBanglaDate(1432, 2, 24).relativeTime(); // "in 1 month"
* BanglaDate.fromBanglaDate(1432, 2, 23).relativeTime(); // "1 month ago" (approx)
* BanglaDate.today('bn').relativeTime(); // "আজ"
* BanglaDate.today().add(1, 'days').relativeTime(); // "tomorrow"
* BanglaDate.today().subtract(1, 'days').relativeTime(); // "yesterday"
* BanglaDate.fromBanglaDate(1430, 1, 1).relativeTime(); // "2 years ago"
*/
relativeTime(): string {
const today = BanglaDate.today();
const diffDays = today.diff(this, "days");
const absDiff = Math.abs(diffDays);
const isFuture = diffDays < 0;
const fmt = (value: number, unit: "day" | "month" | "year"): string => {
const vStr = numberToNumber(value, this.language);
if (this.language === "bn") {
if (unit === "day") {
if (absDiff === 0) return "আজ";
if (absDiff === 1) return isFuture ? "আগামীকাল" : "গতকাল";
return isFuture ? `${vStr} দিন পরে` : `${vStr} দিন আগে`;
}
if (unit === "month")
return isFuture ? `${vStr} মাস পরে` : `${vStr} মাস আগে`;
return isFuture ? `${vStr} বছর পরে` : `${vStr} বছর আগে`;
}
if (this.language === "hi") {
if (unit === "day") {
if (absDiff === 0) return "आज";
if (absDiff === 1) return isFuture ? "आने वाला कल" : "बीता हुआ कल";
return isFuture ? `${vStr} दिन बाद` : `${vStr} दिन पहले`;
}
if (unit === "month")
return isFuture ? `${vStr} महिने बाद` : `${vStr} महिने पहले`;
return isFuture ? `${vStr} साल बाद` : `${vStr} साल पहले`;
}
// English
if (unit === "day") {
if (absDiff === 0) return "today";
if (absDiff === 1) return isFuture ? "tomorrow" : "yesterday";
return isFuture ? `in ${value} days` : `${value} days ago`;
}
if (unit === "month")
return isFuture
? `in ${value} month${value === 1 ? "" : "s"}`
: `${value} month${value === 1 ? "" : "s"} ago`;
return isFuture
? `in ${value} year${value === 1 ? "" : "s"}`
: `${value} year${value === 1 ? "" : "s"} ago`;
};
if (absDiff < 30) return fmt(absDiff, "day");
const monthDiff = Math.abs(today.diff(this, "months"));
// monthDiff can be 0 when ≥30 days have passed but the Bangla month
// boundary hasn't been crossed yet — fall back to displaying days.
if (monthDiff === 0) return fmt(absDiff, "day");
if (monthDiff < 12) return fmt(monthDiff, "month");
return fmt(Math.abs(today.diff(this, "years")), "year");
}
// ── Utility / conversion ─────────────────────────────────────────────────
/**
* Returns a **defensive copy** of the underlying Gregorian `Date` object.
*
* Modifying the returned `Date` does not affect this `BanglaDate` instance.
* Use this when you need to pass the date to APIs that expect a native `Date`,
* such as `Date.prototype.toISOString()` or third-party date libraries.
*
* @returns A new `Date` instance with the same Unix timestamp as this
* `BanglaDate`.
*
* @example
* const d = BanglaDate.fromBanglaDate(1432, 1, 1);
* d.toGregorian().toISOString(); // "2025-04-14T00:00:00.000Z"
*/
toGregorian(): Date {
return new Date(this.gregorianDate.getTime());
}
/**
* Returns a **plain-object snapshot** of this date suitable for
* `JSON.stringify` serialisation.
*
* All numeric fields are raw integers (not localised digit strings).
* The `era` field is the language-aware era label from `getEra()`.
*
* Shape:
* ```json
* {
* "year": 1432,
* "month": 1,
* "day": 5,
* "era": "BA",
* "hours": 6,
* "minutes": 30,
* "seconds": 0,
* "milliseconds": 0
* }
* ```
*
* @returns A plain object representation of this `BanglaDate`.
*
* @example
* const d = BanglaDate.fromBanglaDate(1432, 1, 5, 'bn');
* JSON.stringify(d);
* // '{"year":1432,"month":1,"day":5,"era":"বঙ্গাব্দ","hours":0,...}'
*/
toJSON(): {
year: number;
month: number;
day: number;
era: string;
hours: number;
minutes: number;
seconds: number;
milliseconds: number;
} {
return {
year: this.banglaYear,
month: this.banglaMonthIndex + 1,
day: this.banglaDay,
era: this.getEra(),
hours: this.getHours(),
minutes: this.getMinutes(),
seconds: this.getSeconds(),
milliseconds: this.getMilliseconds(),
};
}
/**
* Returns a **new, independent** `BanglaDate` that is an exact copy of
* this instance — same Gregorian timestamp, same language setting.
*
* Use this before passing a `BanglaDate` to code that may mutate it, or
* when you need to branch two date calculations from the same starting point.
*
* @returns A new `BanglaDate` identical to `this`.
*
* @example
* const a = BanglaDate.fromBanglaDate(1432, 1, 15, 'bn');
* const b = a.clone();
* b.add(10, 'days'); // does not affect `a` (both are immutable anyway)
* a.isSame(b); // true — same date
*/
clone(): BanglaDate {
return new BanglaDate(this.gregorianDate, this.language);
}
/**
* Allows the instance to be used directly in **numeric and string contexts**
* without an explicit method call.
*
* - **Numeric hint** (e.g. `+d`, `d - other`, comparison operators) →
* returns the Unix timestamp in milliseconds (same as `getTime()`).
* - **String hint** (e.g. template literals `` `${d}` ``, string
* concatenation) → returns the `toString()` value.
* - **Default hint** → returns the `toString()` value.
*
* @param hint - The type hint provided by the JS engine: `"number"`,
* `"string"`, or `"default"`.
* @returns The Unix timestamp (ms) for numeric hint, otherwise the
* localised date string from `toString()`.
*
* @example
* const d = BanglaDate.fromBanglaDate(1432, 1, 1, 'bn');
* +d; // 1744588800000 (Unix ms timestamp)
* `Date: ${d}`; // "Date: ১ বৈশাখ ১৪৩২ বঙ্গাব্দ"
*/
[Symbol.toPrimitive](hint: string): string | number {
return hint === "number" ? this.getTime() : this.toString();
}
// ── String Representations ───────────────────────────────────────────────
/**
* Returns the **full localised date string** including the era label.
*
* Format: `"<day> <monthName> <year> <era>"`
*
* All digit characters are localised to the instance's language:
* - `en` → Latin digits, English month names: `"15 Boishakh 1432 BA"`
* - `bn` → Bengali digits, Bengali month names: `"১৫ বৈশাখ ১৪৩২ বঙ্গাব্দ"`
* - `hi` → Devanagari digits, Hindi month names: `"१५ बैशाख १४३२ बंगाब्द"`
*
* This method is called automatically when the instance is coerced to a
* string (e.g. in template literals or string concatenation).
*
* @returns Localised date string in `"DD MMMM YYYY era"` format.
*
* @example
* BanglaDate.fromBanglaDate(1432, 1, 15, 'en').toString();
* // "15 Boishakh 1432 BA"
*
* BanglaDate.fromBanglaDate(1432, 1, 15, 'bn').toString();
* // "১৫ বৈশাখ ১৪৩২ বঙ্গাব্দ"
*/
toString(): string {
const monthName = this.getMonthName();
const day = numberToNumber(this.banglaDay, this.language);
const year = numberToNumber(this.banglaYear, this.language);
return `${day} ${monthName} ${year} ${this.getEra()}`;
}
/**
* Returns a **short date string with the weekday** name, without the era.
*
* Format: `"<weekdayFull>, <day> <monthFull> <year>"`
*
* The weekday and month names are in the instance's language. Digits are
* localised. The year is the full 4-digit Bangla year.
*
* @returns Localised date string in `"WWWW, DD MMMM YYYY"` format.
*
* @example
* BanglaDate.fromBanglaDate(1432, 1, 14, 'en').toDateString();
* // "Monday, 14 Boishakh 1432"
*
* BanglaDate.fromBanglaDate(1432, 1, 14, 'bn').toDateString();
* // "সোমবার, ১৪ বৈশাখ ১৪৩২"
*/
toDateString(): string {
const monthName = this.getMonthName();
return `${this.getWeekDay()}, ${numberToNumber(
this.banglaDay,
this.language
)} ${monthName} ${numberToNumber(this.banglaYear, this.language)}`;
}
// ── Accessor Methods (all return raw numbers) ───────────────────────────
//
// Presentation-layer callers (format, toLocaleDateString, toLocaleString,
// toDateString, toString) should call numberToNumber() themselves when they
// need localised digit strings. Returning numbers here keeps the API
// composable and testable.
/**
* Returns the day of the Bangla month as a raw number.
* Range: 1–31 (months 1–5 can have 31; Chaitra has 30 or 31 in a leap year).
* @returns Bangla day of month (1-based).
*/
getDate(): number {
return this.banglaDay;
}
/**
* Returns the UTC weekday index of the underlying Gregorian date.
* The Bangla calendar shares the same 7-day week as the Gregorian calendar.
* @returns `0` = Sunday, `1` = Monday, … `6` = Saturday.
*/
getDay(): number {
return this.gregorianDate.getUTCDay();
}
/**
* Returns the full 4-digit Bangla year.
* @returns e.g. `1432`.
*/
getFullYear(): number {
return this.banglaYear;
}
/**
* Returns the UTC hour component of the underlying timestamp.
* @returns Integer in the range 0–23.
*/
getHours(): number {
return this.gregorianDate.getUTCHours();
}
/**
* Returns the UTC milliseconds component of the underlying timestamp.
* @returns Integer in the range 0–999.
*/
getMilliseconds(): number {
return this.gregorianDate.getUTCMilliseconds();
}
/**
* Returns the UTC minutes component of the underlying timestamp.
* @returns Integer in the range 0–59.
*/
getMinutes(): number {
return this.gregorianDate.getUTCMinutes();
}
/**
* Returns the 1-indexed Bangla month number.
* @returns `1` = Boishakh, `2` = Jyoishtho, … `12` = Chaitra.
*/
getMonth(): number {
return this.banglaMonthIndex + 1;
}
/**
* Returns the UTC seconds component of the underlying timestamp.
* @returns Integer in the range 0–59.
*/
getSeconds(): number {
return this.gregorianDate.getUTCSeconds();
}
/**
* Returns the Unix timestamp (milliseconds since 1970-01-01T00:00:00Z).
* Identical to the value returned by the underlying `Date.prototype.getTime()`.
* Useful for comparisons, storage, and interop with native Date APIs.
* @returns Milliseconds since Unix epoch.
*/
getTime(): number {
return this.gregorianDate.getTime();
}
/**
* Returns the host's timezone offset from UTC, in minutes.
* Mirrors `Date.prototype.getTimezoneOffset()`.
* A positive value means UTC is ahead (e.g. UTC−5 → `300`);
* a negative value means UTC is behind (e.g. UTC+6 → `-360`).
* @returns Timezone offset in minutes.
*/
getTimezoneOffset(): number {
return this.gregorianDate.getTimezoneOffset();
}
// UTC variants — these mirror the underlying Gregorian UTC getters and
// operate on the raw Gregorian timestamp, not the Bangla calendar fields.
/** Returns the UTC day-of-month of the underlying Gregorian date (1-based). */
getUTCDate(): number {
return this.gregorianDate.getUTCDate();
}
/** Returns the UTC weekday of the underlying Gregorian date (0 = Sunday … 6 = Saturday). */
getU