Tier 3 โ€” Upper-Intermediate

Habit Tracker

Track daily habits, mark them complete, and calculate streaks. Learn working with dates and computing runs of consecutive days.

๐Ÿ Python ๐ŸŸจ JavaScript

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

habits.mjs
// 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

habits.test.mjs
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

  1. Track multiple habits at once โ€” a Map of habit name to its Set of days
  2. Add a longest-ever streak alongside the current streak
  3. Persist completed days to a JSON file so streaks survive restarts
  4. 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

Try It Yourself