Add calendarRange helpers for flights-map date picker window and snapping

This commit is contained in:
2026-04-17 12:16:22 +03:00
parent 5a18e86bec
commit ef04c19e13
2 changed files with 210 additions and 0 deletions
@@ -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();
});
});
+96
View File
@@ -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}`;
}