Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { convertFromInternalToExternalDatePicker } from './utils';
import { convertFromInternalToExternalDatePicker, serializeAbsoluteDateRange } from './utils';

describe('utils', () => {
describe('convertFromInternalToExternalDatePicker', () => {
Expand Down Expand Up @@ -40,5 +40,72 @@ describe('utils', () => {
expect(() => convertFromInternalToExternalDatePicker('>2024-13-50')).not.toThrow();
expect(convertFromInternalToExternalDatePicker('>2024-13-50')).toEqual('>2024-13-50');
});

it('formats an absolute date range correctly', () => {
const startMs = new Date(2025, 0, 1).getTime();
const endMs = new Date(2025, 2, 31).getTime();
expect(convertFromInternalToExternalDatePicker(`tr/${startMs}-${endMs}`)).toEqual(
'Between Jan 01, 2025 and Mar 31, 2025'
);
});

it('formats a relative date range correctly', () => {
expect(convertFromInternalToExternalDatePicker('30d-90d')).toEqual(
'Between 30 and 90 days ago'
);
});

it('formats an open-ended relative range correctly', () => {
expect(convertFromInternalToExternalDatePicker('>365d')).toEqual(
'More than 365 days ago'
);
});

it('returns original value for malformed absolute range', () => {
expect(convertFromInternalToExternalDatePicker('tr/abc-def')).toEqual('tr/abc-def');
expect(convertFromInternalToExternalDatePicker('tr/123')).toEqual('tr/123');
expect(convertFromInternalToExternalDatePicker('tr/')).toEqual('tr/');
});
});

describe('serializeAbsoluteDateRange', () => {
it('serializes to tr/<startMs>-<endMs> with start-of-day and end-of-day boundaries', () => {
const startMs = new Date(2025, 0, 1, 10, 30).getTime();
const endMs = new Date(2025, 2, 31, 8, 0).getTime();
const expectedStartMs = new Date(2025, 0, 1, 0, 0, 0, 0).getTime();
const expectedEndMs = new Date(2025, 2, 31, 23, 59, 59, 999).getTime();
expect(serializeAbsoluteDateRange(startMs, endMs)).toEqual(
`tr/${expectedStartMs}-${expectedEndMs}`
);
});

it('serializes a same-day range spanning the whole day', () => {
const dayMs = new Date(2025, 5, 15, 12, 0).getTime();
const expectedStartMs = new Date(2025, 5, 15, 0, 0, 0, 0).getTime();
const expectedEndMs = new Date(2025, 5, 15, 23, 59, 59, 999).getTime();
expect(serializeAbsoluteDateRange(dayMs, dayMs)).toEqual(
`tr/${expectedStartMs}-${expectedEndMs}`
);
});

it('throws when either input is not a valid date', () => {
const validMs = new Date(2025, 0, 1).getTime();
expect(() => serializeAbsoluteDateRange(NaN, validMs)).toThrow();
expect(() => serializeAbsoluteDateRange(validMs, NaN)).toThrow();
});

it('throws when start date is after end date', () => {
const startMs = new Date(2025, 2, 31).getTime();
const endMs = new Date(2025, 0, 1).getTime();
expect(() => serializeAbsoluteDateRange(startMs, endMs)).toThrow();
});

it('round-trips through convertFromInternalToExternalDatePicker', () => {
const startMs = new Date(2025, 0, 1).getTime();
const endMs = new Date(2025, 2, 31).getTime();
expect(
convertFromInternalToExternalDatePicker(serializeAbsoluteDateRange(startMs, endMs))
).toEqual('Between Jan 01, 2025 and Mar 31, 2025');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,70 @@ export const dateConditions = Object.keys(
dateConditionMap
) as unknown as (keyof typeof dateConditionMap)[];

// Range condition for the date-picker input. Unlike Before/On/After it is not a
// prefix on a single date; it serializes to the backend time-range format tr/<startMs>-<endMs>.
export const dateRangeCondition = 'Between' as const;

const absoluteDateRangeRegex = /^tr\/(\d+)-(\d+)$/;
const relativeDateRangeRegex = /^(\d+)d-(\d+)d$/;
const relativeDateOlderThanRegex = /^>(\d+)d$/;

/**
* Serializes an absolute date range into the backend time-range query format.
* The start is widened to the start of its day and the end to the end of its day
* (local time) so the range covers the full calendar days the user selected.
* @param startMs - Epoch ms within the first day of the range
* @param endMs - Epoch ms within the last day of the range
* @returns Value like "tr/1735689600000-1743465599999"
* @throws If either input is not a valid date or the start date falls after the end date
*/
export function serializeAbsoluteDateRange(startMs: number, endMs: number): string {
if (Number.isNaN(startMs) || Number.isNaN(endMs)) {
throw new Error('Date range start and end must be valid dates');
}

const start = new Date(startMs);
start.setHours(0, 0, 0, 0);
const end = new Date(endMs);
end.setHours(23, 59, 59, 999);

if (start.getTime() > end.getTime()) {
throw new Error('Start date of a date range must not be after the end date');
}

return `tr/${start.getTime()}-${end.getTime()}`;
}

/**
* Formats date picker value like CVE discovered time filter values into user-friendly text
* @param value - Filter value like ">2024-01-01" or "2024-01-01"
* @returns Formatted string like "After January 1, 2024"
* @param value - Filter value like ">2024-01-01", "2024-01-01", "tr/<startMs>-<endMs>", "30d-90d", or ">365d"
* @returns Formatted string like "After January 1, 2024" or "Between 30 and 90 days ago"
*/
export function convertFromInternalToExternalDatePicker(value: string): string {
try {
if (value.startsWith('tr/')) {
const absoluteRangeMatch = value.match(absoluteDateRangeRegex);
if (!absoluteRangeMatch) {
return value; // Return original if the time-range value is malformed
}
const startDate = new Date(Number(absoluteRangeMatch[1]));
const endDate = new Date(Number(absoluteRangeMatch[2]));
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
return value; // Return original if epoch values are invalid
}
return `Between ${getDate(startDate)} and ${getDate(endDate)}`;
}

const relativeRangeMatch = value.match(relativeDateRangeRegex);
if (relativeRangeMatch) {
return `Between ${relativeRangeMatch[1]} and ${relativeRangeMatch[2]} days ago`;
}

const olderThanMatch = value.match(relativeDateOlderThanRegex);
if (olderThanMatch) {
return `More than ${olderThanMatch[1]} days ago`;
}

// Parse the condition prefix and date
const match = value.match(/^([<>]?)(.+)$/);
if (!match) {
Expand Down
Loading