@procedures/ics 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +34 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/days.d.ts +26 -0
- package/dist/days.d.ts.map +1 -0
- package/dist/days.js +46 -0
- package/dist/days.js.map +1 -0
- package/dist/ics.d.ts +5 -1
- package/dist/ics.d.ts.map +1 -1
- package/dist/ics.js +7 -26
- package/dist/ics.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/zone.d.ts +33 -0
- package/dist/zone.d.ts.map +1 -0
- package/dist/zone.js +113 -0
- package/dist/zone.js.map +1 -0
- package/package.json +2 -2
- package/src/days.ts +48 -0
- package/src/ics.ts +9 -33
- package/src/index.ts +3 -0
- package/src/zone.ts +128 -0
package/src/zone.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wall times and dates in IANA time zones, with `Intl` only and no time zone database. This is the
|
|
3
|
+
* approach of `@procedures/schedule`: the offset of a zone at a time comes from `formatToParts`.
|
|
4
|
+
*
|
|
5
|
+
* A wall time is the time that a clock shows, as milliseconds with the arithmetic of UTC. In a
|
|
6
|
+
* zone, the wall time of an instant is the instant plus the offset of the zone at that instant.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { dateTime } from "./days.js";
|
|
10
|
+
|
|
11
|
+
const MINUTE = 60_000;
|
|
12
|
+
const DAY = 86_400_000;
|
|
13
|
+
|
|
14
|
+
/** The fields of a wall time. The year is astronomical: 1 BC is the year 0. */
|
|
15
|
+
interface WallFields {
|
|
16
|
+
readonly year: number;
|
|
17
|
+
readonly month: number;
|
|
18
|
+
readonly day: number;
|
|
19
|
+
readonly hour: number;
|
|
20
|
+
readonly minute: number;
|
|
21
|
+
readonly second: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The formatters by the name of the zone. */
|
|
25
|
+
const formats = new Map<string, Intl.DateTimeFormat>();
|
|
26
|
+
|
|
27
|
+
/** The formatter of a zone. `Intl` throws a `RangeError` for a zone that it does not know. */
|
|
28
|
+
function formatOf(timeZone: string): Intl.DateTimeFormat {
|
|
29
|
+
let format = formats.get(timeZone);
|
|
30
|
+
if (format === undefined) {
|
|
31
|
+
format = new Intl.DateTimeFormat("en-US", {
|
|
32
|
+
timeZone,
|
|
33
|
+
hourCycle: "h23",
|
|
34
|
+
era: "short",
|
|
35
|
+
year: "numeric",
|
|
36
|
+
month: "numeric",
|
|
37
|
+
day: "numeric",
|
|
38
|
+
hour: "numeric",
|
|
39
|
+
minute: "numeric",
|
|
40
|
+
second: "numeric",
|
|
41
|
+
});
|
|
42
|
+
// The cache stays small: a process uses few zones
|
|
43
|
+
if (formats.size >= 1000) formats.clear();
|
|
44
|
+
formats.set(timeZone, format);
|
|
45
|
+
}
|
|
46
|
+
return format;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The fields of the wall time of an instant in a zone. */
|
|
50
|
+
function fieldsAt(time: number, timeZone: string): WallFields {
|
|
51
|
+
const value: Partial<Record<Intl.DateTimeFormatPartTypes, string>> = {};
|
|
52
|
+
for (const part of formatOf(timeZone).formatToParts(time)) if (part.type !== "literal") value[part.type] = part.value;
|
|
53
|
+
const year = Number(value.year);
|
|
54
|
+
return {
|
|
55
|
+
year: value.era === "BC" ? 1 - year : year,
|
|
56
|
+
month: Number(value.month),
|
|
57
|
+
day: Number(value.day),
|
|
58
|
+
hour: Number(value.hour) % 24,
|
|
59
|
+
minute: Number(value.minute),
|
|
60
|
+
second: Number(value.second),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The wall time of the fields. `setUTCFullYear` keeps the years 0 to 99, which `Date.UTC` moves to 1900. */
|
|
65
|
+
function wallOf(fields: WallFields): number {
|
|
66
|
+
const date = new Date(0);
|
|
67
|
+
date.setUTCFullYear(fields.year, fields.month - 1, fields.day);
|
|
68
|
+
date.setUTCHours(fields.hour, fields.minute, fields.second, 0);
|
|
69
|
+
return date.getTime();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The offset of a zone at an instant: the wall time minus the instant, in milliseconds. */
|
|
73
|
+
function offsetAt(time: number, timeZone: string): number {
|
|
74
|
+
return wallOf(fieldsAt(time, timeZone)) - Math.floor(time / 1000) * 1000;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* This function gives the instant of a wall time in a zone, as RFC 5545 (3.3.5) tells. A wall
|
|
79
|
+
* time that the clock shows two times gives its first occurrence. A wall time in a gap uses the
|
|
80
|
+
* offset before the gap: 02:30 on the day of the change to daylight time becomes 03:30.
|
|
81
|
+
*/
|
|
82
|
+
export function zonedInstant(wall: number, timeZone: string): number {
|
|
83
|
+
// The offsets a day before and a day after: a change of the clock near the wall time is between them
|
|
84
|
+
const before = offsetAt(wall - DAY, timeZone);
|
|
85
|
+
const after = offsetAt(wall + DAY, timeZone);
|
|
86
|
+
const offsets = new Set([before, after, offsetAt(wall, timeZone)]);
|
|
87
|
+
const times = [...offsets].map((offset) => wall - offset).filter((time) => time + offsetAt(time, timeZone) === wall);
|
|
88
|
+
return times.length > 0 ? Math.min(...times) : wall - before;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The form of a time of day: `HH:MM` or `HH:MM:SS`. */
|
|
92
|
+
const TIME = /^([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?$/;
|
|
93
|
+
|
|
94
|
+
const pad = (value: number, size = 2): string => String(value).padStart(size, "0");
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* This function gives the UTC offset (`+HH:MM` or `-HH:MM`) of a wall time in an IANA time zone.
|
|
98
|
+
* The text `${date}T${time}${offset}` is then the instant that RFC 5545 gives to the wall time.
|
|
99
|
+
*
|
|
100
|
+
* - The date is `YYYY-MM-DD`, and the time is `HH:MM` or `HH:MM:SS`. Another text throws a `RangeError`.
|
|
101
|
+
* - A wall time that the clock shows two times gives the offset of its first occurrence.
|
|
102
|
+
* - A wall time in a gap gives the offset before the gap, so `02:30-05:00` is 03:30 daylight time.
|
|
103
|
+
* - UTC gives `+00:00`. A zone that `Intl` does not know throws a `RangeError`.
|
|
104
|
+
*/
|
|
105
|
+
export function zonedOffset(date: string, time: string, timeZone: string): string {
|
|
106
|
+
const match = TIME.exec(time);
|
|
107
|
+
if (match === null) throw new RangeError(`zonedOffset: ${JSON.stringify(time)} is not a time in the form HH:MM or HH:MM:SS`);
|
|
108
|
+
const wall = dateTime(date, "zonedOffset") + (Number(match[1]) * 60 + Number(match[2])) * MINUTE + Number(match[3] ?? 0) * 1000;
|
|
109
|
+
const minutes = Math.round((wall - zonedInstant(wall, timeZone)) / MINUTE);
|
|
110
|
+
const sign = minutes < 0 ? "-" : "+";
|
|
111
|
+
return `${sign}${pad(Math.floor(Math.abs(minutes) / 60))}:${pad(Math.abs(minutes) % 60)}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* This function gives the date (`YYYY-MM-DD`) of an instant in an IANA time zone. The instant is
|
|
116
|
+
* a `Date`, milliseconds since 1970, or a text that `Date.parse` reads.
|
|
117
|
+
*
|
|
118
|
+
* - The result is the date that a clock in the zone shows at the instant.
|
|
119
|
+
* - An instant that is not valid, or a year of the result after 9999, throws a `RangeError`.
|
|
120
|
+
* - A zone that `Intl` does not know throws a `RangeError`.
|
|
121
|
+
*/
|
|
122
|
+
export function localDate(instant: Date | number | string, timeZone: string): string {
|
|
123
|
+
const time = instant instanceof Date ? instant.getTime() : typeof instant === "number" ? instant : Date.parse(instant);
|
|
124
|
+
if (!Number.isFinite(time)) throw new RangeError(`localDate: ${JSON.stringify(String(instant))} is not a valid instant`);
|
|
125
|
+
const fields = fieldsAt(time, timeZone);
|
|
126
|
+
if (fields.year < 0 || fields.year > 9999) throw new RangeError(`localDate: the year ${fields.year} is not from 0 to 9999`);
|
|
127
|
+
return `${pad(fields.year, 4)}-${pad(fields.month)}-${pad(fields.day)}`;
|
|
128
|
+
}
|