Files
calendar/packages/features/calendars/lib/timezone-conversion.ts
T
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
c28a0d7db0 fix: handle invalid timezones gracefully in calendar events (#27042)
* fix: handle invalid timezones gracefully in getLuckyUser

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: move timezone validation to getCalendarsEvents with offset conversion

- Move invalid timezone handling from getLuckyUser.ts to getCalendarsEvents.ts
- Add convertOffsetToEtcGmt() to convert offset formats (GMT-05:00, UTC+08:00) to valid IANA Etc/GMT timezones
- Preserves timezone precision for fairness calculations instead of falling back to UTC
- Add unit tests for timezone conversion scenarios

* refactor: extract timezone conversion to dedicated file

- Create timezone-conversion.ts with concise convertOffsetToIanaTimezone and normalizeTimezone functions
- Update getCalendarsEvents.ts to import from the new file
- Follow kebab-case naming convention for utility files

* test: add unit tests for timezone conversion proving Intl.DateTimeFormat behavior

- Add tests proving Intl.DateTimeFormat throws RangeError for offset formats (GMT-05:00, UTC+08:00)
- Add tests for convertOffsetToIanaTimezone function
- Add tests for normalizeTimezone function

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-21 02:51:52 -03:00

44 lines
1.5 KiB
TypeScript

import logger from "@calcom/lib/logger";
const log = logger.getSubLogger({ prefix: ["timezoneConversion"] });
/**
* Converts offset-based timezone formats (GMT±HH:MM, UTC±HH:MM) to IANA Etc/GMT timezones.
* Etc/GMT uses inverted signs: Etc/GMT+5 = UTC-5, Etc/GMT-8 = UTC+8.
*/
export function convertOffsetToIanaTimezone(timezone: string): string | null {
const match = timezone.match(/^(?:GMT|UTC)([+-])(\d{1,2})(?::(\d{2}))?$/i);
if (!match) return null;
const [, sign, hoursStr, minutesStr] = match;
const hours = parseInt(hoursStr, 10);
const minutes = minutesStr ? parseInt(minutesStr, 10) : 0;
// Etc/GMT only supports whole hours up to 14
if (minutes !== 0 || hours > 14) return null;
if (hours === 0) return "Etc/GMT";
return `Etc/GMT${sign === "+" ? "-" : "+"}${hours}`;
}
/**
* Validates and normalizes a timezone string to a valid IANA timezone.
* Converts offset formats to Etc/GMT, falls back to UTC if invalid.
*/
export function normalizeTimezone(timezone: string | undefined): string {
if (!timezone) return "UTC";
try {
Intl.DateTimeFormat(undefined, { timeZone: timezone });
return timezone;
} catch {
const converted = convertOffsetToIanaTimezone(timezone);
if (converted) {
log.info(`Converted offset timezone to IANA format`, { original: timezone, converted });
return converted;
}
log.warn(`Invalid timezone format, falling back to UTC`, { invalidTimezone: timezone });
return "UTC";
}
}