Skip to content

Commit 4259218

Browse files
committed
date search when specifying just a date now searches for any time during that day instead of defaulting to a date range of that date to current time
1 parent d5cd6a6 commit 4259218

7 files changed

Lines changed: 100 additions & 200 deletions

File tree

src/common/IsoDate/IsoDate.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,12 @@ export class IsoDate {
143143
return y;
144144
}
145145

146-
/** Static helper – leap‑year check for any year. */
146+
/** Static function that returns true iff year is a leap‑year */
147147
public static isLeapYear(year: number): boolean {
148148
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
149149
}
150150

151-
/** Static helper – number of days in a given month/year. */
151+
/** Static function that returns number of days in a given month/year. */
152152
public static daysInMonth(year: number, month: number): number {
153153
switch (month) {
154154
case 2:
@@ -164,7 +164,8 @@ export class IsoDate {
164164
}
165165
}
166166

167-
// Utility function to check if a string is a valid ISO date according to IsoDate parsing rules.
167+
/* Utility function to check if a string is a valid ISO date according to IsoDate parsing rules
168+
*/
168169
export function isValidIsoDate(value: string): boolean {
169170
try {
170171
IsoDate.parse(value);
Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, test, expect } from '@jest/globals';
22
import { IsoDate } from './IsoDate.js';
3-
import { IsoDatetime } from './IsoDatetime.js';
3+
import { IsoDatetime, toIsoDatetime } from './IsoDatetime.js';
44

55
describe('IsoDatetime.fromIsoDate', () => {
66
test('converts an IsoDate to midnight UTC IsoDatetime', () => {
@@ -9,4 +9,21 @@ describe('IsoDatetime.fromIsoDate', () => {
99
expect(datetime).toBeInstanceOf(IsoDatetime);
1010
expect(datetime.toString()).toBe('2025-03-15T00:00:00Z');
1111
});
12+
});
13+
14+
15+
describe('IsoDatetime.toIsoDatetime', () => {
16+
test('converts an IsoDate to midnight UTC IsoDatetime', () => {
17+
const date = IsoDate.parse('2025-03-15');
18+
const datetime = toIsoDatetime(date);
19+
expect(datetime).toBeInstanceOf(IsoDatetime);
20+
expect(datetime.toString()).toBe('2025-03-15T00:00:00Z');
21+
});
22+
23+
test('converts an IsoDatetime to midnight UTC IsoDatetime', () => {
24+
const date = IsoDatetime.parse('2025-03-15T00:00:00Z');
25+
const datetime = toIsoDatetime(date);
26+
expect(datetime).toBeInstanceOf(IsoDatetime);
27+
expect(datetime.toString()).toBe('2025-03-15T00:00:00Z');
28+
});
1229
});

src/common/IsoDate/IsoDatetime.test.unit.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* works as designed.
1111
*/
1212
import { describe, test, expect } from '@jest/globals';
13+
import { IsoDate } from './IsoDatetime.js';
1314
import { IsoDatetime } from './IsoDatetime.js';
1415

1516
describe('IsoDatetime.parse – valid inputs', () => {
@@ -61,22 +62,30 @@ describe('IsoDatetime.parse – valid inputs', () => {
6162
expect(dt.toString()).toBe('2025-03-01T02:30:00Z');
6263
});
6364

64-
test('leap year date with offset', () => {
65-
const str = '2024-02-29T23:00:00+01:00';
66-
const dt = IsoDatetime.parse(str);
67-
// 23:00 +01:00 => 22:00Z on leap day
68-
expect(dt.toString()).toBe('2024-02-29T22:00:00Z');
65+
const valid = [
66+
{ input: '2024-02-29T23:00:00+01:00', expected: '2024-02-29T22:00:00Z' }, // leap year date with offset
67+
{ input: '2025-03-01', expected: '2025-03-01T00:00:00Z' }, // date only
68+
{ input: '2025-03-01T12:34:56.789', expected: '2025-03-01T12:34:56.789Z' }, // missing Z
69+
// currently does not allow the following even though it is valid in ISO 8601
70+
// { input: '2025-03', expected: '2024-03' }, // month only
71+
// { input: '2025', expected: '2024' }, // year only
72+
];
73+
74+
valid.forEach(({ input, expected }) => {
75+
test(`IsoDatetime.parse(${input})`, () => {
76+
const datetime = IsoDatetime.parse(input);
77+
expect(datetime.toString()).toBe(expected);
78+
});
6979
});
7080
});
7181

82+
7283
describe('IsoDatetime.parse – invalid inputs', () => {
7384
const invalid = [
74-
'2025-03-01', // date only
7585
'2025-03', // month only
7686
'2025', // year only
7787
'2025-03-01 12:34:56Z', // no 'T' separator
7888
'2025-03-01T12:34Z', // missing seconds
79-
'2025-03-01T12:34:56.789', // missing Z
8089
'2025-13-01T12:34:56Z', // bad month
8190
'2025-00-01T12:34:56Z', // bad month
8291
'2025-02-30T12:34:56Z', // bad number of days in February
@@ -95,6 +104,24 @@ describe('IsoDatetime.parse – invalid inputs', () => {
95104
});
96105
});
97106

107+
108+
describe('IsoDatetime.fromIsoDate()', () => {
109+
const valid = [
110+
{ input: '2025-03-02', expected: '2025-03-02T00:00:00Z' }, // date only
111+
{ input: '2025-03', expected: '2025-03-01T00:00:00Z' }, // month only
112+
{ input: '2025', expected: '2025-01-01T00:00:00Z' }, // year only
113+
];
114+
115+
valid.forEach(({ input, expected }) => {
116+
test(`IsoDatetime.fromIsoDate(${input})`, () => {
117+
const date = IsoDate.parse(input);
118+
const datetime = IsoDatetime.fromIsoDate(date);
119+
expect(datetime.toString()).toBe(expected);
120+
});
121+
});
122+
});
123+
124+
98125
describe('IsoDatetime.toString – formatting', () => {
99126
const tests = [
100127
{ input: '2025-03-01T12:34:56Z', expected: '2025-03-01T12:34:56Z' },

src/common/IsoDate/IsoDatetime.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class IsoDatetime extends IsoDate {
6767
// 4. optional .sss (fractional seconds)
6868
// 5. timezone: Z or ±hh:mm
6969
const regex =
70-
/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?:\.(?<ms>\d+))?(?<tz>Z|[+-]\d{2}:\d{2})$/;
70+
/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})(?:T(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?:\.(?<ms>\d+))?(?<tz>Z|[+-]\d{2}:\d{2})?)?$/;
7171

7272
const match = regex.exec(value);
7373
if (!match || !match.groups) {
@@ -79,12 +79,12 @@ export class IsoDatetime extends IsoDate {
7979
const year = Number(match.groups.year);
8080
const month = Number(match.groups.month);
8181
const day = Number(match.groups.day);
82-
const hour = Number(match.groups.hour);
83-
const minute = Number(match.groups.minute);
84-
const second = Number(match.groups.second);
82+
const hour = Number(match.groups.hour ?? '0');
83+
const minute = Number(match.groups.minute ?? '0');
84+
const second = Number(match.groups.second ?? '0');
8585
const msStr = match.groups.ms ?? '0';
8686
const millisecond = Number(msStr.padEnd(3, '0').substring(0, 3)); // keep three digits
87-
const tz = match.groups.tz;
87+
const tz = match.groups.tz ?? 'Z';
8888

8989
// Validate date components using IsoDate's helpers.
9090
if (year < 1 || year > 2500) {
@@ -144,9 +144,22 @@ export class IsoDatetime extends IsoDate {
144144
);
145145
}
146146

147-
/** Convert this IsoDate to an IsoDatetime at midnight UTC. */
147+
/** Convert this IsoDate to an IsoDatetime
148+
* if date -> at midnight UTC.
149+
* if month -> 1st of month at midnight UTC.
150+
* if year -> 1st of the year at midnight UTC.
151+
*/
148152
public static fromIsoDate(date: IsoDate): IsoDatetime {
149-
const isoString = `${date.toString()}T00:00:00.000Z`;
153+
let isoString;
154+
if (date.isDate()) {
155+
isoString = `${date.toString()}T00:00:00.000Z`;
156+
}
157+
else if (date.isMonth()) {
158+
isoString = `${date.toString()}-01T00:00:00.000Z`;
159+
}
160+
else if (date.isYear()) {
161+
isoString = `${date.toString()}-01-01T00:00:00.000Z`;
162+
}
150163
return IsoDatetime.parse(isoString);
151164
}
152165

@@ -208,4 +221,18 @@ export class IsoDatetime extends IsoDate {
208221
return next;
209222
}
210223

211-
}
224+
}
225+
226+
227+
/* Utility function that always returns a IsoDatetime version
228+
* of a IsoDate or IsoDatetime object, very useful
229+
* when the type of the IsoDate object can be either
230+
*/
231+
export function toIsoDatetime(o: IsoDate | IsoDatetime): IsoDatetime {
232+
if (o instanceof IsoDatetime) {
233+
return o;
234+
}
235+
else { // if (o instanceof IsoDate) {
236+
return IsoDatetime.fromIsoDate(o);
237+
}
238+
}

src/search/SearchQueryBuilder.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,16 @@ export class SearchQueryBuilder {
8888
if (isDate) {
8989
// assemble all the date fields into an array
9090
let dateFields = [];
91+
// if the user had requested an IsoDate, it should stay as an IsoDate and not be cast to an IsoDatetime
92+
// otherwise YYYY or YYYY-MM requests would not work
9193
const startDate = (this._searchText.length > 10) ? IsoDatetime.parse(this._searchText) : IsoDate.parse(this._searchText);
92-
const startDateStr = toSearchDateDslString(startDate)
94+
const stopDate = IsoDatetime.fromIsoDate(startDate).getNextDay()
9395
SearchQueryBuilder.kDateFieldPaths.map(path => {
9496
let field = `{
9597
"range": {
9698
"${path}": {
97-
"gte": "${startDateStr}"
99+
"gte": "${toSearchDateDslString(startDate)}",
100+
"lt": "${toSearchDateDslString(stopDate)}"
98101
}
99102
}
100103
}`;

0 commit comments

Comments
 (0)