Skip to content

Commit 6508748

Browse files
authored
feat(calculate): add configurable grace period for streaks (JhaSourav07#636)
2 parents f7c2828 + bb48f2c commit 6508748

5 files changed

Lines changed: 61 additions & 12 deletions

File tree

app/api/streak/route.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export async function GET(request: Request) {
8383
delta_format,
8484
width,
8585
height,
86+
grace,
8687
} = parseResult.data;
8788

8889
const themeName = theme || 'dark';
@@ -131,6 +132,7 @@ export async function GET(request: Request) {
131132
width: width ? parseInt(width, 10) : undefined,
132133
height: height ? parseInt(height, 10) : undefined,
133134
size,
135+
grace,
134136
};
135137

136138
const calendar = await fetchGitHubContributions(user, {
@@ -144,7 +146,7 @@ export async function GET(request: Request) {
144146
const stats = calculateMonthlyStats(calendar, timezone);
145147
svg = generateMonthlySVG(stats, params);
146148
} else {
147-
const stats = calculateStreak(calendar, timezone);
149+
const stats = calculateStreak(calendar, timezone, undefined, grace);
148150
svg = generateSVG(stats, params, calendar);
149151
}
150152

lib/calculate.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,36 @@ describe('calculateStreak', () => {
153153
expect(result.longestStreak).toBe(2);
154154
});
155155

156+
it('keeps the streak alive with a grace period > 1 (e.g. grace=2)', () => {
157+
// Today is 0, yesterday is 0, but 2 days ago is 1.
158+
// With grace=1 (default), streak is broken. With grace=2, streak is alive.
159+
const calendar = buildCalendar([
160+
0,
161+
0,
162+
0,
163+
0,
164+
0,
165+
0,
166+
0, // week 1
167+
0,
168+
0,
169+
0,
170+
0,
171+
1,
172+
0,
173+
0, // week 2 — today=0, yesterday=0, day before=1
174+
]);
175+
176+
// Using default grace (1)
177+
const resultGrace1 = calculateStreak(calendar, 'UTC', undefined, 1);
178+
expect(resultGrace1.currentStreak).toBe(0);
179+
180+
// Using grace = 2
181+
const resultGrace2 = calculateStreak(calendar, 'UTC', undefined, 2);
182+
expect(resultGrace2.currentStreak).toBe(1);
183+
expect(resultGrace2.longestStreak).toBe(1);
184+
});
185+
156186
it('handles a single active day without crashing (edge case: no "yesterday")', () => {
157187
// A calendar with only one day could make `days[todayIndex - 1]` undefined.
158188
// The function should survive this gracefully and return currentStreak = 1.

lib/calculate.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ const stats = calculateStreak(
5454
export function calculateStreak(
5555
calendar: ContributionCalendar,
5656
timezone: string = 'UTC',
57-
now: Date = new Date()
57+
now: Date = new Date(),
58+
grace: number = 1
5859
): StreakStats {
5960
const weeks = calendar.weeks;
6061
const days = weeks.flatMap((week) => week.contributionDays);
@@ -92,19 +93,25 @@ export function calculateStreak(
9293
};
9394
}
9495

95-
const today = days[todayIndex];
96-
const yesterday = todayIndex > 0 ? days[todayIndex - 1] : null;
97-
98-
// If I committed today, the streak is alive.
99-
// If I haven't committed today, but I committed yesterday,
100-
// the streak is STILL alive (Grace Period).
101-
const isStreakAlive = today.contributionCount > 0 || (yesterday?.contributionCount ?? 0) > 0;
96+
// If I committed today, or any day within the grace period (e.g. yesterday for grace=1),
97+
// the streak is STILL alive.
98+
let isStreakAlive = false;
99+
for (let i = 0; i <= grace; i++) {
100+
const checkIndex = todayIndex - i;
101+
if (checkIndex >= 0 && days[checkIndex].contributionCount > 0) {
102+
isStreakAlive = true;
103+
break;
104+
}
105+
}
102106

103107
if (isStreakAlive) {
104-
// Count backwards from the first day that has a contribution
105-
// starting from either today or yesterday.
106-
let i = today.contributionCount > 0 ? todayIndex : todayIndex - 1;
108+
// Find the most recent day with a contribution within the grace period
109+
let i = todayIndex;
110+
while (i >= todayIndex - grace && i >= 0 && days[i].contributionCount === 0) {
111+
i--;
112+
}
107113

114+
// Count backwards from the first day that has a contribution
108115
while (i >= 0 && days[i].contributionCount > 0) {
109116
currentStreak++;
110117
i--;

lib/validations.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ export const streakParamsSchema = z.object({
8181
delta_format: z.enum(['percent', 'absolute', 'both']).catch('percent').default('percent'),
8282
width: z.string().optional(),
8383
height: z.string().optional(),
84+
grace: z
85+
.string()
86+
.optional()
87+
.transform((val) => {
88+
if (!val) return 1;
89+
const parsed = Number(val);
90+
return isNaN(parsed) ? 1 : Math.max(0, Math.min(parsed, 7));
91+
})
92+
.default(1),
8493
});
8594

8695
export const githubParamsSchema = z.object({

types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@ export interface BadgeParams {
5454
width?: number;
5555
height?: number;
5656
size?: 'small' | 'medium' | 'large';
57+
grace?: number;
5758
}

0 commit comments

Comments
 (0)