chronoshift
Version:
A tiny library for shifting time with timezones
326 lines (301 loc) • 8.78 kB
text/typescript
/*
* Copyright 2014-2015 Metamarkets Group Inc.
* Copyright 2015-2019 Imply Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { fromDate, startOfWeek } from '@internationalized/date';
import type { Timezone } from '../timezone/timezone';
export type AlignFn = (dt: Date, tz: Timezone) => Date;
export type ShiftFn = (dt: Date, tz: Timezone, step: number) => Date;
export type RoundFn = (dt: Date, roundTo: number, tz: Timezone) => Date;
export interface TimeShifterNoCeil {
canonicalLength: number;
siblings: number;
floor: AlignFn;
round: RoundFn;
shift: ShiftFn;
}
export interface TimeShifter extends TimeShifterNoCeil {
ceil: AlignFn;
}
function adjustDay(day: number): number {
return (day + 6) % 7;
}
function floorTo(n: number, roundTo: number): number {
return Math.floor(n / roundTo) * roundTo;
}
function timeShifterFiller(tm: TimeShifterNoCeil): TimeShifter {
const { floor, shift } = tm;
return {
...tm,
ceil: (dt: Date, tz: Timezone) => {
const floored = floor(dt, tz);
if (floored.valueOf() === dt.valueOf()) return dt; // Just like ceil(3) is 3 and not 4
return shift(floored, tz, 1);
},
};
}
export const second = timeShifterFiller({
canonicalLength: 1000,
siblings: 60,
floor: (dt, _tz) => {
// Seconds do not actually need a timezone because all timezones align on seconds... for now...
dt = new Date(dt.valueOf());
dt.setUTCMilliseconds(0);
return dt;
},
round: (dt, roundTo, _tz) => {
const cur = dt.getUTCSeconds();
const adj = floorTo(cur, roundTo);
if (cur !== adj) dt.setUTCSeconds(adj);
return dt;
},
shift: (dt, _tz, step) => {
dt = new Date(dt.valueOf());
dt.setUTCSeconds(dt.getUTCSeconds() + step);
return dt;
},
});
export const minute = timeShifterFiller({
canonicalLength: 60000,
siblings: 60,
floor: (dt, _tz) => {
// Minutes do not actually need a timezone because all timezones align on minutes... for now...
dt = new Date(dt.valueOf());
dt.setUTCSeconds(0, 0);
return dt;
},
round: (dt, roundTo, _tz) => {
const cur = dt.getUTCMinutes();
const adj = floorTo(cur, roundTo);
if (cur !== adj) dt.setUTCMinutes(adj);
return dt;
},
shift: (dt, _tz, step) => {
dt = new Date(dt.valueOf());
dt.setUTCMinutes(dt.getUTCMinutes() + step);
return dt;
},
});
// Movement by hour is tz independent because in every timezone an hour is 60 min
function hourMove(dt: Date, _tz: Timezone, step: number) {
dt = new Date(dt.valueOf());
dt.setUTCHours(dt.getUTCHours() + step);
return dt;
}
export const hour = timeShifterFiller({
canonicalLength: 3600000,
siblings: 24,
floor: (dt, tz) => {
if (tz.isUTC()) {
dt = new Date(dt.valueOf());
dt.setUTCMinutes(0, 0, 0);
return dt;
} else {
return fromDate(dt, tz.toString()).set({ second: 0, minute: 0, millisecond: 0 }).toDate();
}
},
shift: hourMove,
round: (dt, roundTo, tz) => {
if (tz.isUTC()) {
const cur = dt.getUTCHours();
const adj = floorTo(cur, roundTo);
if (cur !== adj) dt.setUTCHours(adj);
} else {
const cur = fromDate(dt, tz.toString()).hour;
const adj = floorTo(cur, roundTo);
if (cur !== adj) return hourMove(dt, tz, adj - cur);
}
return dt;
},
});
export const day = timeShifterFiller({
canonicalLength: 24 * 3600000,
siblings: 30,
floor: (dt, tz) => {
if (tz.isUTC()) {
dt = new Date(dt.valueOf());
dt.setUTCHours(0, 0, 0, 0);
return dt;
} else {
return fromDate(dt, tz.toString())
.set({ hour: 0, second: 0, minute: 0, millisecond: 0 })
.toDate();
}
},
shift: (dt, tz, step) => {
if (tz.isUTC()) {
dt = new Date(dt.valueOf());
dt.setUTCDate(dt.getUTCDate() + step);
return dt;
} else {
return fromDate(dt, tz.toString()).add({ days: step }).toDate();
}
},
round: (dt, roundTo, tz) => {
// Note that we offset day by 1 to make it zero indexed just like d3 does: https://github.com/d3/d3-time/blob/main/src/day.js#L20
if (tz.isUTC()) {
const cur = dt.getUTCDate() - 1;
const adj = floorTo(cur, roundTo);
if (cur !== adj) dt.setUTCDate(adj + 1);
} else {
const cur = fromDate(dt, tz.toString()).day - 1;
const adj = floorTo(cur, roundTo);
if (cur !== adj)
return fromDate(dt, tz.toString())
.set({ day: adj + 1 })
.toDate();
}
return dt;
},
});
export const week = timeShifterFiller({
canonicalLength: 7 * 24 * 3600000,
siblings: 52,
floor: (dt, tz) => {
if (tz.isUTC()) {
dt = new Date(dt.valueOf());
dt.setUTCHours(0, 0, 0, 0);
dt.setUTCDate(dt.getUTCDate() - adjustDay(dt.getUTCDay()));
} else {
const zd = fromDate(dt, tz.toString());
return startOfWeek(
zd.set({ hour: 0, second: 0, minute: 0, millisecond: 0 }),
'fr-FR', // We want the week to start on Monday
).toDate();
}
return dt;
},
shift: (dt, tz, step) => {
if (tz.isUTC()) {
dt = new Date(dt.valueOf());
dt.setUTCDate(dt.getUTCDate() + step * 7);
return dt;
} else {
return fromDate(dt, tz.toString()).add({ weeks: step }).toDate();
}
},
round: () => {
throw new Error('missing week round');
},
});
function monthShift(dt: Date, tz: Timezone, step: number) {
if (tz.isUTC()) {
dt = new Date(dt.valueOf());
dt.setUTCMonth(dt.getUTCMonth() + step);
return dt;
} else {
return fromDate(dt, tz.toString()).add({ months: step }).toDate();
}
}
export const month = timeShifterFiller({
canonicalLength: 30 * 24 * 3600000,
siblings: 12,
floor: (dt, tz) => {
if (tz.isUTC()) {
dt = new Date(dt.valueOf());
dt.setUTCHours(0, 0, 0, 0);
dt.setUTCDate(1);
return dt;
} else {
return fromDate(dt, tz.toString())
.set({ day: 1, hour: 0, second: 0, minute: 0, millisecond: 0 })
.toDate();
}
},
round: (dt, roundTo, tz) => {
if (tz.isUTC()) {
const cur = dt.getUTCMonth();
const adj = floorTo(cur, roundTo);
if (cur !== adj) dt.setUTCMonth(adj);
} else {
const cur = fromDate(dt, tz.toString()).month - 1; // Needs to be zero indexed
const adj = floorTo(cur, roundTo);
if (cur !== adj) return monthShift(dt, tz, adj - cur);
}
return dt;
},
shift: monthShift,
});
function yearShift(dt: Date, tz: Timezone, step: number) {
if (tz.isUTC()) {
dt = new Date(dt.valueOf());
dt.setUTCFullYear(dt.getUTCFullYear() + step);
return dt;
} else {
return fromDate(dt, tz.toString()).add({ years: step }).toDate();
}
}
export const year = timeShifterFiller({
canonicalLength: 365 * 24 * 3600000,
siblings: 1000,
floor: (dt, tz) => {
if (tz.isUTC()) {
dt = new Date(dt.valueOf());
dt.setUTCHours(0, 0, 0, 0);
dt.setUTCMonth(0, 1);
return dt;
} else {
return fromDate(dt, tz.toString())
.set({ month: 1, day: 1, hour: 0, second: 0, minute: 0, millisecond: 0 })
.toDate();
}
},
round: (dt, roundTo, tz) => {
if (tz.isUTC()) {
const cur = dt.getUTCFullYear();
const adj = floorTo(cur, roundTo);
if (cur !== adj) dt.setUTCFullYear(adj);
} else {
const cur = fromDate(dt, tz.toString()).year;
const adj = floorTo(cur, roundTo);
if (cur !== adj) return yearShift(dt, tz, adj - cur);
}
return dt;
},
shift: yearShift,
});
/**
* @deprecated use `new Duration('P${numDays}D')` instead
*/
export function getMultiDayShifter(numDays: number) {
return timeShifterFiller({
canonicalLength: 24 * 3600000 * numDays,
siblings: 1, // This is a no-op dummy
floor: day.floor,
shift: (dt, tz, step) => day.shift(dt, tz, step * numDays),
round: () => {
throw new Error('missing day round');
},
});
}
export interface Shifters {
second: TimeShifter;
minute: TimeShifter;
hour: TimeShifter;
day: TimeShifter;
week: TimeShifter;
month: TimeShifter;
year: TimeShifter;
[key: string]: TimeShifter;
}
export const shifters: Shifters = {
second,
minute,
hour,
day,
week,
month,
year,
};