( dateInput: string | Date, timeStr: string, timezone = 'UTC' )
| 210 | * @returns Date object representing the absolute point in time (UTC). |
| 211 | */ |
| 212 | export function createDateWithTimezone( |
| 213 | dateInput: string | Date, |
| 214 | timeStr: string, |
| 215 | timezone = 'UTC' |
| 216 | ): Date { |
| 217 | try { |
| 218 | // 1. Parse the base date and target time |
| 219 | const baseDate = typeof dateInput === 'string' ? new Date(dateInput) : new Date(dateInput) |
| 220 | const [targetHours, targetMinutes] = parseTimeString(timeStr) |
| 221 | |
| 222 | // Ensure baseDate reflects the date part only, setting time to 00:00:00 in UTC |
| 223 | // This prevents potential issues if dateInput string includes time/timezone info. |
| 224 | const year = baseDate.getUTCFullYear() |
| 225 | const monthIndex = baseDate.getUTCMonth() // 0-based |
| 226 | const day = baseDate.getUTCDate() |
| 227 | |
| 228 | // 2. Create a tentative UTC Date object using the target date and time components |
| 229 | // This assumes, for a moment, that the target H:M were meant for UTC. |
| 230 | const tentativeUTCDate = new Date( |
| 231 | Date.UTC(year, monthIndex, day, targetHours, targetMinutes, 0) |
| 232 | ) |
| 233 | |
| 234 | // 3. If the target timezone is UTC, we're done. |
| 235 | if (timezone === 'UTC') { |
| 236 | return tentativeUTCDate |
| 237 | } |
| 238 | |
| 239 | // 4. Format the tentative UTC date into the target timezone's local time components. |
| 240 | // Use 'en-CA' locale for unambiguous YYYY-MM-DD and 24-hour format. |
| 241 | const formatter = new Intl.DateTimeFormat('en-CA', { |
| 242 | timeZone: timezone, |
| 243 | year: 'numeric', |
| 244 | month: '2-digit', |
| 245 | day: '2-digit', |
| 246 | hour: '2-digit', // Use 2-digit for consistency |
| 247 | minute: '2-digit', |
| 248 | second: '2-digit', |
| 249 | hourCycle: 'h23', // Use 24-hour format (00-23) |
| 250 | }) |
| 251 | |
| 252 | const parts = formatter.formatToParts(tentativeUTCDate) |
| 253 | const getPart = (type: Intl.DateTimeFormatPartTypes) => |
| 254 | parts.find((p) => p.type === type)?.value |
| 255 | |
| 256 | const formattedYear = Number.parseInt(getPart('year') || '0', 10) |
| 257 | const formattedMonth = Number.parseInt(getPart('month') || '0', 10) // 1-based |
| 258 | const formattedDay = Number.parseInt(getPart('day') || '0', 10) |
| 259 | const formattedHour = Number.parseInt(getPart('hour') || '0', 10) |
| 260 | const formattedMinute = Number.parseInt(getPart('minute') || '0', 10) |
| 261 | |
| 262 | // Create a Date object representing the local time *in the target timezone* |
| 263 | // when the tentative UTC date occurs. |
| 264 | // Note: month needs to be adjusted back to 0-based for Date.UTC() |
| 265 | const actualLocalTimeInTargetZone = Date.UTC( |
| 266 | formattedYear, |
| 267 | formattedMonth - 1, |
| 268 | formattedDay, |
| 269 | formattedHour, |
no test coverage detected