From 6a7ee29e450b2a23fe62e57297bfb1f75a5d5534 Mon Sep 17 00:00:00 2001 From: "zenith-developer[bot]" Date: Tue, 2 Jun 2026 07:17:17 +0000 Subject: [PATCH] feat: add completionsInRange and last7Count pure helpers Closes #5 --- src/habits.test.ts | 73 ++++++++++++++++++++++++++++++++++++++++++++++ src/habits.ts | 20 +++++++++++++ 2 files changed, 93 insertions(+) diff --git a/src/habits.test.ts b/src/habits.test.ts index 584117d..4ea965f 100644 --- a/src/habits.test.ts +++ b/src/habits.test.ts @@ -6,6 +6,8 @@ import { dayStr, currentStreak, bestStreak, + completionsInRange, + last7Count, } from "./habits"; describe("createHabit", () => { @@ -88,6 +90,77 @@ describe("currentStreak", () => { }); }); +describe("completionsInRange", () => { + it("returns 0 when there are no completions", () => { + const h = createHabit("Test"); + expect(completionsInRange(h, "2026-06-01", "2026-06-07")).toBe(0); + }); + + it("counts completions strictly inside the range", () => { + let h = createHabit("Test"); + h = toggleCompletion(h, "2026-06-03"); + h = toggleCompletion(h, "2026-06-05"); + expect(completionsInRange(h, "2026-06-01", "2026-06-07")).toBe(2); + }); + + it("includes completions on the fromDay boundary", () => { + let h = createHabit("Test"); + h = toggleCompletion(h, "2026-06-01"); + expect(completionsInRange(h, "2026-06-01", "2026-06-07")).toBe(1); + }); + + it("includes completions on the toDay boundary", () => { + let h = createHabit("Test"); + h = toggleCompletion(h, "2026-06-07"); + expect(completionsInRange(h, "2026-06-01", "2026-06-07")).toBe(1); + }); + + it("excludes completions outside the range", () => { + let h = createHabit("Test"); + h = toggleCompletion(h, "2026-05-31"); // before + h = toggleCompletion(h, "2026-06-08"); // after + expect(completionsInRange(h, "2026-06-01", "2026-06-07")).toBe(0); + }); +}); + +describe("last7Count", () => { + const today = "2026-06-10"; + + it("returns 0 when there are no completions", () => { + const h = createHabit("Test"); + expect(last7Count(h, today)).toBe(0); + }); + + it("counts all 7 completions in a full window", () => { + let h = createHabit("Test"); + for (let d = 4; d <= 10; d++) { + h = toggleCompletion(h, `2026-06-${String(d).padStart(2, "0")}`); + } + expect(last7Count(h, today)).toBe(7); + }); + + it("counts partial completions within the 7-day window", () => { + let h = createHabit("Test"); + h = toggleCompletion(h, "2026-06-05"); // inside (day -5) + h = toggleCompletion(h, "2026-06-10"); // inside (today) + h = toggleCompletion(h, "2026-06-03"); // outside (day -7) + expect(last7Count(h, today)).toBe(2); + }); + + it("includes today and the oldest day of the 7-day window (day -6)", () => { + let h = createHabit("Test"); + h = toggleCompletion(h, "2026-06-04"); // exactly 6 days ago — boundary + h = toggleCompletion(h, "2026-06-10"); // today + expect(last7Count(h, today)).toBe(2); + }); + + it("excludes completions older than 7 days", () => { + let h = createHabit("Test"); + h = toggleCompletion(h, "2026-06-03"); // 7 days ago, outside window + expect(last7Count(h, today)).toBe(0); + }); +}); + describe("bestStreak", () => { it("returns 0 for empty habit", () => { const h = createHabit("Test"); diff --git a/src/habits.ts b/src/habits.ts index 95eef25..7065e8b 100644 --- a/src/habits.ts +++ b/src/habits.ts @@ -72,6 +72,26 @@ export function currentStreak(habit: Habit, today: string): number { return count; } +/** + * Count completions whose date falls within [fromDay, toDay] inclusive. + * Both arguments are YYYY-MM-DD strings; ISO lexicographic order is used. + */ +export function completionsInRange( + habit: Habit, + fromDay: string, + toDay: string +): number { + return habit.completions.filter((d) => d >= fromDay && d <= toDay).length; +} + +/** + * Count completions in the 7-day window ending on (and including) `today`. + */ +export function last7Count(habit: Habit, today: string): number { + const sevenDaysAgo = shiftDay(today, -6); + return completionsInRange(habit, sevenDaysAgo, today); +} + /** * Length of the longest run of consecutive completed days ever recorded. * Returns 0 for a habit with no completions.