Last active
April 8, 2022 15:15
-
-
Save TehShrike/8165ec7fcc7216166ff3b903691496ea to your computer and use it in GitHub Desktop.
get utc offset
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import tz_tokenize_date from './tz_tokenize_date.mjs' | |
const ZERO_HOUR_UTC = `T00:00:00.000Z` | |
const ZERO_HOUR_MINUS_TWENTY_HOURS_UTC = `T00:00:00.000-20:00` | |
const ZERO_HOUR_PLUS_TWENTY_HOURS_UTC = `T00:00:00.000+20:00` | |
export const day_to_offset_at_start_of_day = (iana_timezone_string, iso_day_string) => { | |
const valid_offset = [ | |
iso_day_string + ZERO_HOUR_MINUS_TWENTY_HOURS_UTC, | |
iso_day_string + ZERO_HOUR_UTC, | |
iso_day_string + ZERO_HOUR_PLUS_TWENTY_HOURS_UTC, | |
].map( | |
datetime_string => get_timezone_offset_for_point_in_time(iana_timezone_string, new Date(datetime_string)) | |
).find( | |
offset_ms => validate_offset_for_start_of_day(iana_timezone_string, iso_day_string, offset_ms) | |
) | |
if (typeof valid_offset !== `number`) { | |
throw new Error(`No valid offset found for start of day on ${iso_day_string} at ${iana_timezone_string}`) | |
} | |
return valid_offset | |
} | |
// from https://github.com/bsvetlik/date-fns-tz/blob/eb2bb6209931c5abe1cfcdf2faaa41de5493648a/src/_lib/tzParseTimezone/index.js#L86-L98 | |
export const get_timezone_offset_for_point_in_time = (iana_timezone_string, date_object) => { | |
const [ year, month, day, hour, minute, second ] = tz_tokenize_date( | |
date_object, | |
iana_timezone_string | |
) | |
const asUTC = Date.UTC(year, month - 1, day, hour, minute, second, date_object.getMilliseconds()) | |
return asUTC - date_object.getTime() | |
} | |
export const validate_offset_for_start_of_day = (iana_timezone_string, iso_day_string, offset_ms) => { | |
const point_in_time_guess = new Date(`${iso_day_string}T00:00:00${ms_to_offset_string(offset_ms)}`) | |
const offset_at_that_point_in_time = get_timezone_offset_for_point_in_time(iana_timezone_string, point_in_time_guess) | |
return offset_at_that_point_in_time === offset_ms | |
} | |
const MS_IN_MINUTE = 60 * 1000 | |
const pad2 = number => number.toString().padStart(2, `0`) | |
export const ms_to_offset_string = ms => { | |
const negative = ms < 0 | |
const total_minutes = Math.floor(Math.abs(ms) / MS_IN_MINUTE) | |
const hours_floored = Math.floor(total_minutes / 60) | |
return `${negative ? `-` : `+`}${pad2(hours_floored)}:${pad2(total_minutes % 60)}` | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import test_cases_individually from './test_cases_individually.mjs' | |
import * as assert from 'uvu/assert' | |
import { | |
get_timezone_offset_for_point_in_time, | |
day_to_offset_at_start_of_day, | |
ms_to_offset_string, | |
validate_offset_for_start_of_day, | |
} from './get_utc_offset.mjs' | |
const MS_IN_HOUR = 60 * 60 * 1000 | |
test_cases_individually( | |
`get_timezone_offset_for_point_in_time`, | |
[ | |
[ `15 minutes before Australia changes from 02:00 to 03:00`, `Australia/Melbourne`, new Date(`2020-10-03T15:45:00.000Z`), 10 * MS_IN_HOUR ], | |
[ `0 hour when Australia changes from 02:00 to 03:00`, `Australia/Melbourne`, new Date(`2020-10-03T16:00:00.000Z`), 11 * MS_IN_HOUR ], | |
[ `15 minutes after Australia changes from 02:00 to 03:00`, `Australia/Melbourne`, new Date(`2020-10-03T16:15:00.000Z`), 11 * MS_IN_HOUR ], | |
[ `America/New_York`, `America/New_York`, new Date(`2014-10-25T13:46:20Z`), -4 * MS_IN_HOUR ], | |
[ `Europe/Paris`, `Europe/Paris`, new Date(`2014-10-25T13:46:20Z`), 2 * MS_IN_HOUR ], | |
], | |
([ name ]) => name, | |
([ , input_tz, input_date, expected ]) => assert.is(get_timezone_offset_for_point_in_time(input_tz, input_date), expected) | |
) | |
test_cases_individually( | |
`day_to_offset_at_start_of_day`, | |
[ | |
[ `2020-03-08`, `America/Chicago`, -6 * MS_IN_HOUR ], | |
[ `2020-03-09`, `America/Chicago`, -5 * MS_IN_HOUR ], | |
[ `2020-11-01`, `America/Chicago`, -5 * MS_IN_HOUR ], | |
[ `2020-11-02`, `America/Chicago`, -6 * MS_IN_HOUR ], | |
[ `2020-10-04`, `Australia/Melbourne`, 10 * MS_IN_HOUR ], | |
[ `2020-10-05`, `Australia/Melbourne`, 11 * MS_IN_HOUR ], | |
[ `2020-03-08`, `Europe/London`, 0 ], | |
], | |
([ input_day, input_timezone, expected_output ]) => `${input_timezone}: ${input_day} -> ${expected_output}}`, | |
([ input_day, input_timezone, expected_output ]) => assert.is(day_to_offset_at_start_of_day(input_timezone, input_day), expected_output) | |
) | |
test_cases_individually( | |
`validate_offset_for_start_of_day`, | |
[ | |
[ `2020-03-08`, `America/Chicago`, -6 * MS_IN_HOUR, true ], | |
[ `2020-03-09`, `America/Chicago`, -5 * MS_IN_HOUR, true ], | |
[ `2020-11-01`, `America/Chicago`, -5 * MS_IN_HOUR, true ], | |
[ `2020-11-02`, `America/Chicago`, -6 * MS_IN_HOUR, true ], | |
[ `2020-03-08`, `America/Chicago`, -5 * MS_IN_HOUR, false ], | |
[ `2020-03-09`, `America/Chicago`, -6 * MS_IN_HOUR, false ], | |
[ `2020-11-01`, `America/Chicago`, -6 * MS_IN_HOUR, false ], | |
[ `2020-11-02`, `America/Chicago`, -5 * MS_IN_HOUR, false ], | |
], | |
([ input_day, input_timezone, offset_ms, expected_output ]) => `validate_offset_for_start_of_day(${offset_ms}) should be ${expected_output} at the start of ${input_day} in ${input_timezone}`, | |
([ input_day, input_timezone, offset_ms, expected_output ]) => assert.is(validate_offset_for_start_of_day(input_timezone, input_day, offset_ms), expected_output) | |
) | |
test_cases_individually( | |
`ms_to_offset_string`, | |
[ | |
[ -1 * MS_IN_HOUR, `-01:00` ], | |
[ 2 * MS_IN_HOUR, `+02:00` ], | |
[ -14 * MS_IN_HOUR, `-14:00` ], | |
[ 14 * MS_IN_HOUR, `+14:00` ], | |
[ -3 * MS_IN_HOUR - 30 * 60 * 1000, `-03:30` ], | |
[ 3 * MS_IN_HOUR + 30 * 60 * 1000, `+03:30` ], | |
], | |
([ ms ]) => ms.toString(), | |
([ ms, expected ]) => assert.is(ms_to_offset_string(ms), expected) | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// lifted/trimmed from https://github.com/marnusw/date-fns-tz/blob/1d871f2c7ca76733552d5e22371a5fedcbe3c49f/src/_lib/tzTokenizeDate/index.js | |
/** | |
* Returns the [year, month, day, hour, minute, seconds] tokens of the provided | |
* `date` as it will be rendered in the `timeZone`. | |
*/ | |
export default (date, timeZone) => partsOffset(getDateTimeFormat(timeZone), date) | |
const typeToPos = { | |
year: 0, | |
month: 1, | |
day: 2, | |
hour: 3, | |
minute: 4, | |
second: 5, | |
} | |
function partsOffset(dtf, date) { | |
const formatted = dtf.formatToParts(date) | |
const filled = [] | |
for (let i = 0; i < formatted.length; i++) { | |
const pos = typeToPos[formatted[i].type] | |
if (pos >= 0) { | |
filled[pos] = parseInt(formatted[i].value, 10) | |
} | |
} | |
return filled | |
} | |
// Get a cached Intl.DateTimeFormat instance for the IANA `timeZone`. This can be used | |
// to get deterministic local date/time output according to the `en-US` locale which | |
// can be used to extract local time parts as necessary. | |
const dtfCache = {} | |
function getDateTimeFormat(timeZone) { | |
if (!dtfCache[timeZone]) { | |
dtfCache[timeZone] = new Intl.DateTimeFormat(`en-US`, { | |
hourCycle: `h23`, | |
timeZone, | |
year: `numeric`, | |
month: `2-digit`, | |
day: `2-digit`, | |
hour: `2-digit`, | |
minute: `2-digit`, | |
second: `2-digit`, | |
}) | |
} | |
return dtfCache[timeZone] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment