1. The Problem
Build a habit tracker that records which days a habit was completed and computes the current streak โ the number of consecutive days ending today on which the habit was done. A missing day breaks the streak.
Why this project? Real date arithmetic (not manual day-counting), Set-based membership for instant lookups, and computing consecutive runs โ a pattern that shows up in analytics, gamification, and reporting.
2. How to Think About This
Store completed days as ISO date strings (like "2026-06-24") in a Set โ that gives instant "was this day done?" checks and ignores duplicate marks. To find the streak, start at today and step backwards one day at a time while each day is in the set, counting as you go. The moment a day is missing, stop. Use real date maths (subtracting one day via the Date object) so month and year boundaries are handled correctly.
3. The Build
// Turn a Date into an ISO day string like "2026-06-24".
export function isoDay(date) {
return date.toISOString().slice(0, 10);
}
// Step back exactly one calendar day (handles month/year rollovers).
export function previousDay(date) {
const d = new Date(date);
d.setDate(d.getDate() - 1);
return d;
}
// Record a habit as done on a day (default today). Returns the Set.
export function markDone(completed, day = new Date()) {
completed.add(isoDay(day));
return completed;
}
// Count consecutive completed days ending at `today`.
export function currentStreak(completed, today = new Date()) {
let streak = 0;
let day = today;
while (completed.has(isoDay(day))) {
streak += 1;
day = previousDay(day);
}
return streak;
}
// Demo
const done = new Set();
markDone(done, new Date("2026-06-22"));
markDone(done, new Date("2026-06-23"));
markDone(done, new Date("2026-06-24"));
console.log("Streak:", currentStreak(done, new Date("2026-06-24")));
4. Test & Prove
import { currentStreak, isoDay, previousDay } from "./habits.mjs";
let pass = 0, fail = 0;
function test(desc, fn) {
try { fn(); console.log(" \u2713", desc); pass++; }
catch (e) { console.log(" \u2717", desc, "\u2014", e.message); fail++; }
}
function assert(cond, msg) { if (!cond) throw new Error(msg || "assertion failed"); }
test("three consecutive days give a streak of 3", () => {
const dates = new Set(["2026-06-22", "2026-06-23", "2026-06-24"]);
assert(currentStreak(dates, new Date("2026-06-24")) === 3);
});
test("a missing day stops the streak", () => {
const dates = new Set(["2026-06-22", "2026-06-24"]); // 23rd missing
assert(currentStreak(dates, new Date("2026-06-24")) === 1);
});
test("no completions means a streak of 0", () => {
assert(currentStreak(new Set(), new Date("2026-06-24")) === 0);
});
test("previousDay crosses a month boundary", () => {
assert(isoDay(previousDay(new Date("2026-07-01"))) === "2026-06-30");
});
console.log(`\n${pass} passed, ${fail} failed`);
5. The Interface
node habits.mjs Streak: 3 # Mark today done and recompute each day to build a real streak.
6. Run It
node habits.mjs
node habits.test.mjs๐ฏ Try this next
- Track multiple habits at once โ a Map of habit name to its Set of days
- Add a longest-ever streak alongside the current streak
- Persist completed days to a JSON file so streaks survive restarts
- Add a weekly completion rate (days done รท 7) as a second metric
What you practised
Real date arithmetic with the Date object, Set for instant membership checks, computing consecutive runs by stepping backwards, and writing deterministic tests by passing a fixed today.
Reference: Objects & Sets ยท Control Flow ยท Functions