"use strict"; // Day-number kernel: Howard Hinnant, "chrono-Compatible Low-Level Date Algorithms", // https://howardhinnant.github.io/date_algorithms.html (days_from_civil / civil_from_days). // Fixed-offset arithmetic only: no time zone database, no host clock, no Date.now(). function daysFromCivil(y, m, d) { y = m <= 2 ? y - 1 : y; const era = Math.floor((y >= 0 ? y : y - 399) / 400); const yoe = y - era * 400; const doy = Math.floor((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5) + d - 1; const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy; return era * 146097 + doe - 719468; } function civilFromDays(z) { z += 719468; const era = Math.floor((z >= 0 ? z : z - 146096) / 146097); const doe = z - era * 146097; const yoe = Math.floor((doe - Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096)) / 365); const y = yoe + era * 400; const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100)); const mp = Math.floor((5 * doy + 2) / 153); const d = doy - Math.floor((153 * mp + 2) / 5) + 1; const m = mp + (mp < 10 ? 3 : -9); const yy = y + (m <= 2 ? 1 : 0); return { year: yy, month: m, day: d }; } function isLeapGregorian(y) { return (y % 4 === 0 && y % 100 !== 0) || (y % 400 === 0); } function daysInMonth(y, m) { const dim = [31, isLeapGregorian(y) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; return dim[m - 1]; } function parseISODate(s, label) { const m = /^(-?\d{4})-(\d{2})-(\d{2})$/.exec(String(s).trim()); if (!m) throw new Error((label || "date") + " must be an ISO 8601 date YYYY-MM-DD, got: " + s); const year = parseInt(m[1], 10), month = parseInt(m[2], 10), day = parseInt(m[3], 10); if (month < 1 || month > 12) throw new Error((label || "date") + " month out of range 1-12"); if (day < 1 || day > daysInMonth(year, month)) throw new Error((label || "date") + " day out of range for that month"); return { year, month, day }; } function parseTime(s) { const m = /^(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(String(s).trim()); if (!m) throw new Error("time must be HH:MM or HH:MM:SS, got: " + s); const hh = parseInt(m[1], 10), mm = parseInt(m[2], 10), ss = m[3] ? parseInt(m[3], 10) : 0; if (hh > 23 || mm > 59 || ss > 59) throw new Error("time fields out of range: " + s); return hh * 3600 + mm * 60 + ss; } function parseOffset(s) { const m = /^([+-])(\d{2}):(\d{2})$/.exec(String(s).trim()); if (!m) throw new Error("offset must be like -05:00 or +01:00, got: " + s); const sign = m[1] === "-" ? -1 : 1; const hh = parseInt(m[2], 10), mm = parseInt(m[3], 10); if (hh > 23 || mm > 59) throw new Error("offset fields out of range: " + s); return sign * (hh * 3600 + mm * 60); } function pad2(n) { return String(n).padStart(2, "0"); } function toISODate(y, m, d) { return String(y).padStart(4, "0") + "-" + pad2(m) + "-" + pad2(d); } function localFor(utcTotalSeconds, offsetSeconds, originalDayNumber) { const targetTotal = utcTotalSeconds + offsetSeconds; const dayNumber = Math.floor(targetTotal / 86400); const secondsIntoDay = targetTotal - dayNumber * 86400; const r = civilFromDays(dayNumber); const hh = Math.floor(secondsIntoDay / 3600); const mm = Math.floor((secondsIntoDay % 3600) / 60); const ss = secondsIntoDay % 60; const shift = dayNumber - originalDayNumber; const label = shift === 0 ? "same day" : shift > 0 ? "next day" : "previous day"; return toISODate(r.year, r.month, r.day) + " " + pad2(hh) + ":" + pad2(mm) + ":" + pad2(ss) + " (" + label + ")"; } module.exports = { slug: "meeting-time-across-zones", title: "Meeting time across three fixed offsets", deck_category: "timezones", description: "Converts a single UTC date and time into local wall-clock times at three fixed UTC offsets, noting any date shift.", inputs: [ { name: "utc_date", type: "string", format: "date", required: true, description: "ISO 8601 UTC date of the meeting instant, YYYY-MM-DD" }, { name: "utc_time", type: "string", required: true, description: "UTC wall-clock time of the meeting instant, HH:MM or HH:MM:SS" }, { name: "offset_a", type: "string", required: true, description: "First fixed UTC offset, e.g. -05:00" }, { name: "offset_b", type: "string", required: true, description: "Second fixed UTC offset, e.g. +01:00" }, { name: "offset_c", type: "string", required: true, description: "Third fixed UTC offset, e.g. +09:00" } ], outputs: [ { name: "local_a", description: "Local date and time at offset_a, with same/next/previous day flag" }, { name: "local_b", description: "Local date and time at offset_b, with same/next/previous day flag" }, { name: "local_c", description: "Local date and time at offset_c, with same/next/previous day flag" } ], steps: [ "Parse utc_date and utc_time into a day-number (Hinnant days_from_civil, cited above) and seconds-since-midnight, forming a single UTC instant in total seconds.", "Parse each of offset_a, offset_b, offset_c as a signed HH:MM offset from UTC, in seconds.", "For each offset: local_total_seconds = utc_total_seconds + offset_seconds; local_day_number = floor(local_total_seconds / 86400); seconds_into_day = local_total_seconds - local_day_number * 86400.", "Convert local_day_number back to a civil date and seconds_into_day back to HH:MM:SS.", "Compare local_day_number to the original UTC day-number to report same day, next day, or previous day." ], constants: [], sources: [ { kind: "url", value: "https://howardhinnant.github.io/date_algorithms.html", title: "chrono-Compatible Low-Level Date Algorithms", publisher: "Howard Hinnant", retrieved_utc: "2026-09-30T19:41:35Z", note: "day-number kernel used for the date-rollover arithmetic; no time zone database is used" } ], disclaimer: null, questionText(inputs) { return "If a meeting is at " + inputs.utc_date + " " + inputs.utc_time + " UTC, what is the local time at offsets " + inputs.offset_a + ", " + inputs.offset_b + ", and " + inputs.offset_c + "?"; }, run(inputs) { const d = parseISODate(inputs.utc_date, "utc_date"); const localSeconds = parseTime(inputs.utc_time); const dayNumber = daysFromCivil(d.year, d.month, d.day); const utcTotal = dayNumber * 86400 + localSeconds; const offA = parseOffset(inputs.offset_a); const offB = parseOffset(inputs.offset_b); const offC = parseOffset(inputs.offset_c); const localA = localFor(utcTotal, offA, dayNumber); const localB = localFor(utcTotal, offB, dayNumber); const localC = localFor(utcTotal, offC, dayNumber); const answer = inputs.offset_a + ": " + localA + "; " + inputs.offset_b + ": " + localB + "; " + inputs.offset_c + ": " + localC; return { answer, values: { local_a: localA, local_b: localB, local_c: localC } }; }, examples: [ { name: "worked example", inputs: { utc_date: "2026-09-30", utc_time: "18:30:00", offset_a: "-05:00", offset_b: "+01:00", offset_c: "+09:00" }, expected: { answer: "-05:00: 2026-09-30 13:30:00 (same day); +01:00: 2026-09-30 19:30:00 (same day); +09:00: 2026-10-01 03:30:00 (next day)" }, check: "18:30 UTC minus 5h = 13:30 same day; plus 1h = 19:30 same day; plus 9h = 27:30 = 03:30 the next day" }, { name: "test vector: all same day", inputs: { utc_date: "2026-01-01", utc_time: "12:00:00", offset_a: "+00:00", offset_b: "+02:00", offset_c: "-03:00" }, expected: { answer: "+00:00: 2026-01-01 12:00:00 (same day); +02:00: 2026-01-01 14:00:00 (same day); -03:00: 2026-01-01 09:00:00 (same day)" }, check: "noon UTC stays within the same calendar date at +2h and -3h" }, { name: "test vector: previous day rollover", inputs: { utc_date: "2026-06-15", utc_time: "01:00:00", offset_a: "-08:00", offset_b: "+00:00", offset_c: "+05:30" }, expected: { answer: "-08:00: 2026-06-14 17:00:00 (previous day); +00:00: 2026-06-15 01:00:00 (same day); +05:30: 2026-06-15 06:30:00 (same day)" }, check: "01:00 UTC minus 8h = 17:00 the previous day" } ] };