From a76d8c53410f0d9bb69d1499d40158c209f96d6a Mon Sep 17 00:00:00 2001 From: callumalpass Date: Mon, 19 Oct 2026 09:52:07 +1100 Subject: [PATCH] Accept minute-precision times when adding DTSTART addDTSTARTToRecurrenceRule and updateDTSTARTInRecurrenceRule parsed timed dates with the strict parser, which rejects "2026-06-04T10:30". Task dates are usually stored that way, so completing a materialized occurrence of a timed recurring parent threw (callumalpass/tasknotes#2328). Parse with the relaxed range parser instead. --- src/recurrence.ts | 4 +++- test/recurrence.test.mjs | 45 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/recurrence.ts b/src/recurrence.ts index 8c05c34..5698730 100644 --- a/src/recurrence.ts +++ b/src/recurrence.ts @@ -9,6 +9,7 @@ import { hasTimeComponent, parseDateToLocal, parseDateToUTC, + resolveDateTimeRangeBound, } from "./date"; import type { RecurrenceAnchor, TaskInfo } from "./types"; @@ -452,7 +453,8 @@ function getLookAheadDays(recurrence: string): number { function formatDtstartValue(dateStr: string): string { if (hasTimeComponent(dateStr)) { - const dateTime = parseDateToLocal(dateStr); + // Task dates are often minute precision ("2026-06-04T10:30"), which the strict parser rejects. + const dateTime = resolveDateTimeRangeBound(dateStr, "from"); const year = dateTime.getFullYear(); const month = String(dateTime.getMonth() + 1).padStart(2, "0"); const day = String(dateTime.getDate()).padStart(2, "0"); diff --git a/test/recurrence.test.mjs b/test/recurrence.test.mjs index 81cac53..ddd9324 100644 --- a/test/recurrence.test.mjs +++ b/test/recurrence.test.mjs @@ -41,3 +41,48 @@ test("advances date-only due offset for recurring tasks with timed scheduled dat due: "2026-06-06", }); }); + +test("adds DTSTART for a minute-precision scheduled time (tasknotes#2328)", async () => { + const { addDTSTARTToRecurrenceRule, updateDTSTARTInRecurrenceRule } = await import( + "../dist/esm/recurrence.js" + ); + const withSeconds = addDTSTARTToRecurrenceRule({ + recurrence: "FREQ=DAILY", + scheduled: "2026-06-04T10:30:00", + }); + assert.equal( + addDTSTARTToRecurrenceRule({ recurrence: "FREQ=DAILY", scheduled: "2026-06-04T10:30" }), + withSeconds + ); + assert.equal(updateDTSTARTInRecurrenceRule("FREQ=DAILY", "2026-06-04T10:30"), withSeconds); + assert.match(withSeconds, /^DTSTART:20260604T103000Z;FREQ=DAILY$/); +}); + +test("completing a materialized occurrence of a timed parent advances it (tasknotes#2328)", async () => { + const { buildMaterializedOccurrenceCompletePlan } = await import("../dist/esm/operations.js"); + const plan = buildMaterializedOccurrenceCompletePlan({ + occurrenceTask: { + path: "Tasks/Standup 2026-06-04.md", + title: "Standup", + status: "open", + recurrence_parent: "[[Tasks/Standup]]", + occurrence_date: "2026-06-04", + scheduled: "2026-06-04T10:30", + }, + parentTask: { + path: "Tasks/Standup.md", + title: "Standup", + status: "open", + recurrence: "FREQ=DAILY", + scheduled: "2026-06-04T10:30", + complete_instances: [], + skipped_instances: [], + }, + completedStatus: "done", + currentTimestamp: "2026-06-04T11:00:00.000Z", + completionDate: "2026-06-04", + maintainDueDateOffsetInRecurring: true, + }); + assert.deepEqual(plan.parentUpdates.complete_instances, ["2026-06-04"]); + assert.equal(plan.parentUpdates.scheduled, "2026-06-05T10:30"); +});