Skip to content

Commit 927a21c

Browse files
Merge pull request #52 from nanookclaw/fix/stats-streaks
fix: compute stats streaks from completions
2 parents 078c7c9 + f275ab5 commit 927a21c

2 files changed

Lines changed: 135 additions & 2 deletions

File tree

internal/stats/engine.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,60 @@ func computeTagClusters(tasks []core.Task) []TagCluster {
240240
}
241241

242242
func computeStreaks(tasks []core.Task) StreakData {
243+
completedDays := make(map[time.Time]struct{})
244+
for _, task := range tasks {
245+
if task.CompletedAt == nil {
246+
continue
247+
}
248+
completedAt := task.CompletedAt.Local()
249+
day := time.Date(completedAt.Year(), completedAt.Month(), completedAt.Day(), 0, 0, 0, 0, time.Local)
250+
completedDays[day] = struct{}{}
251+
}
252+
253+
if len(completedDays) == 0 {
254+
return StreakData{Warning: false}
255+
}
256+
257+
days := make([]time.Time, 0, len(completedDays))
258+
for day := range completedDays {
259+
days = append(days, day)
260+
}
261+
sort.Slice(days, func(i, j int) bool { return days[i].Before(days[j]) })
262+
263+
longest := 1
264+
run := 1
265+
for i := 1; i < len(days); i++ {
266+
if days[i].Equal(days[i-1].AddDate(0, 0, 1)) {
267+
run++
268+
} else {
269+
run = 1
270+
}
271+
if run > longest {
272+
longest = run
273+
}
274+
}
275+
276+
now := time.Now().Local()
277+
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
278+
current := streakEndingAt(completedDays, today)
279+
if current == 0 {
280+
current = streakEndingAt(completedDays, today.AddDate(0, 0, -1))
281+
}
282+
243283
return StreakData{
244-
Current: 5,
245-
Longest: 12,
284+
Current: current,
285+
Longest: longest,
246286
Warning: false,
247287
}
248288
}
289+
290+
func streakEndingAt(completedDays map[time.Time]struct{}, end time.Time) int {
291+
streak := 0
292+
for day := end; ; day = day.AddDate(0, 0, -1) {
293+
if _, ok := completedDays[day]; !ok {
294+
break
295+
}
296+
streak++
297+
}
298+
return streak
299+
}

internal/stats/engine_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package stats
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/programmersd21/kairo/internal/core"
8+
)
9+
10+
func completedTaskAt(t time.Time) core.Task {
11+
return core.Task{CompletedAt: &t}
12+
}
13+
14+
func localDay(t time.Time) time.Time {
15+
lt := t.Local()
16+
return time.Date(lt.Year(), lt.Month(), lt.Day(), 12, 0, 0, 0, time.Local)
17+
}
18+
19+
func TestComputeStreaksEmpty(t *testing.T) {
20+
got := computeStreaks(nil)
21+
if got.Current != 0 || got.Longest != 0 || got.Warning {
22+
t.Fatalf("computeStreaks(nil) = %+v, want zero streaks with no warning", got)
23+
}
24+
}
25+
26+
func TestComputeStreaksCountsMultipleTasksOnSameDayOnce(t *testing.T) {
27+
now := time.Now()
28+
today := localDay(now)
29+
yesterday := today.AddDate(0, 0, -1)
30+
31+
got := computeStreaks([]core.Task{
32+
completedTaskAt(today),
33+
completedTaskAt(today.Add(2 * time.Hour)),
34+
completedTaskAt(yesterday),
35+
})
36+
37+
if got.Current != 2 || got.Longest != 2 {
38+
t.Fatalf("computeStreaks duplicate day = %+v, want current=2 longest=2", got)
39+
}
40+
}
41+
42+
func TestComputeStreaksAllowsCurrentStreakThroughYesterday(t *testing.T) {
43+
now := time.Now()
44+
yesterday := localDay(now).AddDate(0, 0, -1)
45+
twoDaysAgo := yesterday.AddDate(0, 0, -1)
46+
47+
got := computeStreaks([]core.Task{
48+
completedTaskAt(twoDaysAgo),
49+
completedTaskAt(yesterday),
50+
})
51+
52+
if got.Current != 2 || got.Longest != 2 {
53+
t.Fatalf("computeStreaks through yesterday = %+v, want current=2 longest=2", got)
54+
}
55+
}
56+
57+
func TestComputeStreaksCurrentIsZeroWhenLatestCompletionIsOlder(t *testing.T) {
58+
old := localDay(time.Now()).AddDate(0, 0, -3)
59+
60+
got := computeStreaks([]core.Task{completedTaskAt(old)})
61+
62+
if got.Current != 0 || got.Longest != 1 {
63+
t.Fatalf("computeStreaks stale completion = %+v, want current=0 longest=1", got)
64+
}
65+
}
66+
67+
func TestComputeStreaksLongestCanExceedCurrent(t *testing.T) {
68+
today := localDay(time.Now())
69+
yesterday := today.AddDate(0, 0, -1)
70+
lastWeek := today.AddDate(0, 0, -7)
71+
72+
got := computeStreaks([]core.Task{
73+
completedTaskAt(lastWeek),
74+
completedTaskAt(lastWeek.AddDate(0, 0, 1)),
75+
completedTaskAt(lastWeek.AddDate(0, 0, 2)),
76+
completedTaskAt(yesterday),
77+
})
78+
79+
if got.Current != 1 || got.Longest != 3 {
80+
t.Fatalf("computeStreaks longest vs current = %+v, want current=1 longest=3", got)
81+
}
82+
}

0 commit comments

Comments
 (0)