Skip to content
Open
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
78 changes: 78 additions & 0 deletions api/internal/domain/user_stats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package domain

import "time"

// UserStats is the aggregated GitHub statistics for a user, populated from a
// single GraphQL query. Replaces the legacy GithubUserData + GithubUserStats
// pair as part of GIT-33. The legacy types remain in this package while the
// migration is in progress and will be removed once the new banner template
// fully consumes UserStats.
type UserStats struct {
Username string
FetchedAt time.Time

TotalRepos int
TotalStars int
Languages map[string]int

OpenPRs int
ClosedPRs int
MergedPRs int

OpenIssues int
ClosedIssues int

TotalCommits int
TotalPRsCreated int
TotalIssuesCreated int
TotalPRReviews int

ContributionCalendar ContributionCalendar
CurrentStreak int
}

type ContributionCalendar struct {
TotalContributions int
Weeks []ContributionWeek
}

type ContributionWeek struct {
FirstDay time.Time
Days []ContributionDay
}

type ContributionDay struct {
Date time.Time
Count int
}

// CurrentStreak counts consecutive trailing days with at least one contribution.
// The most recent day is allowed to be zero (treated as "in progress today")
// without breaking the streak; any earlier zero day terminates the count.
func (c ContributionCalendar) CurrentStreak() int {
if len(c.Weeks) == 0 {
return 0
}

days := make([]ContributionDay, 0, len(c.Weeks)*7)
for _, w := range c.Weeks {
days = append(days, w.Days...)
}
if len(days) == 0 {
return 0
}

i := len(days) - 1
if days[i].Count == 0 {
i--
}

streak := 0
for ; i >= 0; i-- {
if days[i].Count == 0 {
break
}
streak++
}
return streak
}
64 changes: 64 additions & 0 deletions api/internal/domain/user_stats_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package domain

import (
"testing"

"github.com/stretchr/testify/require"
)

func calendarFromCounts(counts ...int) ContributionCalendar {
weeks := make([]ContributionWeek, 0)
var current ContributionWeek
for i, c := range counts {
current.Days = append(current.Days, ContributionDay{Count: c})
if len(current.Days) == 7 || i == len(counts)-1 {
weeks = append(weeks, current)
current = ContributionWeek{}
}
}
return ContributionCalendar{Weeks: weeks}
}

func TestCurrentStreakEmpty(t *testing.T) {
require.Equal(t, 0, ContributionCalendar{}.CurrentStreak())
require.Equal(t, 0, calendarFromCounts().CurrentStreak())
}

func TestCurrentStreakAllZero(t *testing.T) {
cal := calendarFromCounts(0, 0, 0, 0, 0, 0, 0)
require.Equal(t, 0, cal.CurrentStreak())
}

func TestCurrentStreakAllActive(t *testing.T) {
cal := calendarFromCounts(1, 2, 3, 4, 5, 6, 7)
require.Equal(t, 7, cal.CurrentStreak())
}

func TestCurrentStreakInterruptedByZero(t *testing.T) {
cal := calendarFromCounts(5, 5, 5, 0, 2, 3, 4)
require.Equal(t, 3, cal.CurrentStreak())
}

func TestCurrentStreakTodayZeroIgnored(t *testing.T) {
cal := calendarFromCounts(1, 2, 3, 0)
require.Equal(t, 3, cal.CurrentStreak())
}

func TestCurrentStreakTodayZeroAndYesterdayZero(t *testing.T) {
cal := calendarFromCounts(5, 5, 5, 0, 0)
require.Equal(t, 0, cal.CurrentStreak())
}

func TestCurrentStreakSpansMultipleWeeks(t *testing.T) {
cal := calendarFromCounts(
0, 0, 0, 0, 0, 0, 0,
2, 2, 2, 2, 2, 2, 2,
3, 3, 3, 3, 3, 3, 3,
)
require.Equal(t, 14, cal.CurrentStreak())
}

func TestCurrentStreakSingleDayActive(t *testing.T) {
cal := calendarFromCounts(7)
require.Equal(t, 1, cal.CurrentStreak())
}
Loading