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
41 changes: 39 additions & 2 deletions src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,16 +326,53 @@ export class StorageManager {
timestamp: new Date().toISOString()
});

// Update habit streak
// Update habit streak based on fully-completed consecutive days
const habit = data.habits.find(h => h.id === habitId);
if (habit) {
habit.lastCompletedDate = dateStr;
habit.streak = (habit.streak || 0) + 1;
habit.streak = this.calculateHabitStreak(habitId, habit.targetGoal, data.dailyHabitLogs);
}

this.saveData(data);
}

calculateHabitStreak(habitId: string, targetGoal: number, logs: HabitLog[]): number {
// Count completions per date for this habit
const habitLogs = logs.filter(l => l.habitId === habitId);
const countsByDate: Record<string, number> = {};
for (const log of habitLogs) {
countsByDate[log.date] = (countsByDate[log.date] || 0) + 1;
}

// Get dates where fully completed (>= targetGoal), sorted most recent first
const completedDates = Object.keys(countsByDate)
.filter(date => countsByDate[date] >= targetGoal)
.sort()
.reverse();

if (completedDates.length === 0) return 0;

// Count consecutive days going backward from the most recent fully-completed day
let streak = 1;
let currentDate = completedDates[0];

for (let i = 1; i < completedDates.length; i++) {
const [year, month, day] = currentDate.split('-').map(Number);
const prevDay = new Date(year, month - 1, day);
prevDay.setDate(prevDay.getDate() - 1);
const expectedDate = this.formatDate(prevDay);

if (completedDates[i] === expectedDate) {
streak++;
currentDate = completedDates[i];
} else {
break;
}
}

return streak;
}

isHabitCompletedToday(habitId: string): boolean {
const data = this.getData();
const todayStr = this.formatDate(new Date());
Expand Down
68 changes: 67 additions & 1 deletion tests/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,66 @@ describe('StorageManager', () => {
expect(storage.getHabits().length).toBe(0);
});

it('should log habit completion and increment streak', () => {
it('should log habit completion and set streak to 1 when targetGoal met', () => {
const habit = storage.addHabit({ name: 'Exercise' });
storage.logHabitCompletion(habit.id, new Date());
const updated = storage.getHabits().find(h => h.id === habit.id);
expect(updated?.streak).toBe(1);
});

it('should not increment streak when targetGoal is not yet met', () => {
const habit = storage.addHabit({ name: 'Drink Water', targetGoal: 10 });
storage.logHabitCompletion(habit.id, new Date());
storage.logHabitCompletion(habit.id, new Date());
const updated = storage.getHabits().find(h => h.id === habit.id);
expect(updated?.streak).toBe(0);
});

it('should set streak to 1 when targetGoal is fully met', () => {
const habit = storage.addHabit({ name: 'Drink Water', targetGoal: 3 });
storage.logHabitCompletion(habit.id, new Date());
storage.logHabitCompletion(habit.id, new Date());
storage.logHabitCompletion(habit.id, new Date());
const updated = storage.getHabits().find(h => h.id === habit.id);
expect(updated?.streak).toBe(1);
});

it('should count consecutive fully-completed days as streak', () => {
const habit = storage.addHabit({ name: 'Exercise' });
const day1 = new Date(2025, 0, 13);
const day2 = new Date(2025, 0, 14);
const day3 = new Date(2025, 0, 15);
storage.logHabitCompletion(habit.id, day1);
storage.logHabitCompletion(habit.id, day2);
storage.logHabitCompletion(habit.id, day3);
const updated = storage.getHabits().find(h => h.id === habit.id);
expect(updated?.streak).toBe(3);
});

it('should reset streak count when there is a gap between completed days', () => {
const habit = storage.addHabit({ name: 'Exercise' });
const day1 = new Date(2025, 0, 13);
const day3 = new Date(2025, 0, 15);
storage.logHabitCompletion(habit.id, day1);
storage.logHabitCompletion(habit.id, day3);
const updated = storage.getHabits().find(h => h.id === habit.id);
expect(updated?.streak).toBe(1);
});

it('should extend streak when retroactively completing a missed day', () => {
const habit = storage.addHabit({ name: 'Exercise' });
const day1 = new Date(2025, 0, 13);
const day2 = new Date(2025, 0, 14);
const day3 = new Date(2025, 0, 15);
storage.logHabitCompletion(habit.id, day1);
storage.logHabitCompletion(habit.id, day3);
// Streak is 1 (gap at day2)
storage.logHabitCompletion(habit.id, day2);
// Now day1, day2, day3 are all complete → streak = 3
const updated = storage.getHabits().find(h => h.id === habit.id);
expect(updated?.streak).toBe(3);
});

it('should count habit completions for today', () => {
const habit = storage.addHabit({ name: 'Push-ups', targetGoal: 3 });
storage.logHabitCompletion(habit.id, new Date());
Expand All @@ -193,6 +246,19 @@ describe('StorageManager', () => {
expect(storage.countHabitCompletionsForDate(habit.id, '2025-01-15')).toBe(2);
expect(storage.countHabitCompletionsForDate(habit.id, '2025-01-16')).toBe(0);
});

it('should calculate streak correctly using calculateHabitStreak', () => {
const habit = storage.addHabit({ name: 'Drink Water', targetGoal: 2 });
const day1 = new Date(2025, 0, 13);
const day2 = new Date(2025, 0, 14);
storage.logHabitCompletion(habit.id, day1);
storage.logHabitCompletion(habit.id, day1);
storage.logHabitCompletion(habit.id, day2);
storage.logHabitCompletion(habit.id, day2);
const data = storage.getData();
const streak = storage.calculateHabitStreak(habit.id, habit.targetGoal, data.dailyHabitLogs);
expect(streak).toBe(2);
});
});

// ========================
Expand Down