Skip to content

Instantly share code, notes, and snippets.

@TehShrike
Last active April 8, 2022 15:15
Show Gist options
  • Save TehShrike/8165ec7fcc7216166ff3b903691496ea to your computer and use it in GitHub Desktop.
Save TehShrike/8165ec7fcc7216166ff3b903691496ea to your computer and use it in GitHub Desktop.
get utc offset
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)}`
}
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)
)
// 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