From 85bf0219d92e9806657df3cf2c1ad0f58504c138 Mon Sep 17 00:00:00 2001 From: Derek Burgman Date: Sat, 5 Aug 2023 12:50:09 -0500 Subject: [PATCH] fix: fixed dateScheduleDateFilter() timezone usage --- packages/date/src/lib/date/date.block.spec.ts | 14 ++++++- packages/date/src/lib/date/date.block.ts | 41 +++++++++++++++---- .../date/src/lib/date/date.schedule.spec.ts | 10 +---- packages/date/src/lib/date/date.schedule.ts | 39 +++++++++++++++--- .../lib/calendar.schedule.selection.store.ts | 4 +- 5 files changed, 81 insertions(+), 27 deletions(-) diff --git a/packages/date/src/lib/date/date.block.spec.ts b/packages/date/src/lib/date/date.block.spec.ts index 6ec65ddec..5550cec72 100644 --- a/packages/date/src/lib/date/date.block.spec.ts +++ b/packages/date/src/lib/date/date.block.spec.ts @@ -46,12 +46,14 @@ import { getDateBlockTimingFirstEventDateRange, dateBlockTimingFromDateRangeAndEvent, getCurrentDateBlockTimingUtcData, - getCurrentDateBlockTimingOffsetData + getCurrentDateBlockTimingOffsetData, + dateBlockTimingStartForNowInSystemTimezone, + timingIsInExpectedTimezone } from './date.block'; import { MS_IN_DAY, MINUTES_IN_DAY, range, RangeInput, Hours, Day, TimezoneString } from '@dereekb/util'; import { copyHoursAndMinutesFromDate, roundDownToHour, roundDownToMinute } from './date'; import { dateBlockDurationSpanHasNotEndedFilterFunction } from './date.filter'; -import { dateTimezoneUtcNormal, getCurrentSystemOffsetInHours, systemBaseDateToNormalDate, systemNormalDateToBaseDate } from './date.timezone'; +import { dateTimezoneUtcNormal, getCurrentSystemOffsetInHours, systemBaseDateToNormalDate, systemNormalDateToBaseDate, SYSTEM_DATE_TIMEZONE_UTC_NORMAL_INSTANCE } from './date.timezone'; import { formatToISO8601DayString } from './date.format'; describe('isValidDateBlockIndex()', () => { @@ -209,6 +211,14 @@ describe('getCurrentDateBlockTimingOffset()', () => { }); }); +describe('dateBlockTimingStartForSystemTimezone()', () => { + it('should return the DateBlockTimingStart for the system timezone.', () => { + const result = dateBlockTimingStartForNowInSystemTimezone(); + const isInSystemTimezone = timingIsInExpectedTimezone(result, SYSTEM_DATE_TIMEZONE_UTC_NORMAL_INSTANCE); + expect(isInSystemTimezone).toBe(true); + }); +}); + describe('getCurrentDateBlockTimingStartDate()', () => { const utcDate = new Date('2022-01-02T00:00:00Z'); // date in utc. Implies there is no offset to consider. diff --git a/packages/date/src/lib/date/date.block.ts b/packages/date/src/lib/date/date.block.ts index 879affdd5..e45003999 100644 --- a/packages/date/src/lib/date/date.block.ts +++ b/packages/date/src/lib/date/date.block.ts @@ -37,7 +37,7 @@ import { DateDurationSpan } from './date.duration'; import { differenceInDays, differenceInMilliseconds, isBefore, addDays, addMinutes, getSeconds, getMilliseconds, getMinutes, addMilliseconds, hoursToMilliseconds, addHours, differenceInHours, isAfter, minutesToHours, differenceInMinutes, startOfDay } from 'date-fns'; import { isDate, copyHoursAndMinutesFromDate, roundDownToMinute, copyHoursAndMinutesFromNow } from './date'; import { Expose, Type } from 'class-transformer'; -import { DateTimezoneUtcNormalFunctionInput, DateTimezoneUtcNormalInstance, dateTimezoneUtcNormal, getCurrentSystemOffsetInHours, startOfDayInTimezoneDayStringFactory, copyHoursAndMinutesFromDatesWithTimezoneNormal } from './date.timezone'; +import { DateTimezoneUtcNormalFunctionInput, DateTimezoneUtcNormalInstance, dateTimezoneUtcNormal, getCurrentSystemOffsetInHours, startOfDayInTimezoneDayStringFactory, copyHoursAndMinutesFromDatesWithTimezoneNormal, SYSTEM_DATE_TIMEZONE_UTC_NORMAL_INSTANCE } from './date.timezone'; import { IsDate, IsNumber, IsOptional, Min } from 'class-validator'; import { parseISO8601DayStringToDate } from './date.format'; @@ -102,6 +102,13 @@ export type DateBlockArrayRef = { blocks: DateBlockArray; }; +/** + * DateBlockTiming with only the start time. + * + * The start time is midnight of what timezone it is in, and can be used to infer the target timezone offset. + */ +export type DateBlockTimingStart = DateRangeStart; + /** * Is combination of DateRange and DateDurationSpan. The DateRange captures a range of days that a DateBlock takes up, and the DateDurationSpan * captures the Dates at which the Job occurs at. @@ -114,17 +121,12 @@ export type DateBlockArrayRef = { * - The startsAt time should be on the same date as normalized start * - The end time should equal the ending date/time of the final end duration. */ -export interface DateBlockTiming extends DateRange, DateDurationSpan {} +export interface DateBlockTiming extends DateBlockTimingStart, DateRange, DateDurationSpan {} /** - * DateBlockTiming with only the start time. The start time infers what timezone it is in. + * The DateRange component for a DateBlockTiming. The start date is a DateBlockTimingStart. */ -export type DateBlockTimingStart = DateRangeStart; - -/** - * Only the start and end of the date block range. - */ -export type DateBlockTimingStartEndRange = Pick; +export type DateBlockTimingStartEndRange = DateBlockTimingStart & Pick; /** * The start date of a DateBlockTimingStart, along with the endDay which is a normalized day that is at midnight of the last day in the timezone. @@ -413,6 +415,27 @@ export function changeTimingToTimezone(timing: T, time return changeTimingToTimezoneFunction(timezone)(timing); } +export function changeTimingToSystemTimezone(timing: T): T { + return changeTimingToTimezoneFunction(SYSTEM_DATE_TIMEZONE_UTC_NORMAL_INSTANCE)(timing); +} + +export function dateBlockTimingStartForNowInSystemTimezone(): DateBlockTimingStart { + return { + start: startOfDay(new Date()) + }; +} + +/** + * Creates a DateBlockTimingStart for now in the given timezone. + * + * @param timezoneInput + * @returns + */ +export function dateBlockTimingStartForNowInTimezone(timezoneInput: TimingDateTimezoneUtcNormalInput): DateBlockTimingStart { + const dateBlockTimingStartSystemTimezone = dateBlockTimingStartForNowInSystemTimezone(); + return changeTimingToTimezone(dateBlockTimingStartSystemTimezone, timezoneInput); +} + /** * Returns the startsAt date in the current/system timezone for the given date. * diff --git a/packages/date/src/lib/date/date.schedule.spec.ts b/packages/date/src/lib/date/date.schedule.spec.ts index 22cfcbced..8e2a2b068 100644 --- a/packages/date/src/lib/date/date.schedule.spec.ts +++ b/packages/date/src/lib/date/date.schedule.spec.ts @@ -47,19 +47,11 @@ describe('dateScheduleDateFilter()', () => { const schedule: DateScheduleDateFilterConfig = { start, w: '89' }; const weekDaysAndWeekends = dateScheduleDateFilter(schedule); - it('should allow every day of the week (indexes)', () => { - const maxIndex = 14; - const dateBlocks: DateBlockIndex[] = range(0, maxIndex); - const results = dateBlocks.filter(weekDaysAndWeekends); - - expect(results.length).toBe(maxIndex); - }); - it('should allow every day of the week (dates)', () => { const maxIndex = 14; const dateBlocks: Date[] = range(0, maxIndex).map((y) => addDays(start, y)); - const results = dateBlocks.filter(weekDaysAndWeekends); + const results = dateBlocks.filter(weekDaysAndWeekends); expect(results.length).toBe(maxIndex); }); diff --git a/packages/date/src/lib/date/date.schedule.ts b/packages/date/src/lib/date/date.schedule.ts index aa45dd201..8aca59077 100644 --- a/packages/date/src/lib/date/date.schedule.ts +++ b/packages/date/src/lib/date/date.schedule.ts @@ -3,7 +3,30 @@ import { Expose } from 'class-transformer'; import { IsString, Matches, IsOptional, Min, IsArray } from 'class-validator'; import { getDay } from 'date-fns'; import { copyHoursAndMinutesFromDate } from './date'; -import { DateBlock, dateBlockDayOfWeekFactory, DateBlockDurationSpan, DateBlockIndex, dateBlockIndexRange, DateBlockRange, DateBlockRangeOrDateRange, DateBlockRangeWithRange, DateBlocksExpansionFactory, dateBlocksExpansionFactory, dateBlockTiming, DateBlockTiming, DateBlockTimingStartEndRange, dateTimingRelativeIndexFactory, DateTimingRelativeIndexFactoryInput, getCurrentDateBlockTimingStartDate, groupToDateBlockRanges, safeDateBlockTimingFromDateRangeAndEvent } from './date.block'; +import { + changeTimingToSystemTimezone, + changeTimingToTimezone, + DateBlock, + dateBlockDayOfWeekFactory, + DateBlockDurationSpan, + DateBlockIndex, + dateBlockIndexRange, + DateBlockRange, + DateBlockRangeOrDateRange, + DateBlockRangeWithRange, + DateBlocksExpansionFactory, + dateBlocksExpansionFactory, + dateBlockTiming, + DateBlockTiming, + DateBlockTimingStartEndRange, + dateBlockTimingStartForNowInSystemTimezone, + dateBlockTimingStartForNowInTimezone, + dateTimingRelativeIndexFactory, + DateTimingRelativeIndexFactoryInput, + getCurrentDateBlockTimingStartDate, + groupToDateBlockRanges, + safeDateBlockTimingFromDateRangeAndEvent +} from './date.block'; import { dateBlockDurationSpanHasNotStartedFilterFunction, dateBlockDurationSpanHasNotEndedFilterFunction } from './date.filter'; import { DateRange, isSameDateRange } from './date.range'; import { copyHoursAndMinutesFromDatesWithTimezoneNormal } from './date.timezone'; @@ -389,8 +412,10 @@ export type DateScheduleDateFilter = DecisionFunction { +export interface DateScheduleDateFilterConfig extends DateSchedule, Partial { minMaxDateRange?: Maybe>; /** * Whether or not to restrict the start as the min date if a min date is not set in minMaxDateRange. True by default. @@ -415,14 +440,17 @@ export function copyDateScheduleDateFilterConfig(inputFilter: DateScheduleDateFi * @returns */ export function dateScheduleDateFilter(config: DateScheduleDateFilterConfig): DateScheduleDateFilter { - const { w, start: firstDate = new Date(), setStartAsMinDate = true, end, minMaxDateRange } = config; + const { w, start: inputStart, setStartAsMinDate = true, end, minMaxDateRange } = config; + const timingStart = inputStart != null ? changeTimingToSystemTimezone({ start: inputStart }) : dateBlockTimingStartForNowInSystemTimezone(); + const { start: firstDate } = timingStart; + const allowedDays: Set = expandDateScheduleDayCodesToDayOfWeekSet(w); // Start date is either now or the filter's start date. It is never the minMax's start date, since that is irrelevant to the filter's range. const firstDateDay = getDay(firstDate); const dayForIndex = dateBlockDayOfWeekFactory(firstDateDay); - const dateIndexForDate = dateTimingRelativeIndexFactory({ start: firstDate }); + const dateIndexForDate = dateTimingRelativeIndexFactory(timingStart); const indexFloor = setStartAsMinDate ? 0 : Number.MIN_SAFE_INTEGER; const minAllowedIndex = minMaxDateRange?.start != null ? Math.max(indexFloor, dateIndexForDate(minMaxDateRange.start)) : indexFloor; // start date should be the min inde @@ -442,7 +470,8 @@ export function dateScheduleDateFilter(config: DateScheduleDateFilterConfig): Da } day = dayForIndex(i); - return (i >= minAllowedIndex && i <= maxAllowedIndex && allowedDays.has(day) && !excludedIndexes.has(i)) || includedIndexes.has(i); + const result = (i >= minAllowedIndex && i <= maxAllowedIndex && allowedDays.has(day) && !excludedIndexes.has(i)) || includedIndexes.has(i); + return result; }; } diff --git a/packages/dbx-form/calendar/src/lib/calendar.schedule.selection.store.ts b/packages/dbx-form/calendar/src/lib/calendar.schedule.selection.store.ts index 705b04748..0bd7da113 100644 --- a/packages/dbx-form/calendar/src/lib/calendar.schedule.selection.store.ts +++ b/packages/dbx-form/calendar/src/lib/calendar.schedule.selection.store.ts @@ -77,7 +77,7 @@ export interface CalendarScheduleSelectionState extends PartialCalendarScheduleS /** * Filters the days of the schedule to only allow selecting days in the schedule. * - * If filter.start is provided, then the timezone is ignored, if one is present. + * If filter.start is provided, then the timezone in this normal is ignored, if one is present. Convert to the system timezone first. */ filter?: Maybe; /** @@ -99,7 +99,7 @@ export interface CalendarScheduleSelectionState extends PartialCalendarScheduleS */ start: Date; /** - * Timezone to use. OnlyInfluences the output start date. + * Timezone to use. Only influences the output start date. */ timezone?: Maybe; /**