@keenthemes/ktui 1.0.28 → 1.1.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/README.md +27 -0
- package/dist/ktui.js +8780 -6199
- package/dist/ktui.min.js +1 -1
- package/dist/ktui.min.js.map +1 -1
- package/dist/styles.css +2744 -1367
- package/lib/cjs/components/alert/alert.js +1025 -0
- package/lib/cjs/components/alert/alert.js.map +1 -0
- package/lib/cjs/components/alert/index.js +20 -0
- package/lib/cjs/components/alert/index.js.map +1 -0
- package/lib/cjs/components/alert/templates.js +120 -0
- package/lib/cjs/components/alert/templates.js.map +1 -0
- package/lib/cjs/components/alert/types.js +7 -0
- package/lib/cjs/components/alert/types.js.map +1 -0
- package/lib/cjs/components/datepicker/config/config.js +42 -0
- package/lib/cjs/components/datepicker/config/config.js.map +1 -0
- package/lib/cjs/components/datepicker/config/index.js +24 -0
- package/lib/cjs/components/datepicker/config/index.js.map +1 -0
- package/lib/cjs/components/datepicker/config/interfaces.js +7 -0
- package/lib/cjs/components/datepicker/config/interfaces.js.map +1 -0
- package/lib/cjs/components/datepicker/config/types.js +7 -0
- package/lib/cjs/components/datepicker/config/types.js.map +1 -0
- package/lib/cjs/components/datepicker/core/event-manager.js +135 -0
- package/lib/cjs/components/datepicker/core/event-manager.js.map +1 -0
- package/lib/cjs/components/datepicker/core/focus-manager.js +167 -0
- package/lib/cjs/components/datepicker/core/focus-manager.js.map +1 -0
- package/lib/cjs/components/datepicker/core/helpers.js +219 -0
- package/lib/cjs/components/datepicker/core/helpers.js.map +1 -0
- package/lib/cjs/components/datepicker/core/index.js +25 -0
- package/lib/cjs/components/datepicker/core/index.js.map +1 -0
- package/lib/cjs/components/datepicker/core/unified-state-manager.js +394 -0
- package/lib/cjs/components/datepicker/core/unified-state-manager.js.map +1 -0
- package/lib/cjs/components/datepicker/datepicker.js +2066 -763
- package/lib/cjs/components/datepicker/datepicker.js.map +1 -1
- package/lib/cjs/components/datepicker/index.js +19 -8
- package/lib/cjs/components/datepicker/index.js.map +1 -1
- package/lib/cjs/components/datepicker/ui/index.js +23 -0
- package/lib/cjs/components/datepicker/ui/index.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/input/dropdown.js +489 -0
- package/lib/cjs/components/datepicker/ui/input/dropdown.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/input/index.js +23 -0
- package/lib/cjs/components/datepicker/ui/input/index.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/input/segmented-input.js +640 -0
- package/lib/cjs/components/datepicker/ui/input/segmented-input.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/renderers/calendar.js +446 -0
- package/lib/cjs/components/datepicker/ui/renderers/calendar.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/renderers/footer.js +42 -0
- package/lib/cjs/components/datepicker/ui/renderers/footer.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/renderers/header.js +32 -0
- package/lib/cjs/components/datepicker/ui/renderers/header.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/renderers/index.js +25 -0
- package/lib/cjs/components/datepicker/ui/renderers/index.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/renderers/time-picker.js +384 -0
- package/lib/cjs/components/datepicker/ui/renderers/time-picker.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/templates/index.js +22 -0
- package/lib/cjs/components/datepicker/ui/templates/index.js.map +1 -0
- package/lib/cjs/components/datepicker/ui/templates/templates.js +253 -0
- package/lib/cjs/components/datepicker/ui/templates/templates.js.map +1 -0
- package/lib/cjs/components/datepicker/utils/date-formatters.js +88 -0
- package/lib/cjs/components/datepicker/utils/date-formatters.js.map +1 -0
- package/lib/cjs/components/datepicker/utils/date-utils.js +194 -0
- package/lib/cjs/components/datepicker/utils/date-utils.js.map +1 -0
- package/lib/cjs/components/datepicker/utils/index.js +24 -0
- package/lib/cjs/components/datepicker/utils/index.js.map +1 -0
- package/lib/cjs/components/datepicker/utils/time-utils.js +213 -0
- package/lib/cjs/components/datepicker/utils/time-utils.js.map +1 -0
- package/lib/cjs/index.js +6 -1
- package/lib/cjs/index.js.map +1 -1
- package/lib/esm/components/alert/alert.js +1022 -0
- package/lib/esm/components/alert/alert.js.map +1 -0
- package/lib/esm/components/alert/index.js +4 -0
- package/lib/esm/components/alert/index.js.map +1 -0
- package/lib/esm/components/alert/templates.js +112 -0
- package/lib/esm/components/alert/templates.js.map +1 -0
- package/lib/esm/components/alert/types.js +6 -0
- package/lib/esm/components/alert/types.js.map +1 -0
- package/lib/esm/components/datepicker/config/config.js +39 -0
- package/lib/esm/components/datepicker/config/config.js.map +1 -0
- package/lib/esm/components/datepicker/config/index.js +8 -0
- package/lib/esm/components/datepicker/config/index.js.map +1 -0
- package/lib/esm/components/datepicker/config/interfaces.js +6 -0
- package/lib/esm/components/datepicker/config/interfaces.js.map +1 -0
- package/lib/esm/components/datepicker/config/types.js +6 -0
- package/lib/esm/components/datepicker/config/types.js.map +1 -0
- package/lib/esm/components/datepicker/core/event-manager.js +133 -0
- package/lib/esm/components/datepicker/core/event-manager.js.map +1 -0
- package/lib/esm/components/datepicker/core/focus-manager.js +164 -0
- package/lib/esm/components/datepicker/core/focus-manager.js.map +1 -0
- package/lib/esm/components/datepicker/core/helpers.js +211 -0
- package/lib/esm/components/datepicker/core/helpers.js.map +1 -0
- package/lib/esm/components/datepicker/core/index.js +9 -0
- package/lib/esm/components/datepicker/core/index.js.map +1 -0
- package/lib/esm/components/datepicker/core/unified-state-manager.js +391 -0
- package/lib/esm/components/datepicker/core/unified-state-manager.js.map +1 -0
- package/lib/esm/components/datepicker/datepicker.js +2065 -763
- package/lib/esm/components/datepicker/datepicker.js.map +1 -1
- package/lib/esm/components/datepicker/index.js +6 -8
- package/lib/esm/components/datepicker/index.js.map +1 -1
- package/lib/esm/components/datepicker/ui/index.js +7 -0
- package/lib/esm/components/datepicker/ui/index.js.map +1 -0
- package/lib/esm/components/datepicker/ui/input/dropdown.js +486 -0
- package/lib/esm/components/datepicker/ui/input/dropdown.js.map +1 -0
- package/lib/esm/components/datepicker/ui/input/index.js +7 -0
- package/lib/esm/components/datepicker/ui/input/index.js.map +1 -0
- package/lib/esm/components/datepicker/ui/input/segmented-input.js +637 -0
- package/lib/esm/components/datepicker/ui/input/segmented-input.js.map +1 -0
- package/lib/esm/components/datepicker/ui/renderers/calendar.js +443 -0
- package/lib/esm/components/datepicker/ui/renderers/calendar.js.map +1 -0
- package/lib/esm/components/datepicker/ui/renderers/footer.js +39 -0
- package/lib/esm/components/datepicker/ui/renderers/footer.js.map +1 -0
- package/lib/esm/components/datepicker/ui/renderers/header.js +29 -0
- package/lib/esm/components/datepicker/ui/renderers/header.js.map +1 -0
- package/lib/esm/components/datepicker/ui/renderers/index.js +9 -0
- package/lib/esm/components/datepicker/ui/renderers/index.js.map +1 -0
- package/lib/esm/components/datepicker/ui/renderers/time-picker.js +381 -0
- package/lib/esm/components/datepicker/ui/renderers/time-picker.js.map +1 -0
- package/lib/esm/components/datepicker/ui/templates/index.js +6 -0
- package/lib/esm/components/datepicker/ui/templates/index.js.map +1 -0
- package/lib/esm/components/datepicker/ui/templates/templates.js +242 -0
- package/lib/esm/components/datepicker/ui/templates/templates.js.map +1 -0
- package/lib/esm/components/datepicker/utils/date-formatters.js +83 -0
- package/lib/esm/components/datepicker/utils/date-formatters.js.map +1 -0
- package/lib/esm/components/datepicker/utils/date-utils.js +184 -0
- package/lib/esm/components/datepicker/utils/date-utils.js.map +1 -0
- package/lib/esm/components/datepicker/utils/index.js +8 -0
- package/lib/esm/components/datepicker/utils/index.js.map +1 -0
- package/lib/esm/components/datepicker/utils/time-utils.js +201 -0
- package/lib/esm/components/datepicker/utils/time-utils.js.map +1 -0
- package/lib/esm/index.js +4 -0
- package/lib/esm/index.js.map +1 -1
- package/package.json +22 -3
- package/src/components/alert/alert.css +429 -188
- package/src/components/alert/alert.ts +990 -0
- package/src/components/alert/index.ts +4 -0
- package/src/components/alert/templates.ts +110 -0
- package/src/components/alert/tests/accessibility/aria-roles.test.ts +19 -0
- package/src/components/alert/tests/accessibility/focus-management.test.ts +19 -0
- package/src/components/alert/tests/accessibility/keyboard-nav.test.ts +22 -0
- package/src/components/alert/tests/actions/confirm-cancel.test.ts +122 -0
- package/src/components/alert/tests/actions/input-field.test.ts +180 -0
- package/src/components/alert/tests/alert.basic.test.ts +126 -0
- package/src/components/alert/tests/alert.config.test.ts +75 -0
- package/src/components/alert/tests/alert.templates.test.ts +17 -0
- package/src/components/alert/tests/config/attribute-config.test.ts +94 -0
- package/src/components/alert/tests/config/json-config.test.ts +119 -0
- package/src/components/alert/tests/config/merging.test.ts +89 -0
- package/src/components/alert/tests/dismissal/auto-dismiss.test.ts +96 -0
- package/src/components/alert/tests/dismissal/escape-key-dismiss.test.ts +105 -0
- package/src/components/alert/tests/dismissal/manual-dismiss.test.ts +90 -0
- package/src/components/alert/tests/dismissal/outside-click-dismiss.test.ts +91 -0
- package/src/components/alert/tests/edge-cases/invalid-config.test.ts +19 -0
- package/src/components/alert/tests/edge-cases/multiple-alerts.test.ts +19 -0
- package/src/components/alert/tests/rendering/custom-content.test.ts +81 -0
- package/src/components/alert/tests/rendering/info-alert.test.ts +84 -0
- package/src/components/alert/tests/rendering/success-alert.test.ts +100 -0
- package/src/components/alert/tests/templates/default-templates.test.ts +16 -0
- package/src/components/alert/tests/templates/user-templates.test.ts +16 -0
- package/src/components/alert/types.ts +145 -0
- package/src/components/datepicker/__tests__/datepicker-events.test.ts +356 -0
- package/src/components/datepicker/__tests__/datepicker-init.test.ts +343 -0
- package/src/components/datepicker/__tests__/datepicker-integration.test.ts +435 -0
- package/src/components/datepicker/__tests__/datepicker-timezone.test.ts +220 -0
- package/src/components/datepicker/__tests__/segmented-input-focus.test.ts +380 -0
- package/src/components/datepicker/__tests__/selective-state-updates.test.ts +400 -0
- package/src/components/datepicker/__tests__/state-manager.test.ts +421 -0
- package/src/components/datepicker/__tests__/time-preservation.test.ts +387 -0
- package/src/components/datepicker/config/config.ts +40 -0
- package/src/components/datepicker/config/index.ts +8 -0
- package/src/components/datepicker/config/interfaces.ts +82 -0
- package/src/components/datepicker/config/types.ts +188 -0
- package/src/components/datepicker/core/event-manager.ts +159 -0
- package/src/components/datepicker/core/focus-manager.ts +201 -0
- package/src/components/datepicker/core/helpers.ts +231 -0
- package/src/components/datepicker/core/index.ts +9 -0
- package/src/components/datepicker/core/unified-state-manager.ts +459 -0
- package/src/components/datepicker/datepicker.css +429 -1
- package/src/components/datepicker/datepicker.ts +2538 -1277
- package/src/components/datepicker/index.ts +6 -8
- package/src/components/datepicker/ui/index.ts +7 -0
- package/src/components/datepicker/ui/input/dropdown.ts +552 -0
- package/src/components/datepicker/ui/input/index.ts +7 -0
- package/src/components/datepicker/ui/input/segmented-input.ts +638 -0
- package/src/components/datepicker/ui/renderers/__tests__/calendar-optimizations.test.ts +611 -0
- package/src/components/datepicker/ui/renderers/calendar.ts +530 -0
- package/src/components/datepicker/ui/renderers/footer.ts +43 -0
- package/src/components/datepicker/ui/renderers/header.ts +33 -0
- package/src/components/datepicker/ui/renderers/index.ts +9 -0
- package/src/components/datepicker/ui/renderers/time-picker.ts +438 -0
- package/src/components/datepicker/ui/templates/index.ts +6 -0
- package/src/components/datepicker/ui/templates/templates.ts +306 -0
- package/src/components/datepicker/utils/__tests__/date-formatters.test.ts +160 -0
- package/src/components/datepicker/utils/__tests__/date-utils-keys.test.ts +86 -0
- package/src/components/datepicker/utils/__tests__/date-utils-timezone.test.ts +215 -0
- package/src/components/datepicker/utils/date-formatters.ts +85 -0
- package/src/components/datepicker/utils/date-utils.ts +172 -0
- package/src/components/datepicker/utils/index.ts +8 -0
- package/src/components/datepicker/utils/time-utils.ts +221 -0
- package/src/index.ts +7 -1
- package/CONTRIBUTING.md +0 -101
- package/examples/datatable/checkbox-events-test.html +0 -400
- package/examples/datatable/credentials-test.html +0 -423
- package/examples/datatable/remote-checkbox-test.html +0 -365
- package/examples/datatable/sorting-test.html +0 -258
- package/examples/image-input/file-upload-example.html +0 -189
- package/examples/modal/persistent.html +0 -205
- package/examples/modal/remote-select-dropdown.html +0 -166
- package/examples/modal/select-dropdown-container.html +0 -129
- package/examples/select/avatar.html +0 -47
- package/examples/select/basic-usage.html +0 -39
- package/examples/select/country.html +0 -43
- package/examples/select/dark-mode.html +0 -93
- package/examples/select/description.html +0 -53
- package/examples/select/disable-option.html +0 -37
- package/examples/select/disable-select.html +0 -35
- package/examples/select/dropdowncontainer.html +0 -111
- package/examples/select/dynamic-methods.html +0 -273
- package/examples/select/formdata-remote.html +0 -161
- package/examples/select/global-config.html +0 -81
- package/examples/select/icon-multiple.html +0 -50
- package/examples/select/icon.html +0 -48
- package/examples/select/max-selection.html +0 -38
- package/examples/select/modal-container.html +0 -128
- package/examples/select/modal-positioning-test.html +0 -338
- package/examples/select/modal.html +0 -80
- package/examples/select/multiple.html +0 -40
- package/examples/select/native-selected.html +0 -64
- package/examples/select/placeholder.html +0 -40
- package/examples/select/remote-data-preselected.html +0 -283
- package/examples/select/remote-data.html +0 -38
- package/examples/select/search.html +0 -57
- package/examples/select/sizes.html +0 -94
- package/examples/select/tags-enhanced.html +0 -86
- package/examples/select/tags-icons.html +0 -57
- package/examples/select/template-customization.html +0 -61
- package/examples/sticky/README.md +0 -158
- package/examples/sticky/debug-sticky.html +0 -144
- package/examples/sticky/test-runner.html +0 -175
- package/examples/sticky/test-sticky-logic.js +0 -369
- package/examples/sticky/test-sticky-positioning.html +0 -386
- package/examples/toast/example.html +0 -479
- package/lib/cjs/components/datepicker/calendar.js +0 -1061
- package/lib/cjs/components/datepicker/calendar.js.map +0 -1
- package/lib/cjs/components/datepicker/config.js +0 -332
- package/lib/cjs/components/datepicker/config.js.map +0 -1
- package/lib/cjs/components/datepicker/dropdown.js +0 -635
- package/lib/cjs/components/datepicker/dropdown.js.map +0 -1
- package/lib/cjs/components/datepicker/events.js +0 -129
- package/lib/cjs/components/datepicker/events.js.map +0 -1
- package/lib/cjs/components/datepicker/keyboard.js +0 -536
- package/lib/cjs/components/datepicker/keyboard.js.map +0 -1
- package/lib/cjs/components/datepicker/locales.js +0 -78
- package/lib/cjs/components/datepicker/locales.js.map +0 -1
- package/lib/cjs/components/datepicker/templates.js +0 -403
- package/lib/cjs/components/datepicker/templates.js.map +0 -1
- package/lib/cjs/components/datepicker/types.js +0 -23
- package/lib/cjs/components/datepicker/types.js.map +0 -1
- package/lib/cjs/components/datepicker/utils.js +0 -524
- package/lib/cjs/components/datepicker/utils.js.map +0 -1
- package/lib/esm/components/datepicker/calendar.js +0 -1058
- package/lib/esm/components/datepicker/calendar.js.map +0 -1
- package/lib/esm/components/datepicker/config.js +0 -329
- package/lib/esm/components/datepicker/config.js.map +0 -1
- package/lib/esm/components/datepicker/dropdown.js +0 -632
- package/lib/esm/components/datepicker/dropdown.js.map +0 -1
- package/lib/esm/components/datepicker/events.js +0 -126
- package/lib/esm/components/datepicker/events.js.map +0 -1
- package/lib/esm/components/datepicker/keyboard.js +0 -533
- package/lib/esm/components/datepicker/keyboard.js.map +0 -1
- package/lib/esm/components/datepicker/locales.js +0 -74
- package/lib/esm/components/datepicker/locales.js.map +0 -1
- package/lib/esm/components/datepicker/templates.js +0 -390
- package/lib/esm/components/datepicker/templates.js.map +0 -1
- package/lib/esm/components/datepicker/types.js +0 -20
- package/lib/esm/components/datepicker/types.js.map +0 -1
- package/lib/esm/components/datepicker/utils.js +0 -508
- package/lib/esm/components/datepicker/utils.js.map +0 -1
- package/prettier.config.js +0 -9
- package/src/components/datepicker/calendar.ts +0 -1397
- package/src/components/datepicker/config.ts +0 -368
- package/src/components/datepicker/dropdown.ts +0 -757
- package/src/components/datepicker/events.ts +0 -149
- package/src/components/datepicker/keyboard.ts +0 -646
- package/src/components/datepicker/locales.ts +0 -80
- package/src/components/datepicker/templates.ts +0 -792
- package/src/components/datepicker/types.ts +0 -154
- package/src/components/datepicker/utils.ts +0 -631
- package/src/components/select/variants.css +0 -4
- package/tsconfig.json +0 -17
- package/webpack.config.js +0 -118
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
formatDateToLocalString,
|
|
4
|
+
normalizeDateToLocalMidnight,
|
|
5
|
+
isSameLocalDay,
|
|
6
|
+
parseLocalDate
|
|
7
|
+
} from '../date-utils';
|
|
8
|
+
|
|
9
|
+
describe('Date Utils - Timezone Handling', () => {
|
|
10
|
+
describe('formatDateToLocalString', () => {
|
|
11
|
+
it('should format date as YYYY-MM-DD using local timezone', () => {
|
|
12
|
+
const date = new Date(2024, 0, 15, 14, 30, 45); // Jan 15, 2024 2:30:45 PM
|
|
13
|
+
const formatted = formatDateToLocalString(date);
|
|
14
|
+
expect(formatted).toBe('2024-01-15');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should use local timezone components, not UTC', () => {
|
|
18
|
+
// Create a date that would shift if converted to UTC
|
|
19
|
+
// In PST (UTC-8), Jan 15 11:00 PM becomes Jan 16 7:00 AM UTC
|
|
20
|
+
const date = new Date(2024, 0, 15, 23, 0, 0); // Jan 15, 2024 11:00 PM local
|
|
21
|
+
const formatted = formatDateToLocalString(date);
|
|
22
|
+
// Should still be Jan 15 in local timezone, not shifted to Jan 16
|
|
23
|
+
expect(formatted).toBe('2024-01-15');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should pad single-digit months and days with zeros', () => {
|
|
27
|
+
const date = new Date(2024, 0, 5); // Jan 5, 2024
|
|
28
|
+
const formatted = formatDateToLocalString(date);
|
|
29
|
+
expect(formatted).toBe('2024-01-05');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should handle year boundaries correctly', () => {
|
|
33
|
+
const date1 = new Date(2023, 11, 31); // Dec 31, 2023
|
|
34
|
+
const date2 = new Date(2024, 0, 1); // Jan 1, 2024
|
|
35
|
+
expect(formatDateToLocalString(date1)).toBe('2023-12-31');
|
|
36
|
+
expect(formatDateToLocalString(date2)).toBe('2024-01-01');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should handle leap years correctly', () => {
|
|
40
|
+
const date = new Date(2024, 1, 29); // Feb 29, 2024 (leap year)
|
|
41
|
+
const formatted = formatDateToLocalString(date);
|
|
42
|
+
expect(formatted).toBe('2024-02-29');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('normalizeDateToLocalMidnight', () => {
|
|
47
|
+
it('should set time to local midnight (00:00:00)', () => {
|
|
48
|
+
const date = new Date(2024, 0, 15, 14, 30, 45, 123);
|
|
49
|
+
const normalized = normalizeDateToLocalMidnight(date);
|
|
50
|
+
expect(normalized.getHours()).toBe(0);
|
|
51
|
+
expect(normalized.getMinutes()).toBe(0);
|
|
52
|
+
expect(normalized.getSeconds()).toBe(0);
|
|
53
|
+
expect(normalized.getMilliseconds()).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should preserve date components (year, month, day)', () => {
|
|
57
|
+
const date = new Date(2024, 0, 15, 14, 30, 45);
|
|
58
|
+
const normalized = normalizeDateToLocalMidnight(date);
|
|
59
|
+
expect(normalized.getFullYear()).toBe(2024);
|
|
60
|
+
expect(normalized.getMonth()).toBe(0);
|
|
61
|
+
expect(normalized.getDate()).toBe(15);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return a new Date object, not modify the original', () => {
|
|
65
|
+
const date = new Date(2024, 0, 15, 14, 30, 45);
|
|
66
|
+
const normalized = normalizeDateToLocalMidnight(date);
|
|
67
|
+
expect(normalized).not.toBe(date);
|
|
68
|
+
expect(date.getHours()).toBe(14); // Original unchanged
|
|
69
|
+
expect(normalized.getHours()).toBe(0); // New one normalized
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should handle dates already at midnight', () => {
|
|
73
|
+
const date = new Date(2024, 0, 15, 0, 0, 0, 0);
|
|
74
|
+
const normalized = normalizeDateToLocalMidnight(date);
|
|
75
|
+
expect(normalized.getHours()).toBe(0);
|
|
76
|
+
expect(normalized.getDate()).toBe(15);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('isSameLocalDay', () => {
|
|
81
|
+
it('should return true for dates on the same calendar day', () => {
|
|
82
|
+
const date1 = new Date(2024, 0, 15, 10, 0, 0);
|
|
83
|
+
const date2 = new Date(2024, 0, 15, 20, 0, 0);
|
|
84
|
+
expect(isSameLocalDay(date1, date2)).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should return false for dates on different days', () => {
|
|
88
|
+
const date1 = new Date(2024, 0, 15, 10, 0, 0);
|
|
89
|
+
const date2 = new Date(2024, 0, 16, 10, 0, 0);
|
|
90
|
+
expect(isSameLocalDay(date1, date2)).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should return false for dates in different months', () => {
|
|
94
|
+
const date1 = new Date(2024, 0, 15, 10, 0, 0);
|
|
95
|
+
const date2 = new Date(2024, 1, 15, 10, 0, 0);
|
|
96
|
+
expect(isSameLocalDay(date1, date2)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should return false for dates in different years', () => {
|
|
100
|
+
const date1 = new Date(2024, 0, 15, 10, 0, 0);
|
|
101
|
+
const date2 = new Date(2025, 0, 15, 10, 0, 0);
|
|
102
|
+
expect(isSameLocalDay(date1, date2)).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should ignore time components when comparing', () => {
|
|
106
|
+
const date1 = new Date(2024, 0, 15, 0, 0, 0, 0);
|
|
107
|
+
const date2 = new Date(2024, 0, 15, 23, 59, 59, 999);
|
|
108
|
+
expect(isSameLocalDay(date1, date2)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should use local timezone for comparison', () => {
|
|
112
|
+
// Create dates that might differ in UTC but are same in local timezone
|
|
113
|
+
const date1 = new Date(2024, 0, 15, 23, 0, 0); // Jan 15 11 PM local
|
|
114
|
+
const date2 = new Date(2024, 0, 15, 0, 0, 0); // Jan 15 midnight local
|
|
115
|
+
expect(isSameLocalDay(date1, date2)).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('parseLocalDate', () => {
|
|
120
|
+
it('should parse YYYY-MM-DD string to local date at midnight', () => {
|
|
121
|
+
const parsed = parseLocalDate('2024-01-15');
|
|
122
|
+
expect(parsed.getFullYear()).toBe(2024);
|
|
123
|
+
expect(parsed.getMonth()).toBe(0); // January is 0
|
|
124
|
+
expect(parsed.getDate()).toBe(15);
|
|
125
|
+
expect(parsed.getHours()).toBe(0);
|
|
126
|
+
expect(parsed.getMinutes()).toBe(0);
|
|
127
|
+
expect(parsed.getSeconds()).toBe(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should create date in local timezone, not UTC', () => {
|
|
131
|
+
const parsed = parseLocalDate('2024-01-15');
|
|
132
|
+
// The date should represent Jan 15 at local midnight
|
|
133
|
+
// Not Jan 15 at UTC midnight (which could be different day in some timezones)
|
|
134
|
+
expect(parsed.getDate()).toBe(15);
|
|
135
|
+
expect(parsed.getMonth()).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should handle single-digit months and days', () => {
|
|
139
|
+
const parsed = parseLocalDate('2024-01-05');
|
|
140
|
+
expect(parsed.getMonth()).toBe(0);
|
|
141
|
+
expect(parsed.getDate()).toBe(5);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should handle year boundaries', () => {
|
|
145
|
+
const date1 = parseLocalDate('2023-12-31');
|
|
146
|
+
const date2 = parseLocalDate('2024-01-01');
|
|
147
|
+
expect(date1.getFullYear()).toBe(2023);
|
|
148
|
+
expect(date1.getMonth()).toBe(11);
|
|
149
|
+
expect(date1.getDate()).toBe(31);
|
|
150
|
+
expect(date2.getFullYear()).toBe(2024);
|
|
151
|
+
expect(date2.getMonth()).toBe(0);
|
|
152
|
+
expect(date2.getDate()).toBe(1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should handle leap years', () => {
|
|
156
|
+
const parsed = parseLocalDate('2024-02-29');
|
|
157
|
+
expect(parsed.getFullYear()).toBe(2024);
|
|
158
|
+
expect(parsed.getMonth()).toBe(1);
|
|
159
|
+
expect(parsed.getDate()).toBe(29);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should round-trip correctly with formatDateToLocalString', () => {
|
|
163
|
+
const originalDate = new Date(2024, 0, 15, 14, 30, 45);
|
|
164
|
+
const formatted = formatDateToLocalString(originalDate);
|
|
165
|
+
const parsed = parseLocalDate(formatted);
|
|
166
|
+
expect(parsed.getFullYear()).toBe(originalDate.getFullYear());
|
|
167
|
+
expect(parsed.getMonth()).toBe(originalDate.getMonth());
|
|
168
|
+
expect(parsed.getDate()).toBe(originalDate.getDate());
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('Timezone Consistency', () => {
|
|
173
|
+
it('should maintain date consistency across format and parse operations', () => {
|
|
174
|
+
const testDates = [
|
|
175
|
+
new Date(2024, 0, 1), // Jan 1
|
|
176
|
+
new Date(2024, 5, 15), // Jun 15
|
|
177
|
+
new Date(2024, 11, 31), // Dec 31
|
|
178
|
+
new Date(2024, 1, 29), // Feb 29 (leap year)
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
testDates.forEach(date => {
|
|
182
|
+
const formatted = formatDateToLocalString(date);
|
|
183
|
+
const parsed = parseLocalDate(formatted);
|
|
184
|
+
expect(parsed.getFullYear()).toBe(date.getFullYear());
|
|
185
|
+
expect(parsed.getMonth()).toBe(date.getMonth());
|
|
186
|
+
expect(parsed.getDate()).toBe(date.getDate());
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should prevent date shifts from UTC conversion', () => {
|
|
191
|
+
// Create a date late in the day that could shift when converted to UTC
|
|
192
|
+
const lateEvening = new Date(2024, 0, 15, 23, 30, 0);
|
|
193
|
+
const formatted = formatDateToLocalString(lateEvening);
|
|
194
|
+
|
|
195
|
+
// Should remain Jan 15, not shift to Jan 16
|
|
196
|
+
expect(formatted).toBe('2024-01-15');
|
|
197
|
+
|
|
198
|
+
// Parse it back and verify it's still the same day
|
|
199
|
+
const parsed = parseLocalDate(formatted);
|
|
200
|
+
expect(parsed.getDate()).toBe(15);
|
|
201
|
+
expect(parsed.getMonth()).toBe(0);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should handle dates at midnight correctly', () => {
|
|
205
|
+
const midnight = new Date(2024, 0, 15, 0, 0, 0, 0);
|
|
206
|
+
const formatted = formatDateToLocalString(midnight);
|
|
207
|
+
expect(formatted).toBe('2024-01-15');
|
|
208
|
+
|
|
209
|
+
const parsed = parseLocalDate(formatted);
|
|
210
|
+
expect(parsed.getDate()).toBe(15);
|
|
211
|
+
expect(parsed.getHours()).toBe(0);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* date-formatters.ts - Date formatting and comparison utilities for KTDatepicker
|
|
3
|
+
* Provides pure utility functions for date formatting, comparison, and normalization.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Formats a Date object according to the provided format string.
|
|
8
|
+
*
|
|
9
|
+
* Supported tokens:
|
|
10
|
+
* yyyy - 4-digit year
|
|
11
|
+
* yy - 2-digit year
|
|
12
|
+
* MM - 2-digit month (01-12)
|
|
13
|
+
* M - 1/2-digit month (1-12)
|
|
14
|
+
* dd - 2-digit day (01-31)
|
|
15
|
+
* d - 1/2-digit day (1-31)
|
|
16
|
+
* HH - 2-digit hour (00-23)
|
|
17
|
+
* H - 1/2-digit hour (0-23)
|
|
18
|
+
* mm - 2-digit minute (00-59)
|
|
19
|
+
* m - 1/2-digit minute (0-59)
|
|
20
|
+
* ss - 2-digit second (00-59)
|
|
21
|
+
* s - 1/2-digit second (0-59)
|
|
22
|
+
* a - AM/PM indicator
|
|
23
|
+
*
|
|
24
|
+
* @param date Date object to format
|
|
25
|
+
* @param format Format string with tokens
|
|
26
|
+
* @returns Formatted date string, or empty string if date is invalid
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* formatDate(new Date(2024, 0, 15), 'yyyy-MM-dd') // Returns "2024-01-15"
|
|
30
|
+
* formatDate(new Date(2024, 0, 15, 14, 30), 'HH:mm') // Returns "14:30"
|
|
31
|
+
*/
|
|
32
|
+
export function formatDate(date: Date, format: string): string {
|
|
33
|
+
if (!(date instanceof Date) || isNaN(date.getTime())) return '';
|
|
34
|
+
return format
|
|
35
|
+
.replace(/yyyy/g, date.getFullYear().toString())
|
|
36
|
+
.replace(/yy/g, date.getFullYear().toString().slice(-2))
|
|
37
|
+
.replace(/MM/g, String(date.getMonth() + 1).padStart(2, '0'))
|
|
38
|
+
.replace(/M(?![a-zA-Z])/g, String(date.getMonth() + 1))
|
|
39
|
+
.replace(/dd/g, String(date.getDate()).padStart(2, '0'))
|
|
40
|
+
.replace(/d(?![a-zA-Z])/g, String(date.getDate()))
|
|
41
|
+
.replace(/HH/g, String(date.getHours()).padStart(2, '0'))
|
|
42
|
+
.replace(/H(?![a-zA-Z])/g, String(date.getHours()))
|
|
43
|
+
.replace(/mm/g, String(date.getMinutes()).padStart(2, '0'))
|
|
44
|
+
.replace(/m(?![a-zA-Z])/g, String(date.getMinutes()))
|
|
45
|
+
.replace(/ss/g, String(date.getSeconds()).padStart(2, '0'))
|
|
46
|
+
.replace(/s(?![a-zA-Z])/g, String(date.getSeconds()))
|
|
47
|
+
.replace(/a/g, date.getHours() >= 12 ? 'PM' : 'AM');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Checks if two dates represent the same calendar day.
|
|
52
|
+
* Compares year, month, and day components, ignoring time.
|
|
53
|
+
*
|
|
54
|
+
* @param a First date to compare
|
|
55
|
+
* @param b Second date to compare
|
|
56
|
+
* @returns True if both dates represent the same day, false otherwise
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* isSameDay(new Date(2024, 0, 15, 10, 30), new Date(2024, 0, 15, 14, 45)) // Returns true
|
|
60
|
+
* isSameDay(new Date(2024, 0, 15), new Date(2024, 0, 16)) // Returns false
|
|
61
|
+
*/
|
|
62
|
+
export function isSameDay(a: Date, b: Date): boolean {
|
|
63
|
+
return a.getFullYear() === b.getFullYear() &&
|
|
64
|
+
a.getMonth() === b.getMonth() &&
|
|
65
|
+
a.getDate() === b.getDate();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Normalizes a date to local midnight (00:00:00) in the local timezone.
|
|
70
|
+
* Useful for date-only comparisons where time components should be ignored.
|
|
71
|
+
*
|
|
72
|
+
* @param date Date object to normalize
|
|
73
|
+
* @returns New Date object set to local midnight (00:00:00)
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* const date = new Date(2024, 0, 15, 14, 30, 45);
|
|
77
|
+
* const normalized = normalizeDateToMidnight(date);
|
|
78
|
+
* // normalized represents 2024-01-15 00:00:00 in local timezone
|
|
79
|
+
*/
|
|
80
|
+
export function normalizeDateToMidnight(date: Date): Date {
|
|
81
|
+
const normalized = new Date(date);
|
|
82
|
+
normalized.setHours(0, 0, 0, 0);
|
|
83
|
+
return normalized;
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* date-utils.ts - Shared date parsing/formatting utilities for KTDatepicker
|
|
3
|
+
* Exports parseDateFromFormat for use in datepicker and segmented input.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parses a date string according to a format string (supports yyyy, yy, MM, M, dd, d).
|
|
8
|
+
* @param str Date string
|
|
9
|
+
* @param format Format string
|
|
10
|
+
* @returns Date object or null if parsing fails
|
|
11
|
+
*/
|
|
12
|
+
export function parseDateFromFormat(str: string, format: string): Date | null {
|
|
13
|
+
if (!str || !format) return null;
|
|
14
|
+
// Supported tokens: yyyy, yy, MM, M, dd, d
|
|
15
|
+
const tokenRegex = /(yyyy|yy|MM|M|dd|d)/g;
|
|
16
|
+
const tokens: string[] = [];
|
|
17
|
+
let regexStr = '';
|
|
18
|
+
let lastIndex = 0;
|
|
19
|
+
let match;
|
|
20
|
+
while ((match = tokenRegex.exec(format)) !== null) {
|
|
21
|
+
// Add any literal text between tokens
|
|
22
|
+
if (match.index > lastIndex) {
|
|
23
|
+
regexStr += format.slice(lastIndex, match.index).replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
24
|
+
}
|
|
25
|
+
tokens.push(match[0]);
|
|
26
|
+
switch (match[0]) {
|
|
27
|
+
case 'yyyy': regexStr += '(\\d{4})'; break;
|
|
28
|
+
case 'yy': regexStr += '(\\d{2})'; break;
|
|
29
|
+
case 'MM': regexStr += '(\\d{2})'; break;
|
|
30
|
+
case 'M': regexStr += '(\\d{1,2})'; break;
|
|
31
|
+
case 'dd': regexStr += '(\\d{2})'; break;
|
|
32
|
+
case 'd': regexStr += '(\\d{1,2})'; break;
|
|
33
|
+
}
|
|
34
|
+
lastIndex = match.index + match[0].length;
|
|
35
|
+
}
|
|
36
|
+
// Add any trailing literal text
|
|
37
|
+
if (lastIndex < format.length) {
|
|
38
|
+
regexStr += format.slice(lastIndex).replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
39
|
+
}
|
|
40
|
+
const regex = new RegExp('^' + regexStr + '$');
|
|
41
|
+
const matchResult = regex.exec(str);
|
|
42
|
+
if (!matchResult) return null;
|
|
43
|
+
let year = 1970, month = 1, day = 1;
|
|
44
|
+
let matchIdx = 1;
|
|
45
|
+
for (const token of tokens) {
|
|
46
|
+
const val = matchResult[matchIdx++] || '';
|
|
47
|
+
switch (token) {
|
|
48
|
+
case 'yyyy': year = parseInt(val, 10); break;
|
|
49
|
+
case 'yy': year = 2000 + parseInt(val, 10); break;
|
|
50
|
+
case 'MM':
|
|
51
|
+
case 'M': month = parseInt(val, 10); break;
|
|
52
|
+
case 'dd':
|
|
53
|
+
case 'd': day = parseInt(val, 10); break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return new Date(year, month - 1, day);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extracts segment order from a format string for use in segmented input.
|
|
61
|
+
* @param format Format string (e.g., 'dd/MM/yyyy', 'yyyy-MM-dd')
|
|
62
|
+
* @returns Array of segment types in the order they appear in the format
|
|
63
|
+
*/
|
|
64
|
+
export function getSegmentOrderFromFormat(format: string): Array<'day' | 'month' | 'year'> {
|
|
65
|
+
if (!format) return ['month', 'day', 'year']; // Default fallback
|
|
66
|
+
|
|
67
|
+
const segments: Array<'day' | 'month' | 'year'> = [];
|
|
68
|
+
const tokenRegex = /(yyyy|yy|MM|M|dd|d)/g;
|
|
69
|
+
let match;
|
|
70
|
+
|
|
71
|
+
while ((match = tokenRegex.exec(format)) !== null) {
|
|
72
|
+
switch (match[0]) {
|
|
73
|
+
case 'yyyy':
|
|
74
|
+
case 'yy':
|
|
75
|
+
segments.push('year');
|
|
76
|
+
break;
|
|
77
|
+
case 'MM':
|
|
78
|
+
case 'M':
|
|
79
|
+
segments.push('month');
|
|
80
|
+
break;
|
|
81
|
+
case 'dd':
|
|
82
|
+
case 'd':
|
|
83
|
+
segments.push('day');
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Remove duplicates while preserving order
|
|
89
|
+
const uniqueSegments = segments.filter((segment, index) => segments.indexOf(segment) === index);
|
|
90
|
+
|
|
91
|
+
// Ensure we have all required segments, add missing ones at the end
|
|
92
|
+
const requiredSegments: Array<'day' | 'month' | 'year'> = ['day', 'month', 'year'];
|
|
93
|
+
requiredSegments.forEach(segment => {
|
|
94
|
+
if (!uniqueSegments.includes(segment)) {
|
|
95
|
+
uniqueSegments.push(segment);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return uniqueSegments;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Formats a date to a local timezone string in YYYY-MM-DD format.
|
|
104
|
+
* Uses local timezone components to prevent date shifts from UTC conversion.
|
|
105
|
+
* @param date Date object to format
|
|
106
|
+
* @returns Date string in YYYY-MM-DD format using local timezone
|
|
107
|
+
*/
|
|
108
|
+
export function formatDateToLocalString(date: Date): string {
|
|
109
|
+
const year = date.getFullYear();
|
|
110
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
111
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
112
|
+
return `${year}-${month}-${day}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Normalizes a date to local midnight (00:00:00) in the local timezone.
|
|
117
|
+
* Useful for date-only comparisons where time components should be ignored.
|
|
118
|
+
* @param date Date object to normalize
|
|
119
|
+
* @returns New Date object set to local midnight
|
|
120
|
+
*/
|
|
121
|
+
export function normalizeDateToLocalMidnight(date: Date): Date {
|
|
122
|
+
const normalized = new Date(date);
|
|
123
|
+
normalized.setHours(0, 0, 0, 0);
|
|
124
|
+
return normalized;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Checks if two dates represent the same calendar day in local timezone.
|
|
129
|
+
* Compares year, month, and day components, ignoring time.
|
|
130
|
+
* @param a First date
|
|
131
|
+
* @param b Second date
|
|
132
|
+
* @returns True if both dates represent the same day in local timezone
|
|
133
|
+
*/
|
|
134
|
+
export function isSameLocalDay(a: Date, b: Date): boolean {
|
|
135
|
+
return a.getFullYear() === b.getFullYear() &&
|
|
136
|
+
a.getMonth() === b.getMonth() &&
|
|
137
|
+
a.getDate() === b.getDate();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Parses a date string (YYYY-MM-DD format) into a Date object at local midnight.
|
|
142
|
+
* Creates a local timezone date to avoid timezone-related date shifts.
|
|
143
|
+
* @param dateStr Date string in YYYY-MM-DD format
|
|
144
|
+
* @returns Date object representing the date at local midnight
|
|
145
|
+
*/
|
|
146
|
+
export function parseLocalDate(dateStr: string): Date {
|
|
147
|
+
// Parse YYYY-MM-DD string and create local date at midnight
|
|
148
|
+
const [year, month, day] = dateStr.split('-').map(Number);
|
|
149
|
+
return new Date(year, month - 1, day, 0, 0, 0, 0);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Converts a date to a numeric key for O(1) date comparisons.
|
|
154
|
+
* Format: year * 10000 + month * 100 + day
|
|
155
|
+
* This enables efficient Set/Map lookups for date matching.
|
|
156
|
+
* @param date Date object to convert
|
|
157
|
+
* @returns Numeric key representing the date (e.g., 20241225 for Dec 25, 2024)
|
|
158
|
+
*/
|
|
159
|
+
export function getDateKey(date: Date): number {
|
|
160
|
+
return date.getFullYear() * 10000 + (date.getMonth() + 1) * 100 + date.getDate();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Checks if two dates have the same date key (same calendar day).
|
|
165
|
+
* Optimized version using numeric keys for O(1) comparison.
|
|
166
|
+
* @param a First date
|
|
167
|
+
* @param b Second date
|
|
168
|
+
* @returns True if both dates have the same date key
|
|
169
|
+
*/
|
|
170
|
+
export function isSameDayByKey(a: Date, b: Date): boolean {
|
|
171
|
+
return getDateKey(a) === getDateKey(b);
|
|
172
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* time-utils.ts - Time utilities for KTDatepicker
|
|
3
|
+
* Provides time parsing, formatting, validation, and granularity handling.
|
|
4
|
+
* Follows HeroUI best practices for time picker functionality.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { TimeState } from '../config/types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse time string to TimeState object
|
|
11
|
+
* @param timeStr Time string in format 'HH:MM' or 'HH:MM:SS'
|
|
12
|
+
* @returns TimeState object or null if invalid
|
|
13
|
+
*/
|
|
14
|
+
export function parseTimeString(timeStr: string): TimeState | null {
|
|
15
|
+
if (!timeStr || typeof timeStr !== 'string') return null;
|
|
16
|
+
|
|
17
|
+
const timeRegex = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/;
|
|
18
|
+
const match = timeStr.match(timeRegex);
|
|
19
|
+
|
|
20
|
+
if (!match) return null;
|
|
21
|
+
|
|
22
|
+
const hour = parseInt(match[1], 10);
|
|
23
|
+
const minute = parseInt(match[2], 10);
|
|
24
|
+
const second = match[3] ? parseInt(match[3], 10) : 0;
|
|
25
|
+
|
|
26
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { hour, minute, second };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format TimeState to string
|
|
35
|
+
* @param time TimeState object
|
|
36
|
+
* @param granularity Granularity level
|
|
37
|
+
* @param format Time format ('12h' or '24h')
|
|
38
|
+
* @returns Formatted time string
|
|
39
|
+
*/
|
|
40
|
+
export function formatTime(time: TimeState, granularity: 'second' | 'minute' | 'hour' = 'minute', format: '12h' | '24h' = '24h'): string {
|
|
41
|
+
if (!time) return '';
|
|
42
|
+
|
|
43
|
+
let { hour, minute, second } = time;
|
|
44
|
+
let ampm = '';
|
|
45
|
+
|
|
46
|
+
// Handle 12-hour format
|
|
47
|
+
if (format === '12h') {
|
|
48
|
+
ampm = hour >= 12 ? 'PM' : 'AM';
|
|
49
|
+
hour = hour % 12;
|
|
50
|
+
hour = hour === 0 ? 12 : hour;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Format based on granularity
|
|
54
|
+
switch (granularity) {
|
|
55
|
+
case 'hour':
|
|
56
|
+
return format === '12h' ? `${hour} ${ampm}` : `${hour.toString().padStart(2, '0')}`;
|
|
57
|
+
case 'minute':
|
|
58
|
+
return format === '12h'
|
|
59
|
+
? `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')} ${ampm}`
|
|
60
|
+
: `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
|
61
|
+
case 'second':
|
|
62
|
+
return format === '12h'
|
|
63
|
+
? `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')} ${ampm}`
|
|
64
|
+
: `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')}`;
|
|
65
|
+
default:
|
|
66
|
+
return format === '12h'
|
|
67
|
+
? `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')} ${ampm}`
|
|
68
|
+
: `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validate time against constraints
|
|
74
|
+
* @param time TimeState to validate
|
|
75
|
+
* @param minTime Minimum time constraint
|
|
76
|
+
* @param maxTime Maximum time constraint
|
|
77
|
+
* @returns Validation result
|
|
78
|
+
*/
|
|
79
|
+
export function validateTime(time: TimeState, minTime?: string, maxTime?: string): { isValid: boolean; error?: string } {
|
|
80
|
+
if (!time) {
|
|
81
|
+
return { isValid: false, error: 'Time is required' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { hour, minute, second } = time;
|
|
85
|
+
|
|
86
|
+
// Basic range validation
|
|
87
|
+
if (hour < 0 || hour > 23) {
|
|
88
|
+
return { isValid: false, error: 'Hour must be between 0 and 23' };
|
|
89
|
+
}
|
|
90
|
+
if (minute < 0 || minute > 59) {
|
|
91
|
+
return { isValid: false, error: 'Minute must be between 0 and 59' };
|
|
92
|
+
}
|
|
93
|
+
if (second < 0 || second > 59) {
|
|
94
|
+
return { isValid: false, error: 'Second must be between 0 and 59' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Min/Max time validation
|
|
98
|
+
if (minTime) {
|
|
99
|
+
const minTimeState = parseTimeString(minTime);
|
|
100
|
+
if (minTimeState && isTimeBefore(time, minTimeState)) {
|
|
101
|
+
return { isValid: false, error: `Time must be after ${minTime}` };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (maxTime) {
|
|
106
|
+
const maxTimeState = parseTimeString(maxTime);
|
|
107
|
+
if (maxTimeState && isTimeAfter(time, maxTimeState)) {
|
|
108
|
+
return { isValid: false, error: `Time must be before ${maxTime}` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { isValid: true };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if time1 is before time2
|
|
117
|
+
* @param time1 First time
|
|
118
|
+
* @param time2 Second time
|
|
119
|
+
* @returns True if time1 is before time2
|
|
120
|
+
*/
|
|
121
|
+
export function isTimeBefore(time1: TimeState, time2: TimeState): boolean {
|
|
122
|
+
const totalSeconds1 = time1.hour * 3600 + time1.minute * 60 + time1.second;
|
|
123
|
+
const totalSeconds2 = time2.hour * 3600 + time2.minute * 60 + time2.second;
|
|
124
|
+
return totalSeconds1 < totalSeconds2;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if time1 is after time2
|
|
129
|
+
* @param time1 First time
|
|
130
|
+
* @param time2 Second time
|
|
131
|
+
* @returns True if time1 is after time2
|
|
132
|
+
*/
|
|
133
|
+
export function isTimeAfter(time1: TimeState, time2: TimeState): boolean {
|
|
134
|
+
const totalSeconds1 = time1.hour * 3600 + time1.minute * 60 + time1.second;
|
|
135
|
+
const totalSeconds2 = time2.hour * 3600 + time2.minute * 60 + time2.second;
|
|
136
|
+
return totalSeconds1 > totalSeconds2;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get time segments based on granularity
|
|
141
|
+
* @param granularity Time granularity
|
|
142
|
+
* @returns Array of segment types
|
|
143
|
+
*/
|
|
144
|
+
export function getTimeSegments(granularity: 'second' | 'minute' | 'hour'): Array<'hour' | 'minute' | 'second' | 'ampm'> {
|
|
145
|
+
switch (granularity) {
|
|
146
|
+
case 'hour':
|
|
147
|
+
return ['hour'];
|
|
148
|
+
case 'minute':
|
|
149
|
+
return ['hour', 'minute'];
|
|
150
|
+
case 'second':
|
|
151
|
+
return ['hour', 'minute', 'second'];
|
|
152
|
+
default:
|
|
153
|
+
return ['hour', 'minute'];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Convert Date object to TimeState
|
|
159
|
+
* @param date Date object
|
|
160
|
+
* @returns TimeState object
|
|
161
|
+
*/
|
|
162
|
+
export function dateToTimeState(date: Date): TimeState {
|
|
163
|
+
return {
|
|
164
|
+
hour: date.getHours(),
|
|
165
|
+
minute: date.getMinutes(),
|
|
166
|
+
second: date.getSeconds()
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Convert TimeState to Date object (using current date)
|
|
172
|
+
* @param time TimeState object
|
|
173
|
+
* @returns Date object with time applied
|
|
174
|
+
*/
|
|
175
|
+
export function timeStateToDate(time: TimeState): Date {
|
|
176
|
+
const date = new Date();
|
|
177
|
+
date.setHours(time.hour, time.minute, time.second, 0);
|
|
178
|
+
return date;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Apply time to existing date
|
|
183
|
+
* @param date Date object
|
|
184
|
+
* @param time TimeState object
|
|
185
|
+
* @returns New Date object with time applied
|
|
186
|
+
*/
|
|
187
|
+
export function applyTimeToDate(date: Date, time: TimeState): Date {
|
|
188
|
+
const newDate = new Date(date);
|
|
189
|
+
newDate.setHours(time.hour, time.minute, time.second, 0);
|
|
190
|
+
return newDate;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get time step options based on granularity and step
|
|
195
|
+
* @param granularity Time granularity
|
|
196
|
+
* @param step Step increment in minutes
|
|
197
|
+
* @returns Array of available time values
|
|
198
|
+
*/
|
|
199
|
+
export function getTimeStepOptions(granularity: 'second' | 'minute' | 'hour', step: number = 1): number[] {
|
|
200
|
+
const options: number[] = [];
|
|
201
|
+
|
|
202
|
+
switch (granularity) {
|
|
203
|
+
case 'hour':
|
|
204
|
+
for (let i = 0; i < 24; i += Math.max(1, Math.floor(step / 60))) {
|
|
205
|
+
options.push(i);
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
case 'minute':
|
|
209
|
+
for (let i = 0; i < 60; i += step) {
|
|
210
|
+
options.push(i);
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
case 'second':
|
|
214
|
+
for (let i = 0; i < 60; i += Math.max(1, Math.floor(step * 60))) {
|
|
215
|
+
options.push(i);
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return options;
|
|
221
|
+
}
|