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.
This commit is contained in:
lyzno1 2025-10-13 16:43:11 +08:00
parent 920a608e5d
commit 7560e2427d
No known key found for this signature in database
2 changed files with 58 additions and 7 deletions

View File

@ -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]/)
})
})

View File

@ -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)