Skip to content

Commit 9605562

Browse files
authored
Merge pull request #50 from CVEProject/hk/019_date_search
Resolves issue #47, search for dates and date ranges in date fields
2 parents 2768085 + a032893 commit 9605562

26 files changed

+1461
-413
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Notes.md
4949
.env.*
5050

5151
# development and testing files
52+
.notes
5253
aws.temp
5354
bulk/converted*.jsonl
5455
cves/deltaLog.json

ChangeLog.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
# Change Log
22

3-
### 2.2.0-rc1 (Sprint 1)
3+
### 2.2.0-rc3 (Sprint 4)
44
- exact phrase search using double quotes
5+
- date and date range search on date fields, using the following formats (date ranges are inclusive):
6+
- YYYY-MM-DD
7+
- YYYY-MM-DDTHH:MM:SS(.mmm)(Z) (where the .mmm and Z are optional, defaults to .000Z if missing)
8+
- YYYY-MM-DD..YYYY-MM-DD
9+
- YYYY-MM-DDTHH:MM:SS(.mmm)(Z)..YYYY-MM-DDTHH:MM:SS.(mmm)(Z)
510

611
### 2.1.0
712
- wildcard search using "*" and "?"

index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export * from "./src/commands/GenericCommand.js";
2626
export * from "./src/commands/MainCommands.js";
2727

2828
// common
29+
export * from "./src/common/IsoDate/IsoDate.js";
30+
export * from "./src/common/IsoDate/IsoDatetime.js";
31+
export * from "./src/common/IsoDate/IsoDatetimeRange.js";
2932
export * from "./src/common/IsoDate/IsoDateString.js";
3033
export * from "./src/common/Json/Json.js";
3134
export * from "./src/common/comparer/ObjectComparer.js";

package-lock.json

Lines changed: 252 additions & 237 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cve-core",
3-
"version": "2.2.0-rc1",
3+
"version": "2.2.0-rc3",
44
"description": "CVE npm package for working with CVEs",
55
"type": "module",
66
"engines": {

src/adapters/search/OpensearchDatetimeUtils.test.unit.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { IsoDate, IsoDatetime } from '../../common/IsoDate/IsoDatetime';
55

66
describe('OpensearchDatetimeUtils.toSearchDateDslString()', () => {
77
const testcases = [
8+
{ input: '2025-03-01T12:34:56.001Z', expected: '2025-03-01T12:34:56.001Z' },
9+
{ input: '2025-03-01T12:34:56.001', expected: '2025-03-01T12:34:56.001Z' },
810
{ input: '2025-03-01T12:34:56Z', expected: '2025-03-01T12:34:56Z' },
11+
{ input: '2025-03-01T12:34:56', expected: '2025-03-01T12:34:56Z' },
912
{ input: '2025-03-01', expected: '2025-03-01||/d' },
1013
{ input: '2025-03', expected: '2025-03||/M' },
1114
{ input: '2025', expected: '2025||/y' },

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

Lines changed: 74 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -75,47 +75,87 @@ describe('IsoDate.parse – valid inputs', () => {
7575
});
7676
});
7777

78-
describe('IsoDate.parse – invalid inputs', () => {
79-
const invalid = [
80-
'202501', // year+month without hyphen
81-
'20250101', // compact full date (properly rejected in this class)
82-
'250101', // two‑digit year
83-
'2025-13-01', // invalid month
84-
'2025-02-30', // invalid day (Feb 30)
85-
'2025-04-31', // invalid day (April 31)
86-
'-2025-04-31', // invalid year
87-
'--01-01', // leading hyphens
88-
'-2025-01', // leading hyphen before year
89-
'2025--01', // double hyphen between year and month
90-
'2025-01--01', // double hyphen before day
91-
'2025-02-29', // illegal leap year
92-
'2025-01-01T014:00:00:00Z', // datetime does not match in this class
93-
];
9478

95-
invalid.forEach((value) => {
96-
test(`throws for "${value}"`, () => {
97-
expect(() => IsoDate.parse(value)).toThrow(Error);
98-
});
99-
});
79+
describe('IsoDate.parse/toString/isMonth/isDate/isYear', () => {
80+
const tests: Array<{ value: string; isYear?: boolean; isMonth?: boolean; isDate?: boolean; isDatetime?: boolean; threw?: RegExp; }> = [
81+
// valid ISO Date specs
82+
{ value: `2025`, isYear: true },
83+
{ value: ` 2025 `, isYear: true },
84+
{ value: `2025-10`, isMonth: true },
85+
{ value: `2024-02-29`, isDate: true },
86+
// invalid ISO Date specs
87+
{ value: `25`, threw: /Invalid calendar date format/ }, // year must be after 1000 and before 2500
88+
{ value: `1899`, threw: /Year out of range/ }, // year must be after 1900 and before 2100
89+
{ value: `2500`, threw: /Year out of range/ }, // year must be after 1900 and before 2100
90+
{ value: `1/1/25`, threw: /Invalid calendar date format/ }, // bad format
91+
{ value: `abc`, threw: /Invalid calendar date format/ }, // bad format
92+
{ value: `202501`, threw: /Invalid calendar date format/ }, // year+month without hyphen
93+
{ value: `20250101`, threw: /Invalid calendar date format/ }, // compact full date (properly rejected in this class)
94+
{ value: `250101`, threw: /Invalid calendar date format/ }, // two‑digit year
95+
{ value: `-2025-04-31`, threw: /Invalid calendar date format/ }, // leading hyphen
96+
{ value: `-2025-04`, threw: /Invalid calendar date format/ }, // leading hyphen
97+
{ value: `01-01`, threw: /Invalid calendar date format/ },
98+
{ value: `2025--01`, threw: /Invalid calendar date format/ },
99+
{ value: `2025-01-01T014:00:00:00Z`, threw: /Invalid calendar date format/ },
100+
{ value: `2025-13-01`, threw: /Month out of range/ },
101+
{ value: `2025-02-29`, threw: /Day out of range/ }, // not a leap year
102+
{ value: `2025-02-30`, threw: /Day out of range/ },
103+
{ value: `2025-04-31`, threw: /Day out of range/ },
104+
];
100105

101-
invalid.forEach((value) => {
102-
test(`"${value}" is not an IsoDate`, () => {
103-
expect(isValidIsoDate(value)).toBeFalsy();
106+
tests.forEach(({ value, isYear = false, isMonth = false, isDate = false, threw = '' }) => {
107+
test(`properly determines if ${value} is a ${isYear ? 'year' : ''}${isMonth ? 'month' : ''}${isDate ? 'date' : ''}${threw ? 'error' : ''}`, () => {
108+
if (threw) {
109+
expect(() => IsoDate.parse(value)).toThrow(threw);
110+
}
111+
else {
112+
const isoDate = IsoDate.parse(value);
113+
if (isYear) {
114+
expect(isoDate.isYear()).toBeTruthy();
115+
expect(isoDate.isMonth()).toBeFalsy();
116+
expect(isoDate.isDate()).toBeFalsy();
117+
expect(isoDate.isDatetime()).toBeFalsy();
118+
}
119+
else if (isMonth) {
120+
expect(isoDate.isYear()).toBeFalsy();
121+
expect(isoDate.isMonth()).toBeTruthy();
122+
expect(isoDate.isDate()).toBeFalsy();
123+
expect(isoDate.isDatetime()).toBeFalsy();
124+
}
125+
else if (isDate) {
126+
expect(isoDate.isYear()).toBeFalsy();
127+
expect(isoDate.isMonth()).toBeFalsy();
128+
expect(isoDate.isDate()).toBeTruthy();
129+
expect(isoDate.isDatetime()).toBeFalsy();
130+
}
131+
expect(isoDate.toString()).toBe(value.trim());
132+
}
104133
});
105134
});
106135
});
107136

108-
describe('IsoDate.toString', () => {
109-
const tests: Array<{ input: string; expected: string; }> = [
110-
{ input: '2025-01-01', expected: '2025-01-01' },
111-
{ input: '2025-01', expected: '2025-01' },
112-
{ input: '2025', expected: '2025' }
137+
138+
describe('IsoDate.daysInMonth()', () => {
139+
const tests: Array<{ year: number; month: number, expected: number; }> = [
140+
{ year: 2025, month: 1, expected: 31 },
141+
{ year: 2025, month: 2, expected: 28 },
142+
{ year: 2025, month: 3, expected: 31 },
143+
{ year: 2025, month: 4, expected: 30 },
144+
{ year: 2025, month: 5, expected: 31 },
145+
{ year: 2025, month: 6, expected: 30 },
146+
{ year: 2025, month: 7, expected: 31 },
147+
{ year: 2025, month: 8, expected: 31 },
148+
{ year: 2025, month: 9, expected: 30 },
149+
{ year: 2025, month: 10, expected: 31 },
150+
{ year: 2025, month: 11, expected: 30 },
151+
{ year: 2025, month: 12, expected: 31 },
152+
{ year: 2024, month: 2, expected: 29 }, // leap year
113153
];
114154

115-
tests.forEach(({input, expected}) => {
116-
test(`properly prints out '${input}' as '${expected}'`, () => {
117-
const isoDate = IsoDate.parse(input)
118-
expect(isoDate.toString()).toBe(expected)
155+
tests.forEach(({ year, month, expected }) => {
156+
test(`properly calculates ${year}-${month} to have ${expected} days`, () => {
157+
const days = IsoDate.daysInMonth(year, month);
158+
expect(days).toBe(expected);
119159
});
120160
});
121-
})
161+
});

src/common/IsoDate/IsoDate.ts

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,32 @@
3434
*/
3535

3636
export class IsoDate {
37-
37+
38+
/** the string specified by user (may be original or may be modified (e.g., trimmed))
39+
* this is necessary in order for other functions to determine user's intention
40+
* (e.g., when search date ranges, a 2025-02 value in opensearch needs to be specified as
41+
* 2025-02||/M see adapters/search/OpensearchDatetimeUtils)
42+
*/
43+
private _parsedValue: string;
44+
public get parsedValue(): string {
45+
return this._parsedValue;
46+
}
47+
protected set parsedValue(value: string) {
48+
this._parsedValue = value;
49+
}
50+
3851
/** Full year (e.g., 2025) */
3952
public readonly year: number;
4053
/** Month number 1‑12 (optional) */
4154
public readonly month?: number;
4255
/** Day number 1‑31 (optional, requires month) */
4356
public readonly day?: number;
4457

45-
protected constructor(year: number, month?: number, day?: number) {
58+
protected constructor(parsedValue: string, year: number, month?: number, day?: number) {
4659
this.year = year;
4760
if (month !== undefined) this.month = month;
4861
if (day !== undefined) this.day = day;
62+
this.parsedValue = (parsedValue) ?? this.toString();
4963
}
5064

5165
/**
@@ -64,6 +78,7 @@ export class IsoDate {
6478
// /^(?<year>\d{4})(?:[-]?(?<month>\d{2})(?:[-]?(?<day>\d{2})?)?)?$/;
6579
/^(?<year>\d{4})(?:[-](?<month>\d{2})(?:[-](?<day>\d{2})?)?)?$/;
6680

81+
value = value.trim();
6782
const match = regex.exec(value);
6883
if (!match || !match.groups) {
6984
throw new Error(`Invalid calendar date format: "${value}": must be one of YYYY-MM-DD, YYYY-MM, or YYYY`);
@@ -74,7 +89,7 @@ export class IsoDate {
7489
const dayStr = match.groups.day;
7590

7691
// Validate year range (reasonable limits)
77-
if (year < 1 || year > 2500) {
92+
if (year < 1900 || year > 2100) {
7893
throw new Error(`Year out of range: ${year}`);
7994
}
8095

@@ -97,35 +112,40 @@ export class IsoDate {
97112
)}: ${dayStr}`
98113
);
99114
}
100-
return new IsoDate(year, month, day);
115+
return new IsoDate(value, year, month, day);
101116
}
102117

103118
// Month only (no day)
104-
return new IsoDate(year, month);
119+
return new IsoDate(value, year, month);
105120
}
106121

107122
// Year only
108-
return new IsoDate(year);
123+
return new IsoDate(value, year);
109124
}
110125

111126
/** Return true if the stored year is a leap year. */
112127
public isLeapYear(): boolean {
113128
return IsoDate.isLeapYear(this.year);
114129
}
115130

116-
/** Return true if the stored year is a leap year. */
131+
/** Return true if the stored date is a year. */
117132
public isYear(): boolean {
118-
return this.toString().length === 4;
133+
return this.parsedValue.length === 4;
119134
}
120135

121-
/** Return true if the stored year is a leap year. */
136+
/** Return true if the stored date is a month. */
122137
public isMonth(): boolean {
123-
return this.toString().length === 7;
138+
return this.parsedValue.length === 7;
124139
}
125140

126-
/** Return true if the stored year is a leap year. */
141+
/** Return true if the stored date is a date. */
127142
public isDate(): boolean {
128-
return this.toString().length === 10;
143+
return this.parsedValue.length === 10;
144+
}
145+
146+
/** Return true if the stored date is IsoDatetime. */
147+
public isDatetime(): boolean {
148+
return this.parsedValue.length > 10;
129149
}
130150

131151

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ describe('IsoDatetime.parse – valid inputs', () => {
6565
const valid = [
6666
{ input: '2024-02-29T23:00:00+01:00', expected: '2024-02-29T22:00:00Z' }, // leap year date with offset
6767
{ input: '2025-03-01', expected: '2025-03-01T00:00:00Z' }, // date only
68+
{ input: '2025-03-01T12:34:56Z', expected: '2025-03-01T12:34:56Z' }, // missing milliseconds
6869
{ input: '2025-03-01T12:34:56.789', expected: '2025-03-01T12:34:56.789Z' }, // missing Z
6970
// currently does not allow the following even though it is valid in ISO 8601
7071
// { input: '2025-03', expected: '2024-03' }, // month only
@@ -156,6 +157,54 @@ describe('IsoDatetime.toIsoDate', () => {
156157
});
157158
});
158159

160+
161+
describe('IsoDatetime.isMonth/isDate/isYear', () => {
162+
const tests: Array<{ value: string; isYear?: boolean; isMonth?: boolean; isDate?: boolean; isDatetime?: boolean; threw?: string; }> = [
163+
{ value: `2025`, isYear: true },
164+
{ value: `2025-10`, isMonth: true },
165+
{ value: `2025-10-02`, isDate: true },
166+
{ value: `2025-10-02T10:11:12Z`, isDatetime: true },
167+
{ value: `2025-02-29`, threw: "[Error: Day out of range" }, // not a leap year
168+
];
169+
170+
tests.forEach(({ value, isYear = false, isMonth = false, isDate = false, isDatetime = false, threw = '' }) => {
171+
test(`properly determines if ${value} is a ${isYear ? 'year' : ''}${isMonth ? 'month' : ''}${isDate ? 'date' : ''}`, () => {
172+
try {
173+
const isoDatetime = IsoDatetime.parse(value);
174+
if (isYear) {
175+
expect(isoDatetime.isYear()).toBeTruthy();
176+
expect(isoDatetime.isMonth()).toBeFalsy();
177+
expect(isoDatetime.isDate()).toBeFalsy();
178+
expect(isoDatetime.isDatetime()).toBeFalsy();
179+
}
180+
else if (isMonth) {
181+
expect(isoDatetime.isYear()).toBeFalsy();
182+
expect(isoDatetime.isMonth()).toBeTruthy();
183+
expect(isoDatetime.isDate()).toBeFalsy();
184+
expect(isoDatetime.isDatetime()).toBeFalsy();
185+
}
186+
else if (isDate) {
187+
expect(isoDatetime.isYear()).toBeFalsy();
188+
expect(isoDatetime.isMonth()).toBeFalsy();
189+
expect(isoDatetime.isDate()).toBeTruthy();
190+
expect(isoDatetime.isDatetime()).toBeFalsy();
191+
}
192+
else if (isDatetime) {
193+
expect(isoDatetime.isYear()).toBeFalsy();
194+
expect(isoDatetime.isMonth()).toBeFalsy();
195+
expect(isoDatetime.isDate()).toBeFalsy();
196+
expect(isoDatetime.isDatetime()).toBeTruthy();
197+
}
198+
}
199+
catch (e: unknown) {
200+
if (e instanceof Error) {
201+
expect(e.message.startsWith(threw));
202+
}
203+
}
204+
});
205+
});
206+
});
207+
159208
describe('IsoDatetime.getNextDay – day increments and decrements', () => {
160209
test('regular date: March 4 +1 day => March 5', () => {
161210
const dt = IsoDatetime.parse('2024-03-04T00:00:00Z');

0 commit comments

Comments
 (0)