Skip to content

Streak Calculation Logic Duplicated Across 6+ Files #1312

Description

@ionfwsrijan

Problem

The streak calculation logic is duplicated across at least 6 files in the codebase. Three key functions appear multiple times:

  1. dateDiffDays — calculates the difference in days between two dates
  2. toDateStr — converts a date to a YYYY-MM-DD string
  3. Streak calculation logic — takes commit/contribution data and computes current streak, longest streak, etc.

Duplication Map

Version 1: Main streak endpoint

src/app/api/metrics/streak/route.ts

Contains the primary (and most complete) implementation:

  • toDateStr, dateDiffDays ~ lines 17-35
  • Full streak calculation (current streak, longest streak, todayActive)

This is the "authoritative" version.

Version 2: Badge endpoint

src/app/api/badge/streak-shield/route.ts

Contains its own copy of:

  • dateDiffDays (line 11)
  • Streak calculation logic (lines 19-62)
  • Uses different variable naming (commitDates vs dates)

Version 3: Public profile endpoint

src/app/api/public/[username]/route.ts

Contains its own copy of:

  • dateDiffDays (line 10)
  • toDateStr (line 18)
  • Streak calculation logic (lines 83-133)

Version 4: Compare endpoint

src/app/api/metrics/compare/route.ts

Contains its own copy of:

  • toDateStr (line 7)
  • Partial streak-related date handling

Version 5: Repos endpoint

src/app/api/metrics/repos/route.ts

Contains:

  • dateDiffDays helper (line 8)

Version 6: Contributions endpoint

src/app/api/metrics/contributions/route.ts

Contains:

  • Streak-like date grouping logic

Specific Code Examples

In src/app/api/metrics/streak/route.ts:

function toDateStr(date: Date): string {
  return date.toISOString().split("T")[0];
}

function dateDiffDays(date1: Date, date2: Date): number {
  const d1 = new Date(date1.toISOString().split("T")[0]);
  const d2 = new Date(date2.toISOString().split("T")[0]);
  return Math.floor((d2.getTime() - d1.getTime()) / (1000 * 60 * 60 * 24));
}

In src/app/api/badge/streak-shield/route.ts:

function dateDiffDays(date1: Date, date2: Date): number {
  const d1 = new Date(Date.UTC(date1.getFullYear(), date1.getMonth(), date1.getDate()));
  const d2 = new Date(Date.UTC(date2.getFullYear(), date2.getMonth(), date2.getDate()));
  return Math.floor((d2.getTime() - d1.getTime()) / (1000 * 60 * 60 * 24));
}

Notice the slightly different implementation! The streak endpoint uses toISOString().split("T")[0], while the badge endpoint uses Date.UTC(...). These can produce different results for dates near midnight in different time zones.


Impact

  1. Maintenance: Any fix or enhancement to streak logic must be manually applied to 6+ files
  2. Bug risk: The differences in dateDiffDays implementation (ISO string truncation vs Date.UTC) can cause off-by-one errors in streak calculation for users in certain time zones
  3. Code bloat: Approximately 150+ lines of duplicated code across the codebase
  4. Testing gaps: Tests for streak logic exist only for the main streak/route.ts (issue test: add unit tests for streak calculation utility functions #1069), not for the duplicated versions

Required Changes

1. Extract shared functions to a library module

Create src/lib/streak.ts:

export function toDateStr(date: Date): string {
  return date.toISOString().split("T")[0];
}

export function dateDiffDays(date1: Date, date2: Date): number {
  const d1 = new Date(date1.toISOString().split("T")[0]);
  const d2 = new Date(date2.toISOString().split("T")[0]);
  return Math.floor((d2.getTime() - d1.getTime()) / (1000 * 60 * 60 * 24));
}

export function calculateStreak(dates: Date[]): {
  currentStreak: number;
  longestStreak: number;
  todayActive: boolean;
  lastCommitDate: string | null;
} {
  // main streak calculation logic extracted from streak/route.ts
  // ...
}

2. Update all files to import from the shared module

Replace local definitions in:

  • src/app/api/metrics/streak/route.ts → remove local toDateStr, dateDiffDays; add imports
  • src/app/api/badge/streak-shield/route.ts → remove local dateDiffDays and streak logic; add imports
  • src/app/api/public/[username]/route.ts → remove local dateDiffDays, toDateStr, streak logic; add imports
  • src/app/api/metrics/compare/route.ts → remove local toDateStr; add import
  • src/app/api/metrics/repos/route.ts → remove local dateDiffDays; add import
  • src/app/api/metrics/contributions/route.ts → use shared streak grouping logic

3. Handle behavioral differences

Audit each duplicated implementation for subtle differences:

  • streak-shield/route.ts uses Date.UTC() for date normalization → may produce different results than ISO string truncation
  • compare/route.ts may handle edge cases (empty arrays, single dates) differently
  • Choose the correct implementation for the shared library (likely the most-used version from streak/route.ts)

4. Add comprehensive tests

Add tests for the shared src/lib/streak.ts covering:

  • Normal streak calculation
  • Edge cases: no commits, single day, every day, gaps
  • Timezone boundary cases (commits near midnight UTC)
  • dateDiffDays with dates across DST transitions (if applicable)

Verification

  1. All streak-related endpoints should return identical results before and after refactoring
  2. Compare results from:
    • /api/metrics/streak
    • /api/badge/streak-shield
    • /api/public/[username]
  3. Run existing tests (if any) to verify no regression
  4. Verify no remaining duplicate definitions using rg "function dateDiffDays" and rg "function toDateStr"

Metadata

Metadata

Assignees

Labels

gssoc:assignedGSSoC: Issue assigned to a contributorlevel:intermediateGSSoC: Intermediate difficulty (35 pts)type:refactorGSSoC type bonus: refactor (+10 pts)

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions