Skip to content

Commit

Permalink
fix: fixed dateBlockTimingInTimezoneFunction()
Browse files Browse the repository at this point in the history
- fixed issue where the startsAt time was not being evaluated with a proper date normal, causing Asia/Tokyo (and similar date ranges) evaluations to return a different start date than other timezones.
  • Loading branch information
dereekb committed Aug 5, 2023
1 parent b758918 commit 6d1bd8a
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 11 deletions.
28 changes: 27 additions & 1 deletion packages/date/src/lib/date/date.block.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ describe('dateBlockTimingFromDateRangeAndEvent()', () => {

function describeTestsForTimezone(timezone: TimezoneString) {
const startOfTodayInTimezone = dateTimezoneUtcNormal({ timezone }).systemDateToTargetDate(startOfDay(new Date()));
const timing = dateBlockTiming({ startsAt: addHours(startOfTodayInTimezone, 3), duration: 60 }, 2); // 2 days
const timing = dateBlockTiming({ startsAt: addHours(startOfTodayInTimezone, 3), duration: 60 }, 1); // 1 day

describe(`${timezone}`, () => {
it('should return a copy of a timing.', () => {
Expand Down Expand Up @@ -444,6 +444,19 @@ describe('dateBlockTimingInTimezoneFunction()', () => {
expect(result.startsAt).toBeSameSecondAs(startsAt);
expect(result.duration).toBe(duration);
});

it('should create a timing in the afteroon in the UTC timezone.', () => {
const duration = 60;
const startsAt = addHours(startOfToday, 12);
const result = fn({ startsAt, duration }, 2);

const { start } = result;
const utcHours = start.getUTCHours();
expect(utcHours).toBe(utcTimezoneOffsetInHours);

expect(result.startsAt).toBeSameSecondAs(startsAt);
expect(result.duration).toBe(duration);
});
});

describe('America/Denver', () => {
Expand All @@ -464,6 +477,19 @@ describe('dateBlockTimingInTimezoneFunction()', () => {
expect(result.startsAt).toBeSameSecondAs(startsAt);
expect(result.duration).toBe(duration);
});

it('should create a timing in the afteroon in the America/Denver timezone.', () => {
const duration = 60;
const startsAt = addHours(startOfToday, 12);
const result = fn({ startsAt, duration }, 2);

const { start } = result;
const utcHours = start.getUTCHours();
expect(utcHours).toBe(denverTimezoneOffsetInHours);

expect(result.startsAt).toBeSameSecondAs(startsAt);
expect(result.duration).toBe(duration);
});
});
});
});
Expand Down
24 changes: 21 additions & 3 deletions packages/date/src/lib/date/date.block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,8 @@ function _dateBlockTimingFromDateBlockTimingStartEndDayDateRange(dateBlockTiming
/**
* Returns a copy of the input timing with the start time timezone in the given timezone.
*
* The start time is a normal, and should still refer to the same UTC date, but with the given timing's offset.
*
* @param timing
*/
export type ChangeTimingToTimezoneFunction = (<T extends DateRangeStart>(timing: T) => T) & {
Expand Down Expand Up @@ -559,6 +561,15 @@ export function getRelativeDateForDateBlockTiming(timing: DateBlockTimingStart,
*/
export type DateBlockTimingRangeInput = Pick<DateRangeDayDistanceInput, 'distance'> | DateRange | number;

export interface DateBlockTimingOptions {
/**
* Timezone to evaluate the startsAt time in.
*
* Will convert the input startsAt time to a normal in the given timezone, then converts it back to the system timezone.
*/
timezone?: DateTimezoneUtcNormalFunctionInput;
}

/**
* Creates a valid DateBlock timing from the DateDurationSpan and range input.
*
Expand All @@ -573,14 +584,20 @@ export type DateBlockTimingRangeInput = Pick<DateRangeDayDistanceInput, 'distanc
*
* The start date from the inputDate is considered to to have the offset noted in DateBlock, and will be retained.
*/
export function dateBlockTiming(durationInput: DateDurationSpan, inputRange: DateBlockTimingRangeInput): DateBlockTiming {
export function dateBlockTiming(durationInput: DateDurationSpan, inputRange: DateBlockTimingRangeInput, options?: DateBlockTimingOptions): DateBlockTiming {
const { duration } = durationInput;
const { timezone: timezoneInput } = options ?? {};
const timezoneInstance = timezoneInput ? dateTimezoneUtcNormal(timezoneInput) : undefined;

if (duration > MINUTES_IN_DAY) {
throw new Error('dateBlockTiming() duration cannot be longer than 24 hours.');
}

let { startsAt } = durationInput;
let { startsAt: inputStartsAt } = durationInput;

// it is important that startsAt is evaluated the system time normal, as addDays/addMinutes and related functionality rely on the system timezone.
let startsAt = timezoneInstance ? timezoneInstance.systemDateToTargetDate(inputStartsAt) : inputStartsAt;

let numberOfBlockedDays: number;

let inputDate: Date | undefined;
Expand Down Expand Up @@ -620,6 +637,7 @@ export function dateBlockTiming(durationInput: DateDurationSpan, inputRange: Dat
}

const start = range.start;
startsAt = timezoneInstance ? timezoneInstance.targetDateToSystemDate(startsAt) : startsAt;

// calculate end to be the ending date/time of the final duration span
const lastStart = addDays(startsAt, numberOfBlockedDays);
Expand All @@ -644,7 +662,7 @@ export function dateBlockTimingInTimezoneFunction(input: TimingDateTimezoneUtcNo
const changeTimezoneFunction = changeTimingToTimezoneFunction(input);

const fn = ((durationInput: DateDurationSpan, inputRange: DateBlockTimingRangeInput) => {
const timing = dateBlockTiming(durationInput, inputRange);
const timing = dateBlockTiming(durationInput, inputRange, { timezone: changeTimezoneFunction._timezoneInstance });
return changeTimezoneFunction(timing);
}) as Building<DateBlockTimingInTimezoneFunction>;

Expand Down
11 changes: 4 additions & 7 deletions packages/date/src/lib/date/date.schedule.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DateBlockIndex, dateBlockTimingInTimezone, timingIsInExpectedTimezone } from './date.block';
import { DateBlockIndex, dateBlockTimingInTimezone, timingDateTimezoneUtcNormal, timingIsInExpectedTimezone } from './date.block';
import { DateBlock, dateBlockTiming, systemNormalDateToBaseDate, DateScheduleRange, startOfDayInTimezoneDayStringFactory, startOfDayInTimezoneFromISO8601DayString } from '@dereekb/date';
import {
expandDateScheduleFactory,
Expand Down Expand Up @@ -650,9 +650,6 @@ describe('dateBlockTimingForExpandDateScheduleRangeInput()', () => {

expect(result.start).toBeSameSecondAs(timing.start);
expect(result.end).toBeSameSecondAs(timing.end);

const daysCount = differenceInDays(result.end, result.start);
expect(daysCount).toBe(days);
});
});

Expand All @@ -662,7 +659,7 @@ describe('dateBlockTimingForExpandDateScheduleRangeInput()', () => {
const startsAt = addHours(startOfDay, 12); // Noon on 2022-01-02 in America/New_York
const timing = dateBlockTimingInTimezone({ startsAt, duration: 30 }, 1, timezone);

it('should generate a valid DateBlockTiming() with the new duration.', () => {
it('should generate a valid DateBlockTiming with the new duration and same duration.', () => {
expect(timingIsInExpectedTimezone(timing, { timezone })).toBe(true);

const newDuration = 60;
Expand All @@ -684,7 +681,7 @@ describe('dateBlockTimingForExpandDateScheduleRangeInput()', () => {
expect(result.end).toBeSameSecondAs(addMinutes(timing.end, durationDifference)); // 30 minutes later
});

it('should generate a valid DateBlockTiming() with the same event startsAt time.', () => {
it('should generate a valid DateBlockTiming with the same event startsAt time.', () => {
const newStartsAt = addDays(timing.startsAt, 1); // same event schedule

const result = dateBlockTimingForExpandDateScheduleRangeInput({
Expand All @@ -702,7 +699,7 @@ describe('dateBlockTimingForExpandDateScheduleRangeInput()', () => {
expect(result.end).toBeSameSecondAs(timing.end);
});

it('should generate a valid DateBlockTiming() with the new duration and start at time.', () => {
it('should generate a valid DateBlockTiming with the new duration and start at time.', () => {
const newDuration = 45;
const hoursDifference = 1;
const expectedStartsAt = addHours(timing.startsAt, hoursDifference);
Expand Down
23 changes: 23 additions & 0 deletions packages/date/src/lib/date/date.timezone.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,5 +225,28 @@ describe('startOfDayInTimezoneDayStringFactory()', () => {
expect(result.toISOString()).not.toBe(utcDateString);
});
});

describe('America/New_York', () => {
const timezone = 'America/New_York';
const fn = startOfDayInTimezoneDayStringFactory(timezone);
const instance = new DateTimezoneUtcNormalInstance({ timezone });

const expectedDay: ISO8601DayString = `2023-03-12`;
const utcDateString = `${expectedDay}T00:00:00.000Z`;
const utcStart = new Date(utcDateString);

const systemStart = instance.systemDateToBaseDate(utcStart);
const expectedStart = instance.targetDateToBaseDate(utcStart);

it('should return the start of the day date in America/New_York.', () => {
const inputDayString = formatToISO8601DayString(systemStart); // format to ensure that the same day is being passed
expect(expectedDay).toBe(inputDayString);
expect(systemStart).toBeSameSecondAs(startOfDay(systemStart));

const result = fn(inputDayString);
expect(result).toBeSameSecondAs(expectedStart);
expect(result.toISOString()).not.toBe(utcDateString);
});
});
});
});

0 comments on commit 6d1bd8a

Please sign in to comment.