Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Notes.md
.env.*

# development and testing files
.notes
aws.temp
bulk/converted*.jsonl
cves/deltaLog.json
Expand Down
7 changes: 6 additions & 1 deletion ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# Change Log

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

### 2.1.0
- wildcard search using "*" and "?"
Expand Down
3 changes: 3 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export * from "./src/commands/GenericCommand.js";
export * from "./src/commands/MainCommands.js";

// common
export * from "./src/common/IsoDate/IsoDate.js";
export * from "./src/common/IsoDate/IsoDatetime.js";
export * from "./src/common/IsoDate/IsoDatetimeRange.js";
export * from "./src/common/IsoDate/IsoDateString.js";
export * from "./src/common/Json/Json.js";
export * from "./src/common/comparer/ObjectComparer.js";
Expand Down
489 changes: 252 additions & 237 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cve-core",
"version": "2.2.0-rc1",
"version": "2.2.0-rc3",
"description": "CVE npm package for working with CVEs",
"type": "module",
"engines": {
Expand Down
3 changes: 3 additions & 0 deletions src/adapters/search/OpensearchDatetimeUtils.test.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { IsoDate, IsoDatetime } from '../../common/IsoDate/IsoDatetime';

describe('OpensearchDatetimeUtils.toSearchDateDslString()', () => {
const testcases = [
{ input: '2025-03-01T12:34:56.001Z', expected: '2025-03-01T12:34:56.001Z' },
{ input: '2025-03-01T12:34:56.001', expected: '2025-03-01T12:34:56.001Z' },
{ input: '2025-03-01T12:34:56Z', expected: '2025-03-01T12:34:56Z' },
{ input: '2025-03-01T12:34:56', expected: '2025-03-01T12:34:56Z' },
{ input: '2025-03-01', expected: '2025-03-01||/d' },
{ input: '2025-03', expected: '2025-03||/M' },
{ input: '2025', expected: '2025||/y' },
Expand Down
108 changes: 74 additions & 34 deletions src/common/IsoDate/IsoDate.test.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,47 +75,87 @@ describe('IsoDate.parse – valid inputs', () => {
});
});

describe('IsoDate.parse – invalid inputs', () => {
const invalid = [
'202501', // year+month without hyphen
'20250101', // compact full date (properly rejected in this class)
'250101', // two‑digit year
'2025-13-01', // invalid month
'2025-02-30', // invalid day (Feb 30)
'2025-04-31', // invalid day (April 31)
'-2025-04-31', // invalid year
'--01-01', // leading hyphens
'-2025-01', // leading hyphen before year
'2025--01', // double hyphen between year and month
'2025-01--01', // double hyphen before day
'2025-02-29', // illegal leap year
'2025-01-01T014:00:00:00Z', // datetime does not match in this class
];

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

invalid.forEach((value) => {
test(`"${value}" is not an IsoDate`, () => {
expect(isValidIsoDate(value)).toBeFalsy();
tests.forEach(({ value, isYear = false, isMonth = false, isDate = false, threw = '' }) => {
test(`properly determines if ${value} is a ${isYear ? 'year' : ''}${isMonth ? 'month' : ''}${isDate ? 'date' : ''}${threw ? 'error' : ''}`, () => {
if (threw) {
expect(() => IsoDate.parse(value)).toThrow(threw);
}
else {
const isoDate = IsoDate.parse(value);
if (isYear) {
expect(isoDate.isYear()).toBeTruthy();
expect(isoDate.isMonth()).toBeFalsy();
expect(isoDate.isDate()).toBeFalsy();
expect(isoDate.isDatetime()).toBeFalsy();
}
else if (isMonth) {
expect(isoDate.isYear()).toBeFalsy();
expect(isoDate.isMonth()).toBeTruthy();
expect(isoDate.isDate()).toBeFalsy();
expect(isoDate.isDatetime()).toBeFalsy();
}
else if (isDate) {
expect(isoDate.isYear()).toBeFalsy();
expect(isoDate.isMonth()).toBeFalsy();
expect(isoDate.isDate()).toBeTruthy();
expect(isoDate.isDatetime()).toBeFalsy();
}
expect(isoDate.toString()).toBe(value.trim());
}
});
});
});

describe('IsoDate.toString', () => {
const tests: Array<{ input: string; expected: string; }> = [
{ input: '2025-01-01', expected: '2025-01-01' },
{ input: '2025-01', expected: '2025-01' },
{ input: '2025', expected: '2025' }

describe('IsoDate.daysInMonth()', () => {
const tests: Array<{ year: number; month: number, expected: number; }> = [
{ year: 2025, month: 1, expected: 31 },
{ year: 2025, month: 2, expected: 28 },
{ year: 2025, month: 3, expected: 31 },
{ year: 2025, month: 4, expected: 30 },
{ year: 2025, month: 5, expected: 31 },
{ year: 2025, month: 6, expected: 30 },
{ year: 2025, month: 7, expected: 31 },
{ year: 2025, month: 8, expected: 31 },
{ year: 2025, month: 9, expected: 30 },
{ year: 2025, month: 10, expected: 31 },
{ year: 2025, month: 11, expected: 30 },
{ year: 2025, month: 12, expected: 31 },
{ year: 2024, month: 2, expected: 29 }, // leap year
];

tests.forEach(({input, expected}) => {
test(`properly prints out '${input}' as '${expected}'`, () => {
const isoDate = IsoDate.parse(input)
expect(isoDate.toString()).toBe(expected)
tests.forEach(({ year, month, expected }) => {
test(`properly calculates ${year}-${month} to have ${expected} days`, () => {
const days = IsoDate.daysInMonth(year, month);
expect(days).toBe(expected);
});
});
})
});
44 changes: 32 additions & 12 deletions src/common/IsoDate/IsoDate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,32 @@
*/

export class IsoDate {


/** the string specified by user (may be original or may be modified (e.g., trimmed))
* this is necessary in order for other functions to determine user's intention
* (e.g., when search date ranges, a 2025-02 value in opensearch needs to be specified as
* 2025-02||/M see adapters/search/OpensearchDatetimeUtils)
*/
private _parsedValue: string;
public get parsedValue(): string {
return this._parsedValue;
}
protected set parsedValue(value: string) {
this._parsedValue = value;
}

/** Full year (e.g., 2025) */
public readonly year: number;
/** Month number 1‑12 (optional) */
public readonly month?: number;
/** Day number 1‑31 (optional, requires month) */
public readonly day?: number;

protected constructor(year: number, month?: number, day?: number) {
protected constructor(parsedValue: string, year: number, month?: number, day?: number) {
this.year = year;
if (month !== undefined) this.month = month;
if (day !== undefined) this.day = day;
this.parsedValue = (parsedValue) ?? this.toString();
}

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

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

// Validate year range (reasonable limits)
if (year < 1 || year > 2500) {
if (year < 1900 || year > 2100) {
throw new Error(`Year out of range: ${year}`);
}

Expand All @@ -97,35 +112,40 @@ export class IsoDate {
)}: ${dayStr}`
);
}
return new IsoDate(year, month, day);
return new IsoDate(value, year, month, day);
}

// Month only (no day)
return new IsoDate(year, month);
return new IsoDate(value, year, month);
}

// Year only
return new IsoDate(year);
return new IsoDate(value, year);
}

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

/** Return true if the stored year is a leap year. */
/** Return true if the stored date is a year. */
public isYear(): boolean {
return this.toString().length === 4;
return this.parsedValue.length === 4;
}

/** Return true if the stored year is a leap year. */
/** Return true if the stored date is a month. */
public isMonth(): boolean {
return this.toString().length === 7;
return this.parsedValue.length === 7;
}

/** Return true if the stored year is a leap year. */
/** Return true if the stored date is a date. */
public isDate(): boolean {
return this.toString().length === 10;
return this.parsedValue.length === 10;
}

/** Return true if the stored date is IsoDatetime. */
public isDatetime(): boolean {
return this.parsedValue.length > 10;
}


Expand Down
49 changes: 49 additions & 0 deletions src/common/IsoDate/IsoDatetime.test.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ describe('IsoDatetime.parse – valid inputs', () => {
const valid = [
{ input: '2024-02-29T23:00:00+01:00', expected: '2024-02-29T22:00:00Z' }, // leap year date with offset
{ input: '2025-03-01', expected: '2025-03-01T00:00:00Z' }, // date only
{ input: '2025-03-01T12:34:56Z', expected: '2025-03-01T12:34:56Z' }, // missing milliseconds
{ input: '2025-03-01T12:34:56.789', expected: '2025-03-01T12:34:56.789Z' }, // missing Z
// currently does not allow the following even though it is valid in ISO 8601
// { input: '2025-03', expected: '2024-03' }, // month only
Expand Down Expand Up @@ -156,6 +157,54 @@ describe('IsoDatetime.toIsoDate', () => {
});
});


describe('IsoDatetime.isMonth/isDate/isYear', () => {
const tests: Array<{ value: string; isYear?: boolean; isMonth?: boolean; isDate?: boolean; isDatetime?: boolean; threw?: string; }> = [
{ value: `2025`, isYear: true },
{ value: `2025-10`, isMonth: true },
{ value: `2025-10-02`, isDate: true },
{ value: `2025-10-02T10:11:12Z`, isDatetime: true },
{ value: `2025-02-29`, threw: "[Error: Day out of range" }, // not a leap year
];

tests.forEach(({ value, isYear = false, isMonth = false, isDate = false, isDatetime = false, threw = '' }) => {
test(`properly determines if ${value} is a ${isYear ? 'year' : ''}${isMonth ? 'month' : ''}${isDate ? 'date' : ''}`, () => {
try {
const isoDatetime = IsoDatetime.parse(value);
if (isYear) {
expect(isoDatetime.isYear()).toBeTruthy();
expect(isoDatetime.isMonth()).toBeFalsy();
expect(isoDatetime.isDate()).toBeFalsy();
expect(isoDatetime.isDatetime()).toBeFalsy();
}
else if (isMonth) {
expect(isoDatetime.isYear()).toBeFalsy();
expect(isoDatetime.isMonth()).toBeTruthy();
expect(isoDatetime.isDate()).toBeFalsy();
expect(isoDatetime.isDatetime()).toBeFalsy();
}
else if (isDate) {
expect(isoDatetime.isYear()).toBeFalsy();
expect(isoDatetime.isMonth()).toBeFalsy();
expect(isoDatetime.isDate()).toBeTruthy();
expect(isoDatetime.isDatetime()).toBeFalsy();
}
else if (isDatetime) {
expect(isoDatetime.isYear()).toBeFalsy();
expect(isoDatetime.isMonth()).toBeFalsy();
expect(isoDatetime.isDate()).toBeFalsy();
expect(isoDatetime.isDatetime()).toBeTruthy();
}
}
catch (e: unknown) {
if (e instanceof Error) {
expect(e.message.startsWith(threw));
}
}
});
});
});

describe('IsoDatetime.getNextDay – day increments and decrements', () => {
test('regular date: March 4 +1 day => March 5', () => {
const dt = IsoDatetime.parse('2024-03-04T00:00:00Z');
Expand Down
Loading
Loading