feat(season): easter-offset + nth-weekday datumregler för marknads-högtider (MIKRO A)
CI / Typecheck, test & build (push) Failing after 42s
CI / Typecheck, test & build (push) Failing after 42s
This commit is contained in:
@@ -4,7 +4,9 @@ import { createdAt, updatedAt } from "./_shared.js";
|
|||||||
type DateRule =
|
type DateRule =
|
||||||
| { kind: "fixed"; monthDay: string }
|
| { kind: "fixed"; monthDay: string }
|
||||||
| { kind: "range"; startMonthDay: string; endMonthDay: string }
|
| { kind: "range"; startMonthDay: string; endMonthDay: string }
|
||||||
| { kind: "computed"; algorithm: "midsummer" | "easter" | "advent" | "custom" };
|
| { kind: "computed"; algorithm: "midsummer" | "easter" | "advent" | "custom" }
|
||||||
|
| { kind: "easter-offset"; days: number }
|
||||||
|
| { kind: "nth-weekday"; month: number; weekday: number; n: number | "last" };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Datadriven Season & Events Engine (spec §28): midsommar, jul, påsk, kräftskiva,
|
* Datadriven Season & Events Engine (spec §28): midsommar, jul, påsk, kräftskiva,
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ export interface SeasonEventRuleInput {
|
|||||||
dateRule:
|
dateRule:
|
||||||
| { kind: "fixed"; monthDay: string }
|
| { kind: "fixed"; monthDay: string }
|
||||||
| { kind: "range"; startMonthDay: string; endMonthDay: string }
|
| { kind: "range"; startMonthDay: string; endMonthDay: string }
|
||||||
| { kind: "computed"; algorithm: "midsummer" | "easter" | "advent" | "custom" };
|
| { kind: "computed"; algorithm: "midsummer" | "easter" | "advent" | "custom" }
|
||||||
|
| { kind: "easter-offset"; days: number }
|
||||||
|
| { kind: "nth-weekday"; month: number; weekday: number; n: number | "last" };
|
||||||
leadDays: number;
|
leadDays: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +85,19 @@ export function isEventActive(rule: SeasonEventRuleInput, today: Date): boolean
|
|||||||
if (end < start) end = Date.UTC(year + 1, (em ?? 1) - 1, ed ?? 1);
|
if (end < start) end = Date.UTC(year + 1, (em ?? 1) - 1, ed ?? 1);
|
||||||
return t >= start && t <= end;
|
return t >= start && t <= end;
|
||||||
}
|
}
|
||||||
|
case "easter-offset": {
|
||||||
|
const target = new Date(easterSunday(year).getTime() + rule.dateRule.days * DAY);
|
||||||
|
return activeAround(target, 1);
|
||||||
|
}
|
||||||
|
case "nth-weekday": {
|
||||||
|
const target = nthWeekdayOfMonth(
|
||||||
|
year,
|
||||||
|
rule.dateRule.month,
|
||||||
|
rule.dateRule.weekday,
|
||||||
|
rule.dateRule.n,
|
||||||
|
);
|
||||||
|
return activeAround(target, 1);
|
||||||
|
}
|
||||||
case "computed": {
|
case "computed": {
|
||||||
switch (rule.dateRule.algorithm) {
|
switch (rule.dateRule.algorithm) {
|
||||||
case "midsummer":
|
case "midsummer":
|
||||||
@@ -102,3 +117,20 @@ export function isEventActive(rule: SeasonEventRuleInput, today: Date): boolean
|
|||||||
function strip(d: Date): Date {
|
function strip(d: Date): Date {
|
||||||
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** N:te (eller sista) veckodagen i en månad. month 1–12, weekday 0=sön..6=lör. */
|
||||||
|
function nthWeekdayOfMonth(
|
||||||
|
year: number,
|
||||||
|
month: number,
|
||||||
|
weekday: number,
|
||||||
|
n: number | "last",
|
||||||
|
): Date {
|
||||||
|
if (n === "last") {
|
||||||
|
const last = new Date(Date.UTC(year, month, 0)); // sista dagen i månaden
|
||||||
|
const back = (last.getUTCDay() - weekday + 7) % 7;
|
||||||
|
return new Date(Date.UTC(year, month - 1, last.getUTCDate() - back));
|
||||||
|
}
|
||||||
|
const first = new Date(Date.UTC(year, month - 1, 1));
|
||||||
|
const fwd = (weekday - first.getUTCDay() + 7) % 7;
|
||||||
|
return new Date(Date.UTC(year, month - 1, 1 + fwd + (n - 1) * 7));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user