diff --git a/npm/ng-packs/package.json b/npm/ng-packs/package.json index e04e70b5cd..010b83d07c 100644 --- a/npm/ng-packs/package.json +++ b/npm/ng-packs/package.json @@ -111,6 +111,7 @@ "just-compare": "^2.0.0", "lerna": "^4.0.0", "lint-staged": "^13.0.0", + "luxon": "^3.6.1", "ng-packagr": "~19.1.0", "ng-zorro-antd": "~19.0.0", "nx": "~20.3.0", diff --git a/npm/ng-packs/packages/core/src/lib/pipes/utc-to-local.pipe.ts b/npm/ng-packs/packages/core/src/lib/pipes/utc-to-local.pipe.ts index e8bb1807e4..1f06aba74f 100644 --- a/npm/ng-packs/packages/core/src/lib/pipes/utc-to-local.pipe.ts +++ b/npm/ng-packs/packages/core/src/lib/pipes/utc-to-local.pipe.ts @@ -1,7 +1,6 @@ import { Pipe, PipeTransform, Injectable, inject, LOCALE_ID } from '@angular/core'; -import { ConfigStateService, LocalizationService, TimezoneService } from '../services'; +import { ConfigStateService, LocalizationService, TimeService, TimezoneService } from '../services'; import { getShortDateFormat, getShortDateShortTimeFormat, getShortTimeFormat } from '../utils'; -import { formatDate } from '@angular/common'; @Injectable() @Pipe({ @@ -9,6 +8,7 @@ import { formatDate } from '@angular/common'; }) export class UtcToLocalPipe implements PipeTransform { protected readonly timezoneService = inject(TimezoneService); + protected readonly timeService = inject(TimeService); protected readonly configState = inject(ConfigStateService); protected readonly localizationService = inject(LocalizationService); protected readonly locale = inject(LOCALE_ID); @@ -27,11 +27,9 @@ export class UtcToLocalPipe implements PipeTransform { try { if (this.timezoneService.isUtcClockEnabled) { const timeZone = this.timezoneService.timezone; - const options: Intl.DateTimeFormatOptions = { timeZone }; - const localeStr = this.formatWithIntl(date, type, options); - return formatDate(localeStr, format, this.locale); + return this.timeService.formatDateWithStandardOffset(date, format, timeZone); } else { - return formatDate(value, format, this.locale, format); + return this.timeService.formatWithoutTimeZone(date, format); } } catch (err) { return value; @@ -49,20 +47,4 @@ export class UtcToLocalPipe implements PipeTransform { return getShortDateShortTimeFormat(this.configState); } } - - private formatWithIntl( - date: Date, - propType: 'date' | 'datetime' | 'time', - options: Intl.DateTimeFormatOptions, - ): string { - switch (propType) { - case 'date': - return date.toLocaleDateString(this.locale, options); - case 'time': - return date.toLocaleTimeString(this.locale, options); - case 'datetime': - default: - return date.toLocaleString(this.locale, options); - } - } } diff --git a/npm/ng-packs/packages/core/src/lib/services/index.ts b/npm/ng-packs/packages/core/src/lib/services/index.ts index 2fa7d134b5..6b006be74e 100644 --- a/npm/ng-packs/packages/core/src/lib/services/index.ts +++ b/npm/ng-packs/packages/core/src/lib/services/index.ts @@ -24,3 +24,4 @@ export * from './internet-connection-service'; export * from './local-storage-listener.service'; export * from './title-strategy.service'; export * from './timezone.service'; +export * from './time.service'; diff --git a/npm/ng-packs/packages/core/src/lib/services/time.service.ts b/npm/ng-packs/packages/core/src/lib/services/time.service.ts new file mode 100644 index 0000000000..4161d55d07 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/services/time.service.ts @@ -0,0 +1,107 @@ +import { inject, Injectable, LOCALE_ID } from '@angular/core'; +import { DateTime } from 'luxon'; + +@Injectable({ + providedIn: 'root', +}) +export class TimeService { + private locale = inject(LOCALE_ID); + + /** + * Returns the current date and time in the specified timezone. + * + * @param zone - An IANA timezone name (e.g., 'Europe/Istanbul', 'UTC'); defaults to the system's local timezone. + * @returns A Luxon DateTime instance representing the current time in the given timezone. + */ + now(zone = 'local'): DateTime { + return DateTime.now().setZone(zone); + } + + /** + * Converts the input date to the specified timezone, applying any timezone and daylight saving time (DST) adjustments. + * + * This method: + * 1. Parses the input value into a Luxon DateTime object. + * 2. Applies the specified IANA timezone, including any DST shifts based on the given date. + * + * @param value - The ISO string or Date object to convert. + * @param zone - An IANA timezone name (e.g., 'America/New_York'). + * @returns A Luxon DateTime instance adjusted to the specified timezone and DST rules. + */ + toZone(value: string | Date, zone: string): DateTime { + return DateTime.fromISO(value instanceof Date ? value.toISOString() : value, { + zone, + }); + } + + /** + * Formats the input date by applying timezone and daylight saving time (DST) adjustments. + * + * This method: + * 1. Converts the input date to the specified timezone. + * 2. Formats the result using the given format and locale, reflecting any timezone or DST shifts. + * + * @param value - The ISO string or Date object to format. + * @param format - The format string (default: 'ff'). + * @param zone - Optional IANA timezone name (e.g., 'America/New_York'); defaults to the system's local timezone. + * @returns A formatted date string adjusted for the given timezone and DST rules. + */ + format(value: string | Date, format = 'ff', zone = 'local'): string { + return this.toZone(value, zone).setLocale(this.locale).toFormat(format); + } + + /** + * Formats a date using the standard time offset (ignoring daylight saving time) for the specified timezone. + * + * This method: + * 1. Converts the input date to UTC. + * 2. Calculates the standard UTC offset for the given timezone (based on January 1st to avoid DST). + * 3. Applies the standard offset manually to the UTC time. + * 4. Formats the result using the specified format and locale, without applying additional timezone shifts. + * + * @param value - The ISO string or Date object to format. + * @param format - The Luxon format string (default: 'ff'). + * @param zone - Optional IANA timezone name (e.g., 'America/New_York'); if omitted, system local timezone is used. + * @returns A formatted date string adjusted by standard time (non-DST). + */ + formatDateWithStandardOffset(value: string | Date, format = 'ff', zone?: string): string { + const utcDate = + typeof value === 'string' + ? DateTime.fromISO(value, { zone: 'UTC' }) + : DateTime.fromJSDate(value, { zone: 'UTC' }); + + if (!utcDate.isValid) return ''; + + const targetZone = zone ?? DateTime.local().zoneName; + + const januaryDate = DateTime.fromObject( + { year: utcDate.year, month: 1, day: 1 }, + { zone: targetZone }, + ); + const standardOffset = januaryDate.offset; + const dateWithStandardOffset = utcDate.plus({ minutes: standardOffset }); + + return dateWithStandardOffset.setZone('UTC').setLocale(this.locale).toFormat(format); + } + + /** + * Formats the input date using its original clock time, without converting based on timezone or DST + * + * This method: + * 1. Converts the input date to ISO string. + * 2. Calculates the date time in UTC, keeping the local time. + * 3. Formats the result using the specified format and locale, without shifting timezones. + * + * @param value - The ISO string or Date object to format. + * @param format - The format string (default: 'ff'). + * @returns A formatted date string without applying timezone. + */ + formatWithoutTimeZone(value: string | Date, format = 'ff'): string { + const isoString = value instanceof Date ? value.toISOString() : value; + + const dateTime = DateTime.fromISO(isoString) + .setZone('utc', { keepLocalTime: true }) + .setLocale(this.locale); + return dateTime.toFormat(format); + } +} diff --git a/npm/ng-packs/packages/core/src/lib/services/timezone.service.ts b/npm/ng-packs/packages/core/src/lib/services/timezone.service.ts index 5bea4f9934..2d6c103dcd 100644 --- a/npm/ng-packs/packages/core/src/lib/services/timezone.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/timezone.service.ts @@ -21,10 +21,16 @@ export class TimezoneService { }); } - getBrowserTimezone(): string { - return Intl.DateTimeFormat().resolvedOptions().timeZone; - } - + /** + * Returns the effective timezone to be used across the application. + * + * This value is determined based on the clock kind setting in the configuration: + * - If clock kind is not equal to Utc, the browser's local timezone is returned. + * - If clock kind is equal to Utc, the configured timezone (`timeZoneNameFromSettings`) is returned if available; + * otherwise, the browser's timezone is used as a fallback. + * + * @returns The IANA timezone name (e.g., 'Europe/Istanbul', 'America/New_York'). + */ get timezone(): string { if (!this.isUtcClockEnabled) { return this.getBrowserTimezone(); @@ -32,6 +38,23 @@ export class TimezoneService { return this.timeZoneNameFromSettings || this.getBrowserTimezone(); } + /** + * Retrieves the browser's local timezone based on the user's system settings. + * + * @returns The IANA timezone name (e.g., 'Europe/Istanbul', 'America/New_York'). + */ + getBrowserTimezone(): string { + return Intl.DateTimeFormat().resolvedOptions().timeZone; + } + + /** + * Sets the application's timezone in a cookie to persist the user's selected timezone. + * + * This method sets the cookie only if the clock kind setting is set to UTC. + * The cookie is stored using the key defined by `this.cookieKey` and applied to the root path (`/`). + * + * @param timezone - The IANA timezone name to be stored (e.g., 'Europe/Istanbul'). + */ setTimezone(timezone: string): void { if (this.isUtcClockEnabled) { this.document.cookie = `${this.cookieKey}=${timezone}; path=/`;