Skip to content

Commit

Permalink
Extract date operations in DateRangeFilter to utility functions
Browse files Browse the repository at this point in the history
Run date related tests additionally in UTC+02:00 zone.

Signed-off-by: Radoslaw Szwajkowski <[email protected]>
  • Loading branch information
rszwajko committed Oct 12, 2023
1 parent a1b41d1 commit d030b85
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 38 deletions.
2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
63 changes: 33 additions & 30 deletions packages/common/src/components/Filter/DateRangeFilter.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<DateTime>();
const [to, setTo] = useState<DateTime>();
const validFilters = selectedFilters?.filter(isValidInterval) ?? [];

const rangeToOption = (range: Interval): string => range.toISODate().replace('/', ' - ');
const [from, setFrom] = useState<Date>();
const [to, setTo] = useState<Date>();

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<HTMLInputElement>, 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<HTMLInputElement>, 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]);
}
}
};
Expand All @@ -75,7 +78,7 @@ export const DateRangeFilter = ({
>
<InputGroup>
<DatePicker
value={from?.toISODate()}
value={toISODate(from)}
dateFormat={(date) => DateTime.fromJSDate(date).toISODate()}
dateParse={(str) => DateTime.fromISO(str).toJSDate()}
onChange={onFromDateChange}
Expand All @@ -86,12 +89,12 @@ export const DateRangeFilter = ({
appendTo={document.body}
/>
<DatePicker
value={to?.toISODate()}
value={toISODate(to)}
onChange={onToDateChange}
isDisabled={!from?.isValid}
rangeStart={from?.toJSDate()}
isDisabled={isValidJSDate(from)}
rangeStart={from}
aria-label="Interval end"
placeholder="YYYY-MM-DD"
placeholder={placeholderLabel}
appendTo={document.body}
/>
</InputGroup>
Expand Down
6 changes: 2 additions & 4 deletions packages/common/src/components/FilterGroup/matchers.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 = {
Expand Down
30 changes: 30 additions & 0 deletions packages/common/src/utils/__tests__/dates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import {
areSameDayInUTCZero,
changeFormatToISODate,
changeTimeZoneToUTCZero,
isInRange,
isValidDate,
isValidInterval,
parseISOtoJSDate,
toISODate,
toISODateInterval,
} from '../dates';

describe('changeTimeZoneToUTCZero', () => {
Expand Down Expand Up @@ -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');
});
});
35 changes: 32 additions & 3 deletions packages/common/src/utils/dates.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;
};
Expand All @@ -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;
};
Expand All @@ -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;
};

0 comments on commit d030b85

Please sign in to comment.