1+ import { describe , it , expect , vi , beforeEach , afterEach } from 'vitest' ;
2+
3+ function dateDiffDays ( a : string , b : string ) : number {
4+ return (
5+ ( new Date ( b ) . getTime ( ) - new Date ( a ) . getTime ( ) ) / ( 1000 * 60 * 60 * 24 )
6+ ) ;
7+ }
8+
9+ function toDateStr ( d : Date ) : string {
10+ return d . toISOString ( ) . slice ( 0 , 10 ) ;
11+ }
12+
13+ function calculateStreakFromDates (
14+ activeDates : Set < string > ,
15+ freezeDates : Set < string >
16+ ) : {
17+ current : number ;
18+ longest : number ;
19+ lastCommitDate : string | null ;
20+ totalActiveDays : number ;
21+ freezeDates : string [ ] ;
22+ } {
23+ const combinedDates = new Set < string > ( [
24+ ...Array . from ( activeDates ) ,
25+ ...Array . from ( freezeDates ) ,
26+ ] ) ;
27+ const commitDays = Array . from ( combinedDates ) . sort ( ) ;
28+
29+ if ( commitDays . length === 0 ) {
30+ return {
31+ current : 0 ,
32+ longest : 0 ,
33+ lastCommitDate : null ,
34+ totalActiveDays : 0 ,
35+ freezeDates : Array . from ( freezeDates ) ,
36+ } ;
37+ }
38+
39+ let longestStreak = 1 ;
40+ let currentRun = 1 ;
41+ const runs : { start : string ; end : string ; length : number } [ ] = [ ] ;
42+ let runStart = commitDays [ 0 ] ;
43+
44+ for ( let i = 1 ; i < commitDays . length ; i ++ ) {
45+ const diff = dateDiffDays ( commitDays [ i - 1 ] , commitDays [ i ] ) ;
46+ if ( diff === 1 ) {
47+ currentRun ++ ;
48+ if ( currentRun > longestStreak ) {
49+ longestStreak = currentRun ;
50+ }
51+ } else {
52+ runs . push ( { start : runStart , end : commitDays [ i - 1 ] , length : currentRun } ) ;
53+ runStart = commitDays [ i ] ;
54+ currentRun = 1 ;
55+ }
56+ }
57+ runs . push ( {
58+ start : runStart ,
59+ end : commitDays [ commitDays . length - 1 ] ,
60+ length : currentRun ,
61+ } ) ;
62+
63+ const lastDay = commitDays [ commitDays . length - 1 ] ;
64+ const today = toDateStr ( new Date ( ) ) ;
65+ const yesterday = toDateStr ( new Date ( Date . now ( ) - 86400000 ) ) ;
66+
67+ const lastRun = runs [ runs . length - 1 ] ;
68+ const currentStreak =
69+ lastRun . end === today || lastRun . end === yesterday ? lastRun . length : 0 ;
70+
71+ return {
72+ current : currentStreak ,
73+ longest : longestStreak ,
74+ lastCommitDate : lastDay ,
75+ totalActiveDays : commitDays . length ,
76+ freezeDates : Array . from ( freezeDates ) ,
77+ } ;
78+ }
79+
80+ describe ( 'calculateStreakFromDates' , ( ) => {
81+ const realDate = Date ;
82+
83+ beforeEach ( ( ) => {
84+ vi . useFakeTimers ( ) ;
85+ vi . setSystemTime ( new Date ( '2026-05-23' ) ) ;
86+ } ) ;
87+
88+ afterEach ( ( ) => {
89+ vi . useRealTimers ( ) ;
90+ } ) ;
91+
92+ it ( 'returns 0 for empty array' , ( ) => {
93+ const result = calculateStreakFromDates ( new Set ( ) , new Set ( ) ) ;
94+ expect ( result . current ) . toBe ( 0 ) ;
95+ expect ( result . longest ) . toBe ( 0 ) ;
96+ expect ( result . totalActiveDays ) . toBe ( 0 ) ;
97+ expect ( result . lastCommitDate ) . toBeNull ( ) ;
98+ } ) ;
99+
100+ it ( 'returns 1 for single contribution today' , ( ) => {
101+ const result = calculateStreakFromDates ( new Set ( [ '2026-05-23' ] ) , new Set ( ) ) ;
102+ expect ( result . current ) . toBe ( 1 ) ;
103+ expect ( result . longest ) . toBe ( 1 ) ;
104+ expect ( result . totalActiveDays ) . toBe ( 1 ) ;
105+ expect ( result . lastCommitDate ) . toBe ( '2026-05-23' ) ;
106+ } ) ;
107+
108+ it ( 'returns 1 for single contribution yesterday' , ( ) => {
109+ const result = calculateStreakFromDates ( new Set ( [ '2026-05-22' ] ) , new Set ( ) ) ;
110+ expect ( result . current ) . toBe ( 1 ) ;
111+ expect ( result . longest ) . toBe ( 1 ) ;
112+ } ) ;
113+
114+ it ( 'returns 0 when last contribution older than yesterday' , ( ) => {
115+ const result = calculateStreakFromDates ( new Set ( [ '2026-05-20' ] ) , new Set ( ) ) ;
116+ expect ( result . current ) . toBe ( 0 ) ;
117+ expect ( result . longest ) . toBe ( 1 ) ;
118+ } ) ;
119+
120+ it ( 'calculates longest streak correctly' , ( ) => {
121+ const activeDates = new Set ( [
122+ '2026-05-01' ,
123+ '2026-05-02' ,
124+ '2026-05-03' ,
125+ '2026-05-10' ,
126+ '2026-05-11' ,
127+ '2026-05-12' ,
128+ '2026-05-13' ,
129+ ] ) ;
130+ const result = calculateStreakFromDates ( activeDates , new Set ( ) ) ;
131+ expect ( result . longest ) . toBe ( 4 ) ;
132+ } ) ;
133+
134+ it ( 'respects freeze dates in combined calculation' , ( ) => {
135+ const activeDates = new Set ( [ '2026-05-01' , '2026-05-02' , '2026-05-04' ] ) ;
136+ const freezeDates = new Set ( [ '2026-05-03' ] ) ;
137+ const result = calculateStreakFromDates ( activeDates , freezeDates ) ;
138+ expect ( result . longest ) . toBe ( 4 ) ;
139+ expect ( result . current ) . toBe ( 0 ) ;
140+ expect ( result . freezeDates ) . toContain ( '2026-05-03' ) ;
141+ } ) ;
142+
143+ it ( 'counts total active days correctly' , ( ) => {
144+ const activeDates = new Set ( [
145+ '2026-05-01' ,
146+ '2026-05-02' ,
147+ '2026-05-03' ,
148+ '2026-05-05' ,
149+ ] ) ;
150+ const freezeDates = new Set ( [ '2026-05-04' ] ) ;
151+ const result = calculateStreakFromDates ( activeDates , freezeDates ) ;
152+ expect ( result . totalActiveDays ) . toBe ( 5 ) ;
153+ } ) ;
154+
155+ it ( 'handles empty active dates with freeze dates' , ( ) => {
156+ const freezeDates = new Set ( [ '2026-05-01' , '2026-05-02' ] ) ;
157+ const result = calculateStreakFromDates ( new Set ( ) , freezeDates ) ;
158+ expect ( result . current ) . toBe ( 0 ) ;
159+ expect ( result . longest ) . toBe ( 2 ) ;
160+ expect ( result . totalActiveDays ) . toBe ( 2 ) ;
161+ } ) ;
162+
163+ it ( 'returns streak starting today' , ( ) => {
164+ const activeDates = new Set ( [ '2026-05-21' , '2026-05-22' , '2026-05-23' ] ) ;
165+ const result = calculateStreakFromDates ( activeDates , new Set ( ) ) ;
166+ expect ( result . current ) . toBe ( 3 ) ;
167+ } ) ;
168+
169+ it ( 'handles gaps in dates' , ( ) => {
170+ const activeDates = new Set ( [ '2026-05-01' , '2026-05-05' , '2026-05-06' ] ) ;
171+ const result = calculateStreakFromDates ( activeDates , new Set ( ) ) ;
172+ expect ( result . longest ) . toBe ( 2 ) ;
173+ expect ( result . totalActiveDays ) . toBe ( 3 ) ;
174+ } ) ;
175+ } ) ;
0 commit comments