feat(06-01): implement duration-preserving end-tracking helpers
- computeNewTimedEnd: preserves timed duration; 1h floor for stale state - computeNewAllDayEnd: preserves all-day day-span; same-day floor for stale state - Private helpers: dateDiffDays, addDaysISO, localDateISO, localTimeHHMM (local accessors only — WR-05) - All 6 RED tests now GREEN; pre-existing suite unaffected (166/166 pass)
This commit is contained in:
@@ -59,3 +59,91 @@ export function serializeEventDateTime(
|
|||||||
export function localWallClockToUtcIso(date: string, time: string): string {
|
export function localWallClockToUtcIso(date: string, time: string): string {
|
||||||
return new Date(`${date}T${time}:00`).toISOString()
|
return new Date(`${date}T${time}:00`).toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Private date helpers (local accessor pattern — WR-05 constraint) ────────
|
||||||
|
// NEVER use toISOString().slice(0,10) — that returns the UTC date, not the
|
||||||
|
// local date. Always use getFullYear/getMonth/getDate/getHours/getMinutes.
|
||||||
|
|
||||||
|
function pad2(n: number): string {
|
||||||
|
return n < 10 ? `0${n}` : `${n}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Format a Date as 'YYYY-MM-DD' using LOCAL calendar accessors. */
|
||||||
|
function localDateISO(d: Date): string {
|
||||||
|
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Format a Date as 'HH:MM' using LOCAL clock accessors. */
|
||||||
|
function localTimeHHMM(d: Date): string {
|
||||||
|
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the number of whole calendar days between two 'YYYY-MM-DD' strings.
|
||||||
|
* Positive when end > start, negative when end < start.
|
||||||
|
* Uses midnight-local Date arithmetic to stay in the local calendar.
|
||||||
|
*/
|
||||||
|
function dateDiffDays(startDate: string, endDate: string): number {
|
||||||
|
const startMs = new Date(`${startDate}T00:00:00`).getTime()
|
||||||
|
const endMs = new Date(`${endDate}T00:00:00`).getTime()
|
||||||
|
return Math.round((endMs - startMs) / (24 * 60 * 60 * 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a whole number of days to a 'YYYY-MM-DD' string, returning a new
|
||||||
|
* 'YYYY-MM-DD' string. Uses local midnight to avoid DST-boundary issues.
|
||||||
|
*/
|
||||||
|
function addDaysISO(dateStr: string, days: number): string {
|
||||||
|
const d = new Date(`${dateStr}T00:00:00`)
|
||||||
|
d.setDate(d.getDate() + days)
|
||||||
|
return localDateISO(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── D-04: Duration-preservation helpers ─────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a new timed event end when the start changes, preserving duration.
|
||||||
|
*
|
||||||
|
* D-04 contract:
|
||||||
|
* - newEnd = newStart + (oldEnd − oldStart)
|
||||||
|
* - Floor: if oldEnd ≤ oldStart (stale/invalid), newEnd = newStart + 1h
|
||||||
|
*
|
||||||
|
* Returns { endDate: 'YYYY-MM-DD', endTime: 'HH:MM' } using LOCAL accessors
|
||||||
|
* (WR-05: never UTC-sliced).
|
||||||
|
*/
|
||||||
|
export function computeNewTimedEnd(
|
||||||
|
newStartDate: string,
|
||||||
|
newStartTime: string,
|
||||||
|
oldStartDate: string,
|
||||||
|
oldStartTime: string,
|
||||||
|
oldEndDate: string,
|
||||||
|
oldEndTime: string,
|
||||||
|
): { endDate: string; endTime: string } {
|
||||||
|
const oldStartMs = new Date(`${oldStartDate}T${oldStartTime}:00`).getTime()
|
||||||
|
const oldEndMs = new Date(`${oldEndDate}T${oldEndTime}:00`).getTime()
|
||||||
|
const deltaMs = oldEndMs > oldStartMs ? oldEndMs - oldStartMs : 60 * 60 * 1000 // 1h floor
|
||||||
|
const newEndDate = new Date(new Date(`${newStartDate}T${newStartTime}:00`).getTime() + deltaMs)
|
||||||
|
return {
|
||||||
|
endDate: localDateISO(newEndDate),
|
||||||
|
endTime: localTimeHHMM(newEndDate),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a new all-day event end when the start date changes, preserving
|
||||||
|
* the inclusive day-span.
|
||||||
|
*
|
||||||
|
* D-04 contract:
|
||||||
|
* - span = max(0, oldEnd − oldStart) days (Math.max floors negative spans to 0)
|
||||||
|
* - newEnd = newStart + span
|
||||||
|
*
|
||||||
|
* Returns 'YYYY-MM-DD' (inclusive end date).
|
||||||
|
*/
|
||||||
|
export function computeNewAllDayEnd(
|
||||||
|
newStartDate: string,
|
||||||
|
oldStartDate: string,
|
||||||
|
oldEndDate: string,
|
||||||
|
): string {
|
||||||
|
const span = Math.max(0, dateDiffDays(oldStartDate, oldEndDate))
|
||||||
|
return addDaysISO(newStartDate, span)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user