Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/habits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
dayStr,
currentStreak,
bestStreak,
completionsInRange,
last7Count,
} from "./habits";

describe("createHabit", () => {
Expand Down Expand Up @@ -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");
Expand Down
20 changes: 20 additions & 0 deletions src/habits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading