feat(macroplan): vendor plan model and grid from the macroplan app
Copied verbatim (imports resorted) from jclab/macroplan so future upstream syncs stay copy-paste; extraction to a shared package is deferred until drift hurts.
This commit is contained in:
59
src/modules/macroplan/model/week.ts
Normal file
59
src/modules/macroplan/model/week.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// Week math. A "Week" is identified by the ISO date (yyyy-mm-dd) of its Monday.
|
||||
// ISO yyyy-mm-dd strings sort lexicographically == chronologically, so plain
|
||||
// string comparison (`<`, `<=`) is valid week ordering — we lean on that.
|
||||
|
||||
export type WeekId = string // 'yyyy-mm-dd', always a Monday
|
||||
|
||||
/**
|
||||
* Extract a yyyy-mm-dd string from a TOML date (smol-toml's `TomlDate.toISOString()`
|
||||
* returns the authored local date, e.g. "2026-06-01") or a plain string.
|
||||
*/
|
||||
export function toYmd(value: unknown): string {
|
||||
if (value instanceof Date) return value.toISOString().slice(0, 10)
|
||||
if (typeof value === "string") return value.slice(0, 10)
|
||||
throw new Error(`expected a date, got ${JSON.stringify(value)}`)
|
||||
}
|
||||
|
||||
// Anchor at UTC noon so day-of-week / day arithmetic never crosses a DST or
|
||||
// timezone boundary.
|
||||
function utcNoon(ymd: string): Date {
|
||||
const [y, m, d] = ymd.split("-").map(Number)
|
||||
return new Date(Date.UTC(y, m - 1, d, 12))
|
||||
}
|
||||
|
||||
function fmt(dt: Date): string {
|
||||
return dt.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
/** The Monday (yyyy-mm-dd) of the ISO week containing the given date. */
|
||||
export function mondayOf(value: unknown): WeekId {
|
||||
const dt = utcNoon(toYmd(value))
|
||||
const dow = dt.getUTCDay() // 0=Sun .. 6=Sat
|
||||
const shift = dow === 0 ? -6 : 1 - dow
|
||||
dt.setUTCDate(dt.getUTCDate() + shift)
|
||||
return fmt(dt)
|
||||
}
|
||||
|
||||
/** Add n weeks to a Monday WeekId (n may be negative). */
|
||||
export function addWeeks(week: WeekId, n: number): WeekId {
|
||||
const dt = utcNoon(week)
|
||||
dt.setUTCDate(dt.getUTCDate() + n * 7)
|
||||
return fmt(dt)
|
||||
}
|
||||
|
||||
/** Inclusive list of Monday WeekIds from start..end (both must already be Mondays). */
|
||||
export function weekRange(start: WeekId, end: WeekId): WeekId[] {
|
||||
if (start > end) return [start]
|
||||
const weeks: WeekId[] = []
|
||||
for (let w = start; w <= end; w = addWeeks(w, 1)) weeks.push(w)
|
||||
return weeks
|
||||
}
|
||||
|
||||
/** Short column label for a week, e.g. "Jun 15". */
|
||||
export function weekLabel(week: WeekId): string {
|
||||
return utcNoon(week).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
timeZone: "UTC",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user