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..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 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)) - number(date(formula.nextDate).date())) / 86400000)))` | ### Display formulas diff --git a/src/templates/defaultBasesFiles.ts b/src/templates/defaultBasesFiles.ts index 99c4c6106..8bd044a4b 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)) - 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 374426376..edb902e8a 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)) - number(date(formula.nextDate).date())) / 86400000)))'` + ); + + // 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")).toBeLessThanOrEqual(1 / 86_400); + + // Monotonic: earlier in day → larger boost. + expect(boost("2026-04-28T09:00:00Z")).toBeGreaterThan(boost("2026-04-28T17:00:00Z")); + }); });