diff --git a/README.md b/README.md index e24185f..ad98f51 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,69 @@ console.log(date2.toString()) // 2080-03-23 10:15:00 - `NepaliDate.minSupportedNepaliDate()`: Returns the minimum supported Nepali object. - `NepaliDate.maxSupportedNepaliDate()`: Returns the maximum supported Nepali object. +### NepalTimezoneDate + +The `NepalTimezoneDate` class provides Gregorian date/time values in Nepal's timezone (Asia/Kathmandu, UTC+05:45). +It works like JavaScript's `Date`, but always returns values as they would appear in Nepal, regardless of your system's timezone. +It does **not** convert to the Nepali calendar. + +#### Creating a NepalTimezoneDate object + +You can create a `NepalTimezoneDate` object in several ways: + +```javascript +import { NepalTimezoneDate } from 'nepali-datetime' + +// Current date/time in Nepal timezone +const nowNepal = new NepalTimezoneDate() + +// From a UTC date string +const dateNepal = new NepalTimezoneDate('2024-12-28T15:00:35Z') + +// From a JS Date object +const jsDate = new Date('2024-12-28T15:00:35Z') +const nepalDate = new NepalTimezoneDate(jsDate) + +// From Nepal time components (year, month [0-based], date, hour, minute, second, ms) +const npTzDate = new NepalTimezoneDate(2024, 11, 28, 20, 45, 35) +``` + +#### Getting Nepal timezone date components + +```javascript +npTzDate.getYear() // 2024 +npTzDate.getMonth() // 11 (December, 0-based) +npTzDate.getDate() // 28 +npTzDate.getHours() // 20 +npTzDate.getMinutes() // 45 +npTzDate.toString() // "2024-12-28 20:45:35 GMT+0545" +npTzDate.toDate() // JS Date object in UTC +``` + +#### Converting NepalTimezoneDate to NepaliDate + +You can convert a `NepalTimezoneDate` object (Gregorian date in Nepal timezone) to a `NepaliDate` object: + +```javascript +const npTzDate = new NepalTimezoneDate('2024-12-28T15:00:35Z') +const nepaliDate = new NepaliDate(npTzDate) +console.log(nepaliDate.toString()) // e.g. 2081-09-12 20:45:35 +``` + +#### Comparing with JS Date + +```javascript +const systemDate = new Date('2024-12-28T15:00:35Z') +const nepalDate = new NepalTimezoneDate('2024-12-28T15:00:35Z') + +// System Date (depends on your computer's timezone) +console.log(systemDate.getHours()) // e.g. 10 (US), 16 (EU), 20 (Nepal) + +// NepalTimezoneDate (always Nepal time) +console.log(nepalDate.getHours()) // 20 +console.log(nepalDate.toString()) // "2024-12-28 20:45:35 GMT+0545" +``` + ### dateConverter The `dateConverter` module provides core functions for converting dates between the Nepali and English calendars. diff --git a/src/NepalTimezoneDate.ts b/src/NepalTimezoneDate.ts new file mode 100644 index 0000000..7000b2b --- /dev/null +++ b/src/NepalTimezoneDate.ts @@ -0,0 +1,256 @@ +import { + OLD_UTC_OFFSET_IN_MS, + TIMEZONE_TRANSITION_DATE_REFERENCE, + TIMEZONE_TRANSITION_TIMESTAMP, + UTC_OFFSET_IN_MS, +} from './constants' + +/** + * Represents a Gregorian date/time in Nepal's timezone (Asia/Kathmandu, UTC+05:45). + * Behaves like a JavaScript `Date` object, with all getters returning values in Nepal's timezone. + * Does not convert to the Nepali (Bikram Sambat) calendar. + * + * @example + * const date = new NepalTimezoneDate(); + * or, + * const date = new NepalTimezoneDate(2024, 11, 28, 20, 45, 35) // 20:45 is Nepal time + * date.toString() // "2024-12-28 20:45:35 GMT+0545" + * date.toDate() // JS Date object in UTC + * + * @example + * const date = new NepalTimezoneDate(new Date()) + * date.getYear() // Nepal year + * date.getMinutes() // Nepal minutes + */ +class NepalTimezoneDate { + private _date: Date + private _nepalTimezoneSafeDate: { + year: number + month0: number + day: number + hour: number + minute: number + second: number + ms: number + weekDay: number + } + + /** + * Get the Nepali date and time components (Gregorian calendar) from a given date. + * The input can be any date from any timezone, it is converted into the Nepal's timezone (Asia/Kathmandu). + * + * @param date - The input date for which to retrieve the Nepali date and time. + * @returns An object containing the Nepali date and time components. + */ + private static getNepalDateAndTime(date: Date) { + const time = date.getTime() + + // Handling the timezone switch froqm GMT+5:30 to GMT+5:45 + // In javascript the switched time is + // 504901800000: Wed Jan 01 1986 00:15:00 GMT+0545 (Nepal Time) : Adjusted time + const utcOffsetInMs = + time < TIMEZONE_TRANSITION_TIMESTAMP + ? OLD_UTC_OFFSET_IN_MS + : UTC_OFFSET_IN_MS + + // Calculate the Nepali reference date by adding the offset to the input date's unix timestamp + const nepaliRefDate = new Date(time + utcOffsetInMs) + + // Extract the Nepali date and time components + const npYear = nepaliRefDate.getUTCFullYear() + const npMonth0 = nepaliRefDate.getUTCMonth() + const npDay = nepaliRefDate.getUTCDate() + const npHour = nepaliRefDate.getUTCHours() + const npMinutes = nepaliRefDate.getUTCMinutes() + const npSeconds = nepaliRefDate.getUTCSeconds() + const npMs = nepaliRefDate.getUTCMilliseconds() + const npWeekDay = nepaliRefDate.getUTCDay() + + // Return the Nepali date and time components as an object + return { + year: npYear, + month0: npMonth0, + day: npDay, + hour: npHour, + minute: npMinutes, + second: npSeconds, + ms: npMs, + weekDay: npWeekDay, + } + } + + /** + * Get the Date object from the given Nepali date and time components. + * + * @param year - The year component of the Nepali date. + * @param month0 - The month component of the Nepali date (1-12). + * @param date - The day component of the Nepali date. + * @param hour - The hour component of the Nepali time. + * @param minute - The minute component of the Nepali time. + * @param second - The second component of the Nepali time. + * @param ms - The millisecond component of the Nepali time. + * @returns A `Date` object representing the UTC date and time. + */ + private static getDate( + year: number, + month: number, + day: number, + hour: number, + minute: number, + second: number, + ms: number + ): Date { + // Create a new Date object using the given Nepali date and time parameters + const nepaliRefDate = new Date(year, month, day, hour, minute, second, ms) + + let utcOffsetInMs = + nepaliRefDate < TIMEZONE_TRANSITION_DATE_REFERENCE + ? OLD_UTC_OFFSET_IN_MS + : UTC_OFFSET_IN_MS + + // Getting current timezone offset (in milliseconds) + const currentOffsetInMS = -1 * nepaliRefDate.getTimezoneOffset() * 60 * 1000 + + // Subtracting Nepali ref date by Nepali timezone offset and current timezone Offset + const date = new Date( + nepaliRefDate.getTime() - utcOffsetInMs + currentOffsetInMS + ) + + // Return the date object + return date + } + + /** + * Creates a NepalTimezoneDate instance for Asia/Kathmandu timezone (UTC+05:45). + * Accepts: + * - No arguments (current date/time) + * - Unix epoch (number) + * - Date object + * - year, month, ... (Nepal timezone components) + */ + constructor(...args: any[]) { + if (args.length === 0) { + // no arguments - current date/time + this._date = new Date() + } else if (args.length === 1 && typeof args[0] === 'number') { + // Unix epoch (ms) + this._date = new Date(args[0]) + } else if (args.length === 1 && args[0] instanceof Date) { + // Date object + this._date = new Date(args[0]) + } else if ( + args.length >= 2 && + args.length <= 7 && + args.every(arg => typeof arg === 'number') + ) { + // year, month, day, hour, minute, second, ms (Nepal timezone components) + const [year, month, day, hour = 0, minute = 0, second = 0, ms = 0] = args + this._date = NepalTimezoneDate.getDate( + year, + month, + day, + hour, + minute, + second, + ms + ) + } else { + throw new Error('Invalid arguments for NepalTimezoneDate') + } + this._nepalTimezoneSafeDate = NepalTimezoneDate.getNepalDateAndTime(this._date) + } + + /** + * Retrieves the year in Nepal's timezone (Gregorian calendar). + * @returns {number} The full numeric value representing the year + */ + getYear(): number { + return this._nepalTimezoneSafeDate.year + } + + /** + * Retrieves the month in Nepal's timezone (Gregorian calendar). + * @returns {number} The numeric value representing the month + */ + getMonth(): number { + return this._nepalTimezoneSafeDate.month0 + } + + /** + * Retrieves the day of the month in Nepal's timezone (Gregorian calendar). + * @returns {number} The numeric value representing the day of the month. + */ + getDate(): number { + return this._nepalTimezoneSafeDate.day + } + + /** + * Retrieves the hour in Nepal's timezone. + * @returns {number} The numeric value representing the hour + */ + getHours(): number { + return this._nepalTimezoneSafeDate.hour + } + + /** + * Retrieves the minute in Nepal's timezone. + * @returns {number} The numeric value representing the minute + */ + getMinutes(): number { + return this._nepalTimezoneSafeDate.minute + } + + /** + * Retrieves the second in Nepal's timezone. + * @returns {number} The numeric value representing the second + */ + getSeconds(): number { + return this._nepalTimezoneSafeDate.second + } + + /** + * Retrieves the millisecond in Nepal's timezone. + * @returns {number} The numeric value representing the millisecond + */ + getMilliseconds(): number { + return this._nepalTimezoneSafeDate.ms + } + + /** + * Retrieves the day of the week in Nepal's timezone. + * @returns {number} The numeric value representing the day of the week + */ + getDay(): number { + return this._nepalTimezoneSafeDate.weekDay + } + + /** + * Retrieves the Unix timestamp (in milliseconds) of the date. + * @returns {number} The numeric value representing the time in milliseconds. + */ + getTime(): number { + return this._date.getTime() + } + + /** + * Returns a string representation of the NepalTimezoneDate object in Nepal's timezone. + * @returns {string} The string representation in the format "YYYY-MM-DD HH:mm:ss GMT+0545". + */ + toString(): string { + const np = this._nepalTimezoneSafeDate + return ( + `${np.year}-${String(np.month0 + 1).padStart(2, '0')}-${String(np.day).padStart(2, '0')} ` + + `${String(np.hour).padStart(2, '0')}:${String(np.minute).padStart(2, '0')}:${String(np.second).padStart(2, '0')} GMT+0545` + ) + } + + /** + * Returns the underlying Date object (UTC). + * @returns {Date} The equivalent JavaScript Date object. + */ + toDate(): Date { + return new Date(this._date) + } +} + +export default NepalTimezoneDate diff --git a/src/NepaliDate.ts b/src/NepaliDate.ts index 9961d5c..b5e9ef4 100644 --- a/src/NepaliDate.ts +++ b/src/NepaliDate.ts @@ -7,6 +7,7 @@ import { formatNepali, nepaliDateToString, } from './format' +import NepalTimezoneDate from './NepalTimezoneDate' import { simpleParse, @@ -14,7 +15,6 @@ import { parseEnglishDateFormat, parseNepaliFormat, } from './parse' -import { getDate, getNepalDateAndTime } from './utils' import { validateTime } from './validators' /** @@ -124,6 +124,17 @@ class NepaliDate { second?: number, ms?: number ) + + /** + * Creates a NepaliDate instance from a NepalTimezoneDate object. + * + * @param {NepalTimezoneDate} date - The NepalTimezoneDate object. + * @example + * const npTzDate = new NepalTimezoneDate('2024-12-28T15:00:35Z') + * const nepaliDate = new NepaliDate(npTzDate) + */ + constructor(date: NepalTimezoneDate) + constructor(...args: any[]) { if (args.length === 0) { this.initFromCurrentDate() @@ -135,6 +146,8 @@ class NepaliDate { this.parseFromString(args[0]) } else if (args.length === 1 && typeof args[0] === 'number') { this.initFromTimestamp(args[0]) + } else if (args.length === 1 && args[0] instanceof NepalTimezoneDate) { + this._setDateObject(args[0].toDate()) } else if ( args.length === 2 && typeof args[0] === 'string' && @@ -227,22 +240,22 @@ class NepaliDate { * @returns void */ private _setDateObject(date: Date, computeNepaliDate: boolean = true) { + const npTzDate = new NepalTimezoneDate(date) this.timestamp = date // getting Nepal's hour, minute, and weekDay - const { year, month0, day, hour, minute, weekDay } = getNepalDateAndTime(date) - this.yearEn = year - this.monthEn = month0 - this.dayEn = day - this.hour = hour - this.minute = minute - this.weekDay = weekDay + this.yearEn = npTzDate.getYear() + this.monthEn = npTzDate.getMonth() + this.dayEn = npTzDate.getDate() + this.hour = npTzDate.getHours() + this.minute = npTzDate.getMinutes() + this.weekDay = npTzDate.getDay() if (computeNepaliDate) { const [yearNp, month0Np, dayNp] = dateConverter.englishToNepali( - year, - month0, - day + this.yearEn, + this.monthEn, + this.dayEn ) this.year = yearNp this.month = month0Np @@ -539,7 +552,15 @@ class NepaliDate { this.month = month this.day = date this._setDateObject( - getDate(yearEn, month0EN, dayEn, hour, minute, second, ms), + NepalTimezoneDate['getDate']( + yearEn, + month0EN, + dayEn, + hour, + minute, + second, + ms + ), false ) } @@ -624,7 +645,15 @@ class NepaliDate { second: number = 0, ms: number = 0 ): NepaliDate { - const englishDate = getDate(year, month0, date, hour, minute, second, ms) + const englishDate = NepalTimezoneDate['getDate']( + year, + month0, + date, + hour, + minute, + second, + ms + ) return new NepaliDate(englishDate) } diff --git a/src/index.ts b/src/index.ts index e3e4153..a16311d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ import NepaliDate from './NepaliDate' +import NepalTimezoneDate from './NepalTimezoneDate' export default NepaliDate +export { NepalTimezoneDate } diff --git a/src/utils.ts b/src/utils.ts index 8ad755e..7e0c090 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,102 +1,4 @@ -import { - FORMAT_TOKEN_REGEX, - OLD_UTC_OFFSET_IN_MS, - TIMEZONE_TRANSITION_TIMESTAMP, - TIMEZONE_TRANSITION_DATE_REFERENCE, - UTC_OFFSET_IN_MS, -} from './constants' - -/** - * Get the Nepali date and time components (Gregorian calendar) from a given date. - * The input can be any date from any timezone, it is converted into the Nepal's timezone (Asia/Kathmandu). - * - * @param date - The input date for which to retrieve the Nepali date and time. - * @returns An object containing the Nepali date and time components. - */ -export const getNepalDateAndTime = ( - date: Date -): { - year: number - month0: number - day: number - hour: number - minute: number - second: number - ms: number - weekDay: number -} => { - const time = date.getTime() - - // Handling the timezone switch from GMT+5:30 to GMT+5:45 - // In javascript the switched time is - // 504901800000: Wed Jan 01 1986 00:15:00 GMT+0545 (Nepal Time) : Adjusted time - const utcOffsetInMs = - time < TIMEZONE_TRANSITION_TIMESTAMP ? OLD_UTC_OFFSET_IN_MS : UTC_OFFSET_IN_MS - - // Calculate the Nepali reference date by adding the offset to the input date's unix timestamp - const nepaliRefDate = new Date(time + utcOffsetInMs) - - // Extract the Nepali date and time components - const npYear = nepaliRefDate.getUTCFullYear() - const npMonth0 = nepaliRefDate.getUTCMonth() - const npDay = nepaliRefDate.getUTCDate() - const npHour = nepaliRefDate.getUTCHours() - const npMinutes = nepaliRefDate.getUTCMinutes() - const npSeconds = nepaliRefDate.getUTCSeconds() - const npMs = nepaliRefDate.getUTCMilliseconds() - const npWeekDay = nepaliRefDate.getUTCDay() - - // Return the Nepali date and time components as an object - return { - year: npYear, - month0: npMonth0, - day: npDay, - hour: npHour, - minute: npMinutes, - second: npSeconds, - ms: npMs, - weekDay: npWeekDay, - } -} - -/** - * Get the Date object from the given Nepali date and time components. - * - * @param year - The year component of the Nepali date. - * @param month0 - The month component of the Nepali date (1-12). - * @param date - The day component of the Nepali date. - * @param hour - The hour component of the Nepali time. - * @param minute - The minute component of the Nepali time. - * @param second - The second component of the Nepali time. - * @param ms - The millisecond component of the Nepali time. - * @returns A `Date` object representing the UTC date and time. - */ -export const getDate = ( - year: number, - month: number, - day: number, - hour: number, - minute: number, - second: number, - ms: number -): Date => { - // Create a new Date object using the given Nepali date and time parameters - const nepaliRefDate = new Date(year, month, day, hour, minute, second, ms) - - let utcOffsetInMs = - nepaliRefDate < TIMEZONE_TRANSITION_DATE_REFERENCE - ? OLD_UTC_OFFSET_IN_MS - : UTC_OFFSET_IN_MS - - // Getting current timezone offset (in milliseconds) - const currentOffsetInMS = -1 * nepaliRefDate.getTimezoneOffset() * 60 * 1000 - - // Subtracting Nepali ref date by Nepali timezone offset and current timezone Offset - const date = new Date(nepaliRefDate.getTime() - utcOffsetInMs + currentOffsetInMS) - - // Return the date object - return date -} +import { FORMAT_TOKEN_REGEX } from './constants' /** * Parses a format string and extracts individual format tokens. diff --git a/tests/NepaliTimezoneDate.test.ts b/tests/NepaliTimezoneDate.test.ts new file mode 100644 index 0000000..b0f357d --- /dev/null +++ b/tests/NepaliTimezoneDate.test.ts @@ -0,0 +1,136 @@ +import NepalTimezoneDate from '../src/NepalTimezoneDate' + +describe('NepalTimezoneDate', () => { + it('should initialize from a unix epoch number (milliseconds)', () => { + const timestamp = 1756819453 + const npTz = new NepalTimezoneDate(timestamp) + expect(npTz).toBeInstanceOf(NepalTimezoneDate) + expect(npTz.getTime()).toBe(timestamp) + }) + + it('should initialize from a Date object', () => { + const dateObj = new Date() + const npTz = new NepalTimezoneDate(dateObj) + expect(npTz).toBeInstanceOf(NepalTimezoneDate) + expect(npTz.getTime()).toBe(dateObj.getTime()) + }) + + it('should initialize with no arguments as current Nepal time', () => { + const npTz = new NepalTimezoneDate() + expect(npTz).toBeInstanceOf(NepalTimezoneDate) + expect(typeof npTz.getYear()).toBe('number') + expect(typeof npTz.getMonth()).toBe('number') + expect(typeof npTz.getDate()).toBe('number') + expect(typeof npTz.getHours()).toBe('number') + expect(typeof npTz.getMinutes()).toBe('number') + }) + + it('should initialize from a unix timestamp', () => { + const timestamp = 1686501122598 // Sun Jun 11 2023 22:17:02 GMT+0545 (Nepal Time) + const npTz = new NepalTimezoneDate(timestamp) + expect(npTz.getTime()).toBe(timestamp) + expect(npTz.getHours()).toBe(22) + expect(npTz.getMinutes()).toBe(17) + }) + + it('should initialize from a Date object', () => { + const d = new Date('2023-06-11T16:32:02.598Z') + const npTz = new NepalTimezoneDate(d) + expect(npTz.getYear()).toBe(2023) + expect(npTz.getMonth()).toBe(5) + expect(npTz.getDate()).toBe(11) + }) + + it('should initialize correctly with only year, month, and date', () => { + // 2024, 1, 12 => February 12, 2024 + const npTz = new NepalTimezoneDate(2024, 1, 12) + expect(npTz.getYear()).toBe(2024) + expect(npTz.getMonth()).toBe(1) + expect(npTz.getDate()).toBe(12) + expect(npTz.getHours()).toBe(0) + expect(npTz.getMinutes()).toBe(0) + expect(npTz.getSeconds()).toBe(0) + expect(npTz.getMilliseconds()).toBe(0) + expect(npTz.toString()).toBe('2024-02-12 00:00:00 GMT+0545') + }) + + it('should initialize from Nepal time components', () => { + const npTz = new NepalTimezoneDate(2025, 8, 2, 13, 53) + expect(npTz.getYear()).toBe(2025) + expect(npTz.getMonth()).toBe(8) + expect(npTz.getDate()).toBe(2) + expect(npTz.getHours()).toBe(13) + expect(npTz.getMinutes()).toBe(53) + // UTC time should be 8:08 AM + expect(npTz.toDate().getUTCHours()).toBe(8) + expect(npTz.toDate().getUTCMinutes()).toBe(8) + }) + + it('should initialize correctly with year, month, date, hour, minute, second, ms', () => { + // 2024, 1, 12, 10, 30, 0, 0 => February 12, 2024, 10:30:00.000 (Nepal time) + const npTz = new NepalTimezoneDate(2024, 1, 12, 10, 30, 0, 0) + expect(npTz.getYear()).toBe(2024) + expect(npTz.getMonth()).toBe(1) + expect(npTz.getDate()).toBe(12) + expect(npTz.getHours()).toBe(10) + expect(npTz.getMinutes()).toBe(30) + expect(npTz.getSeconds()).toBe(0) + expect(npTz.getMilliseconds()).toBe(0) + expect(npTz.toString()).toBe('2024-02-12 10:30:00 GMT+0545') + // UTC time should be 4:45 AM + expect(npTz.toDate().getUTCHours()).toBe(4) + expect(npTz.toDate().getUTCMinutes()).toBe(45) + }) + + it('should return correct string representation', () => { + const npTz = new NepalTimezoneDate(2024, 11, 28, 20, 45, 35) + expect(npTz.toString()).toBe('2024-12-28 20:45:35 GMT+0545') + }) + + it('should throw error for invalid arguments', () => { + expect(() => new NepalTimezoneDate('invalid' as any)).toThrow( + 'Invalid arguments for NepalTimezoneDate' + ) + expect(() => new NepalTimezoneDate({} as any)).toThrow( + 'Invalid arguments for NepalTimezoneDate' + ) + expect(() => new NepalTimezoneDate(2024, '11' as any, 28)).toThrow( + 'Invalid arguments for NepalTimezoneDate' + ) + }) + + it('should support all getters', () => { + const npTz = new NepalTimezoneDate(2024, 0, 1, 0, 0, 0, 123) + expect(npTz.getYear()).toBe(2024) + expect(npTz.getMonth()).toBe(0) + expect(npTz.getDate()).toBe(1) + expect(npTz.getHours()).toBe(0) + expect(npTz.getMinutes()).toBe(0) + expect(npTz.getSeconds()).toBe(0) + expect(npTz.getMilliseconds()).toBe(123) + expect(npTz.getDay()).toBeGreaterThanOrEqual(0) + expect(npTz.getDay()).toBeLessThanOrEqual(6) + expect(typeof npTz.getTime()).toBe('number') + }) + + it('should handle edge of GMT+5:45 transition', () => { + // 504901800000 = 1986-01-01T00:15:00+05:45 (Gregorian calendar) + const npTz = new NepalTimezoneDate(1986, 0, 1, 0, 15) + expect(npTz.getTime()).toBe(504901800000) + expect(npTz.toString()).toBe('1986-01-01 00:15:00 GMT+0545') + }) + + it('should handle edge of GMT+5:30 transition', () => { + // 504901799999 = 1985-12-31T23:59:59.999+05:30 (Gregorian calendar) + const npTz = new NepalTimezoneDate(1985, 11, 31, 23, 59, 59, 999) + expect(npTz.getTime()).toBe(504901799999) + expect(npTz.toString()).toBe('1985-12-31 23:59:59 GMT+0545') + }) + + it('should return a new Date object from toDate()', () => { + const npTz = new NepalTimezoneDate(2024, 11, 28, 20, 45, 35) + const d = npTz.toDate() + expect(d).toBeInstanceOf(Date) + expect(d.getTime()).toBe(npTz.getTime()) + }) +})