@svgrid/grid 3.0.0 → 3.0.1
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 +116 -88
- package/README.md +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +181 -181
- package/dist/SvAutoComplete.svelte +169 -169
- package/dist/SvAvatar.svelte +75 -75
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +102 -102
- package/dist/SvCircularProgress.svelte +109 -109
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +279 -279
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +163 -163
- package/dist/SvDrawer.svelte +254 -254
- package/dist/SvDropDownList.svelte +378 -378
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGridCellEditor.svelte +748 -744
- package/dist/SvGridChart.svelte +1716 -1716
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +728 -728
- package/dist/SvGridSelect.svelte +270 -270
- package/dist/SvGroupCell.svelte +116 -116
- package/dist/SvListBox.svelte +334 -334
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +293 -293
- package/dist/SvNumberInput.svelte +172 -172
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -133
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvProgress.svelte +116 -116
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRating.svelte +112 -112
- package/dist/SvResult.svelte +73 -73
- package/dist/SvRichText.svelte +211 -211
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +203 -203
- package/dist/SvSwitchButton.svelte +108 -108
- package/dist/SvTagsInput.svelte +115 -115
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +208 -208
- package/dist/SvTree.svelte +444 -444
- package/dist/SvTreeSelect.svelte +239 -239
- package/dist/cdn/{GridMenus-DMihUtma.js → GridMenus-CYzOvmNQ.js} +3 -3
- package/dist/cdn/{GridMenus-nmNDj1a3.js → GridMenus-DFB6iwF8.js} +3 -3
- package/dist/cdn/{SvDateRangeInput-DflbiP7N.js → SvDateRangeInput-CaOuMs8O.js} +1 -1
- package/dist/cdn/{SvDateRangeInput-Bh0A0JkF.js → SvDateRangeInput-DMLKmGEc.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-Bivn8dAP.js → SvDateTimePicker-CCbDZNZB.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-BWwpfB_o.js → SvDateTimePicker-sonaH0oh.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-Ba7rY3eu.js → SvGridCellEditor-BWnTi2N7.js} +2 -2
- package/dist/cdn/{SvGridCellEditor-AXL8cHGO.js → SvGridCellEditor-CjEJMJKc.js} +2 -2
- package/dist/cdn/{SvGridChart-CwhFz7GV.js → SvGridChart-BEJmNNx9.js} +1 -1
- package/dist/cdn/{SvGridChart-Bs2GIR2Q.js → SvGridChart-BwVos976.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-1WXuStzM.js → SvGridChartPanel-BOknOkrP.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-e5_nqNP9.js → SvGridChartPanel-D2PjII4O.js} +1 -1
- package/dist/cdn/{SvGridChartView-SmPW10dI.js → SvGridChartView-00LPUHL1.js} +1 -1
- package/dist/cdn/{SvGridChartView-eSPuJE6g.js → SvGridChartView-BhUEJ5ki.js} +1 -1
- package/dist/cdn/{SvGridDropdown-B1ZcLpgs.js → SvGridDropdown-D0VdjeR8.js} +1 -1
- package/dist/cdn/{SvGridDropdown-CtdbHcIS.js → SvGridDropdown-D13MtJ2j.js} +1 -1
- package/dist/cdn/{date-format-DZT7T1wf.js → date-format-BNii4zeD.js} +2 -2
- package/dist/cdn/{date-format-CcP1tafP.js → date-format-BnnHlqGw.js} +2 -2
- package/dist/cdn/{src-B_YS5AOc.js → src-C8QjSaq-.js} +42 -44
- package/dist/cdn/{src-DYXXpuSk.js → src-D0hO2eYN.js} +41 -43
- package/dist/cdn/svgrid.js +7 -7
- package/dist/cdn/svgrid.svelte-external.js +7 -7
- package/dist/chart-export.js +8 -8
- package/dist/editing.js +22 -13
- package/package.json +11 -11
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +181 -181
- package/src/SvAutoComplete.svelte +169 -169
- package/src/SvAvatar.svelte +75 -75
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +102 -102
- package/src/SvCircularProgress.svelte +109 -109
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +279 -279
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +163 -163
- package/src/SvDrawer.svelte +254 -254
- package/src/SvDropDownList.svelte +378 -378
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.types.ts +2092 -2092
- package/src/SvGridCellEditor.svelte +748 -744
- package/src/SvGridChart.svelte +1716 -1716
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +728 -728
- package/src/SvGridSelect.svelte +270 -270
- package/src/SvGroupCell.svelte +116 -116
- package/src/SvListBox.svelte +334 -334
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +293 -293
- package/src/SvNumberInput.svelte +172 -172
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -133
- package/src/SvPopover.svelte +197 -197
- package/src/SvProgress.svelte +116 -116
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRating.svelte +112 -112
- package/src/SvResult.svelte +73 -73
- package/src/SvRichText.svelte +211 -211
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +203 -203
- package/src/SvSwitchButton.svelte +108 -108
- package/src/SvTagsInput.svelte +115 -115
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +208 -208
- package/src/SvTree.svelte +444 -444
- package/src/SvTreeSelect.svelte +239 -239
- package/src/a11y/dismissable.test.ts +119 -119
- package/src/a11y/dismissable.ts +114 -114
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1419 -1419
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +846 -846
- package/src/builtin-editors.grid.test.ts +83 -83
- package/src/cell-formatting.ts +171 -171
- package/src/cell-render.test.ts +513 -513
- package/src/cell-render.ts +496 -496
- package/src/cell-values.ts +148 -148
- package/src/chart-export.ts +202 -202
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2321 -2321
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/column-groups.ts +78 -78
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1865 -1865
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createListbox.svelte.ts +250 -250
- package/src/createMenu.svelte.ts +224 -224
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +322 -322
- package/src/datetime/date-core.ts +206 -206
- package/src/datetime/date-restrict.ts +61 -61
- package/src/datetime/timezone.ts +135 -135
- package/src/dock-manager-model.ts +596 -596
- package/src/dock-model.ts +374 -374
- package/src/editing.test.ts +974 -974
- package/src/editing.ts +618 -609
- package/src/editor-contract.ts +171 -171
- package/src/editor-registry.grid.test.ts +144 -144
- package/src/editor-registry.ts +122 -122
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +601 -601
- package/src/filter-operators.ts +160 -160
- package/src/filtering/excel-filters.ts +325 -325
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/group-display.test.ts +167 -167
- package/src/group-display.ts +200 -200
- package/src/headless.ts +87 -87
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard-handlers.ts +270 -270
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +179 -179
- package/src/menus.ts +597 -597
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/positioning.ts +268 -268
- package/src/render-component.ts +28 -28
- package/src/row-drag.test.ts +401 -401
- package/src/row-drag.ts +419 -419
- package/src/row-resize.test.ts +524 -524
- package/src/row-resize.ts +228 -228
- package/src/scheduler-ical.ts +181 -181
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +873 -873
- package/src/selection.test.ts +885 -885
- package/src/server-data-source.test.ts +383 -383
- package/src/server-data-source.ts +469 -469
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +488 -488
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/summaries.ts +113 -113
- package/src/svgrid-wrapper.types.ts +563 -563
- package/src/svgrid.async-editor-options.test.ts +273 -273
- package/src/svgrid.auto-row-height.test.ts +204 -204
- package/src/svgrid.behavior.test.ts +910 -910
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.comments-autocomplete.test.ts +127 -127
- package/src/svgrid.context-menu.test.ts +147 -147
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-depth.test.ts +163 -163
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +382 -377
- package/src/svgrid.filter-menu-scroll.test.ts +112 -112
- package/src/svgrid.grand-total.test.ts +188 -188
- package/src/svgrid.group-display-mode.test.ts +171 -171
- package/src/svgrid.group-footers.test.ts +121 -121
- package/src/svgrid.group-pagination.test.ts +153 -153
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.tree-data.test.ts +186 -186
- package/src/svgrid.wrapper.test.ts +63 -63
- package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +288 -288
- package/src/toast-store.svelte.ts +250 -250
- package/src/toast-store.test.ts +147 -147
- package/src/tree-row-model.test.ts +168 -168
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-localization.test.ts +113 -113
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +322 -322
package/src/datetime/timezone.ts
CHANGED
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure, framework-free IANA-timezone helpers, built on `Intl` (no date library).
|
|
3
|
-
*
|
|
4
|
-
* The scheduler renders in a chosen `timeZone` via a "pseudo-local" trick:
|
|
5
|
-
* {@link toZonedLocal} turns an absolute instant into a Date whose *local*
|
|
6
|
-
* wall-clock equals the wall-clock in `timeZone`, so all the existing
|
|
7
|
-
* local-based date math (startOfDay, minuteOfDay, layout) runs unchanged; then
|
|
8
|
-
* {@link fromZonedLocal} converts a user edit back to a real instant. When
|
|
9
|
-
* `timeZone` is undefined every function is the identity on the browser's zone.
|
|
10
|
-
*
|
|
11
|
-
* These functions are DST-aware and take no ambient "now" (safe for the model /
|
|
12
|
-
* headless contexts that forbid `Date.now()`).
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/** A wall-clock time broken into fields, as it reads in a specific time zone. */
|
|
16
|
-
export type ZoneParts = {
|
|
17
|
-
year: number
|
|
18
|
-
month: number // 1-12
|
|
19
|
-
day: number
|
|
20
|
-
hour: number // 0-23
|
|
21
|
-
minute: number
|
|
22
|
-
second: number
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const fmtCache = new Map<string, Intl.DateTimeFormat>()
|
|
26
|
-
function partsFormat(timeZone: string): Intl.DateTimeFormat {
|
|
27
|
-
let f = fmtCache.get(timeZone)
|
|
28
|
-
if (!f) {
|
|
29
|
-
f = new Intl.DateTimeFormat('en-US', {
|
|
30
|
-
timeZone,
|
|
31
|
-
hourCycle: 'h23',
|
|
32
|
-
year: 'numeric',
|
|
33
|
-
month: '2-digit',
|
|
34
|
-
day: '2-digit',
|
|
35
|
-
hour: '2-digit',
|
|
36
|
-
minute: '2-digit',
|
|
37
|
-
second: '2-digit',
|
|
38
|
-
})
|
|
39
|
-
fmtCache.set(timeZone, f)
|
|
40
|
-
}
|
|
41
|
-
return f
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** The wall-clock parts of `instant` in `timeZone` (browser-local if undefined). */
|
|
45
|
-
export function zoneParts(instant: Date, timeZone?: string): ZoneParts {
|
|
46
|
-
if (!timeZone) {
|
|
47
|
-
return {
|
|
48
|
-
year: instant.getFullYear(),
|
|
49
|
-
month: instant.getMonth() + 1,
|
|
50
|
-
day: instant.getDate(),
|
|
51
|
-
hour: instant.getHours(),
|
|
52
|
-
minute: instant.getMinutes(),
|
|
53
|
-
second: instant.getSeconds(),
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
const map: Record<string, number> = {}
|
|
57
|
-
for (const p of partsFormat(timeZone).formatToParts(instant)) {
|
|
58
|
-
if (p.type !== 'literal') map[p.type] = Number(p.value)
|
|
59
|
-
}
|
|
60
|
-
let hour = map.hour ?? 0
|
|
61
|
-
if (hour === 24) hour = 0 // some engines emit '24' for midnight under h23
|
|
62
|
-
return { year: map.year!, month: map.month!, day: map.day!, hour, minute: map.minute ?? 0, second: map.second ?? 0 }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Milliseconds `timeZone` is ahead of UTC at `instant` (DST-aware; negative west of UTC). */
|
|
66
|
-
export function zoneOffsetMs(instant: Date, timeZone?: string): number {
|
|
67
|
-
if (!timeZone) return -instant.getTimezoneOffset() * 60_000
|
|
68
|
-
const p = zoneParts(instant, timeZone)
|
|
69
|
-
const asUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second)
|
|
70
|
-
// `asUtc` reads the wall-clock as if it were UTC; its distance from the real
|
|
71
|
-
// instant (truncated to whole seconds, matching the parts' resolution) is the offset.
|
|
72
|
-
return asUtc - Math.floor(instant.getTime() / 1000) * 1000
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** An instant -> a Date whose LOCAL fields equal its wall-clock in `timeZone`. */
|
|
76
|
-
export function toZonedLocal(instant: Date, timeZone?: string): Date {
|
|
77
|
-
if (!timeZone) return instant
|
|
78
|
-
const p = zoneParts(instant, timeZone)
|
|
79
|
-
return new Date(p.year, p.month - 1, p.day, p.hour, p.minute, p.second, instant.getMilliseconds())
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** The instant whose wall-clock in `timeZone` is the given Y-M-D H:M:S. */
|
|
83
|
-
export function instantFromWallClock(
|
|
84
|
-
year: number,
|
|
85
|
-
month: number, // 1-12
|
|
86
|
-
day: number,
|
|
87
|
-
hour: number,
|
|
88
|
-
minute: number,
|
|
89
|
-
second = 0,
|
|
90
|
-
timeZone?: string,
|
|
91
|
-
ms = 0,
|
|
92
|
-
): Date {
|
|
93
|
-
if (!timeZone) return new Date(year, month - 1, day, hour, minute, second, ms)
|
|
94
|
-
const wallUtc = Date.UTC(year, month - 1, day, hour, minute, second, ms)
|
|
95
|
-
// Assume the wall-clock is UTC, subtract the offset there, then correct once
|
|
96
|
-
// in case the candidate instant lands on the other side of a DST transition.
|
|
97
|
-
const off1 = zoneOffsetMs(new Date(wallUtc), timeZone)
|
|
98
|
-
let inst = new Date(wallUtc - off1)
|
|
99
|
-
const off2 = zoneOffsetMs(inst, timeZone)
|
|
100
|
-
if (off2 !== off1) inst = new Date(wallUtc - off2)
|
|
101
|
-
return inst
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Inverse of {@link toZonedLocal}: a pseudo-local Date -> the real instant. */
|
|
105
|
-
export function fromZonedLocal(pseudo: Date, timeZone?: string): Date {
|
|
106
|
-
if (!timeZone) return pseudo
|
|
107
|
-
return instantFromWallClock(
|
|
108
|
-
pseudo.getFullYear(),
|
|
109
|
-
pseudo.getMonth() + 1,
|
|
110
|
-
pseudo.getDate(),
|
|
111
|
-
pseudo.getHours(),
|
|
112
|
-
pseudo.getMinutes(),
|
|
113
|
-
pseudo.getSeconds(),
|
|
114
|
-
timeZone,
|
|
115
|
-
pseudo.getMilliseconds(),
|
|
116
|
-
)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Short zone name at `instant`, e.g. "EDT" / "GMT+5:30" - for a ruler header. */
|
|
120
|
-
export function zoneAbbr(instant: Date, timeZone?: string): string {
|
|
121
|
-
if (!timeZone) return ''
|
|
122
|
-
const parts = new Intl.DateTimeFormat('en-US', { timeZone, hour: '2-digit', timeZoneName: 'short' }).formatToParts(instant)
|
|
123
|
-
return parts.find((p) => p.type === 'timeZoneName')?.value ?? ''
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Validate an IANA zone id; returns it if usable, else undefined (falls back to local). */
|
|
127
|
-
export function normalizeTimeZone(timeZone?: string): string | undefined {
|
|
128
|
-
if (!timeZone) return undefined
|
|
129
|
-
try {
|
|
130
|
-
new Intl.DateTimeFormat('en-US', { timeZone })
|
|
131
|
-
return timeZone
|
|
132
|
-
} catch {
|
|
133
|
-
return undefined
|
|
134
|
-
}
|
|
135
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Pure, framework-free IANA-timezone helpers, built on `Intl` (no date library).
|
|
3
|
+
*
|
|
4
|
+
* The scheduler renders in a chosen `timeZone` via a "pseudo-local" trick:
|
|
5
|
+
* {@link toZonedLocal} turns an absolute instant into a Date whose *local*
|
|
6
|
+
* wall-clock equals the wall-clock in `timeZone`, so all the existing
|
|
7
|
+
* local-based date math (startOfDay, minuteOfDay, layout) runs unchanged; then
|
|
8
|
+
* {@link fromZonedLocal} converts a user edit back to a real instant. When
|
|
9
|
+
* `timeZone` is undefined every function is the identity on the browser's zone.
|
|
10
|
+
*
|
|
11
|
+
* These functions are DST-aware and take no ambient "now" (safe for the model /
|
|
12
|
+
* headless contexts that forbid `Date.now()`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** A wall-clock time broken into fields, as it reads in a specific time zone. */
|
|
16
|
+
export type ZoneParts = {
|
|
17
|
+
year: number
|
|
18
|
+
month: number // 1-12
|
|
19
|
+
day: number
|
|
20
|
+
hour: number // 0-23
|
|
21
|
+
minute: number
|
|
22
|
+
second: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const fmtCache = new Map<string, Intl.DateTimeFormat>()
|
|
26
|
+
function partsFormat(timeZone: string): Intl.DateTimeFormat {
|
|
27
|
+
let f = fmtCache.get(timeZone)
|
|
28
|
+
if (!f) {
|
|
29
|
+
f = new Intl.DateTimeFormat('en-US', {
|
|
30
|
+
timeZone,
|
|
31
|
+
hourCycle: 'h23',
|
|
32
|
+
year: 'numeric',
|
|
33
|
+
month: '2-digit',
|
|
34
|
+
day: '2-digit',
|
|
35
|
+
hour: '2-digit',
|
|
36
|
+
minute: '2-digit',
|
|
37
|
+
second: '2-digit',
|
|
38
|
+
})
|
|
39
|
+
fmtCache.set(timeZone, f)
|
|
40
|
+
}
|
|
41
|
+
return f
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The wall-clock parts of `instant` in `timeZone` (browser-local if undefined). */
|
|
45
|
+
export function zoneParts(instant: Date, timeZone?: string): ZoneParts {
|
|
46
|
+
if (!timeZone) {
|
|
47
|
+
return {
|
|
48
|
+
year: instant.getFullYear(),
|
|
49
|
+
month: instant.getMonth() + 1,
|
|
50
|
+
day: instant.getDate(),
|
|
51
|
+
hour: instant.getHours(),
|
|
52
|
+
minute: instant.getMinutes(),
|
|
53
|
+
second: instant.getSeconds(),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const map: Record<string, number> = {}
|
|
57
|
+
for (const p of partsFormat(timeZone).formatToParts(instant)) {
|
|
58
|
+
if (p.type !== 'literal') map[p.type] = Number(p.value)
|
|
59
|
+
}
|
|
60
|
+
let hour = map.hour ?? 0
|
|
61
|
+
if (hour === 24) hour = 0 // some engines emit '24' for midnight under h23
|
|
62
|
+
return { year: map.year!, month: map.month!, day: map.day!, hour, minute: map.minute ?? 0, second: map.second ?? 0 }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Milliseconds `timeZone` is ahead of UTC at `instant` (DST-aware; negative west of UTC). */
|
|
66
|
+
export function zoneOffsetMs(instant: Date, timeZone?: string): number {
|
|
67
|
+
if (!timeZone) return -instant.getTimezoneOffset() * 60_000
|
|
68
|
+
const p = zoneParts(instant, timeZone)
|
|
69
|
+
const asUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second)
|
|
70
|
+
// `asUtc` reads the wall-clock as if it were UTC; its distance from the real
|
|
71
|
+
// instant (truncated to whole seconds, matching the parts' resolution) is the offset.
|
|
72
|
+
return asUtc - Math.floor(instant.getTime() / 1000) * 1000
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** An instant -> a Date whose LOCAL fields equal its wall-clock in `timeZone`. */
|
|
76
|
+
export function toZonedLocal(instant: Date, timeZone?: string): Date {
|
|
77
|
+
if (!timeZone) return instant
|
|
78
|
+
const p = zoneParts(instant, timeZone)
|
|
79
|
+
return new Date(p.year, p.month - 1, p.day, p.hour, p.minute, p.second, instant.getMilliseconds())
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The instant whose wall-clock in `timeZone` is the given Y-M-D H:M:S. */
|
|
83
|
+
export function instantFromWallClock(
|
|
84
|
+
year: number,
|
|
85
|
+
month: number, // 1-12
|
|
86
|
+
day: number,
|
|
87
|
+
hour: number,
|
|
88
|
+
minute: number,
|
|
89
|
+
second = 0,
|
|
90
|
+
timeZone?: string,
|
|
91
|
+
ms = 0,
|
|
92
|
+
): Date {
|
|
93
|
+
if (!timeZone) return new Date(year, month - 1, day, hour, minute, second, ms)
|
|
94
|
+
const wallUtc = Date.UTC(year, month - 1, day, hour, minute, second, ms)
|
|
95
|
+
// Assume the wall-clock is UTC, subtract the offset there, then correct once
|
|
96
|
+
// in case the candidate instant lands on the other side of a DST transition.
|
|
97
|
+
const off1 = zoneOffsetMs(new Date(wallUtc), timeZone)
|
|
98
|
+
let inst = new Date(wallUtc - off1)
|
|
99
|
+
const off2 = zoneOffsetMs(inst, timeZone)
|
|
100
|
+
if (off2 !== off1) inst = new Date(wallUtc - off2)
|
|
101
|
+
return inst
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Inverse of {@link toZonedLocal}: a pseudo-local Date -> the real instant. */
|
|
105
|
+
export function fromZonedLocal(pseudo: Date, timeZone?: string): Date {
|
|
106
|
+
if (!timeZone) return pseudo
|
|
107
|
+
return instantFromWallClock(
|
|
108
|
+
pseudo.getFullYear(),
|
|
109
|
+
pseudo.getMonth() + 1,
|
|
110
|
+
pseudo.getDate(),
|
|
111
|
+
pseudo.getHours(),
|
|
112
|
+
pseudo.getMinutes(),
|
|
113
|
+
pseudo.getSeconds(),
|
|
114
|
+
timeZone,
|
|
115
|
+
pseudo.getMilliseconds(),
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Short zone name at `instant`, e.g. "EDT" / "GMT+5:30" - for a ruler header. */
|
|
120
|
+
export function zoneAbbr(instant: Date, timeZone?: string): string {
|
|
121
|
+
if (!timeZone) return ''
|
|
122
|
+
const parts = new Intl.DateTimeFormat('en-US', { timeZone, hour: '2-digit', timeZoneName: 'short' }).formatToParts(instant)
|
|
123
|
+
return parts.find((p) => p.type === 'timeZoneName')?.value ?? ''
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Validate an IANA zone id; returns it if usable, else undefined (falls back to local). */
|
|
127
|
+
export function normalizeTimeZone(timeZone?: string): string | undefined {
|
|
128
|
+
if (!timeZone) return undefined
|
|
129
|
+
try {
|
|
130
|
+
new Intl.DateTimeFormat('en-US', { timeZone })
|
|
131
|
+
return timeZone
|
|
132
|
+
} catch {
|
|
133
|
+
return undefined
|
|
134
|
+
}
|
|
135
|
+
}
|