date-fns
Version:
Modern JavaScript date utility library
143 lines (132 loc) • 4.66 kB
JavaScript
;
exports.getDstTransitions = getDstTransitions;
exports.getTzOffsetTransitions = getTzOffsetTransitions;
/**
* Fetch the start and end of DST for the local time
* zone in a given year.
* We'll assume that DST start & end are the first
* forward and the last back transitions in the year,
* except transitions in Jan or Dec which are likely
* to be permanent TZ changes rather than DST changes.
* @param year
* @returns object with two Date-valued properties:
* - `start` is the first instant of DST in the Spring,
* or undefined if there's no DST in this year.
* - `end` is the first instant of standard time
* in the Fall, or undefined if there's no DST in
* this year.
*/
function getDstTransitions(year) {
const result = {
start: undefined,
end: undefined,
};
const transitions = getTzOffsetTransitions(year);
for (let i = 0; i < transitions.length; i++) {
const t = transitions[i];
const month = t.date.getMonth();
if (month > 0 && month < 11) {
if (t.type === "forward") result.start = t.date;
if (t.type === "back" && !result.end) result.end = t.date;
}
}
return result;
}
function isValidDate(date) {
return date instanceof Date && !isNaN(date.getTime());
}
const MINUTE = 1000 * 60;
function firstTickInLocalDay(date) {
const dateNumber = date.getDate();
let prev = date;
let d = date;
do {
prev = d;
d = new Date(d.getTime() - MINUTE);
} while (dateNumber === d.getDate());
return prev;
}
function fiveMinutesLater(date) {
return new Date(date.getTime() + 5 * MINUTE);
}
function oneDayLater(date) {
const d = new Date(date);
d.setDate(d.getDate() + 1);
return firstTickInLocalDay(d);
}
function previousTickTimezoneOffset(date) {
const d = new Date(date.getTime() - 1);
return d.getTimezoneOffset();
}
/**
* Fetch all timezone-offset transitions in a given
* year. These are almost always DST transitions,
* but sometimes there are non-DST changes, e.g.
* when a country changes its time zone
* @param year
* @returns array of objects, each with the following
* propeerties:
* - `date` - a `Date` representing the first instant
* when the new timezone offset is effective.
* - `type` - either `forward` for skippnig time like
* the Spring transition to DST.
* - `before` - the timezone offset before the transition.
* For example, the UTC-0400 offset will return -240.
* To match how times are displayed in ISO 8601 format,
* the sign of this value is reversed from the return
* value of `Date.getTimezoneOffset`.
* - `after` - the timezone offset after the transition.
* Examples and caveats are the same as `before`.
*/
function getTzOffsetTransitions(year) {
// start at the end of the previous day
let date = firstTickInLocalDay(new Date(year, 0, 1));
if (!isValidDate(date)) {
throw new Error("Invalid Date");
}
let baseTzOffset = previousTickTimezoneOffset(date);
const transitions = [];
do {
let tzOffset = date.getTimezoneOffset();
if (baseTzOffset !== tzOffset) {
if (tzOffset !== previousTickTimezoneOffset(date)) {
// Transition is the first tick of a local day.
transitions.push({
date: date,
type: tzOffset < baseTzOffset ? "forward" : "back",
before: -baseTzOffset,
after: -tzOffset,
});
baseTzOffset = tzOffset;
} else {
// transition was not at the start of the day, so it must have happened
// yesterday. Back up one day and find the minute where it happened.
let transitionDate = new Date(date.getTime());
transitionDate.setDate(transitionDate.getDate() - 1);
// Iterate through each 5 mins of the day until we find a transition.
// TODO: this could be optimized to search hours then minutes or by or
// by using a binary search.
const dayNumber = transitionDate.getDate();
while (
isValidDate(transitionDate) &&
transitionDate.getDate() === dayNumber
) {
tzOffset = transitionDate.getTimezoneOffset();
if (baseTzOffset !== tzOffset) {
transitions.push({
date: transitionDate,
type: tzOffset < baseTzOffset ? "forward" : "back",
before: -baseTzOffset,
after: -tzOffset,
});
baseTzOffset = tzOffset;
break; // assuming only 1 transition per day
}
transitionDate = fiveMinutesLater(transitionDate);
}
}
}
date = oneDayLater(date);
} while (date.getFullYear() === year);
return transitions;
}