From 8e40c530dc14677a1a5abb12de371edd2e19c64e Mon Sep 17 00:00:00 2001 From: Loukas Andreadelis Date: Tue, 28 Apr 2026 17:34:40 +0300 Subject: [PATCH 1/2] feat: add time-of-day component to urgencyScore formula --- docs/releases/unreleased.md | 7 ++++ docs/views/default-base-templates.md | 2 +- src/templates/defaultBasesFiles.ts | 7 ++-- .../unit/templates/defaultBasesFiles.test.ts | 38 +++++++++++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index aa1214690..002e2ed28 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -23,3 +23,10 @@ Example: ``` --> + +## Changed + +- Updated the `urgencyScore` formula in default base templates to factor in time-of-day, so timed values that are earlier within a day rank above later ones at the same priority and date + - Adds a 0..1 boost computed from the fractional day of `nextDate`, keeping priority weight and the days-until-next term as the dominant signals + - Resolves a tie-break that previously depended on file-iteration order for same-priority same-date timed tasks + - Date-only values fall back to midnight, so they sit at the top of their day bucket diff --git a/docs/views/default-base-templates.md b/docs/views/default-base-templates.md index b4274a5ec..8543ddfb5 100644 --- a/docs/views/default-base-templates.md +++ b/docs/views/default-base-templates.md @@ -95,7 +95,7 @@ These formulas work with either due date or scheduled date, useful for finding t | Formula | Description | Expression | |---------|-------------|------------| | `priorityWeight` | Numeric weight for priority sorting (lower = higher priority) | `if(priority=="none",0,if(priority=="low",1,if(priority=="normal",2,if(priority=="high",3,999))))` | -| `urgencyScore` | Combines priority and next date proximity (due or scheduled, higher = more urgent) | `if(!due && !scheduled, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext))` | +| `urgencyScore` | Combines priority, next date proximity, and time-of-day (due or scheduled, higher = more urgent) | `if(!due && !scheduled, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext) + (1 - ((number(date(formula.nextDate)) / 86400000) - (number(date(formula.nextDate)) / 86400000).floor())))` | ### Display formulas diff --git a/src/templates/defaultBasesFiles.ts b/src/templates/defaultBasesFiles.ts index 99c4c6106..f87a7a328 100644 --- a/src/templates/defaultBasesFiles.ts +++ b/src/templates/defaultBasesFiles.ts @@ -381,9 +381,10 @@ function generateAllFormulas(plugin: TaskNotesPlugin): Record { // === SORTING/SCORING FORMULAS === - // Urgency score: combines priority weight and days until next date (due or scheduled) - // Higher score = more urgent. Overdue tasks get bonus, no date gets just priority - urgencyScore: `if(!${dueProperty} && !${scheduledProperty}, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext))`, + // Urgency score: combines priority weight, days until next date (due or scheduled), and time-of-day. + // Higher = more urgent. The 0..1 time-of-day term ranks earlier-in-day tasks above later same-day + // tasks at the same priority. Date-only values fall back to midnight. + urgencyScore: `if(!${dueProperty} && !${scheduledProperty}, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext) + (1 - ((number(date(formula.nextDate)) / 86400000) - (number(date(formula.nextDate)) / 86400000).floor())))`, // === DISPLAY FORMULAS === diff --git a/tests/unit/templates/defaultBasesFiles.test.ts b/tests/unit/templates/defaultBasesFiles.test.ts index 374426376..4f6de5139 100644 --- a/tests/unit/templates/defaultBasesFiles.test.ts +++ b/tests/unit/templates/defaultBasesFiles.test.ts @@ -68,4 +68,42 @@ describe("defaultBasesFiles", () => { expect((template.match(/column: tasknotes_manual_order/g) ?? []).length).toBe(3); expect(template).toContain('name: "Projects"'); }); + + it("includes a time-of-day component in urgencyScore so earlier values rank higher", () => { + // Without the time-of-day term, two tasks at the same priority and same date but + // different times scored identically and tie-broke on file-iteration order. The + // 0..1 boost (1 - hourFraction(nextDate)) ranks earlier values above later ones + // while staying smaller than the priority and days components so cross-day order + // is preserved. + const template = generateBasesFileTemplate("open-tasks-view", createMockPlugin() as any); + + expect(template).toContain( + `urgencyScore: 'if(!due && !scheduled, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext) + (1 - ((number(date(formula.nextDate)) / 86400000) - (number(date(formula.nextDate)) / 86400000).floor())))'` + ); + + // Guard against the time-naive form returning + expect(template).not.toMatch( + /urgencyScore: 'if\(!due && !scheduled, formula\.priorityWeight, formula\.priorityWeight \+ max\(0, 10 - formula\.daysUntilNext\)\)'/ + ); + }); + + it("time-of-day boost is monotonic and bounded in [0, 1]", () => { + // Verifies the math invariant the formula relies on, independent of YAML shape. + // boost = 1 - fractional_day(ms_since_epoch). A given Date earlier in its day + // must yield a strictly larger boost than the same date later in the day. + const boost = (iso: string) => { + const ms = Date.parse(iso); + const dayMs = ms / 86_400_000; + return 1 - (dayMs - Math.floor(dayMs)); + }; + + expect(boost("2026-04-28T00:00:00Z")).toBe(1); + expect(boost("2026-04-28T09:00:00Z")).toBeCloseTo(0.625, 3); + expect(boost("2026-04-28T17:00:00Z")).toBeCloseTo(0.292, 3); + expect(boost("2026-04-28T23:59:59Z")).toBeGreaterThan(0); + expect(boost("2026-04-28T23:59:59Z")).toBeLessThan(1 / 86_400); + + // Monotonic: earlier in day → larger boost. + expect(boost("2026-04-28T09:00:00Z")).toBeGreaterThan(boost("2026-04-28T17:00:00Z")); + }); }); From ea51d4267b8bb19861792826d1904acef001c4f3 Mon Sep 17 00:00:00 2001 From: Loukas Andreadelis Date: Tue, 28 Apr 2026 17:49:17 +0300 Subject: [PATCH 2/2] fix: address Copilot review on urgencyScore time-of-day formula --- docs/views/default-base-templates.md | 2 +- src/templates/defaultBasesFiles.ts | 2 +- tests/unit/templates/defaultBasesFiles.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/views/default-base-templates.md b/docs/views/default-base-templates.md index 8543ddfb5..ec690629a 100644 --- a/docs/views/default-base-templates.md +++ b/docs/views/default-base-templates.md @@ -95,7 +95,7 @@ These formulas work with either due date or scheduled date, useful for finding t | Formula | Description | Expression | |---------|-------------|------------| | `priorityWeight` | Numeric weight for priority sorting (lower = higher priority) | `if(priority=="none",0,if(priority=="low",1,if(priority=="normal",2,if(priority=="high",3,999))))` | -| `urgencyScore` | Combines priority, next date proximity, and time-of-day (due or scheduled, higher = more urgent) | `if(!due && !scheduled, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext) + (1 - ((number(date(formula.nextDate)) / 86400000) - (number(date(formula.nextDate)) / 86400000).floor())))` | +| `urgencyScore` | Combines priority, next date proximity, and time-of-day (due or scheduled, higher = more urgent) | `if(!due && !scheduled, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext) + (1 - ((number(date(formula.nextDate)) - number(date(formula.nextDate).date())) / 86400000)))` | ### Display formulas diff --git a/src/templates/defaultBasesFiles.ts b/src/templates/defaultBasesFiles.ts index f87a7a328..8bd044a4b 100644 --- a/src/templates/defaultBasesFiles.ts +++ b/src/templates/defaultBasesFiles.ts @@ -384,7 +384,7 @@ function generateAllFormulas(plugin: TaskNotesPlugin): Record { // Urgency score: combines priority weight, days until next date (due or scheduled), and time-of-day. // Higher = more urgent. The 0..1 time-of-day term ranks earlier-in-day tasks above later same-day // tasks at the same priority. Date-only values fall back to midnight. - urgencyScore: `if(!${dueProperty} && !${scheduledProperty}, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext) + (1 - ((number(date(formula.nextDate)) / 86400000) - (number(date(formula.nextDate)) / 86400000).floor())))`, + urgencyScore: `if(!${dueProperty} && !${scheduledProperty}, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext) + (1 - ((number(date(formula.nextDate)) - number(date(formula.nextDate).date())) / 86400000)))`, // === DISPLAY FORMULAS === diff --git a/tests/unit/templates/defaultBasesFiles.test.ts b/tests/unit/templates/defaultBasesFiles.test.ts index 4f6de5139..edb902e8a 100644 --- a/tests/unit/templates/defaultBasesFiles.test.ts +++ b/tests/unit/templates/defaultBasesFiles.test.ts @@ -78,7 +78,7 @@ describe("defaultBasesFiles", () => { const template = generateBasesFileTemplate("open-tasks-view", createMockPlugin() as any); expect(template).toContain( - `urgencyScore: 'if(!due && !scheduled, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext) + (1 - ((number(date(formula.nextDate)) / 86400000) - (number(date(formula.nextDate)) / 86400000).floor())))'` + `urgencyScore: 'if(!due && !scheduled, formula.priorityWeight, formula.priorityWeight + max(0, 10 - formula.daysUntilNext) + (1 - ((number(date(formula.nextDate)) - number(date(formula.nextDate).date())) / 86400000)))'` ); // Guard against the time-naive form returning @@ -101,7 +101,7 @@ describe("defaultBasesFiles", () => { expect(boost("2026-04-28T09:00:00Z")).toBeCloseTo(0.625, 3); expect(boost("2026-04-28T17:00:00Z")).toBeCloseTo(0.292, 3); expect(boost("2026-04-28T23:59:59Z")).toBeGreaterThan(0); - expect(boost("2026-04-28T23:59:59Z")).toBeLessThan(1 / 86_400); + expect(boost("2026-04-28T23:59:59Z")).toBeLessThanOrEqual(1 / 86_400); // Monotonic: earlier in day → larger boost. expect(boost("2026-04-28T09:00:00Z")).toBeGreaterThan(boost("2026-04-28T17:00:00Z"));