datetime-lube 1.0.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 +92 -0
- package/LICENSE +21 -0
- package/README.md +138 -0
- package/package.json +64 -0
- package/public.d.ts +611 -0
- package/src/add.js +148 -0
- package/src/calendar.js +88 -0
- package/src/date_to_string.js +106 -0
- package/src/diff.js +143 -0
- package/src/end_of.js +118 -0
- package/src/index.js +8 -0
- package/src/start_of.js +113 -0
- package/src/string_to_date.js +178 -0
- package/src/time_of.js +15 -0
- package/src/time_unit.js +6 -0
- package/src/time_zone_offset.js +22 -0
- package/src/zone.js +235 -0
- package/types/add.d.ts +34 -0
- package/types/add.d.ts.map +1 -0
- package/types/calendar.d.ts +25 -0
- package/types/calendar.d.ts.map +1 -0
- package/types/date_to_string.d.ts +29 -0
- package/types/date_to_string.d.ts.map +1 -0
- package/types/diff.d.ts +22 -0
- package/types/diff.d.ts.map +1 -0
- package/types/end_of.d.ts +20 -0
- package/types/end_of.d.ts.map +1 -0
- package/types/index.d.ts +10 -0
- package/types/index.d.ts.map +1 -0
- package/types/start_of.d.ts +20 -0
- package/types/start_of.d.ts.map +1 -0
- package/types/string_to_date.d.ts +36 -0
- package/types/string_to_date.d.ts.map +1 -0
- package/types/time_of.d.ts +7 -0
- package/types/time_of.d.ts.map +1 -0
- package/types/time_unit.d.ts +8 -0
- package/types/time_unit.d.ts.map +1 -0
- package/types/time_zone_offset.d.ts +15 -0
- package/types/time_zone_offset.d.ts.map +1 -0
- package/types/zone.d.ts +67 -0
- package/types/zone.d.ts.map +1 -0
package/src/start_of.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { days_of, wall_of_fields } from "./calendar.js"
|
|
2
|
+
import time_of from "./time_of.js"
|
|
3
|
+
import {
|
|
4
|
+
check_zone,
|
|
5
|
+
fields_of,
|
|
6
|
+
instant_of,
|
|
7
|
+
offset_in,
|
|
8
|
+
offset_of,
|
|
9
|
+
transition_of,
|
|
10
|
+
unit_start
|
|
11
|
+
} from "./zone.js"
|
|
12
|
+
/** @type {Record<string, number | undefined>} */
|
|
13
|
+
const unit_ms = { H: 3600000, m: 60000, s: 1000 }
|
|
14
|
+
/** @type {Record<string, number>} */
|
|
15
|
+
const units = { D: 3, M: 2, W: 3, Y: 1 }
|
|
16
|
+
const units_of = new Set(
|
|
17
|
+
[ "D", "H", "M", "W", "Y", "m", "s" ]
|
|
18
|
+
)
|
|
19
|
+
/**
|
|
20
|
+
* Returns a new Date at the start of the given unit, such as midnight of the same day.
|
|
21
|
+
* A week starts on Monday, as in ISO 8601.
|
|
22
|
+
* @param {Date} date
|
|
23
|
+
* @param {Exclude<import("../public.js").Unit, "sss">} unit
|
|
24
|
+
* ```
|
|
25
|
+
* "Y" | "M" | "W" | "D" | "H" | "m" | "s"
|
|
26
|
+
* ```
|
|
27
|
+
* @param {import("../public.js").TimeZone=} zone
|
|
28
|
+
* @returns {Date}
|
|
29
|
+
* @throws
|
|
30
|
+
* ```
|
|
31
|
+
* RangeError(`startOf: Invalid unit "${unit}"`) // The unit is not one of the listed ones
|
|
32
|
+
* RangeError(`startOf: Invalid time zone "${zone}"`) // Intl.DateTimeFormat rejects the zone
|
|
33
|
+
* TypeError(`startOf: Expected a Date, got ${typeof date}`)
|
|
34
|
+
* ```
|
|
35
|
+
* @example startOf(new Date, "D", "Asia/Seoul") //=> Date { midnight in Seoul }
|
|
36
|
+
*/
|
|
37
|
+
export default function(date, unit, zone) {
|
|
38
|
+
const time = time_of("startOf", date)
|
|
39
|
+
if (!units_of.has(unit)) throw new RangeError(
|
|
40
|
+
`startOf: Invalid unit "${String(unit)}"`
|
|
41
|
+
)
|
|
42
|
+
check_zone("startOf", zone)
|
|
43
|
+
if (isNaN(time)) return new Date(NaN)
|
|
44
|
+
const ms = unit_ms[unit]
|
|
45
|
+
if (ms) {
|
|
46
|
+
if (zone !== undefined) {
|
|
47
|
+
const offset = offset_of(time, zone)
|
|
48
|
+
const rest = ((time + offset) % ms + ms) % ms
|
|
49
|
+
return new Date(
|
|
50
|
+
offset_of(time - rest, zone) == offset
|
|
51
|
+
? time - rest
|
|
52
|
+
: unit_start(time, ms, zone)
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
const rest = ms > 60000
|
|
56
|
+
? date.getMinutes() * 60000 + date.getSeconds() * 1000 + date.getMilliseconds()
|
|
57
|
+
: ms > 1000
|
|
58
|
+
? date.getSeconds() * 1000 + date.getMilliseconds()
|
|
59
|
+
: date.getMilliseconds()
|
|
60
|
+
const result = new Date(time - rest)
|
|
61
|
+
if (
|
|
62
|
+
result.getTimezoneOffset() == date.getTimezoneOffset()
|
|
63
|
+
&& result.getSeconds() == (ms > 1000 ? 0 : date.getSeconds())
|
|
64
|
+
) return result
|
|
65
|
+
result.setTime(unit_start(time, ms))
|
|
66
|
+
return result
|
|
67
|
+
}
|
|
68
|
+
const from = /** @type {number} */(units[unit])/**/
|
|
69
|
+
if (zone === undefined) {
|
|
70
|
+
const result = new Date(time)
|
|
71
|
+
result.setHours(
|
|
72
|
+
unit == "D"
|
|
73
|
+
? 0
|
|
74
|
+
: unit == "W"
|
|
75
|
+
? -24 * ((result.getDay() + 6) % 7)
|
|
76
|
+
: unit == "M"
|
|
77
|
+
? 24 - 24 * result.getDate()
|
|
78
|
+
: -24 * (days_of(
|
|
79
|
+
result.getFullYear(),
|
|
80
|
+
result.getMonth(),
|
|
81
|
+
result.getDate()
|
|
82
|
+
) - days_of(result.getFullYear(), 0, 1)),
|
|
83
|
+
0,
|
|
84
|
+
0,
|
|
85
|
+
0
|
|
86
|
+
)
|
|
87
|
+
if (result.getTime() <= time && !(result.getHours() || result.getMinutes() || result.getSeconds())) return result
|
|
88
|
+
}
|
|
89
|
+
const fields = fields_of(date, zone)
|
|
90
|
+
if (unit == "W") fields[2] = /** @type {number} */(fields[2])/**/ - (/** @type {number} */(fields[7])/**/ + 6) % 7
|
|
91
|
+
for (let i = from; i < 7; i++) fields[i] = i == 2 ? 1 : 0
|
|
92
|
+
const wall = wall_of_fields(fields)
|
|
93
|
+
if (zone !== undefined) {
|
|
94
|
+
const start = instant_of(wall, zone, true)
|
|
95
|
+
if (start <= time) return new Date(start)
|
|
96
|
+
}
|
|
97
|
+
const before = offset_in(wall - 86400000, zone)
|
|
98
|
+
const guess = wall - before
|
|
99
|
+
const after = offset_in(guess, zone)
|
|
100
|
+
const earliest = after == before
|
|
101
|
+
? wall - offset_in(time, zone)
|
|
102
|
+
: wall - after
|
|
103
|
+
if (!(earliest >= -8.64e15)) return new Date(NaN)
|
|
104
|
+
return new Date(
|
|
105
|
+
transition_of(
|
|
106
|
+
earliest,
|
|
107
|
+
after == before || guess > time
|
|
108
|
+
? time
|
|
109
|
+
: guess,
|
|
110
|
+
zone
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { fields_of_wall, last_day } from "./calendar.js"
|
|
2
|
+
import { check_zone, date_of, fields_of } from "./zone.js"
|
|
3
|
+
const comment_regex = /\([^)]*\)?/g
|
|
4
|
+
/** @type {Map<string, (number | number[])[]>} */
|
|
5
|
+
const formats = new Map()
|
|
6
|
+
const iso_regex = /^([+-]\d{6}|\d{4})(?:-(\d{2})(?:-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?)?)?([^]*)$/
|
|
7
|
+
const token_regex = /DD|HH|MM|YYYY|mm|sss?/g
|
|
8
|
+
/** @type {Record<string, number[]>} */
|
|
9
|
+
const tokens = {
|
|
10
|
+
DD: [ 2, 2, 1, 31 ],
|
|
11
|
+
HH: [ 3, 2, 0, 23 ],
|
|
12
|
+
MM: [ 1, 2, 1, 12 ],
|
|
13
|
+
YYYY: [ 0, 4, 0, 275760 ],
|
|
14
|
+
mm: [ 4, 2, 0, 59 ],
|
|
15
|
+
ss: [ 5, 2, 0, 59 ],
|
|
16
|
+
sss: [ 6, 3, 0, 999 ]
|
|
17
|
+
}
|
|
18
|
+
const zone_regex = /(?<![a-z])(?:[cemp][ds]t|gmt|utc?|z)(?![a-z])|(?:\s|[ap]m|\d:\d\d?(?::\d\d?(?:\.\d*)?)?)[+-]\d/i
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} format
|
|
21
|
+
* @returns {(number | number[])[]}
|
|
22
|
+
*/
|
|
23
|
+
function compile(format) {
|
|
24
|
+
let parts = formats.get(format)
|
|
25
|
+
if (parts) return parts
|
|
26
|
+
parts = []
|
|
27
|
+
let last = 0
|
|
28
|
+
for (const { 0: token, index = 0 } of format.matchAll(token_regex)) {
|
|
29
|
+
if (index > last) parts.push(
|
|
30
|
+
[ ...format.slice(last, index) ].length
|
|
31
|
+
)
|
|
32
|
+
parts.push(
|
|
33
|
+
/** @type {number[]} */(tokens[token])/**/
|
|
34
|
+
)
|
|
35
|
+
last = index + token.length
|
|
36
|
+
}
|
|
37
|
+
if (last < format.length) parts.push(
|
|
38
|
+
[ ...format.slice(last) ].length
|
|
39
|
+
)
|
|
40
|
+
if (formats.size >= 1000) formats.clear()
|
|
41
|
+
formats.set(format, parts)
|
|
42
|
+
return parts
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* @param {RegExpExecArray} match
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
function out_of_range(match) {
|
|
49
|
+
const [ , year, month, day, hour, minute, second ] = match
|
|
50
|
+
if (month === undefined) return false
|
|
51
|
+
const index = +month - 1
|
|
52
|
+
return index < 0 || index > 11 || day !== undefined && (+day < 1 || +day > last_day(+(year ?? 0), index)) || +(hour ?? 0) > (+(minute ?? 0) || +(second ?? 0) || +(match[7] ?? 0)
|
|
53
|
+
? 23
|
|
54
|
+
: 24) || +(minute ?? 0) > 59 || +(second ?? 0) > 59
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Converts a date string to a Date object based on the provided format.
|
|
58
|
+
* Without a format the platform parser reads the string, so an offset such as `"Z"` is applied,
|
|
59
|
+
* while a date without a time is read as midnight. Out-of-range values give an invalid Date only in an
|
|
60
|
+
* ECMAScript date string: V8 rolls `"2025/02/30"` over to March 2, so check user input with a format.
|
|
61
|
+
* With a format, letters that are not a time unit only reserve their place, and a unit reads up to its own length of digits (`"sss"` reads them as a fraction, so `".5"` is 500 ms).
|
|
62
|
+
* `"YYYY"` also reads an expanded year such as `"+012345"`, and text left after the format gives an invalid Date.
|
|
63
|
+
* Missing units default to 0000-01-01T00:00:00.000, where a time zone is at its local mean time, so
|
|
64
|
+
* read a time alone in `"UTC"`. An invalid value returns an invalid Date.
|
|
65
|
+
* A wall clock inside a daylight saving gap moves forward, and one that occurs twice takes the
|
|
66
|
+
* first occurrence, as `new Date(year, month, day, hour)` does.
|
|
67
|
+
* @param {string} date
|
|
68
|
+
* @param {string=} format
|
|
69
|
+
* ```js
|
|
70
|
+
* e.g. "YYYY-MM-DDTHH:mm:ss.sss"
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* Supported time units:
|
|
74
|
+
* - "YYYY": Years
|
|
75
|
+
* - "MM": Months
|
|
76
|
+
* - "DD": Days
|
|
77
|
+
* - "HH": Hours
|
|
78
|
+
* - "mm": Minutes
|
|
79
|
+
* - "ss": Seconds
|
|
80
|
+
* - "sss": Milliseconds
|
|
81
|
+
* @param {import("../public.js").TimeZone=} zone
|
|
82
|
+
* @returns {Date}
|
|
83
|
+
* @throws
|
|
84
|
+
* ```
|
|
85
|
+
* RangeError(`stringToDate: Invalid time zone "${zone}"`) // Intl.DateTimeFormat rejects the zone
|
|
86
|
+
* TypeError(`stringToDate: Expected a string, got ${typeof date}`)
|
|
87
|
+
* ```
|
|
88
|
+
* @example stringToDate("2024-07-15 09:00", "YYYY-MM-DD HH:mm", "Asia/Seoul")
|
|
89
|
+
*/
|
|
90
|
+
export default function(date, format, zone) {
|
|
91
|
+
if (typeof date != "string") throw new TypeError(
|
|
92
|
+
`stringToDate: Expected a string, got ${date === null ? "null" : typeof date}`
|
|
93
|
+
)
|
|
94
|
+
check_zone("stringToDate", zone)
|
|
95
|
+
if (format === undefined) {
|
|
96
|
+
const native = new Date(date)
|
|
97
|
+
const valid = !isNaN(native.getTime())
|
|
98
|
+
if (!valid && zone === undefined) return native
|
|
99
|
+
const match = iso_regex.exec(date)
|
|
100
|
+
if (match && out_of_range(match)) return new Date(NaN)
|
|
101
|
+
const parsed = match?.[8] === ""
|
|
102
|
+
? match
|
|
103
|
+
: null
|
|
104
|
+
if (parsed) {
|
|
105
|
+
if (zone === undefined && parsed[4] !== undefined) return native
|
|
106
|
+
if (!valid && isNaN(Date.parse(`${date}Z`))) return native
|
|
107
|
+
const fraction = parsed[7]
|
|
108
|
+
return date_of(
|
|
109
|
+
[
|
|
110
|
+
+(parsed[1] ?? NaN),
|
|
111
|
+
+(parsed[2] ?? 1) - 1,
|
|
112
|
+
+(parsed[3] ?? 1),
|
|
113
|
+
+(parsed[4] ?? 0),
|
|
114
|
+
+(parsed[5] ?? 0),
|
|
115
|
+
+(parsed[6] ?? 0),
|
|
116
|
+
fraction === undefined
|
|
117
|
+
? 0
|
|
118
|
+
: +fraction.slice(0, 3).padEnd(3, "0")
|
|
119
|
+
],
|
|
120
|
+
zone
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
if (zone === undefined) return native
|
|
124
|
+
const text = date.replace(comment_regex, "")
|
|
125
|
+
if (zone_regex.test(text)) return native
|
|
126
|
+
const wall = new Date(`${text} GMT`)
|
|
127
|
+
if (!valid && isNaN(wall.getTime())) return native
|
|
128
|
+
return date_of(
|
|
129
|
+
isNaN(wall.getTime())
|
|
130
|
+
? fields_of(native)
|
|
131
|
+
: fields_of_wall(wall.getTime()),
|
|
132
|
+
zone
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
const fields = [ 0, 0, 1, 0, 0, 0, 0 ]
|
|
136
|
+
let index = 0
|
|
137
|
+
for (const part of compile(format)) {
|
|
138
|
+
if (typeof part == "number") {
|
|
139
|
+
for (let i = 0; i < part; i++) {
|
|
140
|
+
const code = date.charCodeAt(index)
|
|
141
|
+
index += code >= 0xd800 && code < 0xdc00 && date.charCodeAt(index + 1) >= 0xdc00 && date.charCodeAt(index + 1) < 0xe000
|
|
142
|
+
? 2
|
|
143
|
+
: 1
|
|
144
|
+
}
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
let text = ""
|
|
148
|
+
let length = /** @type {number} */(part[1])/**/
|
|
149
|
+
let min = /** @type {number} */(part[2])/**/
|
|
150
|
+
let sign = ""
|
|
151
|
+
const code = date.charCodeAt(index)
|
|
152
|
+
if (part[0] == 0 && (code == 43 || code == 45)) {
|
|
153
|
+
sign = code == 45
|
|
154
|
+
? "-"
|
|
155
|
+
: "+"
|
|
156
|
+
index++
|
|
157
|
+
length = 6
|
|
158
|
+
min = -271821
|
|
159
|
+
}
|
|
160
|
+
while (text.length < length && date.charCodeAt(index) > 47 && date.charCodeAt(index) < 58) text += date[index++]
|
|
161
|
+
if (!text || sign && (text.length < 6 || sign == "-" && !+text)) return new Date(NaN)
|
|
162
|
+
if (part[0] == 6) {
|
|
163
|
+
while (date.charCodeAt(index) > 47 && date.charCodeAt(index) < 58) index++
|
|
164
|
+
}
|
|
165
|
+
const value = part[0] == 6
|
|
166
|
+
? +text.padEnd(3, "0")
|
|
167
|
+
: +(sign + text)
|
|
168
|
+
if (value > /** @type {number} */(part[3])/**/ || value < min) return new Date(NaN)
|
|
169
|
+
fields[/** @type {number} */(part[0])/**/] = /** @type {number} */(part[0])/**/ == 1
|
|
170
|
+
? value - 1
|
|
171
|
+
: value
|
|
172
|
+
}
|
|
173
|
+
if (index != date.length || /** @type {number} */(fields[2])/**/ > last_day(
|
|
174
|
+
/** @type {number} */(fields[0])/**/,
|
|
175
|
+
/** @type {number} */(fields[1])/**/
|
|
176
|
+
)) return new Date(NaN)
|
|
177
|
+
return date_of(fields, zone)
|
|
178
|
+
}
|
package/src/time_of.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const get_time = Date.prototype.getTime
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} name
|
|
4
|
+
* @param {Date} date
|
|
5
|
+
* @returns {number}
|
|
6
|
+
*/
|
|
7
|
+
export default function(name, date) {
|
|
8
|
+
try {
|
|
9
|
+
return get_time.call(date)
|
|
10
|
+
} catch {
|
|
11
|
+
throw new TypeError(
|
|
12
|
+
`${name}: Expected a Date, got ${date === null ? "null" : typeof date}`
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/time_unit.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import time_of from "./time_of.js"
|
|
2
|
+
import { check_zone, offset_of } from "./zone.js"
|
|
3
|
+
/**
|
|
4
|
+
* Returns the offset of the time zone at the given date in milliseconds, east of UTC positive.
|
|
5
|
+
* Daylight saving time is taken from the given date, so a summer date gives the summer offset.
|
|
6
|
+
* @param {Date} date
|
|
7
|
+
* @param {import("../public.js").TimeZone} zone
|
|
8
|
+
* @returns {number}
|
|
9
|
+
* @throws
|
|
10
|
+
* ```
|
|
11
|
+
* RangeError(`timeZoneOffset: Invalid time zone "${zone}"`) // Intl.DateTimeFormat rejects the zone
|
|
12
|
+
* TypeError(`timeZoneOffset: Expected a Date, got ${typeof date}`)
|
|
13
|
+
* ```
|
|
14
|
+
* @example timeZoneOffset(new Date, "Asia/Seoul") //=> 32400000
|
|
15
|
+
*/
|
|
16
|
+
export default function(date, zone) {
|
|
17
|
+
const time = time_of("timeZoneOffset", date)
|
|
18
|
+
check_zone("timeZoneOffset", zone)
|
|
19
|
+
return isNaN(time)
|
|
20
|
+
? NaN
|
|
21
|
+
: offset_of(time, zone)
|
|
22
|
+
}
|
package/src/zone.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { fields_of_wall, wall_of_fields } from "./calendar.js"
|
|
2
|
+
const offset_regex = /GMT([+-])([0-9][0-9]):([0-9][0-9])(?::([0-9][0-9]))?/
|
|
3
|
+
/** @type {Map<string, Intl.DateTimeFormat>} */
|
|
4
|
+
const canonical = new Map()
|
|
5
|
+
/** @type {Map<string, Intl.DateTimeFormat>} */
|
|
6
|
+
const formatters = new Map()
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} name
|
|
9
|
+
* @param {string=} zone
|
|
10
|
+
*/
|
|
11
|
+
function check_zone(name, zone) {
|
|
12
|
+
if (zone === undefined || formatters.has(zone)) return
|
|
13
|
+
try {
|
|
14
|
+
formatter_of(zone)
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw new RangeError(
|
|
17
|
+
`${name}: Invalid time zone "${String(zone)}"`,
|
|
18
|
+
{ cause: error }
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* @param {number[]} fields
|
|
24
|
+
* @param {string=} zone
|
|
25
|
+
* @returns {Date}
|
|
26
|
+
*/
|
|
27
|
+
function date_of(fields, zone) {
|
|
28
|
+
if (zone !== undefined) return new Date(
|
|
29
|
+
instant_of(wall_of_fields(fields), zone)
|
|
30
|
+
)
|
|
31
|
+
const year = /** @type {number} */(fields[0])/**/
|
|
32
|
+
if (year >= 0 && year < 100) {
|
|
33
|
+
const date = new Date(0)
|
|
34
|
+
date.setFullYear(year, fields[1], fields[2])
|
|
35
|
+
date.setHours(
|
|
36
|
+
/** @type {number} */(fields[3])/**/,
|
|
37
|
+
fields[4],
|
|
38
|
+
fields[5],
|
|
39
|
+
fields[6]
|
|
40
|
+
)
|
|
41
|
+
return date
|
|
42
|
+
}
|
|
43
|
+
return new Date(
|
|
44
|
+
year,
|
|
45
|
+
/** @type {number} */(fields[1])/**/,
|
|
46
|
+
fields[2],
|
|
47
|
+
fields[3],
|
|
48
|
+
fields[4],
|
|
49
|
+
fields[5],
|
|
50
|
+
fields[6]
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* @param {number} wall
|
|
55
|
+
* @param {number} end
|
|
56
|
+
* @param {number} time
|
|
57
|
+
* @param {string=} zone
|
|
58
|
+
* @returns {number}
|
|
59
|
+
*/
|
|
60
|
+
function end_at(wall, end, time, zone) {
|
|
61
|
+
if (end < time) return wall - offset_in(time, zone) - 1
|
|
62
|
+
const next = end + 1
|
|
63
|
+
const offset = offset_in(next, zone)
|
|
64
|
+
if (next + offset == wall || offset_in(end, zone) != offset) return end
|
|
65
|
+
const transition = transition_of(time, next, zone)
|
|
66
|
+
return transition + offset_in(transition, zone) >= wall
|
|
67
|
+
? transition - 1
|
|
68
|
+
: end
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* @param {Date} date
|
|
72
|
+
* @param {string=} zone
|
|
73
|
+
* @returns {number[]}
|
|
74
|
+
*/
|
|
75
|
+
function fields_of(date, zone) {
|
|
76
|
+
if (zone === undefined) return [
|
|
77
|
+
date.getFullYear(),
|
|
78
|
+
date.getMonth(),
|
|
79
|
+
date.getDate(),
|
|
80
|
+
date.getHours(),
|
|
81
|
+
date.getMinutes(),
|
|
82
|
+
date.getSeconds(),
|
|
83
|
+
date.getMilliseconds(),
|
|
84
|
+
date.getDay()
|
|
85
|
+
]
|
|
86
|
+
const time = date.getTime()
|
|
87
|
+
return fields_of_wall(time + offset_of(time, zone))
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} zone
|
|
91
|
+
* @returns {Intl.DateTimeFormat}
|
|
92
|
+
*/
|
|
93
|
+
function formatter_of(zone) {
|
|
94
|
+
let formatter = formatters.get(zone)
|
|
95
|
+
if (formatter) return formatter
|
|
96
|
+
formatter = new Intl.DateTimeFormat(
|
|
97
|
+
"en-US",
|
|
98
|
+
{
|
|
99
|
+
timeZone: zone,
|
|
100
|
+
timeZoneName: "longOffset"
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
const name = formatter.resolvedOptions().timeZone
|
|
104
|
+
const known = canonical.get(name)
|
|
105
|
+
if (known) {
|
|
106
|
+
formatter = known
|
|
107
|
+
} else {
|
|
108
|
+
if (canonical.size >= 1000) canonical.clear()
|
|
109
|
+
canonical.set(name, formatter)
|
|
110
|
+
}
|
|
111
|
+
if (formatters.size >= 1000) formatters.clear()
|
|
112
|
+
formatters.set(zone, formatter)
|
|
113
|
+
return formatter
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* @param {number} wall
|
|
117
|
+
* @param {string} zone
|
|
118
|
+
* @param {boolean=} exact
|
|
119
|
+
* @returns {number}
|
|
120
|
+
*/
|
|
121
|
+
function instant_of(wall, zone, exact) {
|
|
122
|
+
const before = offset_of(wall - 86400000, zone)
|
|
123
|
+
const guess = wall - before
|
|
124
|
+
const offset = offset_of(guess, zone)
|
|
125
|
+
if (offset == before) return guess
|
|
126
|
+
const candidate = wall - offset
|
|
127
|
+
return offset_of(candidate, zone) == offset
|
|
128
|
+
? candidate
|
|
129
|
+
: exact
|
|
130
|
+
? NaN
|
|
131
|
+
: guess
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* @param {number} time
|
|
135
|
+
* @param {string=} zone
|
|
136
|
+
* @returns {number}
|
|
137
|
+
*/
|
|
138
|
+
function offset_in(time, zone) {
|
|
139
|
+
return zone === undefined
|
|
140
|
+
? wall_of_fields(fields_of(new Date(time))) - time
|
|
141
|
+
: offset_of(time, zone)
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* @param {number} time
|
|
145
|
+
* @param {string} zone
|
|
146
|
+
* @returns {number}
|
|
147
|
+
*/
|
|
148
|
+
function offset_of(time, zone) {
|
|
149
|
+
const match = offset_regex.exec(
|
|
150
|
+
formatter_of(zone).format(
|
|
151
|
+
time < -8.64e15
|
|
152
|
+
? -8.64e15
|
|
153
|
+
: time <= 8.64e15
|
|
154
|
+
? time
|
|
155
|
+
: 8.64e15
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
if (!match) return 0
|
|
159
|
+
return (match[1] == "-" ? -1 : 1) * (
|
|
160
|
+
(+(match[2] ?? 0) * 60 + +(match[3] ?? 0)) * 60000
|
|
161
|
+
+ +(match[4] ?? 0) * 1000
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* @param {number} from
|
|
166
|
+
* @param {number} to
|
|
167
|
+
* @param {string=} zone
|
|
168
|
+
* @returns {number}
|
|
169
|
+
*/
|
|
170
|
+
function transition_of(from, to, zone) {
|
|
171
|
+
const offset = offset_in(to, zone)
|
|
172
|
+
while (to - from > 1) {
|
|
173
|
+
const middle = Math.floor((from + to) / 2)
|
|
174
|
+
if (offset_in(middle, zone) == offset) to = middle
|
|
175
|
+
else from = middle
|
|
176
|
+
}
|
|
177
|
+
return to
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* @param {number} time
|
|
181
|
+
* @param {number} ms
|
|
182
|
+
* @param {string=} zone
|
|
183
|
+
* @returns {number}
|
|
184
|
+
*/
|
|
185
|
+
function unit_end(time, ms, zone) {
|
|
186
|
+
const bucket = Math.floor(
|
|
187
|
+
(time + offset_in(time, zone)) / ms
|
|
188
|
+
)
|
|
189
|
+
let at = time
|
|
190
|
+
for (;;) {
|
|
191
|
+
const offset = offset_in(at, zone)
|
|
192
|
+
const end = (bucket + 1) * ms - offset - 1
|
|
193
|
+
if (!(end <= 8.64e15)) return NaN
|
|
194
|
+
if (offset_in(end, zone) == offset) return end
|
|
195
|
+
const transition = transition_of(at, end, zone)
|
|
196
|
+
const wall = transition + offset_in(transition, zone)
|
|
197
|
+
if (Math.floor(wall / ms) != bucket || wall <= transition - 1 + offset) return transition - 1
|
|
198
|
+
at = transition
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* @param {number} time
|
|
203
|
+
* @param {number} ms
|
|
204
|
+
* @param {string=} zone
|
|
205
|
+
* @returns {number}
|
|
206
|
+
*/
|
|
207
|
+
function unit_start(time, ms, zone) {
|
|
208
|
+
const bucket = Math.floor(
|
|
209
|
+
(time + offset_in(time, zone)) / ms
|
|
210
|
+
)
|
|
211
|
+
let at = time
|
|
212
|
+
for (;;) {
|
|
213
|
+
const offset = offset_in(at, zone)
|
|
214
|
+
const start = bucket * ms - offset
|
|
215
|
+
if (!(start >= -8.64e15)) return NaN
|
|
216
|
+
if (offset_in(start, zone) == offset) return start
|
|
217
|
+
const transition = transition_of(start, at, zone)
|
|
218
|
+
const before = transition - 1
|
|
219
|
+
const wall = before + offset_in(before, zone)
|
|
220
|
+
if (Math.floor(wall / ms) != bucket || wall >= transition + offset) return transition
|
|
221
|
+
at = before
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export {
|
|
225
|
+
check_zone,
|
|
226
|
+
date_of,
|
|
227
|
+
end_at,
|
|
228
|
+
fields_of,
|
|
229
|
+
instant_of,
|
|
230
|
+
offset_in,
|
|
231
|
+
offset_of,
|
|
232
|
+
transition_of,
|
|
233
|
+
unit_end,
|
|
234
|
+
unit_start
|
|
235
|
+
}
|
package/types/add.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculates and adds the specified time duration to the provided date and returns a new Date.
|
|
3
|
+
* Years and months are clamped to the end of the month, e.g. Jan 31 + 1M is Feb 28 or 29.
|
|
4
|
+
* Days follow the clock, so a day over a daylight saving change is 23 or 25 hours,
|
|
5
|
+
* while hours, minutes, seconds and milliseconds are elapsed time.
|
|
6
|
+
* A part of zero adds nothing; any other part of years, months, weeks or days lands on a clock time,
|
|
7
|
+
* which moves forward out of a daylight saving gap and takes the first occurrence of an hour that repeats.
|
|
8
|
+
* @param {Date} date
|
|
9
|
+
* @param {string} sum
|
|
10
|
+
* ```
|
|
11
|
+
* e.g. "1Y2M 3D"
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Supported time units:
|
|
15
|
+
* - "Y": Years
|
|
16
|
+
* - "M": Months
|
|
17
|
+
* - "W": Weeks
|
|
18
|
+
* - "D": Days
|
|
19
|
+
* - "H": Hours
|
|
20
|
+
* - "m": Minutes
|
|
21
|
+
* - "s": Seconds
|
|
22
|
+
* - "sss": Milliseconds
|
|
23
|
+
* @param {import("../public.js").TimeZone=} zone
|
|
24
|
+
* @returns {Date}
|
|
25
|
+
* @throws
|
|
26
|
+
* ```
|
|
27
|
+
* RangeError(`add: Invalid duration "${sum}"`) // sum is empty, or a part of it is not a number followed by a unit
|
|
28
|
+
* RangeError(`add: Invalid time zone "${zone}"`) // Intl.DateTimeFormat rejects the zone
|
|
29
|
+
* TypeError(`add: Expected a Date, got ${typeof date}`)
|
|
30
|
+
* ```
|
|
31
|
+
* @example add(new Date, "-1D") //=> Date { yesterday }
|
|
32
|
+
*/
|
|
33
|
+
export default function (date: Date, sum: string, zone?: import("../public.js").TimeZone | undefined): Date;
|
|
34
|
+
//# sourceMappingURL=add.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../src/add.js"],"names":[],"mappings":"AAqEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,CAAC,OAAO,WAAU,IAAI,EAzBjB,IAyBiB,EAAE,GAAG,EAxBtB,MAwBsB,EAAE,IAAI,AAVpC,CACA,EADQ,OAAO,cAAc,EAAE,QAAQ,YAUH,GAT1B,IAAI,CAuDhB"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {number} year
|
|
3
|
+
* @param {number} month
|
|
4
|
+
* @param {number} day
|
|
5
|
+
* @returns {number}
|
|
6
|
+
*/
|
|
7
|
+
declare function days_of(year: number, month: number, day: number): number;
|
|
8
|
+
/**
|
|
9
|
+
* @param {number} wall
|
|
10
|
+
* @returns {number[]}
|
|
11
|
+
*/
|
|
12
|
+
declare function fields_of_wall(wall: number): number[];
|
|
13
|
+
/**
|
|
14
|
+
* @param {number} year
|
|
15
|
+
* @param {number} month
|
|
16
|
+
* @returns {number}
|
|
17
|
+
*/
|
|
18
|
+
declare function last_day(year: number, month: number): number;
|
|
19
|
+
/**
|
|
20
|
+
* @param {number[]} fields
|
|
21
|
+
* @returns {number}
|
|
22
|
+
*/
|
|
23
|
+
declare function wall_of_fields(fields: number[]): number;
|
|
24
|
+
export { days_of, fields_of_wall, last_day, wall_of_fields };
|
|
25
|
+
//# sourceMappingURL=calendar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../src/calendar.js"],"names":[],"mappings":"AACA;;;;;GAKG;AACH,iBAAS,OAAO,CAAC,IAAI,EALV,MAKU,EAAE,KAAK,EAJjB,MAIiB,EAAE,GAAG,EAHtB,MAGsB,GAFpB,MAAM,CAmBlB;AACD;;;GAGG;AACH,iBAAS,cAAc,CAAC,IAAI,EAHjB,MAGiB,GAFf,MAAM,EAAE,CA6BpB;AACD;;;;GAIG;AACH,iBAAS,QAAQ,CAAC,IAAI,EAJX,MAIW,EAAE,KAAK,EAHlB,MAGkB,GAFhB,MAAM,CAMlB;AACD;;;GAGG;AACH,iBAAS,cAAc,CAAC,MAAM,EAHnB,MAAM,EAGa,GAFjB,MAAM,CAYlB;AACD,OAAO,EACN,OAAO,EACP,cAAc,EACd,QAAQ,EACR,cAAc,EACd,CAAA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a Date object to a formatted string representation.
|
|
3
|
+
* Letters that are not a time unit are kept as they are, so `"HHhmm"` gives `"11h22"`.
|
|
4
|
+
* A year before 0 or after 9999 is printed as an expanded year, such as `"+012345"`.
|
|
5
|
+
* @param {Date} date
|
|
6
|
+
* @param {string=} format
|
|
7
|
+
* ```js
|
|
8
|
+
* = "YYYY-MM-DDTHH:mm:ss.sss"
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* Supported time units:
|
|
12
|
+
* - "YYYY": Years
|
|
13
|
+
* - "MM": Months
|
|
14
|
+
* - "DD": Days
|
|
15
|
+
* - "HH": Hours
|
|
16
|
+
* - "mm": Minutes
|
|
17
|
+
* - "ss": Seconds
|
|
18
|
+
* - "sss": Milliseconds
|
|
19
|
+
* @param {import("../public.js").TimeZone=} zone
|
|
20
|
+
* @returns {string}
|
|
21
|
+
* @throws
|
|
22
|
+
* ```
|
|
23
|
+
* RangeError(`dateToString: Invalid time zone "${zone}"`) // Intl.DateTimeFormat rejects the zone
|
|
24
|
+
* TypeError(`dateToString: Expected a Date, got ${typeof date}`)
|
|
25
|
+
* ```
|
|
26
|
+
* @example dateToString(new Date, "YYYY-MM-DD HH:mm", "Asia/Seoul") //=> "2024-07-15 21:00"
|
|
27
|
+
*/
|
|
28
|
+
export default function (date: Date, format?: string | undefined, zone?: import("../public.js").TimeZone | undefined): string;
|
|
29
|
+
//# sourceMappingURL=date_to_string.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date_to_string.d.ts","sourceRoot":"","sources":["../src/date_to_string.js"],"names":[],"mappings":"AAsDA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,CAAC,OAAO,WACb,IAAI,EAxBM,IAwBN,EACJ,MAAM,AAxBJ,CAaA,EAbQ,MAAM,YAwBkB,EAClC,IAAI,AAZF,CACA,EADQ,OAAO,cAAc,EAAE,QAAQ,YAYrC,GAXQ,MAAM,CAgClB"}
|
package/types/diff.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts the whole units from one date to another, negative when the second date is earlier.
|
|
3
|
+
* Years, months, weeks and days follow the clock, so a day over a daylight saving change still
|
|
4
|
+
* counts as one day, while hours, minutes, seconds and milliseconds are elapsed time.
|
|
5
|
+
* @param {Date} from
|
|
6
|
+
* @param {Date} to
|
|
7
|
+
* @param {import("../public.js").Unit} unit
|
|
8
|
+
* ```
|
|
9
|
+
* "Y" | "M" | "W" | "D" | "H" | "m" | "s" | "sss"
|
|
10
|
+
* ```
|
|
11
|
+
* @param {import("../public.js").TimeZone=} zone
|
|
12
|
+
* @returns {number}
|
|
13
|
+
* @throws
|
|
14
|
+
* ```
|
|
15
|
+
* RangeError(`diff: Invalid unit "${unit}"`) // The unit is not one of the listed ones
|
|
16
|
+
* RangeError(`diff: Invalid time zone "${zone}"`) // Intl.DateTimeFormat rejects the zone
|
|
17
|
+
* TypeError(`diff: Expected a Date, got ${typeof from}`) // from or to is not a Date
|
|
18
|
+
* ```
|
|
19
|
+
* @example diff(start, end, "D") //=> 30
|
|
20
|
+
*/
|
|
21
|
+
export default function (from: Date, to: Date, unit: import("../public.js").Unit, zone?: import("../public.js").TimeZone | undefined): number;
|
|
22
|
+
//# sourceMappingURL=diff.d.ts.map
|