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/add.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { last_day } from "./calendar.js"
|
|
2
|
+
import time_of from "./time_of.js"
|
|
3
|
+
import { check_zone, date_of, fields_of } from "./zone.js"
|
|
4
|
+
/** @type {Map<string, (number | string)[]>} */
|
|
5
|
+
const durations = new Map()
|
|
6
|
+
const duration_regex = /([+-]?\d+)(sss|[YMWDHms])/g
|
|
7
|
+
/** @type {Record<string, number | undefined>} */
|
|
8
|
+
const unit_ms = {
|
|
9
|
+
H: 3600000,
|
|
10
|
+
m: 60000,
|
|
11
|
+
s: 1000,
|
|
12
|
+
sss: 1
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* @param {Date} date
|
|
16
|
+
* @param {number} months
|
|
17
|
+
* @param {string=} zone
|
|
18
|
+
* @returns {Date}
|
|
19
|
+
*/
|
|
20
|
+
function add_months(date, months, zone) {
|
|
21
|
+
if (zone === undefined) {
|
|
22
|
+
const month = date.getMonth() + months
|
|
23
|
+
const year = date.getFullYear() + Math.floor(month / 12)
|
|
24
|
+
const rest = (month % 12 + 12) % 12
|
|
25
|
+
const result = new Date(date.getTime())
|
|
26
|
+
result.setFullYear(
|
|
27
|
+
year,
|
|
28
|
+
rest,
|
|
29
|
+
Math.min(
|
|
30
|
+
date.getDate(),
|
|
31
|
+
last_day(year, rest)
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
return result
|
|
35
|
+
}
|
|
36
|
+
const fields = fields_of(date, zone)
|
|
37
|
+
const month = /** @type {number} */(fields[1])/**/ + months
|
|
38
|
+
const year = /** @type {number} */(fields[0])/**/ + Math.floor(month / 12)
|
|
39
|
+
const rest = (month % 12 + 12) % 12
|
|
40
|
+
fields[0] = year
|
|
41
|
+
fields[1] = rest
|
|
42
|
+
fields[2] = Math.min(
|
|
43
|
+
/** @type {number} */(fields[2])/**/,
|
|
44
|
+
last_day(year, rest)
|
|
45
|
+
)
|
|
46
|
+
return date_of(fields, zone)
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} sum
|
|
50
|
+
* @returns {(number | string)[]}
|
|
51
|
+
*/
|
|
52
|
+
function compile(sum) {
|
|
53
|
+
let steps = durations.get(sum)
|
|
54
|
+
if (steps) return steps
|
|
55
|
+
steps = []
|
|
56
|
+
if (!/\S/.test(sum) || /\S/.test(
|
|
57
|
+
sum.replace(duration_regex, "")
|
|
58
|
+
)) throw new RangeError(
|
|
59
|
+
`add: Invalid duration "${String(sum)}"`
|
|
60
|
+
)
|
|
61
|
+
for (const [ , value = "", unit = "" ] of sum.matchAll(duration_regex)) {
|
|
62
|
+
if (!+value) continue
|
|
63
|
+
if (unit == "W") steps.push("D", +value * 7)
|
|
64
|
+
else steps.push(unit, +value)
|
|
65
|
+
}
|
|
66
|
+
if (durations.size >= 1000) durations.clear()
|
|
67
|
+
durations.set(sum, steps)
|
|
68
|
+
return steps
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Calculates and adds the specified time duration to the provided date and returns a new Date.
|
|
72
|
+
* Years and months are clamped to the end of the month, e.g. Jan 31 + 1M is Feb 28 or 29.
|
|
73
|
+
* Days follow the clock, so a day over a daylight saving change is 23 or 25 hours,
|
|
74
|
+
* while hours, minutes, seconds and milliseconds are elapsed time.
|
|
75
|
+
* A part of zero adds nothing; any other part of years, months, weeks or days lands on a clock time,
|
|
76
|
+
* which moves forward out of a daylight saving gap and takes the first occurrence of an hour that repeats.
|
|
77
|
+
* @param {Date} date
|
|
78
|
+
* @param {string} sum
|
|
79
|
+
* ```
|
|
80
|
+
* e.g. "1Y2M 3D"
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* Supported time units:
|
|
84
|
+
* - "Y": Years
|
|
85
|
+
* - "M": Months
|
|
86
|
+
* - "W": Weeks
|
|
87
|
+
* - "D": Days
|
|
88
|
+
* - "H": Hours
|
|
89
|
+
* - "m": Minutes
|
|
90
|
+
* - "s": Seconds
|
|
91
|
+
* - "sss": Milliseconds
|
|
92
|
+
* @param {import("../public.js").TimeZone=} zone
|
|
93
|
+
* @returns {Date}
|
|
94
|
+
* @throws
|
|
95
|
+
* ```
|
|
96
|
+
* RangeError(`add: Invalid duration "${sum}"`) // sum is empty, or a part of it is not a number followed by a unit
|
|
97
|
+
* RangeError(`add: Invalid time zone "${zone}"`) // Intl.DateTimeFormat rejects the zone
|
|
98
|
+
* TypeError(`add: Expected a Date, got ${typeof date}`)
|
|
99
|
+
* ```
|
|
100
|
+
* @example add(new Date, "-1D") //=> Date { yesterday }
|
|
101
|
+
*/
|
|
102
|
+
export default function(date, sum, zone) {
|
|
103
|
+
const time = time_of("add", date)
|
|
104
|
+
const steps = compile(sum)
|
|
105
|
+
check_zone("add", zone)
|
|
106
|
+
const result = new Date(time)
|
|
107
|
+
if (isNaN(time)) return result
|
|
108
|
+
if (zone === undefined) {
|
|
109
|
+
for (let i = 0; i < steps.length; i += 2) {
|
|
110
|
+
const unit = /** @type {string} */(steps[i])/**/
|
|
111
|
+
const value = /** @type {number} */(steps[i + 1])/**/
|
|
112
|
+
const ms = unit_ms[unit]
|
|
113
|
+
if (ms) result.setTime(result.getTime() + value * ms)
|
|
114
|
+
else if (unit == "D") result.setDate(result.getDate() + value)
|
|
115
|
+
else result.setTime(
|
|
116
|
+
add_months(
|
|
117
|
+
result,
|
|
118
|
+
unit == "Y"
|
|
119
|
+
? value * 12
|
|
120
|
+
: value
|
|
121
|
+
).getTime()
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
return result
|
|
125
|
+
}
|
|
126
|
+
let shifted = result
|
|
127
|
+
for (let i = 0; i < steps.length; i += 2) {
|
|
128
|
+
const unit = /** @type {string} */(steps[i])/**/
|
|
129
|
+
const value = /** @type {number} */(steps[i + 1])/**/
|
|
130
|
+
const ms = unit_ms[unit]
|
|
131
|
+
if (ms) {
|
|
132
|
+
shifted = new Date(shifted.getTime() + value * ms)
|
|
133
|
+
} else if (unit == "D") {
|
|
134
|
+
const fields = fields_of(shifted, zone)
|
|
135
|
+
fields[2] = /** @type {number} */(fields[2])/**/ + value
|
|
136
|
+
shifted = date_of(fields, zone)
|
|
137
|
+
} else {
|
|
138
|
+
shifted = add_months(
|
|
139
|
+
shifted,
|
|
140
|
+
unit == "Y"
|
|
141
|
+
? value * 12
|
|
142
|
+
: value,
|
|
143
|
+
zone
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return shifted
|
|
148
|
+
}
|
package/src/calendar.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const month_days = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]
|
|
2
|
+
/**
|
|
3
|
+
* @param {number} year
|
|
4
|
+
* @param {number} month
|
|
5
|
+
* @param {number} day
|
|
6
|
+
* @returns {number}
|
|
7
|
+
*/
|
|
8
|
+
function days_of(year, month, day) {
|
|
9
|
+
if (month < 0 || month > 11) {
|
|
10
|
+
year += Math.floor(month / 12)
|
|
11
|
+
month = (month % 12 + 12) % 12
|
|
12
|
+
}
|
|
13
|
+
if (month < 2) year--
|
|
14
|
+
const era = Math.floor(year / 400)
|
|
15
|
+
const yoe = year - era * 400
|
|
16
|
+
return era * 146097
|
|
17
|
+
+ yoe * 365
|
|
18
|
+
+ Math.floor(yoe / 4)
|
|
19
|
+
- Math.floor(yoe / 100)
|
|
20
|
+
+ Math.floor(
|
|
21
|
+
(153 * (month < 2 ? month + 10 : month - 2) + 2) / 5
|
|
22
|
+
)
|
|
23
|
+
+ day
|
|
24
|
+
- 719469
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* @param {number} wall
|
|
28
|
+
* @returns {number[]}
|
|
29
|
+
*/
|
|
30
|
+
function fields_of_wall(wall) {
|
|
31
|
+
const days = Math.floor(wall / 86400000)
|
|
32
|
+
let rest = wall - days * 86400000
|
|
33
|
+
const hour = Math.floor(rest / 3600000)
|
|
34
|
+
rest -= hour * 3600000
|
|
35
|
+
const minute = Math.floor(rest / 60000)
|
|
36
|
+
rest -= minute * 60000
|
|
37
|
+
const second = Math.floor(rest / 1000)
|
|
38
|
+
const z = days + 719468
|
|
39
|
+
const era = Math.floor(z / 146097)
|
|
40
|
+
const doe = z - era * 146097
|
|
41
|
+
const yoe = Math.floor(
|
|
42
|
+
(doe - Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096)) / 365
|
|
43
|
+
)
|
|
44
|
+
const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100))
|
|
45
|
+
const mp = Math.floor((5 * doy + 2) / 153)
|
|
46
|
+
const month = mp < 10 ? mp + 2 : mp - 10
|
|
47
|
+
return [
|
|
48
|
+
yoe + era * 400 + (month < 2 ? 1 : 0),
|
|
49
|
+
month,
|
|
50
|
+
doy - Math.floor((153 * mp + 2) / 5) + 1,
|
|
51
|
+
hour,
|
|
52
|
+
minute,
|
|
53
|
+
second,
|
|
54
|
+
rest - second * 1000,
|
|
55
|
+
(days % 7 + 11) % 7
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* @param {number} year
|
|
60
|
+
* @param {number} month
|
|
61
|
+
* @returns {number}
|
|
62
|
+
*/
|
|
63
|
+
function last_day(year, month) {
|
|
64
|
+
return month == 1 && !(year % 4) && (year % 100 || !(year % 400))
|
|
65
|
+
? 29
|
|
66
|
+
: /** @type {number} */(month_days[month])/**/
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* @param {number[]} fields
|
|
70
|
+
* @returns {number}
|
|
71
|
+
*/
|
|
72
|
+
function wall_of_fields(fields) {
|
|
73
|
+
return days_of(
|
|
74
|
+
/** @type {number} */(fields[0])/**/,
|
|
75
|
+
/** @type {number} */(fields[1])/**/,
|
|
76
|
+
/** @type {number} */(fields[2])/**/
|
|
77
|
+
) * 86400000
|
|
78
|
+
+ /** @type {number} */(fields[3])/**/ * 3600000
|
|
79
|
+
+ /** @type {number} */(fields[4])/**/ * 60000
|
|
80
|
+
+ /** @type {number} */(fields[5])/**/ * 1000
|
|
81
|
+
+ /** @type {number} */(fields[6])/**/
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
days_of,
|
|
85
|
+
fields_of_wall,
|
|
86
|
+
last_day,
|
|
87
|
+
wall_of_fields
|
|
88
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import time_of from "./time_of.js"
|
|
2
|
+
import { check_zone, fields_of } from "./zone.js"
|
|
3
|
+
/** @type {Map<string, (string | number[])[]>} */
|
|
4
|
+
const formats = new Map()
|
|
5
|
+
const token_regex = /DD|HH|MM|YYYY|mm|sss?/g
|
|
6
|
+
const year_token = [ 0, 4, 0 ]
|
|
7
|
+
/** @type {Record<string, number[]>} */
|
|
8
|
+
const tokens = {
|
|
9
|
+
DD: [ 2, 2, 0 ],
|
|
10
|
+
HH: [ 3, 2, 0 ],
|
|
11
|
+
MM: [ 1, 2, 1 ],
|
|
12
|
+
YYYY: year_token,
|
|
13
|
+
mm: [ 4, 2, 0 ],
|
|
14
|
+
ss: [ 5, 2, 0 ],
|
|
15
|
+
sss: [ 6, 3, 0 ]
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} format
|
|
19
|
+
* @returns {(string | number[])[]}
|
|
20
|
+
*/
|
|
21
|
+
function compile(format) {
|
|
22
|
+
let parts = formats.get(format)
|
|
23
|
+
if (parts) return parts
|
|
24
|
+
parts = []
|
|
25
|
+
let last = 0
|
|
26
|
+
for (const { 0: token, index = 0 } of format.matchAll(token_regex)) {
|
|
27
|
+
if (index > last) parts.push(format.slice(last, index))
|
|
28
|
+
parts.push(
|
|
29
|
+
/** @type {number[]} */(tokens[token])/**/
|
|
30
|
+
)
|
|
31
|
+
last = index + token.length
|
|
32
|
+
}
|
|
33
|
+
if (last < format.length) parts.push(format.slice(last))
|
|
34
|
+
if (formats.size >= 1000) formats.clear()
|
|
35
|
+
formats.set(format, parts)
|
|
36
|
+
return parts
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* @param {number} value
|
|
40
|
+
* @param {number} length
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
function pad(value, length) {
|
|
44
|
+
return String(value).padStart(length, "0")
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* @param {number} year
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
function year_text(year) {
|
|
51
|
+
return year >= 0 && year <= 9999
|
|
52
|
+
? String(year).padStart(4, "0")
|
|
53
|
+
: (year < 0 ? "-" : "+") + String(Math.abs(year)).padStart(6, "0")
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Converts a Date object to a formatted string representation.
|
|
57
|
+
* Letters that are not a time unit are kept as they are, so `"HHhmm"` gives `"11h22"`.
|
|
58
|
+
* A year before 0 or after 9999 is printed as an expanded year, such as `"+012345"`.
|
|
59
|
+
* @param {Date} date
|
|
60
|
+
* @param {string=} format
|
|
61
|
+
* ```js
|
|
62
|
+
* = "YYYY-MM-DDTHH:mm:ss.sss"
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* Supported time units:
|
|
66
|
+
* - "YYYY": Years
|
|
67
|
+
* - "MM": Months
|
|
68
|
+
* - "DD": Days
|
|
69
|
+
* - "HH": Hours
|
|
70
|
+
* - "mm": Minutes
|
|
71
|
+
* - "ss": Seconds
|
|
72
|
+
* - "sss": Milliseconds
|
|
73
|
+
* @param {import("../public.js").TimeZone=} zone
|
|
74
|
+
* @returns {string}
|
|
75
|
+
* @throws
|
|
76
|
+
* ```
|
|
77
|
+
* RangeError(`dateToString: Invalid time zone "${zone}"`) // Intl.DateTimeFormat rejects the zone
|
|
78
|
+
* TypeError(`dateToString: Expected a Date, got ${typeof date}`)
|
|
79
|
+
* ```
|
|
80
|
+
* @example dateToString(new Date, "YYYY-MM-DD HH:mm", "Asia/Seoul") //=> "2024-07-15 21:00"
|
|
81
|
+
*/
|
|
82
|
+
export default function(
|
|
83
|
+
date,
|
|
84
|
+
format = "YYYY-MM-DDTHH:mm:ss.sss",
|
|
85
|
+
zone
|
|
86
|
+
) {
|
|
87
|
+
const time = time_of("dateToString", date)
|
|
88
|
+
check_zone("dateToString", zone)
|
|
89
|
+
if (isNaN(time)) return "Invalid Date"
|
|
90
|
+
const fields = fields_of(date, zone)
|
|
91
|
+
const parts = compile(format)
|
|
92
|
+
let result = ""
|
|
93
|
+
for (const part of parts) {
|
|
94
|
+
result += typeof part == "string"
|
|
95
|
+
? part
|
|
96
|
+
: part === year_token
|
|
97
|
+
? year_text(
|
|
98
|
+
/** @type {number} */(fields[0])/**/
|
|
99
|
+
)
|
|
100
|
+
: pad(
|
|
101
|
+
/** @type {number} */(fields[/** @type {number} */(part[0])/**/])/**/ + /** @type {number} */(part[2])/**/,
|
|
102
|
+
/** @type {number} */(part[1])/**/
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
return result
|
|
106
|
+
}
|
package/src/diff.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { days_of, last_day, wall_of_fields } from "./calendar.js"
|
|
2
|
+
import time_of from "./time_of.js"
|
|
3
|
+
import { check_zone, date_of, fields_of } from "./zone.js"
|
|
4
|
+
/** @type {Record<string, number | undefined>} */
|
|
5
|
+
const unit_ms = {
|
|
6
|
+
H: 3600000,
|
|
7
|
+
m: 60000,
|
|
8
|
+
s: 1000,
|
|
9
|
+
sss: 1
|
|
10
|
+
}
|
|
11
|
+
const units_of = new Set(
|
|
12
|
+
[ "D", "H", "M", "W", "Y", "m", "s", "sss" ]
|
|
13
|
+
)
|
|
14
|
+
/**
|
|
15
|
+
* @param {number[]} from_fields
|
|
16
|
+
* @param {number} months
|
|
17
|
+
* @param {number} to_wall
|
|
18
|
+
* @param {number} end
|
|
19
|
+
* @param {string=} zone
|
|
20
|
+
* @returns {number}
|
|
21
|
+
*/
|
|
22
|
+
function anchor_after(
|
|
23
|
+
from_fields,
|
|
24
|
+
months,
|
|
25
|
+
to_wall,
|
|
26
|
+
end,
|
|
27
|
+
zone
|
|
28
|
+
) {
|
|
29
|
+
const month = /** @type {number} */(from_fields[1])/**/ + months
|
|
30
|
+
const year = /** @type {number} */(from_fields[0])/**/ + Math.floor(month / 12)
|
|
31
|
+
const index = (month % 12 + 12) % 12
|
|
32
|
+
const fields = [
|
|
33
|
+
year,
|
|
34
|
+
index,
|
|
35
|
+
Math.min(
|
|
36
|
+
/** @type {number} */(from_fields[2])/**/,
|
|
37
|
+
last_day(year, index)
|
|
38
|
+
),
|
|
39
|
+
/** @type {number} */(from_fields[3])/**/,
|
|
40
|
+
/** @type {number} */(from_fields[4])/**/,
|
|
41
|
+
/** @type {number} */(from_fields[5])/**/,
|
|
42
|
+
/** @type {number} */(from_fields[6])/**/
|
|
43
|
+
]
|
|
44
|
+
const distance = wall_of_fields(fields) - to_wall
|
|
45
|
+
if (Math.abs(distance) >= 172800000) return distance
|
|
46
|
+
const time = date_of(fields, zone).getTime()
|
|
47
|
+
return isNaN(time)
|
|
48
|
+
? distance
|
|
49
|
+
: time - end
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Counts the whole units from one date to another, negative when the second date is earlier.
|
|
53
|
+
* Years, months, weeks and days follow the clock, so a day over a daylight saving change still
|
|
54
|
+
* counts as one day, while hours, minutes, seconds and milliseconds are elapsed time.
|
|
55
|
+
* @param {Date} from
|
|
56
|
+
* @param {Date} to
|
|
57
|
+
* @param {import("../public.js").Unit} unit
|
|
58
|
+
* ```
|
|
59
|
+
* "Y" | "M" | "W" | "D" | "H" | "m" | "s" | "sss"
|
|
60
|
+
* ```
|
|
61
|
+
* @param {import("../public.js").TimeZone=} zone
|
|
62
|
+
* @returns {number}
|
|
63
|
+
* @throws
|
|
64
|
+
* ```
|
|
65
|
+
* RangeError(`diff: Invalid unit "${unit}"`) // The unit is not one of the listed ones
|
|
66
|
+
* RangeError(`diff: Invalid time zone "${zone}"`) // Intl.DateTimeFormat rejects the zone
|
|
67
|
+
* TypeError(`diff: Expected a Date, got ${typeof from}`) // from or to is not a Date
|
|
68
|
+
* ```
|
|
69
|
+
* @example diff(start, end, "D") //=> 30
|
|
70
|
+
*/
|
|
71
|
+
export default function(from, to, unit, zone) {
|
|
72
|
+
const start = time_of("diff", from)
|
|
73
|
+
const end = time_of("diff", to)
|
|
74
|
+
if (!units_of.has(unit)) throw new RangeError(
|
|
75
|
+
`diff: Invalid unit "${String(unit)}"`
|
|
76
|
+
)
|
|
77
|
+
check_zone("diff", zone)
|
|
78
|
+
if (isNaN(start) || isNaN(end)) return NaN
|
|
79
|
+
const ms = unit_ms[unit]
|
|
80
|
+
if (ms) return Math.trunc((end - start) / ms) + 0
|
|
81
|
+
const from_fields = fields_of(from, zone)
|
|
82
|
+
const to_fields = fields_of(to, zone)
|
|
83
|
+
if (unit == "D" || unit == "W") {
|
|
84
|
+
const days = days_of(
|
|
85
|
+
/** @type {number} */(to_fields[0])/**/,
|
|
86
|
+
/** @type {number} */(to_fields[1])/**/,
|
|
87
|
+
/** @type {number} */(to_fields[2])/**/
|
|
88
|
+
) - days_of(
|
|
89
|
+
/** @type {number} */(from_fields[0])/**/,
|
|
90
|
+
/** @type {number} */(from_fields[1])/**/,
|
|
91
|
+
/** @type {number} */(from_fields[2])/**/
|
|
92
|
+
)
|
|
93
|
+
return unit == "D"
|
|
94
|
+
? days
|
|
95
|
+
: Math.trunc(days / 7) + 0
|
|
96
|
+
}
|
|
97
|
+
let months = (/** @type {number} */(to_fields[0])/**/ - /** @type {number} */(from_fields[0])/**/) * 12
|
|
98
|
+
+ /** @type {number} */(to_fields[1])/**/ - /** @type {number} */(from_fields[1])/**/
|
|
99
|
+
const to_day = /** @type {number} */(to_fields[2])/**/
|
|
100
|
+
const from_day = /** @type {number} */(from_fields[2])/**/
|
|
101
|
+
if (Math.abs(to_day - from_day) > 2 && to_day < 27 && from_day < 27) {
|
|
102
|
+
if (months > 0 && to_day < from_day) months--
|
|
103
|
+
else if (months < 0 && to_day > from_day) months++
|
|
104
|
+
} else {
|
|
105
|
+
const to_wall = wall_of_fields(to_fields)
|
|
106
|
+
if (end >= start) {
|
|
107
|
+
months = Math.max(months, 0)
|
|
108
|
+
while (months && anchor_after(
|
|
109
|
+
from_fields,
|
|
110
|
+
months,
|
|
111
|
+
to_wall,
|
|
112
|
+
end,
|
|
113
|
+
zone
|
|
114
|
+
) > 0) months--
|
|
115
|
+
if (anchor_after(
|
|
116
|
+
from_fields,
|
|
117
|
+
months + 1,
|
|
118
|
+
to_wall,
|
|
119
|
+
end,
|
|
120
|
+
zone
|
|
121
|
+
) <= 0) months++
|
|
122
|
+
} else {
|
|
123
|
+
months = Math.min(months, 0)
|
|
124
|
+
while (months && anchor_after(
|
|
125
|
+
from_fields,
|
|
126
|
+
months,
|
|
127
|
+
to_wall,
|
|
128
|
+
end,
|
|
129
|
+
zone
|
|
130
|
+
) < 0) months++
|
|
131
|
+
if (anchor_after(
|
|
132
|
+
from_fields,
|
|
133
|
+
months - 1,
|
|
134
|
+
to_wall,
|
|
135
|
+
end,
|
|
136
|
+
zone
|
|
137
|
+
) >= 0) months--
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return unit == "Y"
|
|
141
|
+
? Math.trunc(months / 12) + 0
|
|
142
|
+
: months
|
|
143
|
+
}
|
package/src/end_of.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
days_of,
|
|
3
|
+
fields_of_wall,
|
|
4
|
+
last_day,
|
|
5
|
+
wall_of_fields
|
|
6
|
+
} from "./calendar.js"
|
|
7
|
+
import time_of from "./time_of.js"
|
|
8
|
+
import {
|
|
9
|
+
check_zone,
|
|
10
|
+
end_at,
|
|
11
|
+
instant_of,
|
|
12
|
+
offset_in,
|
|
13
|
+
offset_of,
|
|
14
|
+
unit_end
|
|
15
|
+
} from "./zone.js"
|
|
16
|
+
/** @type {Record<string, number | undefined>} */
|
|
17
|
+
const unit_ms = { H: 3600000, m: 60000, s: 1000 }
|
|
18
|
+
/** @type {Record<string, number>} */
|
|
19
|
+
const units = { D: 3, M: 2, W: 3, Y: 1 }
|
|
20
|
+
const units_of = new Set(
|
|
21
|
+
[ "D", "H", "M", "W", "Y", "m", "s" ]
|
|
22
|
+
)
|
|
23
|
+
/**
|
|
24
|
+
* Returns a new Date at the last millisecond of the given unit, such as 23:59:59.999 of the day.
|
|
25
|
+
* A week ends on Sunday, as in ISO 8601.
|
|
26
|
+
* @param {Date} date
|
|
27
|
+
* @param {Exclude<import("../public.js").Unit, "sss">} unit
|
|
28
|
+
* ```
|
|
29
|
+
* "Y" | "M" | "W" | "D" | "H" | "m" | "s"
|
|
30
|
+
* ```
|
|
31
|
+
* @param {import("../public.js").TimeZone=} zone
|
|
32
|
+
* @returns {Date}
|
|
33
|
+
* @throws
|
|
34
|
+
* ```
|
|
35
|
+
* RangeError(`endOf: Invalid unit "${unit}"`) // The unit is not one of the listed ones
|
|
36
|
+
* RangeError(`endOf: Invalid time zone "${zone}"`) // Intl.DateTimeFormat rejects the zone
|
|
37
|
+
* TypeError(`endOf: Expected a Date, got ${typeof date}`)
|
|
38
|
+
* ```
|
|
39
|
+
* @example endOf(new Date, "M") //=> Date { last millisecond of this month }
|
|
40
|
+
*/
|
|
41
|
+
export default function(date, unit, zone) {
|
|
42
|
+
const time = time_of("endOf", date)
|
|
43
|
+
if (!units_of.has(unit)) throw new RangeError(
|
|
44
|
+
`endOf: Invalid unit "${String(unit)}"`
|
|
45
|
+
)
|
|
46
|
+
check_zone("endOf", zone)
|
|
47
|
+
if (isNaN(time)) return new Date(NaN)
|
|
48
|
+
const ms = unit_ms[unit]
|
|
49
|
+
if (ms) {
|
|
50
|
+
if (zone !== undefined) {
|
|
51
|
+
const offset = offset_of(time, zone)
|
|
52
|
+
const rest = ms - ((time + offset) % ms + ms) % ms
|
|
53
|
+
return new Date(
|
|
54
|
+
offset_of(time + rest - 1, zone) == offset
|
|
55
|
+
? time + rest - 1
|
|
56
|
+
: unit_end(time, ms, zone)
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
const rest = ms - (
|
|
60
|
+
ms > 60000
|
|
61
|
+
? date.getMinutes() * 60000 + date.getSeconds() * 1000 + date.getMilliseconds()
|
|
62
|
+
: ms > 1000
|
|
63
|
+
? date.getSeconds() * 1000 + date.getMilliseconds()
|
|
64
|
+
: date.getMilliseconds()
|
|
65
|
+
)
|
|
66
|
+
const result = new Date(time + rest - 1)
|
|
67
|
+
if (
|
|
68
|
+
result.getTimezoneOffset() == date.getTimezoneOffset()
|
|
69
|
+
&& result.getSeconds() == (ms > 1000 ? 59 : date.getSeconds())
|
|
70
|
+
) return result
|
|
71
|
+
result.setTime(unit_end(time, ms))
|
|
72
|
+
return result
|
|
73
|
+
}
|
|
74
|
+
const from = /** @type {number} */(units[unit])/**/
|
|
75
|
+
let end = time
|
|
76
|
+
if (zone === undefined) {
|
|
77
|
+
const result = new Date(time)
|
|
78
|
+
result.setHours(
|
|
79
|
+
unit == "D"
|
|
80
|
+
? 24
|
|
81
|
+
: unit == "W"
|
|
82
|
+
? 168 - 24 * ((result.getDay() + 6) % 7)
|
|
83
|
+
: unit == "M"
|
|
84
|
+
? 24 * (last_day(
|
|
85
|
+
result.getFullYear(),
|
|
86
|
+
result.getMonth()
|
|
87
|
+
) - result.getDate() + 1)
|
|
88
|
+
: 24 * (days_of(result.getFullYear() + 1, 0, 1) - days_of(
|
|
89
|
+
result.getFullYear(),
|
|
90
|
+
result.getMonth(),
|
|
91
|
+
result.getDate()
|
|
92
|
+
)),
|
|
93
|
+
0,
|
|
94
|
+
0,
|
|
95
|
+
0
|
|
96
|
+
)
|
|
97
|
+
end = result.getTime() - 1
|
|
98
|
+
if (end >= time && !(result.getHours() || result.getMinutes() || result.getSeconds())) {
|
|
99
|
+
result.setTime(end)
|
|
100
|
+
return result
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const offset = offset_in(time, zone)
|
|
104
|
+
const fields = fields_of_wall(time + offset)
|
|
105
|
+
if (unit == "W") fields[2] = /** @type {number} */(fields[2])/**/ + 7 - (/** @type {number} */(fields[7])/**/ + 6) % 7
|
|
106
|
+
else fields[from - 1] = /** @type {number} */(fields[from - 1])/**/ + 1
|
|
107
|
+
for (let i = from; i < 7; i++) fields[i] = i == 2 ? 1 : 0
|
|
108
|
+
const wall = wall_of_fields(fields)
|
|
109
|
+
if (zone !== undefined) {
|
|
110
|
+
const next = offset_of(wall - offset, zone)
|
|
111
|
+
if (
|
|
112
|
+
next == offset
|
|
113
|
+
|| wall - next > time && offset_of(wall - next, zone) == next
|
|
114
|
+
) return new Date(wall - next - 1)
|
|
115
|
+
end = instant_of(wall, zone) - 1
|
|
116
|
+
}
|
|
117
|
+
return new Date(end_at(wall, end, time, zone))
|
|
118
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { default as add } from "./add.js"
|
|
2
|
+
export { default as dateToString } from "./date_to_string.js"
|
|
3
|
+
export { default as diff } from "./diff.js"
|
|
4
|
+
export { default as endOf } from "./end_of.js"
|
|
5
|
+
export { default as startOf } from "./start_of.js"
|
|
6
|
+
export { default as stringToDate } from "./string_to_date.js"
|
|
7
|
+
export { default as timeUnit } from "./time_unit.js"
|
|
8
|
+
export { default as timeZoneOffset } from "./time_zone_offset.js"
|