From d030b85b48d3ac93ef7f19da31da55105beff961 Mon Sep 17 00:00:00 2001 From: Radoslaw Szwajkowski Date: Thu, 12 Oct 2023 12:31:15 +0200 Subject: [PATCH] Extract date operations in DateRangeFilter to utility functions Run date related tests additionally in UTC+02:00 zone. Signed-off-by: Radoslaw Szwajkowski --- packages/common/package.json | 2 +- .../src/components/Filter/DateRangeFilter.tsx | 63 ++++++++++--------- .../src/components/FilterGroup/matchers.ts | 6 +- .../common/src/utils/__tests__/dates.test.ts | 30 +++++++++ packages/common/src/utils/dates.ts | 35 ++++++++++- 5 files changed, 98 insertions(+), 38 deletions(-) diff --git a/packages/common/package.json b/packages/common/package.json index 8bb3cd2d6..279ec6689 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -21,7 +21,7 @@ "start": "rollup -c --bundleConfigAsCjs --watch", "lint": "eslint . && stylelint \"src/**/*.css\" --allow-empty-input", "lint:fix": "eslint . --fix && stylelint \"src/**/*.css\" --allow-empty-input --fix", - "test": "TZ=UTC jest", + "test": "TZ=UTC jest && TZ=UTC+02:00 jest src/utils/__tests__/dates.test.ts", "test:coverage": "TZ=UTC jest --coverage", "test:updateSnapshot": "TZ=UTC jest --updateSnapshot", "storybook": "storybook dev -p 6006", diff --git a/packages/common/src/components/Filter/DateRangeFilter.tsx b/packages/common/src/components/Filter/DateRangeFilter.tsx index 451c50c78..6bd04c22b 100644 --- a/packages/common/src/components/Filter/DateRangeFilter.tsx +++ b/packages/common/src/components/Filter/DateRangeFilter.tsx @@ -1,7 +1,20 @@ import React, { FormEvent, useState } from 'react'; -import { DateTime, Interval } from 'luxon'; +import { DateTime } from 'luxon'; -import { DatePicker, InputGroup, ToolbarFilter } from '@patternfly/react-core'; +import { + DatePicker, + InputGroup, + isValidDate as isValidJSDate, + ToolbarFilter, +} from '@patternfly/react-core'; + +import { + isValidDate, + isValidInterval, + parseISOtoJSDate, + toISODate, + toISODateInterval, +} from '../../utils'; import { FilterTypeProps } from './types'; @@ -24,43 +37,33 @@ export const DateRangeFilter = ({ placeholderLabel, showFilter = true, }: FilterTypeProps) => { - const validFilters = - selectedFilters - ?.map((str) => Interval.fromISO(str)) - ?.filter((range: Interval) => range.isValid) ?? []; - - const [from, setFrom] = useState(); - const [to, setTo] = useState(); + const validFilters = selectedFilters?.filter(isValidInterval) ?? []; - const rangeToOption = (range: Interval): string => range.toISODate().replace('/', ' - '); + const [from, setFrom] = useState(); + const [to, setTo] = useState(); - const optionToRange = (option: string): Interval => Interval.fromISO(option.replace(' - ', '/')); + const rangeToOption = (range: string): string => range.replace('/', ' - '); + const optionToRange = (option: string): string => option.replace(' - ', '/'); const clearSingleRange = (option) => { const target = optionToRange(option); - onFilterUpdate([ - ...validFilters.filter((range) => range.equals(target)).map((range) => range.toISODate()), - ]); + onFilterUpdate([...validFilters.filter((range) => range !== target)]); }; const onFromDateChange = (even: FormEvent, value: string) => { - if (value?.length === 10 && DateTime.fromISO(value).isValid) { - setFrom(DateTime.fromISO(value)); + if (value?.length === 10 && isValidDate(value)) { + setFrom(parseISOtoJSDate(value)); setTo(undefined); } }; const onToDateChange = (even: FormEvent, value: string) => { - if (value?.length === 10 && DateTime.fromISO(value).isValid) { - const newTo = DateTime.fromISO(value); + if (value?.length === 10 && isValidDate(value)) { + const newTo = parseISOtoJSDate(value); setTo(newTo); - const target = Interval.fromDateTimes(from, newTo); - if (target.isValid) { - onFilterUpdate( - [...validFilters.filter((range) => !range.equals(target)), target].map((range) => - range.toISODate(), - ), - ); + const target = toISODateInterval(from, newTo); + if (target) { + onFilterUpdate([...validFilters.filter((range) => range !== target), target]); } } }; @@ -75,7 +78,7 @@ export const DateRangeFilter = ({ > DateTime.fromJSDate(date).toISODate()} dateParse={(str) => DateTime.fromISO(str).toJSDate()} onChange={onFromDateChange} @@ -86,12 +89,12 @@ export const DateRangeFilter = ({ appendTo={document.body} /> diff --git a/packages/common/src/components/FilterGroup/matchers.ts b/packages/common/src/components/FilterGroup/matchers.ts index 8e14c2d00..e160d7210 100644 --- a/packages/common/src/components/FilterGroup/matchers.ts +++ b/packages/common/src/components/FilterGroup/matchers.ts @@ -1,7 +1,6 @@ import jsonpath from 'jsonpath'; -import { DateTime, Interval } from 'luxon'; -import { areSameDayInUTCZero, ResourceField } from '../../utils'; +import { areSameDayInUTCZero, isInRange, ResourceField } from '../../utils'; import { DateFilter, DateRangeFilter, @@ -111,8 +110,7 @@ const dateMatcher = { const dateRangeMatcher = { filterType: 'dateRange', - matchValue: (value: string) => (filter: string) => - Interval.fromISO(filter).contains(DateTime.fromISO(value)), + matchValue: (value: string) => (filter: string) => isInRange(filter, value), }; const sliderMatcher = { diff --git a/packages/common/src/utils/__tests__/dates.test.ts b/packages/common/src/utils/__tests__/dates.test.ts index d3a204cc7..727685fb7 100644 --- a/packages/common/src/utils/__tests__/dates.test.ts +++ b/packages/common/src/utils/__tests__/dates.test.ts @@ -2,9 +2,12 @@ import { areSameDayInUTCZero, changeFormatToISODate, changeTimeZoneToUTCZero, + isInRange, isValidDate, + isValidInterval, parseISOtoJSDate, toISODate, + toISODateInterval, } from '../dates'; describe('changeTimeZoneToUTCZero', () => { @@ -75,3 +78,30 @@ describe('areSameDayInUTCZero', () => { expect(areSameDayInUTCZero(undefined, '2023-foo')).toBeFalsy(); }); }); + +describe('isInRange', () => { + test('date in range', () => { + expect(isInRange('2023-10-30/2023-10-31', '2023-10-31T01:30:00.000+02:00')).toBeTruthy(); + }); + test('date before range', () => { + expect(isInRange('2023-10-31/2023-11-01', '2023-10-31T01:30:00.000+02:00')).toBeFalsy(); + }); +}); + +describe('isValidInterval', () => { + test('2023-10-30/2023-10-31', () => { + expect(isValidInterval('2023-10-30/2023-10-31')).toBeTruthy(); + }); + test('invalid format', () => { + expect(isValidInterval('2023-foo-30/2023-10-31')).toBeFalsy(); + }); + test('invalid days', () => { + expect(isValidInterval('2023-10-60/2023-10-31')).toBeFalsy(); + }); +}); + +describe('toISODateInterval', () => { + test('unix epoch as start and end', () => { + expect(toISODateInterval(new Date(0), new Date(0))).toBe('1970-01-01/1970-01-01'); + }); +}); diff --git a/packages/common/src/utils/dates.ts b/packages/common/src/utils/dates.ts index feb39e208..9fe6ce05b 100644 --- a/packages/common/src/utils/dates.ts +++ b/packages/common/src/utils/dates.ts @@ -1,4 +1,4 @@ -import { DateTime } from 'luxon'; +import { DateTime, Interval } from 'luxon'; /** * Converts a given ISO date time string to UTC+00:00 time zone. @@ -28,7 +28,7 @@ export const changeFormatToISODate = (isoDateString: string): string | undefined * @param date * @returns ISO date string if input is valid or undefined otherwise. */ -export const toISODate = (date: Date): string => { +export const toISODate = (date: Date): string | undefined => { const dt = DateTime.fromJSDate(date); return dt.isValid ? dt.toISODate() : undefined; }; @@ -40,7 +40,7 @@ export const isValidDate = (isoDateString: string) => DateTime.fromISO(isoDateSt * @param isoDateString * @returns JS Date instance if input is valid or undefined otherwise. */ -export const parseISOtoJSDate = (isoDateString: string) => { +export const parseISOtoJSDate = (isoDateString: string): Date | undefined => { const date = DateTime.fromISO(isoDateString); return date.isValid ? date.toJSDate() : undefined; }; @@ -56,3 +56,32 @@ export const areSameDayInUTCZero = (dateTime: string, calendarDate: string): boo // which results in shifting to previous day for zones with positive offsets return DateTime.fromISO(dateTime).toUTC().hasSame(DateTime.fromISO(calendarDate), 'day'); }; + +/** + * + * @param interval ISO time interval with date part only (no time, no time zone) + * @param date ISO date time + * @returns true if the provided date is in the time interval + */ +export const isInRange = (interval: string, date: string): boolean => + Interval.fromISO(interval).contains( + DateTime.fromISO(date).toUTC().setZone('local', { keepCalendarTime: true }), + ); + +/** + * + * @param interval ISO time interval + * @returns true if valid + */ +export const isValidInterval = (interval: string): boolean => Interval.fromISO(interval).isValid; + +/** + * + * @param from start date (inclusive) + * @param to end date (exclusive) + * @returns ISO time interval with date part only (no time, no time zone) + */ +export const toISODateInterval = (from: Date, to: Date): string | undefined => { + const target = Interval.fromDateTimes(DateTime.fromJSDate(from), DateTime.fromJSDate(to)); + return target.isValid ? target.toISODate() : undefined; +};