Skip to content
136 changes: 117 additions & 19 deletions src/scripts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1233,29 +1233,61 @@ interface WindowWithTimeline extends Window {

(window as WindowWithTimeline).initializeTimeline = initializeTimeline;

// Cache for processed timezone data to avoid expensive recalculations
interface ProcessedTimezoneData {
julyTimeZones: TimeZone[];
decemberTimeZones: TimeZone[];
userTimezone: string;
currentYear: number;
}

// Cache for timezone data by year to avoid expensive recalculations
const processedTimezoneCache = new Map<number, ProcessedTimezoneData>();

/**
* Get all supported timezones that the browser knows about,
* ordered starting with the user's current timezone and going around the world
* @param date Optional date to calculate timezone offsets for (defaults to current date)
* @returns Array of timezone objects ordered from user's timezone around the globe
* Initialize timezone data by processing all browser timezones for July and December
* This expensive operation should only be done once on page load
*/
export function getAllTimezonesOrdered(date?: Date): TimeZone[] {
// Get user's timezone using Temporal (polyfill ensures availability)
function initializeTimezoneData(year: number = new Date().getFullYear()): ProcessedTimezoneData {
const userTimezone = Temporal.Now.timeZoneId();

const now = date || new Date();

// Get all supported timezones (comprehensive list)
const allTimezones = Intl.supportedValuesOf('timeZone');

// Create timezone objects with current offsets using Intl (proven compatibility)
const timezoneData = allTimezones.map(iana => {
// Create dates for July 1st and December 31st to capture DST variations
const julyDate = new Date(year, 6, 1); // July 1st
const decemberDate = new Date(year, 11, 31); // December 31st

console.log(`Processing ${allTimezones.length} timezones for July and December variants...`);

// Process timezones for July (typically DST active in Northern Hemisphere)
const julyTimeZones = processTimezonesForDate(allTimezones, julyDate, userTimezone);
Copy link

Copilot AI Aug 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an inconsistency between the July date creation (July 1st) and the later comment mentioning June 1st for summer time calculations. The getTimezoneVariations function uses June 1st while the new code uses July 1st. This inconsistency could lead to different DST calculations.

Suggested change
// Create dates for July 1st and December 31st to capture DST variations
const julyDate = new Date(year, 6, 1); // July 1st
const decemberDate = new Date(year, 11, 31); // December 31st
console.log(`Processing ${allTimezones.length} timezones for July and December variants...`);
// Process timezones for July (typically DST active in Northern Hemisphere)
const julyTimeZones = processTimezonesForDate(allTimezones, julyDate, userTimezone);
// Create dates for June 1st and December 31st to capture DST variations
const juneDate = new Date(year, 5, 1); // June 1st
const decemberDate = new Date(year, 11, 31); // December 31st
console.log(`Processing ${allTimezones.length} timezones for July and December variants...`);
// Process timezones for June (typically DST active in Northern Hemisphere)
const juneTimeZones = processTimezonesForDate(allTimezones, juneDate, userTimezone);

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an inconsistency between the July date creation (July 1st) and the later comment mentioning June 1st for summer time calculations. The getTimezoneVariations function uses June 1st while the new code uses July 1st. This inconsistency could lead to different DST calculations.

@copilot lots of inconsistencies here.


// Process timezones for December (typically Standard time in Northern Hemisphere)
const decemberTimeZones = processTimezonesForDate(allTimezones, decemberDate, userTimezone);

console.log('Timezone processing complete');

return {
julyTimeZones,
decemberTimeZones,
userTimezone,
currentYear: year,
};
}

/**
* Process all timezones for a specific date to get their offsets and display names
*/
function processTimezonesForDate(timezoneIanas: readonly string[], date: Date, userTimezone: string): TimeZone[] {
// Create timezone objects with offsets for the specific date
const timezoneData = timezoneIanas.map(iana => {
const formatter = new Intl.DateTimeFormat('en', {
timeZone: iana,
timeZoneName: 'longOffset',
});

const offsetStr = formatter.formatToParts(now).find(part => part.type === 'timeZoneName')?.value || '+00:00';
const offsetStr = formatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || '+00:00';

// Parse offset string like "GMT+05:30" or "GMT-08:00"
const offsetMatch = offsetStr.match(/GMT([+-])(\d{2}):(\d{2})/);
Expand All @@ -1272,24 +1304,24 @@ export function getAllTimezonesOrdered(date?: Date): TimeZone[] {
timeZone: iana,
timeZoneName: 'long',
});
const displayName = displayFormatter.formatToParts(now).find(part => part.type === 'timeZoneName')?.value || iana;
const displayName = displayFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || iana;

return {
name: createTimezoneDisplayName(iana, offset, now),
name: createTimezoneDisplayName(iana, offset, date),
offset,
displayName,
iana,
cityName: extractCityName(iana),
abbreviation: getTimezoneAbbreviation(displayName, iana, now),
abbreviation: getTimezoneAbbreviation(displayName, iana, date),
};
});

// Get user's timezone offset
// Get user's timezone offset for this date
const userTimezoneData = timezoneData.find(tz => tz.iana === userTimezone);
const userOffset = userTimezoneData?.offset || 0;

// Sort timezones: start with user's timezone, then go around the world
const sortedTimezones = timezoneData.sort((a, b) => {
return timezoneData.sort((a, b) => {
// Calculate distance from user's timezone, wrapping around
const getDistance = (offset: number): number => {
let distance = offset - userOffset;
Expand All @@ -1307,18 +1339,84 @@ export function getAllTimezonesOrdered(date?: Date): TimeZone[] {
}
return a.name.localeCompare(b.name);
});
}

/**
* Determine which timezone set to use based on the actual DST status for a given date.
* This checks the user's timezone offset for the given date and compares it to the
* July and December timezone data to determine which set matches.
*/
function getTimezoneSetForDate(date: Date, processedData: ProcessedTimezoneData): TimeZone[] {
const userTimezone = processedData.userTimezone;

// Get the actual offset for the user's timezone on the given date
const formatter = new Intl.DateTimeFormat('en', {
timeZone: userTimezone,
timeZoneName: 'longOffset',
});

const offsetStr = formatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || '+00:00';

// Parse offset string like "GMT+05:30" or "GMT-08:00"
const offsetMatch = offsetStr.match(/GMT([+-])(\d{2}):(\d{2})/);
let currentOffset = 0;
if (offsetMatch && offsetMatch[2] && offsetMatch[3]) {
const sign = offsetMatch[1] === '+' ? 1 : -1;
const hours = parseInt(offsetMatch[2], 10);
const minutes = parseInt(offsetMatch[3], 10);
currentOffset = sign * (hours + minutes / 60);
}

// Find the user's timezone in both July and December sets
const julyUserTz = processedData.julyTimeZones.find(tz => tz.iana === userTimezone);
const decemberUserTz = processedData.decemberTimeZones.find(tz => tz.iana === userTimezone);

// Compare the current offset to determine which set to use
if (julyUserTz && Math.abs(currentOffset - julyUserTz.offset) < 0.1) {
return processedData.julyTimeZones;
} else if (decemberUserTz && Math.abs(currentOffset - decemberUserTz.offset) < 0.1) {
return processedData.decemberTimeZones;
}

// Fallback: if we can't determine, use July set as default
return processedData.julyTimeZones;
}

/**
* Get all supported timezones that the browser knows about,
* ordered starting with the user's current timezone and going around the world
* @param date Optional date to calculate timezone offsets for (defaults to current date)
* @returns Array of timezone objects ordered from user's timezone around the globe
*/
export function getAllTimezonesOrdered(date?: Date): TimeZone[] {
const now = date || new Date();
const currentYear = now.getFullYear();

// Check if we have cached data for this year
if (!processedTimezoneCache.has(currentYear)) {
console.log('Initializing timezone data for year', currentYear);
const processedData = initializeTimezoneData(currentYear);
processedTimezoneCache.set(currentYear, processedData);
}

// Get cached data for this year
const processedData = processedTimezoneCache.get(currentYear);
if (!processedData) {
throw new Error(`Failed to get timezone data for year ${currentYear}`);
}

return sortedTimezones;
// Return appropriate timezone set based on actual DST status for the date
return getTimezoneSetForDate(now, processedData);
}

/**
* Get timezone variations for a given IANA identifier using fixed dates
* Returns both summer (June 1st) and winter (December 31st) variations
* Returns both July (June 1st) and December (December 31st) variations
Copy link

Copilot AI Aug 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation is inconsistent - it mentions 'July (June 1st)' which is confusing. It should either say 'July (July 1st)' to match the new caching implementation or 'Summer (June 1st)' to match the actual implementation in this function.

Suggested change
* Returns both July (June 1st) and December (December 31st) variations
* Returns both summer (June 1st) and winter (December 31st) variations

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation is inconsistent - it mentions 'July (June 1st)' which is confusing. It should either say 'July (July 1st)' to match the new caching implementation or 'Summer (June 1st)' to match the actual implementation in this function.

@copilot please ensure we're using June 1st everywhere and that we rename the variables and comments appropriately

*/
function getTimezoneVariations(iana: string, year: number = new Date().getFullYear()): TimeZone[] {
const variations: TimeZone[] = [];

// Use June 1st for summer time and December 31st for winter time
// Use June 1st for July time and December 31st for December time
Copy link

Copilot AI Aug 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is misleading as it mentions 'June 1st for July time' but the code actually uses June 1st (summerDate) for summer calculations. The comment should be updated to accurately reflect the dates being used or the variable names should be updated to match the comment.

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is misleading as it mentions 'June 1st for July time' but the code actually uses June 1st (summerDate) for summer calculations. The comment should be updated to accurately reflect the dates being used or the variable names should be updated to match the comment.

@copilot please address

const summerDate = new Date(year, 5, 1); // June 1st
const winterDate = new Date(year, 11, 31); // December 31st

Expand Down