From 7560e2427da9c72251f3f0c68cd123fa73b3d99d Mon Sep 17 00:00:00 2001 From: lyzno1 Date: Mon, 13 Oct 2025 16:43:11 +0800 Subject: [PATCH] fix(timezone): support half-hour and 45-minute timezone offsets Critical regression fix for convertTimezoneToOffsetStr: Issues Fixed: - Previous regex /^([+-]?\d{1,2}):00/ only matched :00 offsets - This caused half-hour offsets (e.g., India +05:30) to return UTC+0 - Even if matched, parseInt only parsed hours, losing minute info Changes: - Update regex to /^([+-]?\d{1,2}):(\d{2})/ to match all offset formats - Parse both hours and minutes separately - Output format: "UTC+5:30" for non-zero minutes, "UTC+8" for whole hours - Preserve leading zeros in minute part (e.g., "UTC+5:30" not "UTC+5:3") Test Coverage: - Added 8 comprehensive tests covering: * Default/invalid timezone handling * Whole hour offsets (positive/negative) * Zero offset (UTC) * Half-hour offsets (India +5:30, Australia +9:30) * 45-minute offset (Chatham +12:45) * Leading zero preservation in minutes All 14 tests passing. Verified with timezone.json entries at lines 967, 1135, 1251. --- .../date-and-time-picker/utils/dayjs.spec.ts | 48 +++++++++++++++++++ .../base/date-and-time-picker/utils/dayjs.ts | 17 ++++--- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/web/app/components/base/date-and-time-picker/utils/dayjs.spec.ts b/web/app/components/base/date-and-time-picker/utils/dayjs.spec.ts index 549ab01029..5c891126b5 100644 --- a/web/app/components/base/date-and-time-picker/utils/dayjs.spec.ts +++ b/web/app/components/base/date-and-time-picker/utils/dayjs.spec.ts @@ -1,5 +1,6 @@ import dayjs from './dayjs' import { + convertTimezoneToOffsetStr, getDateWithTimezone, isDayjsObject, toDayjs, @@ -65,3 +66,50 @@ describe('dayjs utilities', () => { expect(result?.minute()).toBe(0) }) }) + +describe('convertTimezoneToOffsetStr', () => { + test('should return default UTC+0 for undefined timezone', () => { + expect(convertTimezoneToOffsetStr(undefined)).toBe('UTC+0') + }) + + test('should return default UTC+0 for invalid timezone', () => { + expect(convertTimezoneToOffsetStr('Invalid/Timezone')).toBe('UTC+0') + }) + + test('should handle whole hour positive offsets without leading zeros', () => { + expect(convertTimezoneToOffsetStr('Asia/Shanghai')).toBe('UTC+8') + expect(convertTimezoneToOffsetStr('Pacific/Auckland')).toBe('UTC+12') + expect(convertTimezoneToOffsetStr('Pacific/Apia')).toBe('UTC+13') + }) + + test('should handle whole hour negative offsets without leading zeros', () => { + expect(convertTimezoneToOffsetStr('Pacific/Niue')).toBe('UTC-11') + expect(convertTimezoneToOffsetStr('Pacific/Honolulu')).toBe('UTC-10') + expect(convertTimezoneToOffsetStr('America/New_York')).toBe('UTC-5') + }) + + test('should handle zero offset', () => { + expect(convertTimezoneToOffsetStr('Europe/London')).toBe('UTC+0') + expect(convertTimezoneToOffsetStr('UTC')).toBe('UTC+0') + }) + + test('should handle half-hour offsets (30 minutes)', () => { + // India Standard Time: UTC+5:30 + expect(convertTimezoneToOffsetStr('Asia/Kolkata')).toBe('UTC+5:30') + // Australian Central Time: UTC+9:30 + expect(convertTimezoneToOffsetStr('Australia/Adelaide')).toBe('UTC+9:30') + expect(convertTimezoneToOffsetStr('Australia/Darwin')).toBe('UTC+9:30') + }) + + test('should handle 45-minute offsets', () => { + // Chatham Time: UTC+12:45 + expect(convertTimezoneToOffsetStr('Pacific/Chatham')).toBe('UTC+12:45') + }) + + test('should preserve leading zeros in minute part for non-zero minutes', () => { + // Ensure +05:30 is displayed as "UTC+5:30", not "UTC+5:3" + const result = convertTimezoneToOffsetStr('Asia/Kolkata') + expect(result).toMatch(/UTC[+-]\d+:30/) + expect(result).not.toMatch(/UTC[+-]\d+:3[^0]/) + }) +}) diff --git a/web/app/components/base/date-and-time-picker/utils/dayjs.ts b/web/app/components/base/date-and-time-picker/utils/dayjs.ts index 671c2f2bde..6b032b3929 100644 --- a/web/app/components/base/date-and-time-picker/utils/dayjs.ts +++ b/web/app/components/base/date-and-time-picker/utils/dayjs.ts @@ -107,15 +107,18 @@ export const convertTimezoneToOffsetStr = (timezone?: string) => { const tzItem = tz.find(item => item.value === timezone) if (!tzItem) return DEFAULT_OFFSET_STR - // Extract offset from name format like "-11:00 Niue Time - Alofi" - // Name format is always "{offset}:00 {timezone name}" - const offsetMatch = tzItem.name.match(/^([+-]?\d{1,2}):00/) + // Extract offset from name format like "-11:00 Niue Time" or "+05:30 India Time" + // Name format is always "{offset}:{minutes} {timezone name}" + const offsetMatch = tzItem.name.match(/^([+-]?\d{1,2}):(\d{2})/) if (!offsetMatch) return DEFAULT_OFFSET_STR - // Parse offset as integer to remove leading zeros (e.g., "-05" → "-5") - const offsetNum = Number.parseInt(offsetMatch[1], 10) - const sign = offsetNum >= 0 ? '+' : '' - return `UTC${sign}${offsetNum}` + // Parse hours and minutes separately + const hours = Number.parseInt(offsetMatch[1], 10) + const minutes = Number.parseInt(offsetMatch[2], 10) + const sign = hours >= 0 ? '+' : '' + // If minutes are non-zero, include them in the output (e.g., "UTC+5:30") + // Otherwise, only show hours (e.g., "UTC+8") + return minutes !== 0 ? `UTC${sign}${hours}:${offsetMatch[2]}` : `UTC${sign}${hours}` } export const isDayjsObject = (value: unknown): value is Dayjs => dayjs.isDayjs(value)