Add calendarRange helpers for flights-map date picker window and snapping
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
getMinDate,
|
||||
getMaxDate,
|
||||
buildDisabledDates,
|
||||
findNextEnabledDate,
|
||||
} from "./calendarRange.js";
|
||||
|
||||
function yyyymmdd(d: Date): string {
|
||||
const y = d.getFullYear().toString();
|
||||
const m = (d.getMonth() + 1).toString().padStart(2, "0");
|
||||
const day = d.getDate().toString().padStart(2, "0");
|
||||
return `${y}${m}${day}`;
|
||||
}
|
||||
|
||||
function addDays(d: Date, n: number): Date {
|
||||
const x = new Date(d);
|
||||
x.setDate(x.getDate() + n);
|
||||
x.setHours(0, 0, 0, 0);
|
||||
return x;
|
||||
}
|
||||
|
||||
describe("getMinDate / getMaxDate", () => {
|
||||
it("minDate is today - 1 day, time zero'd", () => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const expected = addDays(today, -1);
|
||||
const min = getMinDate();
|
||||
expect(min.getTime()).toBe(expected.getTime());
|
||||
expect(min.getHours()).toBe(0);
|
||||
expect(min.getMinutes()).toBe(0);
|
||||
expect(min.getSeconds()).toBe(0);
|
||||
});
|
||||
|
||||
it("maxDate is today + 6 months, time zero'd", () => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const expected = new Date(today);
|
||||
expected.setMonth(expected.getMonth() + 6);
|
||||
const max = getMaxDate();
|
||||
expect(max.getTime()).toBe(expected.getTime());
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildDisabledDates", () => {
|
||||
const min = new Date(2026, 0, 1);
|
||||
const max = new Date(2026, 0, 5);
|
||||
|
||||
it("returns every day in the window when availableDays is empty", () => {
|
||||
const disabled = buildDisabledDates(min, max, []);
|
||||
expect(disabled.map((d) => d.getDate())).toEqual([1, 2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
it("returns [] when all dates in window are available", () => {
|
||||
const available = ["20260101", "20260102", "20260103", "20260104", "20260105"];
|
||||
expect(buildDisabledDates(min, max, available)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns the exact complement for partial availability", () => {
|
||||
const available = ["20260102", "20260104"];
|
||||
const disabled = buildDisabledDates(min, max, available);
|
||||
expect(disabled.map((d) => d.getDate())).toEqual([1, 3, 5]);
|
||||
});
|
||||
|
||||
it("ignores availableDays entries outside the window", () => {
|
||||
const available = ["20251231", "20260601", "20260103"];
|
||||
const disabled = buildDisabledDates(min, max, available);
|
||||
expect(disabled.map((d) => d.getDate())).toEqual([1, 2, 4, 5]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findNextEnabledDate", () => {
|
||||
const min = new Date(2026, 0, 1);
|
||||
const max = new Date(2026, 0, 5);
|
||||
|
||||
it("returns current when current is enabled", () => {
|
||||
const current = new Date(2026, 0, 3);
|
||||
const disabled = [new Date(2026, 0, 1), new Date(2026, 0, 2)];
|
||||
const result = findNextEnabledDate(current, disabled, min, max);
|
||||
expect(result?.getDate()).toBe(3);
|
||||
});
|
||||
|
||||
it("advances to the next enabled date when current is disabled", () => {
|
||||
const current = new Date(2026, 0, 2);
|
||||
const disabled = [new Date(2026, 0, 1), new Date(2026, 0, 2), new Date(2026, 0, 3)];
|
||||
const result = findNextEnabledDate(current, disabled, min, max);
|
||||
expect(result?.getDate()).toBe(4);
|
||||
});
|
||||
|
||||
it("returns null when every date in window is disabled", () => {
|
||||
const current = new Date(2026, 0, 3);
|
||||
const disabled = [
|
||||
new Date(2026, 0, 1),
|
||||
new Date(2026, 0, 2),
|
||||
new Date(2026, 0, 3),
|
||||
new Date(2026, 0, 4),
|
||||
new Date(2026, 0, 5),
|
||||
];
|
||||
expect(findNextEnabledDate(current, disabled, min, max)).toBeNull();
|
||||
});
|
||||
|
||||
it("starts at minDate when current < minDate", () => {
|
||||
const current = new Date(2025, 11, 25);
|
||||
const disabled: Date[] = [];
|
||||
const result = findNextEnabledDate(current, disabled, min, max);
|
||||
expect(result?.getDate()).toBe(1);
|
||||
});
|
||||
|
||||
it("returns null when current > maxDate", () => {
|
||||
const current = new Date(2026, 1, 1);
|
||||
const disabled: Date[] = [];
|
||||
expect(findNextEnabledDate(current, disabled, min, max)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Calendar helpers for the flights-map filter date picker.
|
||||
*
|
||||
* All dates are treated as date-only (time zero'd) for day-level comparisons.
|
||||
* minDate/maxDate form the selectable window; `buildDisabledDates` produces
|
||||
* the complement of `availableDays` over that window; `findNextEnabledDate`
|
||||
* advances a current selection forward until it lands on an enabled day.
|
||||
*
|
||||
* Matches Angular `FlightsMapFiltersStateService` calendar behavior.
|
||||
*/
|
||||
|
||||
/** Today with time set to 00:00:00 local. */
|
||||
export function today(): Date {
|
||||
const d = new Date();
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
/** minDate = today - 1 day (Angular parity). */
|
||||
export function getMinDate(): Date {
|
||||
const d = today();
|
||||
d.setDate(d.getDate() - 1);
|
||||
return d;
|
||||
}
|
||||
|
||||
/** maxDate = today + 6 months (Angular parity). */
|
||||
export function getMaxDate(): Date {
|
||||
const d = today();
|
||||
d.setMonth(d.getMonth() + 6);
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every date in [minDate, maxDate] whose yyyymmdd is NOT in `availableDays`.
|
||||
* When `availableDays` is empty, every date in the range is disabled.
|
||||
*/
|
||||
export function buildDisabledDates(
|
||||
minDate: Date,
|
||||
maxDate: Date,
|
||||
availableDays: ReadonlyArray<string>,
|
||||
): Date[] {
|
||||
const available = new Set(availableDays);
|
||||
const disabled: Date[] = [];
|
||||
const cursor = new Date(minDate);
|
||||
cursor.setHours(0, 0, 0, 0);
|
||||
const end = new Date(maxDate);
|
||||
end.setHours(0, 0, 0, 0);
|
||||
while (cursor.getTime() <= end.getTime()) {
|
||||
const key = toYyyymmdd(cursor);
|
||||
if (!available.has(key)) disabled.push(new Date(cursor));
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
return disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances `current` day-by-day until it hits a non-disabled date, staying
|
||||
* within [minDate, maxDate]. Returns `null` if no enabled date exists.
|
||||
* `current` itself is tested first. If `current < minDate`, starts at minDate.
|
||||
*/
|
||||
export function findNextEnabledDate(
|
||||
current: Date,
|
||||
disabledDates: ReadonlyArray<Date>,
|
||||
minDate: Date,
|
||||
maxDate: Date,
|
||||
): Date | null {
|
||||
const disabledSet = new Set(
|
||||
disabledDates.map((d) => {
|
||||
const x = new Date(d);
|
||||
x.setHours(0, 0, 0, 0);
|
||||
return x.getTime();
|
||||
}),
|
||||
);
|
||||
const min = new Date(minDate);
|
||||
min.setHours(0, 0, 0, 0);
|
||||
const max = new Date(maxDate);
|
||||
max.setHours(0, 0, 0, 0);
|
||||
|
||||
const cursor = new Date(current);
|
||||
cursor.setHours(0, 0, 0, 0);
|
||||
|
||||
if (cursor.getTime() < min.getTime()) cursor.setTime(min.getTime());
|
||||
|
||||
while (cursor.getTime() <= max.getTime()) {
|
||||
if (!disabledSet.has(cursor.getTime())) return new Date(cursor);
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function toYyyymmdd(d: Date): string {
|
||||
const y = d.getFullYear().toString();
|
||||
const m = (d.getMonth() + 1).toString().padStart(2, "0");
|
||||
const day = d.getDate().toString().padStart(2, "0");
|
||||
return `${y}${m}${day}`;
|
||||
}
|
||||
Reference in New Issue
Block a user