Skip to content

Commit

Permalink
perf: improve performance when selecting long ranges (#2537)
Browse files Browse the repository at this point in the history
* Add RangeLong example

* Improve performance for long date range

Improve for the use case when excludeDisabled && disabled is false.

* Add more long range examples

* Improve useRange performance for long date range

* Fix RangeLongExcludeDisabled example

* Add rangeMatchModifiers utils, use it in useRange hook

* Minor refactor

* Update performance test to use RangeLongExcludeDisabled

* Fix import

* Attend to PR change requests

* Add rangeContainsDayOfWeek.test.ts

* Rename rangeMatchModifiers to rangeContainsModifiers

* Add rangeContainsModifiers.test.ts

Fix bugs detected by the tests

* Improve rangeContainsModifiers.test.ts

* Refactor rangeContainsModifiers, add areRangesOverlapping utils

* Improve docs

* Fix dateMatchModifiers docs

* Add areRangesOverlapping.test.ts

* Improve long range examples

---------

Co-authored-by: Giampaolo Bellavite <io@gpbl.dev>
  • Loading branch information
rodgobbi and gpbl authored Nov 2, 2024
1 parent 903745c commit b091985
Show file tree
Hide file tree
Showing 13 changed files with 577 additions and 31 deletions.
4 changes: 1 addition & 3 deletions examples/RangeLong.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type DateRange, DayPicker } from "react-day-picker";

export function RangeLong() {
const [range, setRange] = useState<DateRange | undefined>({
from: new Date("0001-01-01"),
from: new Date(100, 0, 1),
to: new Date(2024, 9, 10)
});

Expand All @@ -15,8 +15,6 @@ export function RangeLong() {
defaultMonth={new Date(2024, 9)}
selected={range}
onSelect={setRange}
excludeDisabled
disabled={new Date("2000-01-01")}
/>
);
}
22 changes: 22 additions & 0 deletions examples/RangeLongExcludeDisabled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { useState } from "react";

import { type DateRange, DayPicker } from "react-day-picker";

export function RangeLongExcludeDisabled() {
const [range, setRange] = useState<DateRange | undefined>({
from: new Date(100, 0, 1),
to: new Date(2024, 9, 10)
});

return (
<DayPicker
id="test"
mode="range"
defaultMonth={new Date(2024, 9)}
selected={range}
onSelect={setRange}
excludeDisabled
disabled={new Date(2000, 0, 1)}
/>
);
}
2 changes: 2 additions & 0 deletions examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ export * from "./OutsideDays";
export * from "./PastDatesDisabled";
export * from "./Range";
export * from "./RangeExcludeDisabled";
export * from "./RangeLong";
export * from "./RangeMinMax";
export * from "./RangeRequired";
export * from "./RangeShiftKey";
export * from "./RangeLong";
export * from "./RangeLongExcludeDisabled";
export * from "./Rtl";
export * from "./Single";
export * from "./SingleControlled";
Expand Down
9 changes: 6 additions & 3 deletions performance-tests/flows/range-long.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
* @param {import("puppeteer").Page} page
*/
export async function rangeLong(flow, page) {
await flow.navigate("http://localhost:4173/?example=RangeLong", {
name: "Range selection with long range"
});
await flow.navigate(
"http://localhost:4173/?example=RangeLongExcludeDisabled",
{
name: "Range selection with long range"
}
);
await flow.startTimespan({
name: "Day selection"
});
Expand Down
30 changes: 12 additions & 18 deletions src/selection/useRange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
PropsRange,
Selection
} from "../types/index.js";
import { addToRange, dateMatchModifiers } from "../utils/index.js";
import { addToRange, rangeContainsModifiers } from "../utils/index.js";
import { rangeIncludesDate } from "../utils/rangeIncludesDate.js";

export function useRange<T extends DayPickerProps>(
Expand Down Expand Up @@ -43,23 +43,17 @@ export function useRange<T extends DayPickerProps>(
? addToRange(triggerDate, selected, min, max, required, dateLib)
: undefined;

if (newRange?.from && newRange.to) {
let newDate = newRange.from;
const totalDays = dateLib.differenceInCalendarDays(newRange.to, newDate);

for (let i = 0; i < totalDays; i++) {
newDate = dateLib.addDays(newDate, 1);

if (
excludeDisabled &&
disabled &&
dateMatchModifiers(newDate, disabled, dateLib)
) {
// if a disabled days is found, the range is reset
newRange.from = triggerDate;
newRange.to = undefined;
break;
}
if (excludeDisabled && disabled && newRange?.from && newRange.to) {
if (
rangeContainsModifiers(
{ from: newRange.from, to: newRange.to },
disabled,
dateLib
)
) {
// if a disabled days is found, the range is reset
newRange.from = triggerDate;
newRange.to = undefined;
}
}

Expand Down
60 changes: 60 additions & 0 deletions src/utils/areRangesOverlapping.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { defaultDateLib } from "../classes/DateLib";

import { areRangesOverlapping } from "./areRangesOverlapping";

const sunday = new Date(2024, 8, 1);
const monday = new Date(2024, 8, 2);
const tuesday = new Date(2024, 8, 3);
const thursday = new Date(2024, 8, 5);
const saturday = new Date(2024, 8, 7);
const nextWeekSunday = new Date(2024, 8, 8);

const leftRange = { from: monday, to: saturday };

test('should return true when matching the "from" date', () => {
const rightRange = { from: sunday, to: monday };
const result = areRangesOverlapping(leftRange, rightRange, defaultDateLib);
expect(result).toBe(true);
});

test('should return true when matching the "to" date', () => {
const rightRange = { from: saturday, to: nextWeekSunday };
const result = areRangesOverlapping(leftRange, rightRange, defaultDateLib);
expect(result).toBe(true);
});

test("should return true when left date range contains right date range", () => {
const rightRange = { from: tuesday, to: thursday };
const result = areRangesOverlapping(leftRange, rightRange, defaultDateLib);
expect(result).toBe(true);
});

test("should return true when right date range contains left date range", () => {
const rightRange = { from: sunday, to: nextWeekSunday };
const result = areRangesOverlapping(leftRange, rightRange, defaultDateLib);
expect(result).toBe(true);
});

test("should return true when a date range is inverted", () => {
const rightRange = { to: sunday, from: nextWeekSunday };
const result = areRangesOverlapping(leftRange, rightRange, defaultDateLib);
expect(result).toBe(true);
});

test('should return false on the edge of the "from" date', () => {
const rightRange = { from: new Date(2000, 1, 1), to: sunday };
const result = areRangesOverlapping(leftRange, rightRange, defaultDateLib);
expect(result).toBe(false);
});

test('should return false on the edge of the "to" date', () => {
const rightRange = { from: nextWeekSunday, to: new Date(2077, 1, 1) };
const result = areRangesOverlapping(leftRange, rightRange, defaultDateLib);
expect(result).toBe(false);
});

test("should return false when a date range is inverted", () => {
const rightRange = { to: nextWeekSunday, from: new Date(2077, 1, 1) };
const result = areRangesOverlapping(leftRange, rightRange, defaultDateLib);
expect(result).toBe(false);
});
21 changes: 21 additions & 0 deletions src/utils/areRangesOverlapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defaultDateLib } from "../classes/index.js";

import { rangeIncludesDate } from "./rangeIncludesDate.js";

/**
* Determines whether a given range overlaps with another range.
*
* @group Utilities
*/
export function areRangesOverlapping(
rangeLeft: { from: Date; to: Date },
rangeRight: { from: Date; to: Date },
dateLib = defaultDateLib
): boolean {
return (
rangeIncludesDate(rangeLeft, rangeRight.from, false, dateLib) ||
rangeIncludesDate(rangeLeft, rangeRight.to, false, dateLib) ||
rangeIncludesDate(rangeRight, rangeLeft.from, false, dateLib) ||
rangeIncludesDate(rangeRight, rangeLeft.to, false, dateLib)
);
}
14 changes: 7 additions & 7 deletions src/utils/dateMatchModifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ import {
* ```tsx
* const date = new Date(2022, 5, 19);
* const matcher1: DateRange = {
* from: new Date(2021, 12, 21),
* to: new Date(2021, 12, 30)
* }
* from: new Date(2021, 12, 21),
* to: new Date(2021, 12, 30)
* };
* const matcher2: DateRange = {
* from: new Date(2022, 5, 1),
* to: new Date(2022, 5, 23)
* }
* const dateMatchModifiers(date, [matcher1, matcher2]); // true, since day is in the matcher1 range.
* from: new Date(2022, 5, 1),
* to: new Date(2022, 5, 23)
* };
* dateMatchModifiers(date, [matcher1, matcher2]); // true, since day is in the matcher1 range.
* ```
*
* @group Utilities
Expand Down
3 changes: 3 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export * from "./addToRange.js";
export * from "./areRangesOverlapping.js";
export * from "./dateMatchModifiers.js";
export * from "./rangeContainsDayOfWeek.js";
export * from "./rangeIncludesDate.js";
export * from "./rangeContainsModifiers.js";
export * from "./typeguards.js";
49 changes: 49 additions & 0 deletions src/utils/rangeContainsDayOfWeek.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { defaultDateLib } from "../classes/DateLib";
import { DayOfWeek } from "../types";

import { rangeContainsDayOfWeek } from "./rangeContainsDayOfWeek";

const sunday = new Date(2024, 8, 1); // day of the week 0
const monday = new Date(2024, 8, 2); // day of the week 1
const friday = new Date(2024, 8, 6); // day of the week 5
const saturday = new Date(2024, 8, 7); // day of the week 6
const nextWeekSunday = new Date(2024, 8, 8); // day of the week 0

describe("should return false", () => {
const testCases: Array<[{ from: Date; to: Date }, DayOfWeek]> = [
[{ from: monday, to: saturday }, { dayOfWeek: 0 }],
[{ from: monday, to: friday }, { dayOfWeek: [0, 6] }],
[{ from: sunday, to: friday }, { dayOfWeek: 6 }]
];

for (const [range, dayOfWeek] of testCases) {
it(`range from ${range.from} to ${range.to} should not contain ${JSON.stringify(dayOfWeek)}`, () => {
expect(rangeContainsDayOfWeek(range, dayOfWeek, defaultDateLib)).toBe(
false
);
});
}
});

describe("should return true", () => {
const testCases: Array<[{ from: Date; to: Date }, DayOfWeek]> = [
[{ from: sunday, to: saturday }, { dayOfWeek: 0 }],
[{ from: monday, to: friday }, { dayOfWeek: 1 }],
[{ from: monday, to: friday }, { dayOfWeek: 2 }],
[{ from: monday, to: friday }, { dayOfWeek: 3 }],
[{ from: monday, to: friday }, { dayOfWeek: 4 }],
[{ from: monday, to: friday }, { dayOfWeek: 5 }],
[{ from: monday, to: saturday }, { dayOfWeek: 6 }],
[{ from: monday, to: saturday }, { dayOfWeek: [0, 6] }],
[{ from: monday, to: nextWeekSunday }, { dayOfWeek: 0 }],
[{ from: monday, to: nextWeekSunday }, { dayOfWeek: 6 }]
];

for (const [range, dayOfWeek] of testCases) {
it(`range from ${range.from} to ${range.to} should contain ${JSON.stringify(dayOfWeek)}`, () => {
expect(rangeContainsDayOfWeek(range, dayOfWeek, defaultDateLib)).toBe(
true
);
});
}
});
37 changes: 37 additions & 0 deletions src/utils/rangeContainsDayOfWeek.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { defaultDateLib, type DateLib } from "../classes/DateLib.js";
import type { DayOfWeek } from "../types/index.js";

/**
* Returns whether a date range matches against a given {@link DayOfWeek}.
*
* ```tsx
* const range: DateRange = {
* from: new Date(2024, 8, 1), // day of the week 0
* to: new Date(2024, 8, 6) // day of the week 5
* };
* rangeContainsDayOfWeek(date, { dayOfWeek: 0 }); // true
* ```
*
* @group Utilities
*/
export function rangeContainsDayOfWeek(
range: { from: Date; to: Date },
matcher: DayOfWeek,
dateLib: DateLib = defaultDateLib
) {
const dayOfWeekArr = !Array.isArray(matcher.dayOfWeek)
? [matcher.dayOfWeek]
: matcher.dayOfWeek;
let date = range.from;
const totalDays = dateLib.differenceInCalendarDays(range.to, range.from);

// iterate at maximum one week or the total days if the range is shorter than one week
const totalDaysLimit = Math.min(totalDays, 6);
for (let i = 0; i <= totalDaysLimit; i++) {
if (dayOfWeekArr.includes(date.getDay())) {
return true;
}
date = dateLib.addDays(date, 1);
}
return false;
}
Loading

0 comments on commit b091985

Please sign in to comment.