@@ -270,6 +270,84 @@ describe('calculateStreak', () => {
270270 expect ( result . longestStreak ) . toBe ( 10 ) ;
271271 expect ( result . currentStreak ) . toBe ( 5 ) ;
272272 } ) ;
273+
274+ it ( 'correctly handles leap years and non-leap years during the Feb 28 to Mar 1 transition' , ( ) => {
275+ // Helper to construct a ContributionCalendar with explicit dates
276+ const buildCustomCalendar = (
277+ daysData : { date : string ; count : number } [ ]
278+ ) : ContributionCalendar => {
279+ const weeks = [ ] ;
280+ for ( let i = 0 ; i < daysData . length ; i += 7 ) {
281+ const slice = daysData . slice ( i , i + 7 ) ;
282+ weeks . push ( {
283+ contributionDays : slice . map ( ( day ) => ( {
284+ contributionCount : day . count ,
285+ date : day . date ,
286+ } ) ) ,
287+ } ) ;
288+ }
289+ return {
290+ totalContributions : daysData . reduce ( ( sum , d ) => sum + d . count , 0 ) ,
291+ weeks,
292+ } ;
293+ } ;
294+
295+ // --- Case 1: Non-Leap Year (2023) ---
296+ // In 2023, Feb has 28 days. Feb 28 is followed directly by Mar 1.
297+ const nonLeapCalendar = buildCustomCalendar ( [
298+ { date : '2023-02-27' , count : 1 } ,
299+ { date : '2023-02-28' , count : 1 } ,
300+ { date : '2023-03-01' , count : 1 } ,
301+ { date : '2023-03-02' , count : 1 } ,
302+ ] ) ;
303+
304+ // Evaluating on March 2, 2023:
305+ // With commits on Feb 27, Feb 28, Mar 1, and Mar 2, the streak should be continuous (4 days).
306+ const resultNonLeap = calculateStreak ( nonLeapCalendar , 'UTC' , new Date ( '2023-03-02T12:00:00Z' ) ) ;
307+ expect ( nonLeapCalendar . totalContributions ) . toBe ( 4 ) ;
308+ expect ( resultNonLeap . currentStreak ) . toBe ( 4 ) ;
309+ expect ( resultNonLeap . longestStreak ) . toBe ( 4 ) ;
310+
311+ // --- Case 2: Leap Year (2024) ---
312+ // In 2024, Feb has 29 days.
313+ // If they commit on Feb 28, Feb 29, and Mar 1: streak should be 3.
314+ const leapCalendarContinuous = buildCustomCalendar ( [
315+ { date : '2024-02-27' , count : 0 } ,
316+ { date : '2024-02-28' , count : 1 } ,
317+ { date : '2024-02-29' , count : 1 } ,
318+ { date : '2024-03-01' , count : 1 } ,
319+ ] ) ;
320+
321+ const resultLeapContinuous = calculateStreak (
322+ leapCalendarContinuous ,
323+ 'UTC' ,
324+ new Date ( '2024-03-01T12:00:00Z' )
325+ ) ;
326+ expect ( resultLeapContinuous . currentStreak ) . toBe ( 3 ) ;
327+ expect ( resultLeapContinuous . longestStreak ) . toBe ( 3 ) ;
328+
329+ // --- Case 3: Leap Year (2024) with a gap on Feb 29 ---
330+ // In 2024, if they commit on Feb 28 and Mar 1 but miss Feb 29:
331+ // Evaluating on Mar 1 (grace period = 1):
332+ // Today (Mar 1) has 1 commit. Yesterday (Feb 29) has 0 commits.
333+ // Since grace is 1, the streak is alive.
334+ // However, since Feb 29 is 0, the backward count stops after today (Mar 1).
335+ // So the current streak should be 1, and the longest streak should be 1.
336+ const leapCalendarWithGap = buildCustomCalendar ( [
337+ { date : '2024-02-27' , count : 0 } ,
338+ { date : '2024-02-28' , count : 1 } ,
339+ { date : '2024-02-29' , count : 0 } , // Gap on leap day!
340+ { date : '2024-03-01' , count : 1 } ,
341+ ] ) ;
342+
343+ const resultLeapGap = calculateStreak (
344+ leapCalendarWithGap ,
345+ 'UTC' ,
346+ new Date ( '2024-03-01T12:00:00Z' )
347+ ) ;
348+ expect ( resultLeapGap . currentStreak ) . toBe ( 1 ) ;
349+ expect ( resultLeapGap . longestStreak ) . toBe ( 1 ) ;
350+ } ) ;
273351} ) ;
274352it ( 'handles massive single-day commit spike timeline' , ( ) => {
275353 const calendar = buildCalendar ( [
0 commit comments