diff --git a/lib/calculate.test.ts b/lib/calculate.test.ts index bf41d58eb..1745aece2 100644 --- a/lib/calculate.test.ts +++ b/lib/calculate.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'; import { calculateStreak, calculateMonthlyStats, + calculateSafePercentage, calculateWrappedStats, aggregateCalendars, isStreakAlive, @@ -2821,3 +2822,14 @@ describe('DST transition boundary handling', () => { expect(result.currentStreak).toBe(1); }); }); + +describe('calculateSafePercentage utility metric verification', () => { + it('safely yields 0 when denominator total is 0', () => { + expect(calculateSafePercentage(10, 0)).toBe(0); + }); + + it('correctly rounds regular integer percentages', () => { + expect(calculateSafePercentage(1, 3)).toBe(33); // 33.333... rounds to 33 + expect(calculateSafePercentage(2, 3)).toBe(67); // 66.666... rounds to 67 + }); +}); diff --git a/lib/calculate.ts b/lib/calculate.ts index f8b1f31d6..555ca3783 100644 --- a/lib/calculate.ts +++ b/lib/calculate.ts @@ -7,6 +7,19 @@ import type { MonthlyStats, } from '../types'; +/* ========================================================================== + * UTILITY FUNCTIONS + * ========================================================================== */ + +/** + * Safely calculates and rounds a percentage fraction to prevent NaN or + * Infinity division values when the total denominator resolves to zero. + */ +export function calculateSafePercentage(part: number, total: number): number { + if (total === 0) return 0; + return Math.round((part / total) * 100); +} + /* ========================================================================== * STREAK & CALENDAR CALCULATIONS * ========================================================================== */ @@ -91,6 +104,7 @@ export function getLocalTodayStr(now: Date, timezone: string): string { return now.toISOString().split('T')[0]; } } + export function isStreakAlive( today?: { contributionCount: number } | null, yesterday?: { contributionCount: number } | null @@ -532,15 +546,14 @@ export function calculateWrappedStats(calendar?: ContributionCalendar | null) { ? 'N/A' : Object.keys(monthCounts).reduce((a, b) => (monthCounts[a] > monthCounts[b] ? a : b)); + const total = weekendCommits + weekdayCommits; + return { totalContributions: calendar.totalContributions || 0, mostActiveDate: mostActiveDay.date, highestDailyCount: mostActiveDay.count, busiestMonth: busiestMonthStr, - weekendRatio: (() => { - const total = weekendCommits + weekdayCommits; - return total > 0 ? Math.round((weekendCommits / total) * 100) : 0; - })(), + weekendRatio: calculateSafePercentage(weekendCommits, total), }; }