@aurodesignsystem/auro-library 5.12.3 → 5.13.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 +14 -0
- package/package.json +1 -1
- package/scripts/runtime/Focusables/test/Focusables.test.js +75 -55
- package/scripts/runtime/dateUtilities/dateFormatter.mjs +228 -102
- package/scripts/runtime/dateUtilities/dateFormatter.test.js +284 -0
- package/scripts/runtime/dateUtilities/dateUtilities.mjs +40 -41
- package/scripts/runtime/dateUtilities/dateUtilities.test.js +80 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Semantic Release Automated Changelog
|
|
2
2
|
|
|
3
|
+
# [5.13.0](https://github.com/AlaskaAirlines/auro-library/compare/v5.12.3...v5.13.0) (2026-05-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add ISO date formatting support AB[#1495381](https://github.com/AlaskaAirlines/auro-library/issues/1495381) ([006947a](https://github.com/AlaskaAirlines/auro-library/commit/006947abf0736ec8efa25fe7861d99327e459a3b))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Performance Improvements
|
|
12
|
+
|
|
13
|
+
* **dateutil:** add null check guard ([e5187f4](https://github.com/AlaskaAirlines/auro-library/commit/e5187f45a96aeec66c286ba9ca858e5bce9b6860))
|
|
14
|
+
* **dateutil:** clean up some dup logics ([bbed2ee](https://github.com/AlaskaAirlines/auro-library/commit/bbed2ee4257ff1d107505158809eaa8cc3245685))
|
|
15
|
+
* refactor structure and performance ([42247a0](https://github.com/AlaskaAirlines/auro-library/commit/42247a03392c80dd29ab3447bf65b73fc461ba38))
|
|
16
|
+
|
|
3
17
|
## [5.12.3](https://github.com/AlaskaAirlines/auro-library/compare/v5.12.2...v5.12.3) (2026-04-30)
|
|
4
18
|
|
|
5
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aurodesignsystem/auro-library",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.13.0",
|
|
4
4
|
"description": "This repository holds shared scripts, utilities, and workflows utilized across repositories along the Auro Design System.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
/* eslint-disable max-classes-per-file */
|
|
2
|
-
import {
|
|
2
|
+
import { expect, fixtureSync, html } from "@open-wc/testing";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
isFocusableComponent,
|
|
6
|
-
getFocusableElements
|
|
7
|
-
} from '../Focusables.mjs';
|
|
4
|
+
import { getFocusableElements, isFocusableComponent } from "../Focusables.mjs";
|
|
8
5
|
|
|
9
|
-
describe(
|
|
10
|
-
it(
|
|
6
|
+
describe("isFocusableComponent", () => {
|
|
7
|
+
it("returns true for enabled custom focusable components", async () => {
|
|
11
8
|
for (const tag of [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
"auro-checkbox",
|
|
10
|
+
"auro-radio",
|
|
11
|
+
"auro-dropdown",
|
|
12
|
+
"auro-button",
|
|
13
|
+
"auro-combobox",
|
|
14
|
+
"auro-input",
|
|
15
|
+
"auro-counter",
|
|
16
|
+
"auro-select",
|
|
17
|
+
"auro-datepicker",
|
|
18
|
+
"auro-hyperlink",
|
|
19
|
+
"auro-accordion",
|
|
15
20
|
]) {
|
|
16
21
|
const el = document.createElement(tag);
|
|
17
|
-
if (tag ===
|
|
18
|
-
el.setAttribute(
|
|
22
|
+
if (tag === "auro-hyperlink") {
|
|
23
|
+
el.setAttribute("href", "#");
|
|
19
24
|
}
|
|
20
25
|
document.body.appendChild(el);
|
|
21
26
|
expect(isFocusableComponent(el)).to.be.true;
|
|
@@ -23,16 +28,24 @@ describe('isFocusableComponent', () => {
|
|
|
23
28
|
}
|
|
24
29
|
});
|
|
25
30
|
|
|
26
|
-
it(
|
|
31
|
+
it("returns false for custom components with disabled attribute", async () => {
|
|
27
32
|
for (const tag of [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
"auro-checkbox",
|
|
34
|
+
"auro-radio",
|
|
35
|
+
"auro-dropdown",
|
|
36
|
+
"auro-button",
|
|
37
|
+
"auro-combobox",
|
|
38
|
+
"auro-input",
|
|
39
|
+
"auro-counter",
|
|
40
|
+
"auro-select",
|
|
41
|
+
"auro-datepicker",
|
|
42
|
+
"auro-hyperlink",
|
|
43
|
+
"auro-accordion",
|
|
31
44
|
]) {
|
|
32
45
|
const el = document.createElement(tag);
|
|
33
|
-
el.setAttribute(
|
|
34
|
-
if (tag ===
|
|
35
|
-
el.setAttribute(
|
|
46
|
+
el.setAttribute("disabled", "");
|
|
47
|
+
if (tag === "auro-hyperlink") {
|
|
48
|
+
el.setAttribute("href", "#");
|
|
36
49
|
}
|
|
37
50
|
document.body.appendChild(el);
|
|
38
51
|
expect(isFocusableComponent(el)).to.be.false;
|
|
@@ -40,24 +53,26 @@ describe('isFocusableComponent', () => {
|
|
|
40
53
|
}
|
|
41
54
|
});
|
|
42
55
|
|
|
43
|
-
it(
|
|
44
|
-
const el = document.createElement(
|
|
56
|
+
it("returns false for auro-hyperlink without href", async () => {
|
|
57
|
+
const el = document.createElement("auro-hyperlink");
|
|
45
58
|
document.body.appendChild(el);
|
|
46
59
|
expect(isFocusableComponent(el)).to.be.false;
|
|
47
60
|
el.remove();
|
|
48
61
|
});
|
|
49
62
|
|
|
50
|
-
it(
|
|
51
|
-
const el = document.createElement(
|
|
63
|
+
it("returns false for non-custom elements", async () => {
|
|
64
|
+
const el = document.createElement("div");
|
|
52
65
|
document.body.appendChild(el);
|
|
53
66
|
expect(isFocusableComponent(el)).to.be.false;
|
|
54
67
|
el.remove();
|
|
55
68
|
});
|
|
56
69
|
});
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
// fixtureSync is used instead of fixture to avoid requestAnimationFrame, which Chrome
|
|
72
|
+
// pauses in background tabs when multiple test files run concurrently under WTR.
|
|
73
|
+
describe("getFocusableElements", () => {
|
|
74
|
+
it("finds standard focusable elements", () => {
|
|
75
|
+
const el = fixtureSync(html`
|
|
61
76
|
<div>
|
|
62
77
|
<button id="btn"></button>
|
|
63
78
|
<input id="input">
|
|
@@ -67,11 +82,17 @@ describe('getFocusableElements', () => {
|
|
|
67
82
|
</div>
|
|
68
83
|
`);
|
|
69
84
|
const focusables = getFocusableElements(el);
|
|
70
|
-
expect(focusables.map(e => e.id)).to.include.members([
|
|
85
|
+
expect(focusables.map((e) => e.id)).to.include.members([
|
|
86
|
+
"btn",
|
|
87
|
+
"input",
|
|
88
|
+
"link",
|
|
89
|
+
"ta",
|
|
90
|
+
"sel",
|
|
91
|
+
]);
|
|
71
92
|
});
|
|
72
93
|
|
|
73
|
-
it(
|
|
74
|
-
const el =
|
|
94
|
+
it("skips disabled elements", () => {
|
|
95
|
+
const el = fixtureSync(html`
|
|
75
96
|
<div>
|
|
76
97
|
<button id="btn" disabled></button>
|
|
77
98
|
<input id="input" disabled>
|
|
@@ -83,8 +104,8 @@ describe('getFocusableElements', () => {
|
|
|
83
104
|
expect(focusables.length).to.equal(0);
|
|
84
105
|
});
|
|
85
106
|
|
|
86
|
-
it(
|
|
87
|
-
const el =
|
|
107
|
+
it("finds custom focusable components", () => {
|
|
108
|
+
const el = fixtureSync(html`
|
|
88
109
|
<div>
|
|
89
110
|
<auro-checkbox id="cb"></auro-checkbox>
|
|
90
111
|
<auro-hyperlink id="hl" href="#"></auro-hyperlink>
|
|
@@ -92,11 +113,11 @@ describe('getFocusableElements', () => {
|
|
|
92
113
|
</div>
|
|
93
114
|
`);
|
|
94
115
|
const focusables = getFocusableElements(el);
|
|
95
|
-
expect(focusables.map(e => e.id)).to.include.members([
|
|
116
|
+
expect(focusables.map((e) => e.id)).to.include.members(["cb", "hl", "ab"]);
|
|
96
117
|
});
|
|
97
118
|
|
|
98
|
-
it(
|
|
99
|
-
const el =
|
|
119
|
+
it("skips disabled custom components", () => {
|
|
120
|
+
const el = fixtureSync(html`
|
|
100
121
|
<div>
|
|
101
122
|
<auro-checkbox id="cb" disabled></auro-checkbox>
|
|
102
123
|
<auro-hyperlink id="hl" disabled href="#"></auro-hyperlink>
|
|
@@ -106,60 +127,59 @@ describe('getFocusableElements', () => {
|
|
|
106
127
|
expect(focusables.length).to.equal(0);
|
|
107
128
|
});
|
|
108
129
|
|
|
109
|
-
it(
|
|
130
|
+
it("finds elements in shadow DOM", () => {
|
|
110
131
|
class ShadowEl extends HTMLElement {
|
|
111
132
|
constructor() {
|
|
112
133
|
super();
|
|
113
|
-
this.attachShadow({ mode:
|
|
134
|
+
this.attachShadow({ mode: "open" });
|
|
114
135
|
}
|
|
115
136
|
connectedCallback() {
|
|
116
|
-
|
|
137
|
+
const btn = document.createElement("button");
|
|
138
|
+
btn.id = "shadowBtn";
|
|
139
|
+
this.shadowRoot.appendChild(btn);
|
|
117
140
|
}
|
|
118
141
|
}
|
|
119
|
-
customElements.define(
|
|
120
|
-
const el =
|
|
142
|
+
customElements.define("shadow-el", ShadowEl);
|
|
143
|
+
const el = fixtureSync(html`
|
|
121
144
|
<div>
|
|
122
145
|
<shadow-el id="host"></shadow-el>
|
|
123
146
|
</div>
|
|
124
147
|
`);
|
|
125
|
-
await elementUpdated(el);
|
|
126
148
|
const focusables = getFocusableElements(el);
|
|
127
|
-
|
|
128
|
-
|
|
149
|
+
const shadowBtn = el
|
|
150
|
+
.querySelector("shadow-el")
|
|
151
|
+
.shadowRoot.getElementById("shadowBtn");
|
|
129
152
|
expect(focusables).to.include(shadowBtn);
|
|
130
153
|
});
|
|
131
154
|
|
|
132
|
-
it(
|
|
155
|
+
it("finds elements assigned to slots", () => {
|
|
133
156
|
class SlotEl extends HTMLElement {
|
|
134
157
|
constructor() {
|
|
135
158
|
super();
|
|
136
|
-
this.attachShadow({ mode:
|
|
159
|
+
this.attachShadow({ mode: "open" });
|
|
137
160
|
}
|
|
138
161
|
connectedCallback() {
|
|
139
|
-
this.shadowRoot.
|
|
162
|
+
this.shadowRoot.appendChild(document.createElement("slot"));
|
|
140
163
|
}
|
|
141
164
|
}
|
|
142
|
-
customElements.define(
|
|
143
|
-
const el =
|
|
165
|
+
customElements.define("slot-el", SlotEl);
|
|
166
|
+
const el = fixtureSync(html`
|
|
144
167
|
<slot-el>
|
|
145
168
|
<button id="slottedBtn"></button>
|
|
146
169
|
</slot-el>
|
|
147
170
|
`);
|
|
148
|
-
await elementUpdated(el);
|
|
149
171
|
const focusables = getFocusableElements(el);
|
|
150
|
-
const slottedBtn = el.querySelector(
|
|
172
|
+
const slottedBtn = el.querySelector("#slottedBtn");
|
|
151
173
|
expect(focusables).to.include(slottedBtn);
|
|
152
174
|
});
|
|
153
175
|
|
|
154
|
-
it(
|
|
155
|
-
|
|
156
|
-
const el = await fixture(html`
|
|
176
|
+
it("does not return duplicates", () => {
|
|
177
|
+
const el = fixtureSync(html`
|
|
157
178
|
<div>
|
|
158
179
|
<button id="btn"></button>
|
|
159
180
|
</div>
|
|
160
181
|
`);
|
|
161
182
|
const focusables = getFocusableElements(el);
|
|
162
|
-
|
|
163
|
-
expect(focusables.filter(e => e.id === 'btn').length).to.equal(1);
|
|
183
|
+
expect(focusables.filter((e) => e.id === "btn").length).to.equal(1);
|
|
164
184
|
});
|
|
165
|
-
});
|
|
185
|
+
});
|
|
@@ -1,104 +1,230 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return acc;
|
|
43
|
-
}, {});
|
|
44
|
-
|
|
45
|
-
// If we found all the parts, return the result
|
|
46
|
-
if (result.month && result.year) {
|
|
47
|
-
return result;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Throw an error to let the dev know we were unable to parse the date string
|
|
51
|
-
throw new Error('AuroDatepickerUtilities | parseDate: Unable to parse date string');
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Convert a date object to string format.
|
|
56
|
-
* @param {Object} date - Date to convert to string.
|
|
57
|
-
* @param {String} locale - Optional locale to use for the date string. Defaults to user's locale.
|
|
58
|
-
* @returns {String} Returns the date as a string.
|
|
59
|
-
*/
|
|
60
|
-
this.getDateAsString = (date, locale = undefined) => date.toLocaleDateString(locale, {
|
|
61
|
-
year: "numeric",
|
|
62
|
-
month: "2-digit",
|
|
63
|
-
day: "2-digit",
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Converts a date string to a North American date format.
|
|
68
|
-
* @param {String} dateStr - Date to validate.
|
|
69
|
-
* @param {String} format - Date format to validate against.
|
|
70
|
-
* @returns {Boolean}
|
|
71
|
-
*/
|
|
72
|
-
this.toNorthAmericanFormat = (dateStr, format) => {
|
|
73
|
-
|
|
74
|
-
if (format === 'mm/dd/yyyy') {
|
|
75
|
-
return dateStr;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const parsedDate = this.parseDate(dateStr, format);
|
|
79
|
-
|
|
80
|
-
if (!parsedDate) {
|
|
81
|
-
throw new Error('AuroDatepickerUtilities | toNorthAmericanFormat: Unable to parse date string');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const { month, day, year } = parsedDate;
|
|
85
|
-
|
|
86
|
-
const dateParts = [];
|
|
87
|
-
if (month) {
|
|
88
|
-
dateParts.push(month);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (day) {
|
|
92
|
-
dateParts.push(day);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (year) {
|
|
96
|
-
dateParts.push(year);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return dateParts.join('/');
|
|
100
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @description Splits a date string into its parts according to the provided format. Does NOT validate that the result is a real calendar date — use `parseDate` when validation is required.
|
|
3
|
+
* @param {string} dateStr - Date string to parse.
|
|
4
|
+
* @param {string} format - Date format to parse.
|
|
5
|
+
* @returns {{ month?: string, day?: string, year?: string }|undefined}
|
|
6
|
+
*/
|
|
7
|
+
function getDateParts(dateStr, format) {
|
|
8
|
+
if (!dateStr) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const formatSeparatorMatch = format.match(/[/.-]/);
|
|
13
|
+
let valueParts;
|
|
14
|
+
let formatParts;
|
|
15
|
+
|
|
16
|
+
if (formatSeparatorMatch) {
|
|
17
|
+
const separator = formatSeparatorMatch[0];
|
|
18
|
+
valueParts = dateStr.split(separator);
|
|
19
|
+
formatParts = format.split(separator);
|
|
20
|
+
} else {
|
|
21
|
+
if (dateStr.match(/[/.-]/)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"AuroDatepickerUtilities | parseDate: Date string has no separators",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (dateStr.length !== format.length) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"AuroDatepickerUtilities | parseDate: Date string and format length do not match",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
valueParts = [dateStr];
|
|
34
|
+
formatParts = [format];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (valueParts.length !== formatParts.length) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`AuroDatepickerUtilities | parseDate: Date string and format do not match : ${dateStr} vs ${format}`,
|
|
40
|
+
);
|
|
101
41
|
}
|
|
102
|
-
};
|
|
103
42
|
|
|
104
|
-
|
|
43
|
+
const result = formatParts.reduce((acc, part, index) => {
|
|
44
|
+
const value = valueParts[index];
|
|
45
|
+
|
|
46
|
+
if (/m/iu.test(part) && part.length === value.length) {
|
|
47
|
+
acc.month = value;
|
|
48
|
+
} else if (/d/iu.test(part) && part.length === value.length) {
|
|
49
|
+
acc.day = value;
|
|
50
|
+
} else if (/y/iu.test(part) && part.length === value.length) {
|
|
51
|
+
acc.year = value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return acc;
|
|
55
|
+
}, {});
|
|
56
|
+
|
|
57
|
+
if (!result.month && !result.day && !result.year) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
"AuroDatepickerUtilities | parseDate: Unable to parse date string",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isCalendarDate(year, month, day) {
|
|
67
|
+
let yearNumber = Number(year);
|
|
68
|
+
const monthNumber = Number(month);
|
|
69
|
+
const dayNumber = Number(day);
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
!Number.isInteger(yearNumber) ||
|
|
73
|
+
!Number.isInteger(monthNumber) ||
|
|
74
|
+
!Number.isInteger(dayNumber)
|
|
75
|
+
) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Handle 2-digit years by converting them to 4-digit years based on a cutoff. This allows for parsing of 2-digit year formats while still validating the resulting date.
|
|
80
|
+
if (yearNumber < 100 && yearNumber >= 50) {
|
|
81
|
+
yearNumber += 1900;
|
|
82
|
+
} else if (yearNumber < 50) {
|
|
83
|
+
yearNumber += 2000;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const stringified = `${String(yearNumber).padStart(4, "0")}-${String(monthNumber).padStart(2, "0")}-${String(dayNumber).padStart(2, "0")}`;
|
|
87
|
+
const date = new Date(stringified.replace(/[.-]/g, "/"));
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
!Number.isNaN(date.getTime()) && toISOFormatString(date) === stringified
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @description Parses a date string into its components and validates that the result is a real calendar date. Use `getDateParts` instead when raw splitting without validation is needed (e.g. for in-progress input).
|
|
96
|
+
*
|
|
97
|
+
* Partial formats are supported: components absent from `format` default to `year → "0"`,
|
|
98
|
+
* `month → "01"`, `day → "01"` for calendar validation only. The returned object contains
|
|
99
|
+
* only the fields actually present in the format string — missing fields are never injected.
|
|
100
|
+
* @param {string} dateStr - Date string to parse.
|
|
101
|
+
* @param {string} format - Date format to parse.
|
|
102
|
+
* @returns {{ month?: string, day?: string, year?: string }|undefined}
|
|
103
|
+
* @throws {Error} Throws when the parsed result does not represent a valid calendar date.
|
|
104
|
+
*/
|
|
105
|
+
function parseDate(dateStr, format = "mm/dd/yyyy") {
|
|
106
|
+
if (!dateStr || !format) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const result = getDateParts(dateStr.trim(), format);
|
|
110
|
+
|
|
111
|
+
if (!result) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const lowerFormat = format.toLowerCase();
|
|
116
|
+
const year = lowerFormat.includes("yy") ? result.year : "0";
|
|
117
|
+
const month = lowerFormat.includes("mm") ? result.month : "01";
|
|
118
|
+
const day = lowerFormat.includes("dd") ? result.day : "01";
|
|
119
|
+
|
|
120
|
+
if (isCalendarDate(year, month, day)) {
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
throw new Error(
|
|
125
|
+
`AuroDatepickerUtilities | parseDate: Date string is not a valid date ${JSON.stringify(result)} with format ${format}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Convert a date object to string format.
|
|
131
|
+
* @param {Object} date - Date to convert to string.
|
|
132
|
+
* @param {String} locale - Optional locale to use for the date string. Defaults to user's locale.
|
|
133
|
+
* @returns {String} Returns the date as a string.
|
|
134
|
+
*/
|
|
135
|
+
function getDateAsString(date, locale = undefined) {
|
|
136
|
+
return date.toLocaleDateString(locale, {
|
|
137
|
+
year: "numeric",
|
|
138
|
+
month: "2-digit",
|
|
139
|
+
day: "2-digit",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Converts a date string to a North American date format.
|
|
145
|
+
* @param {String} dateStr - Date to validate.
|
|
146
|
+
* @param {String} format - Date format to validate against.
|
|
147
|
+
* @returns {String}
|
|
148
|
+
*/
|
|
149
|
+
function toNorthAmericanFormat(dateStr, format) {
|
|
150
|
+
if (format === "mm/dd/yyyy") {
|
|
151
|
+
return dateStr;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const parsedDate = parseDate(dateStr, format);
|
|
155
|
+
|
|
156
|
+
if (!parsedDate) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
"AuroDatepickerUtilities | toNorthAmericanFormat: Unable to parse date string",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const { month, day, year } = parsedDate;
|
|
163
|
+
|
|
164
|
+
return [month, day, year].filter(Boolean).join("/");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Validates that a date string matches the provided format and represents a real calendar date.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} dateStr - Date string to validate.
|
|
171
|
+
* @param {string} [format="yyyy-mm-dd"] - Format of the date string.
|
|
172
|
+
* @returns {boolean} True when the date string is valid for the provided format, otherwise false.
|
|
173
|
+
*/
|
|
174
|
+
function isValidDate(dateStr, format = "yyyy-mm-dd") {
|
|
175
|
+
try {
|
|
176
|
+
if (typeof dateStr !== "string" || !dateStr || format?.length < 8) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (parseDate(dateStr, format)) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Converts a JavaScript Date instance to a simple ISO-like date string. This returns only the calendar date portion without any time or timezone information.
|
|
191
|
+
*
|
|
192
|
+
* @param {Date} date - Date instance to convert to an ISO-like string.
|
|
193
|
+
* @returns {string} A string in the format "yyyy-mm-dd" representing the provided date.
|
|
194
|
+
* @throws {Error} Throws an error when the input is not a valid Date instance.
|
|
195
|
+
*/
|
|
196
|
+
function toISOFormatString(date) {
|
|
197
|
+
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
"AuroDatepickerUtilities | toISOFormatString: Input must be a valid Date instance",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return `${String(date.getFullYear()).padStart(4, "0")}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Converts a date string into a JavaScript Date instance. This method supports ISO formatted strings and other formats that can be parsed by the formatter.
|
|
207
|
+
*
|
|
208
|
+
* @param {String} dateStr - Date string to convert into a Date object.
|
|
209
|
+
* @param {String} format - Date format used to parse the string when it is not in ISO format.
|
|
210
|
+
* @returns {Date|null} Returns a Date instance for valid input or null for non-string input.
|
|
211
|
+
* @throws {Error} Throws when parsing fails for non-ISO string input.
|
|
212
|
+
*/
|
|
213
|
+
function stringToDateInstance(dateStr, format = "yyyy-mm-dd") {
|
|
214
|
+
if (typeof dateStr !== "string") {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const { month, day, year } = parseDate(dateStr, format);
|
|
219
|
+
return new Date(`${year}/${month}/${day}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const dateFormatter = {
|
|
223
|
+
parseDate,
|
|
224
|
+
getDateParts,
|
|
225
|
+
getDateAsString,
|
|
226
|
+
toNorthAmericanFormat,
|
|
227
|
+
isValidDate,
|
|
228
|
+
toISOFormatString,
|
|
229
|
+
stringToDateInstance,
|
|
230
|
+
};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { expect } from "@open-wc/testing";
|
|
2
|
+
import { dateFormatter } from "./dateFormatter.mjs";
|
|
3
|
+
|
|
4
|
+
describe("dateFormatter.parseDate", () => {
|
|
5
|
+
describe("null / undefined / empty input", () => {
|
|
6
|
+
it("returns undefined for null", () => {
|
|
7
|
+
expect(dateFormatter.parseDate(null)).to.be.undefined;
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("returns undefined for undefined", () => {
|
|
11
|
+
expect(dateFormatter.parseDate(undefined)).to.be.undefined;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("returns undefined for empty string", () => {
|
|
15
|
+
expect(dateFormatter.parseDate("")).to.be.undefined;
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("dateFormatter.toNorthAmericanFormat", () => {
|
|
21
|
+
it("returns the string unchanged for mm/dd/yyyy", () => {
|
|
22
|
+
expect(
|
|
23
|
+
dateFormatter.toNorthAmericanFormat("01/15/2024", "mm/dd/yyyy"),
|
|
24
|
+
).to.equal("01/15/2024");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("reorders dd/mm/yyyy to mm/dd/yyyy", () => {
|
|
28
|
+
expect(
|
|
29
|
+
dateFormatter.toNorthAmericanFormat("15/01/2024", "dd/mm/yyyy"),
|
|
30
|
+
).to.equal("01/15/2024");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("reorders yyyy-mm-dd to mm/dd/yyyy", () => {
|
|
34
|
+
expect(
|
|
35
|
+
dateFormatter.toNorthAmericanFormat("2024-01-15", "yyyy-mm-dd"),
|
|
36
|
+
).to.equal("01/15/2024");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("throws when the date string cannot be parsed", () => {
|
|
40
|
+
expect(() =>
|
|
41
|
+
dateFormatter.toNorthAmericanFormat(null, "dd/mm/yyyy"),
|
|
42
|
+
).to.throw(Error);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("dateFormatter.stringToDateInstance", () => {
|
|
47
|
+
describe("ISO input", () => {
|
|
48
|
+
const isoCases = [
|
|
49
|
+
{ input: "2024-01-15", description: "typical ISO date" },
|
|
50
|
+
{ input: "1999-12-31", description: "end of year ISO date" },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
isoCases.forEach(({ input, description }) => {
|
|
54
|
+
it(`creates Date directly from ISO string for: ${description}`, () => {
|
|
55
|
+
const result = dateFormatter.stringToDateInstance(input);
|
|
56
|
+
|
|
57
|
+
expect(result).to.be.instanceOf(Date);
|
|
58
|
+
expect(result.toISOString().slice(0, 10)).to.equal(input);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("non-ISO input with explicit format", () => {
|
|
64
|
+
it("returns a Date using parseDate result for dd/mm/yyyy format", () => {
|
|
65
|
+
const inputStr = "15/01/2024";
|
|
66
|
+
const expectedYear = 2024;
|
|
67
|
+
const expectedMonth = 1; // January (1-based)
|
|
68
|
+
const expectedDay = 15;
|
|
69
|
+
|
|
70
|
+
const result = dateFormatter.stringToDateInstance(inputStr, "dd/mm/yyyy");
|
|
71
|
+
|
|
72
|
+
expect(result).to.be.instanceOf(Date);
|
|
73
|
+
expect(result.getFullYear()).to.equal(expectedYear);
|
|
74
|
+
expect(result.getMonth()).to.equal(expectedMonth - 1);
|
|
75
|
+
expect(result.getDate()).to.equal(expectedDay);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("default format behaviour", () => {
|
|
80
|
+
it("uses default format parameter when format is not provided", () => {
|
|
81
|
+
const inputStr = "2024-02-29";
|
|
82
|
+
|
|
83
|
+
const result = dateFormatter.stringToDateInstance(inputStr);
|
|
84
|
+
|
|
85
|
+
expect(result).to.be.instanceOf(Date);
|
|
86
|
+
expect(result.getFullYear()).to.equal(2024);
|
|
87
|
+
expect(result.getMonth()).to.equal(1);
|
|
88
|
+
expect(result.getDate()).to.equal(29);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("month adjustment", () => {
|
|
93
|
+
it("correctly adjusts month from 1-based to 0-based when creating Date", () => {
|
|
94
|
+
const inputStr = "2024-12-31";
|
|
95
|
+
|
|
96
|
+
const result = dateFormatter.stringToDateInstance(inputStr);
|
|
97
|
+
|
|
98
|
+
expect(result.getFullYear()).to.equal(2024);
|
|
99
|
+
expect(result.getMonth()).to.equal(11);
|
|
100
|
+
expect(result.getDate()).to.equal(31);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("dateFormatter.isValidDate", () => {
|
|
106
|
+
describe("valid date inputs", () => {
|
|
107
|
+
const validCases = [
|
|
108
|
+
{
|
|
109
|
+
args: ["2024-02-29"],
|
|
110
|
+
description: "valid leap day ISO (default format)",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
args: ["1999-12-31", "yyyy-mm-dd"],
|
|
114
|
+
description: "valid ISO with explicit format",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
args: ["12/31/1999", "mm/dd/yyyy"],
|
|
118
|
+
description: "valid mm/dd/yyyy",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
args: ["31-01-2020", "dd-mm-yyyy"],
|
|
122
|
+
description: "valid dd-mm-yyyy",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
args: ["01/15/01", "mm/dd/yy"],
|
|
126
|
+
description: "2-digit year below 50 normalizes to 20xx (01 → 2001)",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
args: ["12/31/99", "mm/dd/yy"],
|
|
130
|
+
description: "2-digit year >= 50 normalizes to 19xx (99 → 1999)",
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
validCases.forEach(({ args, description }) => {
|
|
135
|
+
it(`returns true for: ${description}`, () => {
|
|
136
|
+
const result = dateFormatter.isValidDate(...args);
|
|
137
|
+
expect(result, `Expected ${JSON.stringify(args)} to be a valid date`).to
|
|
138
|
+
.be.true;
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("invalid date inputs", () => {
|
|
144
|
+
const invalidCases = [
|
|
145
|
+
{
|
|
146
|
+
args: ["02/2024", "mm/yyyy"],
|
|
147
|
+
description: "short format mm/yyyy",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
args: ["2024/02", "yyyy/mm"],
|
|
151
|
+
description: "short format yyyy/mm",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
args: ["2024", "yyyy"],
|
|
155
|
+
description: "short format yyyy",
|
|
156
|
+
},
|
|
157
|
+
{ args: ["2024-02-30"], description: "invalid ISO day" },
|
|
158
|
+
{
|
|
159
|
+
args: ["12/31/1999", "yyyy-mm-dd"],
|
|
160
|
+
description: "non-ISO string with yyyy-mm-dd format",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
args: ["02/30/2024", "mm/dd/yyyy"],
|
|
164
|
+
description: "invalid mm/dd/yyyy with impossible day",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
args: [null, "mm/dd/yyyy"],
|
|
168
|
+
description: "non-string date value",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
args: ["13/2024", "mm/yyyy"],
|
|
172
|
+
description: "invalid mm/yyyy with month out of range",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
args: ["00/24", "mm/yy"],
|
|
176
|
+
description: "invalid mm/yy with month below range",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
args: ["02/24", "mm/yy"],
|
|
180
|
+
description: "invalid mm/yy",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
args: ["2024/13", "yyyy/mm"],
|
|
184
|
+
description: "invalid yyyy/mm with month out of range",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
args: ["ab24", "yyyy"],
|
|
188
|
+
description: "invalid yyyy with non-numeric value",
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
args: ["2a", "yy"],
|
|
192
|
+
description: "invalid yy with non-numeric value",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
args: ["24", "yy"],
|
|
196
|
+
description: "invalid yy",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
args: ["00", "mm"],
|
|
200
|
+
description: "invalid mm below range",
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
args: ["12", "mm"],
|
|
204
|
+
description: "invalid mm",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
args: ["00", "dd"],
|
|
208
|
+
description: "invalid dd below range",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
args: ["31", "dd"],
|
|
212
|
+
description: "invalid dd",
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
args: ["32", "dd"],
|
|
216
|
+
description: "invalid dd above range",
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
invalidCases.forEach(({ args, description }) => {
|
|
221
|
+
it(`returns false for: ${description}`, () => {
|
|
222
|
+
const result = dateFormatter.isValidDate(...args);
|
|
223
|
+
expect(result, `Expected ${JSON.stringify(args)} to be an invalid date`)
|
|
224
|
+
.to.be.false;
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("dateFormatter.toISOFormatString", () => {
|
|
231
|
+
describe("valid Date inputs", () => {
|
|
232
|
+
const validCases = [
|
|
233
|
+
{
|
|
234
|
+
input: new Date("2024/01/15"),
|
|
235
|
+
expected: "2024-01-15",
|
|
236
|
+
description: "typical date",
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
input: new Date("1999/12/31"),
|
|
240
|
+
expected: "1999-12-31",
|
|
241
|
+
description: "end of year",
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
input: new Date("2024/02/29"),
|
|
245
|
+
expected: "2024-02-29",
|
|
246
|
+
description: "leap day",
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
input: new Date("2000/01/01"),
|
|
250
|
+
expected: "2000-01-01",
|
|
251
|
+
description: "first day of millennium",
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
input: new Date("2024/12/31"),
|
|
255
|
+
expected: "2024-12-31",
|
|
256
|
+
description: "last day of year",
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
validCases.forEach(({ input, expected, description }) => {
|
|
261
|
+
it(`returns "${expected}" for: ${description}`, () => {
|
|
262
|
+
const result = dateFormatter.toISOFormatString(input);
|
|
263
|
+
expect(result).to.equal(expected);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe("invalid inputs", () => {
|
|
269
|
+
const invalidCases = [
|
|
270
|
+
{ input: new Date("not-a-date"), description: "invalid Date instance" },
|
|
271
|
+
{ input: "2024-01-15", description: "string input" },
|
|
272
|
+
{ input: null, description: "null input" },
|
|
273
|
+
{ input: undefined, description: "undefined input" },
|
|
274
|
+
{ input: 20240115, description: "number input" },
|
|
275
|
+
{ input: {}, description: "plain object input" },
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
invalidCases.forEach(({ input, description }) => {
|
|
279
|
+
it(`throws for: ${description}`, () => {
|
|
280
|
+
expect(() => dateFormatter.toISOFormatString(input)).to.throw(Error);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -3,12 +3,11 @@ import { AuroDateUtilitiesBase } from "./baseDateUtilities.mjs";
|
|
|
3
3
|
import { dateFormatter } from "./dateFormatter.mjs";
|
|
4
4
|
|
|
5
5
|
export class AuroDateUtilities extends AuroDateUtilitiesBase {
|
|
6
|
-
|
|
7
6
|
/**
|
|
8
7
|
* Returns the current century.
|
|
9
8
|
* @returns {String} The current century.
|
|
10
9
|
*/
|
|
11
|
-
getCentury
|
|
10
|
+
getCentury() {
|
|
12
11
|
return String(new Date().getFullYear()).slice(0, 2);
|
|
13
12
|
}
|
|
14
13
|
|
|
@@ -17,14 +16,12 @@ export class AuroDateUtilities extends AuroDateUtilitiesBase {
|
|
|
17
16
|
* @param {String} year - The year to convert to four digits.
|
|
18
17
|
* @returns {String} The four digit year.
|
|
19
18
|
*/
|
|
20
|
-
getFourDigitYear
|
|
21
|
-
|
|
19
|
+
getFourDigitYear(year) {
|
|
22
20
|
const strYear = String(year).trim();
|
|
23
21
|
return strYear.length <= 2 ? this.getCentury() + strYear : strYear;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
constructor() {
|
|
27
|
-
|
|
28
25
|
super();
|
|
29
26
|
|
|
30
27
|
/**
|
|
@@ -33,7 +30,8 @@ export class AuroDateUtilities extends AuroDateUtilitiesBase {
|
|
|
33
30
|
* @param {Object} date2 - Second date to compare.
|
|
34
31
|
* @returns {Boolean} Returns true if the dates match.
|
|
35
32
|
*/
|
|
36
|
-
this.datesMatch = (date1, date2) =>
|
|
33
|
+
this.datesMatch = (date1, date2) =>
|
|
34
|
+
new Date(date1).getTime() === new Date(date2).getTime();
|
|
37
35
|
|
|
38
36
|
/**
|
|
39
37
|
* Returns true if value passed in is a valid date.
|
|
@@ -42,54 +40,41 @@ export class AuroDateUtilities extends AuroDateUtilitiesBase {
|
|
|
42
40
|
* @returns {Boolean}
|
|
43
41
|
*/
|
|
44
42
|
this.validDateStr = (date, format) => {
|
|
45
|
-
|
|
46
43
|
// The length we expect the date string to be
|
|
47
|
-
const dateStrLength = format
|
|
44
|
+
const dateStrLength = format?.length || 0;
|
|
48
45
|
|
|
49
46
|
// Guard Clause: Date and format are defined
|
|
50
47
|
if (typeof date === "undefined" || typeof format === "undefined") {
|
|
51
|
-
throw new Error(
|
|
48
|
+
throw new Error(
|
|
49
|
+
"AuroDatepickerUtilities | validateDateStr: Date and format are required",
|
|
50
|
+
);
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
// Guard Clause: Date should be of type string
|
|
55
54
|
if (typeof date !== "string") {
|
|
56
|
-
throw new Error(
|
|
55
|
+
throw new Error(
|
|
56
|
+
"AuroDatepickerUtilities | validateDateStr: Date must be a string",
|
|
57
|
+
);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
// Guard Clause: Format should be of type string
|
|
60
61
|
if (typeof format !== "string") {
|
|
61
|
-
throw new Error(
|
|
62
|
+
throw new Error(
|
|
63
|
+
"AuroDatepickerUtilities | validateDateStr: Format must be a string",
|
|
64
|
+
);
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
// Guard Clause: Length is what we expect it to be
|
|
65
68
|
if (date.length !== dateStrLength) {
|
|
66
69
|
return false;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
// Get a formatted date string and parse it
|
|
70
|
-
const dateParts = dateFormatter.parseDate(date, format);
|
|
71
|
-
|
|
72
|
-
// Guard Clause: Date parse succeeded
|
|
73
|
-
if (!dateParts) {
|
|
74
|
-
return false;
|
|
75
70
|
}
|
|
76
71
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const dateObj = new Date(this.getFourDigitYear(dateParts.year), dateParts.month - 1, dateParts.day || 1);
|
|
82
|
-
|
|
83
|
-
// Get the date string of the date object we created from the string date
|
|
84
|
-
const actualDateStr = dateFormatter.getDateAsString(dateObj, "en-US");
|
|
85
|
-
|
|
86
|
-
// Guard Clause: Generated date matches date string input
|
|
87
|
-
if (expectedDateStr !== actualDateStr) {
|
|
72
|
+
// Get a formatted date string and parse and validate it
|
|
73
|
+
try {
|
|
74
|
+
return Boolean(dateFormatter.parseDate(date, format));
|
|
75
|
+
} catch (error) {
|
|
88
76
|
return false;
|
|
89
77
|
}
|
|
90
|
-
|
|
91
|
-
// If we passed all other checks, we can assume the date is valid
|
|
92
|
-
return true;
|
|
93
78
|
};
|
|
94
79
|
|
|
95
80
|
/**
|
|
@@ -99,10 +84,11 @@ export class AuroDateUtilities extends AuroDateUtilitiesBase {
|
|
|
99
84
|
* @returns {boolean}
|
|
100
85
|
*/
|
|
101
86
|
this.dateAndFormatMatch = (value, format) => {
|
|
102
|
-
|
|
103
87
|
// Ensure we have both values we need to do the comparison
|
|
104
88
|
if (!value || !format) {
|
|
105
|
-
throw new Error(
|
|
89
|
+
throw new Error(
|
|
90
|
+
"AuroFormValidation | dateFormatMatch: value and format are required",
|
|
91
|
+
);
|
|
106
92
|
}
|
|
107
93
|
|
|
108
94
|
// If the lengths are different, they cannot match
|
|
@@ -111,11 +97,10 @@ export class AuroDateUtilities extends AuroDateUtilitiesBase {
|
|
|
111
97
|
}
|
|
112
98
|
|
|
113
99
|
// Get the parts of the date
|
|
114
|
-
const dateParts = dateFormatter.
|
|
100
|
+
const dateParts = dateFormatter.getDateParts(value, format);
|
|
115
101
|
|
|
116
102
|
// Validator for day
|
|
117
103
|
const dayValueIsValid = (day) => {
|
|
118
|
-
|
|
119
104
|
// Guard clause: if there is no day in the dateParts, we can ignore this check.
|
|
120
105
|
if (!dateParts.day) {
|
|
121
106
|
return true;
|
|
@@ -131,7 +116,9 @@ export class AuroDateUtilities extends AuroDateUtilitiesBase {
|
|
|
131
116
|
|
|
132
117
|
// Guard clause: ensure day is a valid integer
|
|
133
118
|
if (Number.isNaN(numDay)) {
|
|
134
|
-
throw new Error(
|
|
119
|
+
throw new Error(
|
|
120
|
+
"AuroDatepickerUtilities | dayValueIsValid: Unable to parse day value integer",
|
|
121
|
+
);
|
|
135
122
|
}
|
|
136
123
|
|
|
137
124
|
// Guard clause: ensure day is within the valid range
|
|
@@ -145,6 +132,10 @@ export class AuroDateUtilities extends AuroDateUtilitiesBase {
|
|
|
145
132
|
|
|
146
133
|
// Validator for month
|
|
147
134
|
const monthValueIsValid = (month) => {
|
|
135
|
+
// Guard clause: if there is no month in the dateParts, we can ignore this check.
|
|
136
|
+
if (!dateParts.month) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
148
139
|
|
|
149
140
|
// Guard clause: ensure month exists.
|
|
150
141
|
if (!month) {
|
|
@@ -156,7 +147,9 @@ export class AuroDateUtilities extends AuroDateUtilitiesBase {
|
|
|
156
147
|
|
|
157
148
|
// Guard clause: ensure month is a valid integer
|
|
158
149
|
if (Number.isNaN(numMonth)) {
|
|
159
|
-
throw new Error(
|
|
150
|
+
throw new Error(
|
|
151
|
+
"AuroDatepickerUtilities | monthValueIsValid: Unable to parse month value integer",
|
|
152
|
+
);
|
|
160
153
|
}
|
|
161
154
|
|
|
162
155
|
// Guard clause: ensure month is within the valid range
|
|
@@ -170,6 +163,10 @@ export class AuroDateUtilities extends AuroDateUtilitiesBase {
|
|
|
170
163
|
|
|
171
164
|
// Validator for year
|
|
172
165
|
const yearIsValid = (_year) => {
|
|
166
|
+
// Guard clause: if there is no year in the dateParts, we can ignore this check.
|
|
167
|
+
if (!dateParts.year) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
173
170
|
|
|
174
171
|
// Guard clause: ensure year exists.
|
|
175
172
|
if (!_year) {
|
|
@@ -184,7 +181,9 @@ export class AuroDateUtilities extends AuroDateUtilitiesBase {
|
|
|
184
181
|
|
|
185
182
|
// Guard clause: ensure year is a valid integer
|
|
186
183
|
if (Number.isNaN(numYear)) {
|
|
187
|
-
throw new Error(
|
|
184
|
+
throw new Error(
|
|
185
|
+
"AuroDatepickerUtilities | yearValueIsValid: Unable to parse year value integer",
|
|
186
|
+
);
|
|
188
187
|
}
|
|
189
188
|
|
|
190
189
|
// Guard clause: ensure year is within the valid range
|
|
@@ -200,7 +199,7 @@ export class AuroDateUtilities extends AuroDateUtilitiesBase {
|
|
|
200
199
|
const checks = [
|
|
201
200
|
monthValueIsValid(dateParts.month),
|
|
202
201
|
dayValueIsValid(dateParts.day),
|
|
203
|
-
yearIsValid(dateParts.year)
|
|
202
|
+
yearIsValid(dateParts.year),
|
|
204
203
|
];
|
|
205
204
|
|
|
206
205
|
// If any of the checks failed, the date format does not match and the result is invalid
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { expect } from "@open-wc/testing";
|
|
2
|
+
import { AuroDateUtilities } from "./dateUtilities.mjs";
|
|
3
|
+
|
|
4
|
+
const utils = new AuroDateUtilities();
|
|
5
|
+
|
|
6
|
+
describe("AuroDateUtilities.validDateStr", () => {
|
|
7
|
+
it("throws when date is undefined", () => {
|
|
8
|
+
expect(() => utils.validDateStr(undefined, "mm/dd/yyyy")).to.throw(Error);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("throws when format is undefined", () => {
|
|
12
|
+
expect(() => utils.validDateStr("01/15/2024", undefined)).to.throw(Error);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("throws when date is not a string", () => {
|
|
16
|
+
expect(() => utils.validDateStr(20240115, "mm/dd/yyyy")).to.throw(Error);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns false when date length does not match format length", () => {
|
|
20
|
+
expect(utils.validDateStr("01/15", "mm/dd/yyyy")).to.be.false;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns true for a valid date matching its format", () => {
|
|
24
|
+
expect(utils.validDateStr("01/15/2024", "mm/dd/yyyy")).to.be.true;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns false for an invalid calendar date (Feb 30)", () => {
|
|
28
|
+
expect(utils.validDateStr("02/30/2024", "mm/dd/yyyy")).to.be.false;
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("AuroDateUtilities.dateAndFormatMatch", () => {
|
|
33
|
+
it("throws when value is falsy", () => {
|
|
34
|
+
expect(() => utils.dateAndFormatMatch(null, "mm/dd/yyyy")).to.throw(Error);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("throws when format is falsy", () => {
|
|
38
|
+
expect(() => utils.dateAndFormatMatch("01/15/2024", null)).to.throw(Error);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns false when value length differs from format length", () => {
|
|
42
|
+
expect(utils.dateAndFormatMatch("01/15", "mm/dd/yyyy")).to.be.false;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns true for a value that matches the format", () => {
|
|
46
|
+
expect(utils.dateAndFormatMatch("01/15/2024", "mm/dd/yyyy")).to.be.true;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns false when month is below range", () => {
|
|
50
|
+
expect(utils.dateAndFormatMatch("00/15/2024", "mm/dd/yyyy")).to.be.false;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns false when month is above range", () => {
|
|
54
|
+
expect(utils.dateAndFormatMatch("13/15/2024", "mm/dd/yyyy")).to.be.false;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns false when day is below range", () => {
|
|
58
|
+
expect(utils.dateAndFormatMatch("01/00/2024", "mm/dd/yyyy")).to.be.false;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns false when day is above range", () => {
|
|
62
|
+
expect(utils.dateAndFormatMatch("01/32/2024", "mm/dd/yyyy")).to.be.false;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns false when year is below range", () => {
|
|
66
|
+
expect(utils.dateAndFormatMatch("01/15/1899", "mm/dd/yyyy")).to.be.false;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns false when year is above range", () => {
|
|
70
|
+
expect(utils.dateAndFormatMatch("01/15/2401", "mm/dd/yyyy")).to.be.false;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("skips day check when format has no day component", () => {
|
|
74
|
+
expect(utils.dateAndFormatMatch("01/2024", "mm/yyyy")).to.be.true;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("skips month check when format has no month component", () => {
|
|
78
|
+
expect(utils.dateAndFormatMatch("15/2024", "dd/yyyy")).to.be.true;
|
|
79
|
+
});
|
|
80
|
+
});
|