From bc7b28bab1c5db72940563684cc9f7c92ac50032 Mon Sep 17 00:00:00 2001 From: ShafinNigamana Date: Fri, 12 Jun 2026 12:44:35 +0530 Subject: [PATCH] test(calculate-empty-fallback): verify Edge Cases & Empty/Missing Inputs Verification (Variation 1) --- lib/calculate.empty-fallback.test.ts | 320 +++++++++++++++++++++++++++ lib/calculate.ts | 250 +++++++++++++++------ 2 files changed, 497 insertions(+), 73 deletions(-) create mode 100644 lib/calculate.empty-fallback.test.ts diff --git a/lib/calculate.empty-fallback.test.ts b/lib/calculate.empty-fallback.test.ts new file mode 100644 index 000000000..e8b5695bd --- /dev/null +++ b/lib/calculate.empty-fallback.test.ts @@ -0,0 +1,320 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect } from 'vitest'; +import { + isStreakAlive, + findTodayIndex, + calculateStreak, + calculateMonthlyStats, + aggregateCalendars, + chunkDaysIntoWeeks, + calculateWrappedStats, +} from './calculate'; + +describe('calculate-empty-fallback', () => { + describe('isStreakAlive', () => { + it('should return false when today is missing/null/undefined and yesterday is null/undefined/0', () => { + expect(isStreakAlive(null, null)).toBe(false); + expect(isStreakAlive(undefined, undefined)).toBe(false); + expect(isStreakAlive(null, { contributionCount: 0 })).toBe(false); + }); + + it('should return true if today is missing but yesterday has contributionCount > 0', () => { + expect(isStreakAlive(null, { contributionCount: 5 })).toBe(true); + expect(isStreakAlive(undefined, { contributionCount: 1 })).toBe(true); + }); + + it('should return true if today is present and has contributionCount > 0', () => { + expect(isStreakAlive({ contributionCount: 1 }, null)).toBe(true); + }); + }); + + describe('findTodayIndex', () => { + it('should return -1 when days is null, undefined, or empty', () => { + expect(findTodayIndex(null)).toBe(-1); + expect(findTodayIndex(undefined)).toBe(-1); + expect(findTodayIndex([])).toBe(-1); + }); + + it('should fallback to UTC and find today index successfully even with invalid timezone', () => { + const now = new Date('2026-06-12T12:00:00Z'); + const days = [ + { date: '2026-06-11', contributionCount: 1 }, + { date: '2026-06-12', contributionCount: 2 }, + { date: '2026-06-13', contributionCount: 3 }, + ]; + // "Invalid/Timezone" should trigger catch block formatting to UTC (which gives 2026-06-12) + const index = findTodayIndex(days, 'Invalid/Timezone', now); + expect(index).toBe(1); + }); + + it('should safely skip null/undefined days in the days array', () => { + const now = new Date('2026-06-12T12:00:00Z'); + const days = [null as any, undefined as any, { date: '2026-06-12', contributionCount: 2 }]; + const index = findTodayIndex(days, 'UTC', now); + expect(index).toBe(2); + }); + }); + + describe('calculateStreak', () => { + it('should return default StreakStats structure when calendar is null/undefined', () => { + const now = new Date('2026-06-12T12:00:00Z'); + const expectedDate = '2026-06-12'; + + const resultNull = calculateStreak(null, 'UTC', now); + expect(resultNull).toEqual({ + currentStreak: 0, + longestStreak: 0, + totalContributions: 0, + todayDate: expectedDate, + }); + + const resultUndefined = calculateStreak(undefined, 'UTC', now); + expect(resultUndefined).toEqual({ + currentStreak: 0, + longestStreak: 0, + totalContributions: 0, + todayDate: expectedDate, + }); + }); + + it('should handle missing weeks or null contributionDays safely', () => { + const now = new Date('2026-06-12T12:00:00Z'); + const calendar = { + totalContributions: 15, + weeks: [ + { contributionDays: [null as any] }, + undefined as any, + { + contributionDays: [{ date: '2026-06-12', contributionCount: 3 }], + }, + ], + }; + + const result = calculateStreak(calendar, 'UTC', now); + expect(result.currentStreak).toBe(1); + expect(result.longestStreak).toBe(1); + expect(result.totalContributions).toBe(15); + expect(result.todayDate).toBe('2026-06-12'); + }); + + it('should handle invalid timezone input gracefully and fallback to UTC', () => { + const now = new Date('2026-06-12T12:00:00Z'); + const calendar = { + totalContributions: 5, + weeks: [ + { + contributionDays: [{ date: '2026-06-12', contributionCount: 5 }], + }, + ], + }; + + const result = calculateStreak(calendar, 'Invalid/Timezone', now); + expect(result.currentStreak).toBe(1); + expect(result.longestStreak).toBe(1); + expect(result.todayDate).toBe('2026-06-12'); + }); + }); + + describe('calculateMonthlyStats', () => { + it('should return empty/default MonthlyStats structure when calendar is null/undefined', () => { + const now = new Date('2026-06-12T12:00:00Z'); + + const resultNull = calculateMonthlyStats(null, 'UTC', now); + expect(resultNull).toEqual({ + currentMonthTotal: 0, + previousMonthTotal: 0, + deltaPercentage: null, + deltaAbsolute: 0, + currentMonthName: 'June', + }); + + const resultUndefined = calculateMonthlyStats(undefined, 'UTC', now); + expect(resultUndefined).toEqual({ + currentMonthTotal: 0, + previousMonthTotal: 0, + deltaPercentage: null, + deltaAbsolute: 0, + currentMonthName: 'June', + }); + }); + + it('should gracefully handle empty weeks or contributionDays list', () => { + const now = new Date('2026-06-12T12:00:00Z'); + const calendar = { + totalContributions: 0, + weeks: [], + }; + + const result = calculateMonthlyStats(calendar, 'UTC', now); + expect(result).toEqual({ + currentMonthTotal: 0, + previousMonthTotal: 0, + deltaPercentage: null, + deltaAbsolute: 0, + currentMonthName: 'June', + }); + }); + + it('should handle invalid timezones safely', () => { + const now = new Date('2026-06-12T12:00:00Z'); + const calendar = { + totalContributions: 0, + weeks: [], + }; + + const result = calculateMonthlyStats(calendar, 'Invalid/Timezone', now); + expect(result.currentMonthName).toBe('June'); + }); + + it('should handle days with null fields or missing contribution count', () => { + const now = new Date('2026-06-12T12:00:00Z'); + const calendar = { + totalContributions: 5, + weeks: [ + { + contributionDays: [ + { date: '2026-06-12', contributionCount: null as any }, + { date: '2026-05-12', contributionCount: undefined as any }, + null as any, + ], + }, + ], + }; + + const result = calculateMonthlyStats(calendar, 'UTC', now); + expect(result.currentMonthTotal).toBe(0); + expect(result.previousMonthTotal).toBe(0); + expect(result.deltaAbsolute).toBe(0); + }); + }); + + describe('aggregateCalendars', () => { + it('should return empty calendar structure when parameter is null, undefined, or empty array', () => { + expect(aggregateCalendars(null)).toEqual({ totalContributions: 0, weeks: [] }); + expect(aggregateCalendars(undefined)).toEqual({ totalContributions: 0, weeks: [] }); + expect(aggregateCalendars([])).toEqual({ totalContributions: 0, weeks: [] }); + }); + + it('should safely filter out null/undefined calendar entries and proceed', () => { + const calendars = [ + null as any, + { + totalContributions: 10, + weeks: [ + { + contributionDays: [{ date: '2026-06-12', contributionCount: 10 }], + }, + ], + }, + undefined as any, + ]; + + const result = aggregateCalendars(calendars); + expect(result.totalContributions).toBe(10); + expect(result.weeks[0].contributionDays[0]).toEqual({ + date: '2026-06-12', + contributionCount: 10, + }); + }); + + it('should handle incomplete weeks and contribution days', () => { + const calendars = [ + { + totalContributions: 5, + weeks: [ + { + contributionDays: [ + null as any, + { date: '2026-06-11', contributionCount: null as any }, + ], + }, + ], + }, + ]; + const result = aggregateCalendars(calendars); + expect(result.totalContributions).toBe(5); + }); + }); + + describe('chunkDaysIntoWeeks', () => { + it('should return empty array when parameter is null, undefined, or empty', () => { + expect(chunkDaysIntoWeeks(null)).toEqual([]); + expect(chunkDaysIntoWeeks(undefined)).toEqual([]); + expect(chunkDaysIntoWeeks([])).toEqual([]); + }); + + it('should skip null values, missing dates, or invalid date values without throwing errors', () => { + const days = [ + null as any, + { date: '', contributionCount: 5 }, + { date: 'invalid-date-format', contributionCount: 3 }, + { date: '2026-06-08', contributionCount: 1 }, // Monday + { date: '2026-06-09', contributionCount: 2 }, // Tuesday + ]; + + const result = chunkDaysIntoWeeks(days); + expect(result.length).toBe(1); + expect(result[0].contributionDays.length).toBe(2); + expect(result[0].contributionDays[0].date).toBe('2026-06-08'); + expect(result[0].contributionDays[1].date).toBe('2026-06-09'); + }); + }); + + describe('calculateWrappedStats', () => { + it('should return default wrapped stats structure when calendar is null/undefined', () => { + const expected = { + totalContributions: 0, + mostActiveDate: 'N/A', + highestDailyCount: 0, + busiestMonth: 'N/A', + weekendRatio: 0, + }; + + expect(calculateWrappedStats(null)).toEqual(expected); + expect(calculateWrappedStats(undefined)).toEqual(expected); + }); + + it('should return default values when calendar has empty or invalid weeks/days structure', () => { + const calendar = { + totalContributions: 10, + weeks: [ + { + contributionDays: [null as any, { date: 'invalid-date', contributionCount: 5 }], + }, + ], + }; + + const result = calculateWrappedStats(calendar); + expect(result).toEqual({ + totalContributions: 10, + mostActiveDate: 'N/A', + highestDailyCount: 0, + busiestMonth: 'N/A', + weekendRatio: 0, + }); + }); + + it('should process valid and partially invalid mixed days correctly', () => { + const calendar = { + totalContributions: 15, + weeks: [ + { + contributionDays: [ + { date: '2026-06-07', contributionCount: 5 }, // Sunday (weekend) + { date: '2026-06-08', contributionCount: 10 }, // Monday (weekday) + { date: 'invalid-date', contributionCount: 100 }, + ], + }, + ], + }; + + const result = calculateWrappedStats(calendar); + expect(result.totalContributions).toBe(15); + expect(result.mostActiveDate).toBe('2026-06-08'); + expect(result.highestDailyCount).toBe(10); + expect(result.busiestMonth).toBe('2026-06'); + // weekendRatio = 5 / (5 + 10) * 100 = 33% + expect(result.weekendRatio).toBe(33); + }); + }); +}); diff --git a/lib/calculate.ts b/lib/calculate.ts index 18e83551f..ae77a86a5 100644 --- a/lib/calculate.ts +++ b/lib/calculate.ts @@ -6,34 +6,69 @@ import type { ContributionCalendar, ContributionDay, StreakStats, MonthlyStats } * ========================================================================== */ export function isStreakAlive( - today: { contributionCount: number }, - yesterday: { contributionCount: number } | null + today?: { contributionCount: number } | null, + yesterday?: { contributionCount: number } | null ): boolean { + if (!today) { + return (yesterday?.contributionCount ?? 0) > 0; + } return today.contributionCount > 0 || (yesterday?.contributionCount ?? 0) > 0; } -export function findTodayIndex(days: ContributionDay[], timezone: string, now: Date): number { - const localTodayStr = new Intl.DateTimeFormat('en-CA', { - timeZone: timezone, - }).format(now); +export function findTodayIndex( + days?: ContributionDay[] | null, + timezone?: string | null, + now?: Date | null +): number { + if (!days || !Array.isArray(days)) { + return -1; + } + const tz = timezone || 'UTC'; + const currentDate = now || new Date(); + + let localTodayStr: string; + try { + localTodayStr = new Intl.DateTimeFormat('en-CA', { + timeZone: tz, + }).format(currentDate); + } catch { + localTodayStr = new Intl.DateTimeFormat('en-CA', { + timeZone: 'UTC', + }).format(currentDate); + } - const localTodayIndex = days.findIndex((d) => d.date === localTodayStr); + const localTodayIndex = days.findIndex((d) => d && d.date === localTodayStr); - // If today's date isn't present in the calendar, return -1 so callers can - // decide whether falling back to the last available day is appropriate. - // Previously we always returned the last index which could cause an - // overstated current streak when the calendar is partial or stale. return localTodayIndex !== -1 ? localTodayIndex : -1; } export function calculateStreak( - calendar: ContributionCalendar, + calendar?: ContributionCalendar | null, timezone: string = 'UTC', now: Date = new Date(), grace: number = 1 ): StreakStats { - const weeks = calendar?.weeks || []; - const days = weeks.flatMap((week) => week?.contributionDays || []); + const localTodayStr = (() => { + try { + return new Intl.DateTimeFormat('en-CA', { timeZone: timezone || 'UTC' }).format( + now || new Date() + ); + } catch { + return new Intl.DateTimeFormat('en-CA', { timeZone: 'UTC' }).format(now || new Date()); + } + })(); + + if (!calendar) { + return { + currentStreak: 0, + longestStreak: 0, + totalContributions: 0, + todayDate: localTodayStr, + }; + } + + const weeks = calendar.weeks || []; + const days = weeks.flatMap((week) => week?.contributionDays || []).filter(Boolean); let currentStreak = 0; let longestStreak = 0; @@ -41,7 +76,7 @@ export function calculateStreak( // 1. Calculate Longest Streak (Standard loop) for (const day of days) { - if (day.contributionCount > 0) { + if (day && day.contributionCount > 0) { tempStreak++; if (tempStreak > longestStreak) longestStreak = tempStreak; } else { @@ -50,38 +85,28 @@ export function calculateStreak( } // 2. Calculate Current Streak (Backwards loop with Grace Period) - const localTodayStr = new Intl.DateTimeFormat('en-CA', { timeZone: timezone }).format(now); let todayIndex = findTodayIndex(days, timezone, now); - // If the calendar doesn't contain today's date, only fall back to the - // last available day when the local date is after the calendar's last - // reported date (i.e. the calendar is stale). Otherwise, avoid guessing - // and treat today's data as missing to prevent overstating the streak. if (todayIndex < 0) { const lastIndex = days.length - 1; if (lastIndex < 0) { return { currentStreak: 0, longestStreak: 0, - totalContributions: calendar.totalContributions, + totalContributions: calendar.totalContributions || 0, todayDate: localTodayStr, }; } - const lastDateStr = days[lastIndex].date; + const lastDateStr = days[lastIndex]?.date; - // Compare YYYY-MM-DD strings lexicographically — this works for ISO dates. - if (localTodayStr > lastDateStr) { - // Local date is after the last reported date → calendar is stale. + if (lastDateStr && localTodayStr > lastDateStr) { todayIndex = lastIndex; } else { - // Calendar contains dates after (or unrelated to) local today, or - // today is simply missing from a partial range — don't assume the - // streak is alive based on the last day. return { currentStreak: 0, longestStreak, - totalContributions: calendar.totalContributions, + totalContributions: calendar.totalContributions || 0, todayDate: localTodayStr, }; } @@ -90,7 +115,7 @@ export function calculateStreak( let isStreakAlive = false; for (let i = 0; i <= grace; i++) { const checkIndex = todayIndex - i; - if (checkIndex >= 0 && days[checkIndex].contributionCount > 0) { + if (checkIndex >= 0 && days[checkIndex] && days[checkIndex].contributionCount > 0) { isStreakAlive = true; break; } @@ -98,10 +123,10 @@ export function calculateStreak( if (isStreakAlive) { let i = todayIndex; - while (i >= todayIndex - grace && i >= 0 && days[i].contributionCount === 0) { + while (i >= todayIndex - grace && i >= 0 && days[i] && days[i].contributionCount === 0) { i--; } - while (i >= 0 && days[i].contributionCount > 0) { + while (i >= 0 && days[i] && days[i].contributionCount > 0) { currentStreak++; i--; } @@ -114,20 +139,52 @@ export function calculateStreak( return { currentStreak, longestStreak, - totalContributions: calendar.totalContributions, + totalContributions: calendar.totalContributions || 0, todayDate, }; } export function calculateMonthlyStats( - calendar: ContributionCalendar, + calendar?: ContributionCalendar | null, timezone: string = 'UTC', now: Date = new Date() ): MonthlyStats { - const weeks = calendar?.weeks || []; - const days = weeks.flatMap((week) => week?.contributionDays || []); + const currentMonthName = (() => { + try { + return new Intl.DateTimeFormat('en-US', { + timeZone: timezone || 'UTC', + month: 'long', + }).format(now || new Date()); + } catch { + return new Intl.DateTimeFormat('en-US', { + timeZone: 'UTC', + month: 'long', + }).format(now || new Date()); + } + })(); + + if (!calendar) { + return { + currentMonthTotal: 0, + previousMonthTotal: 0, + deltaPercentage: null, + deltaAbsolute: 0, + currentMonthName, + }; + } + + const weeks = calendar.weeks || []; + const days = weeks.flatMap((week) => week?.contributionDays || []).filter(Boolean); + + let localTodayStr: string; + try { + localTodayStr = new Intl.DateTimeFormat('en-CA', { timeZone: timezone || 'UTC' }).format( + now || new Date() + ); + } catch { + localTodayStr = new Intl.DateTimeFormat('en-CA', { timeZone: 'UTC' }).format(now || new Date()); + } - const localTodayStr = new Intl.DateTimeFormat('en-CA', { timeZone: timezone }).format(now); const [currentYearStr, currentMonthStr] = localTodayStr.split('-'); const currentYear = parseInt(currentYearStr, 10); const currentMonth = parseInt(currentMonthStr, 10); @@ -146,10 +203,12 @@ export function calculateMonthlyStats( let previousMonthTotal = 0; for (const day of days) { - if (day.date.startsWith(currentMonthPrefix)) { - currentMonthTotal += day.contributionCount; - } else if (day.date.startsWith(prevMonthPrefix)) { - previousMonthTotal += day.contributionCount; + if (day && day.date) { + if (day.date.startsWith(currentMonthPrefix)) { + currentMonthTotal += day.contributionCount || 0; + } else if (day.date.startsWith(prevMonthPrefix)) { + previousMonthTotal += day.contributionCount || 0; + } } } @@ -159,26 +218,23 @@ export function calculateMonthlyStats( let firstDate = ''; let lastDate = ''; if (days.length > 0) { - let minDate = days[0].date; - let maxDate = days[0].date; + let minDate = days[0]?.date || ''; + let maxDate = days[0]?.date || ''; for (const d of days) { - if (d.date < minDate) minDate = d.date; - if (d.date > maxDate) maxDate = d.date; + if (d && d.date) { + if (!minDate || d.date < minDate) minDate = d.date; + if (!maxDate || d.date > maxDate) maxDate = d.date; + } } firstDate = minDate; lastDate = maxDate; } - const hasDays = days.length > 0; + const hasDays = days.length > 0 && firstDate !== '' && lastDate !== ''; const isPrevMonthComplete = hasDays && firstDate <= expectedPrevMonthStart; const isCurrentMonthComplete = hasDays && lastDate >= expectedCurrentMonthEnd; const isCalendarComplete = isPrevMonthComplete && isCurrentMonthComplete; - const currentMonthName = new Intl.DateTimeFormat('en-US', { - timeZone: timezone, - month: 'long', - }).format(now); - const deltaAbsolute = currentMonthTotal - previousMonthTotal; // When there is no baseline (previous month = 0), or the calendar is incomplete, // the percentage change is mathematically undefined or untrustworthy. @@ -208,13 +264,18 @@ export function calculateMonthlyStats( * Aggregates multiple user contribution calendars into a single "Mega-City" calendar. * Used for Organization and Team dashboards. */ -export function aggregateCalendars(calendars: ContributionCalendar[]): ContributionCalendar { - if (calendars.length === 0) { +export function aggregateCalendars( + calendars?: ContributionCalendar[] | null +): ContributionCalendar { + if (!calendars || !Array.isArray(calendars) || calendars.length === 0) { return { totalContributions: 0, weeks: [] }; } // Calculate total contributions across all calendars - const totalContributions = calendars.reduce((sum, cal) => sum + cal.totalContributions, 0); + const totalContributions = calendars.reduce( + (sum, cal) => sum + (cal?.totalContributions || 0), + 0 + ); // Use a Map keyed by the date string 'YYYY-MM-DD' to safely aggregate daily counts const dateMap = new Map(); @@ -222,20 +283,26 @@ export function aggregateCalendars(calendars: ContributionCalendar[]): Contribut // Find the calendar with the most weeks to serve as our structural base let baseCalendar = calendars[0]; for (const cal of calendars) { - if ((cal.weeks?.length || 0) > (baseCalendar.weeks?.length || 0)) { + if (!cal) continue; + if ((cal.weeks?.length || 0) > (baseCalendar?.weeks?.length || 0)) { baseCalendar = cal; } // Populate the Map with all contributions from all calendars (cal.weeks || []).forEach((week) => { (week?.contributionDays || []).forEach((day) => { - const currentCount = dateMap.get(day.date) || 0; - dateMap.set(day.date, currentCount + day.contributionCount); + if (day && day.date) { + const currentCount = dateMap.get(day.date) || 0; + dateMap.set(day.date, currentCount + (day.contributionCount || 0)); + } }); }); } - // Deep clone the base calendar so we don't mutate the original object + if (!baseCalendar) { + return { totalContributions: 0, weeks: [] }; + } + // Deep clone the base calendar so we don't mutate the original object const aggregatedBase = JSON.parse(JSON.stringify(baseCalendar)) as ContributionCalendar; @@ -244,15 +311,19 @@ export function aggregateCalendars(calendars: ContributionCalendar[]): Contribut // Re-map the structural base using our aggregated date map (aggregatedBase.weeks || []).forEach((week) => { (week?.contributionDays || []).forEach((day) => { - day.contributionCount = dateMap.get(day.date) || 0; + if (day && day.date) { + day.contributionCount = dateMap.get(day.date) || 0; + } }); }); const existingDates = new Set(); (aggregatedBase.weeks || []).forEach((week) => { - (week.contributionDays || []).forEach((day) => { - existingDates.add(day.date); + (week?.contributionDays || []).forEach((day) => { + if (day && day.date) { + existingDates.add(day.date); + } }); }); @@ -268,6 +339,10 @@ export function aggregateCalendars(calendars: ContributionCalendar[]): Contribut } missingDays.sort((a, b) => a.date.localeCompare(b.date)); + + if (!aggregatedBase.weeks) { + aggregatedBase.weeks = []; + } for (const day of missingDays) { aggregatedBase.weeks.push({ contributionDays: [day], @@ -282,12 +357,23 @@ export function aggregateCalendars(calendars: ContributionCalendar[]): Contribut * renderers keep their week (column) and weekday (row) grid instead of collapsing * every day into a single week. */ -export function chunkDaysIntoWeeks(days: ContributionDay[]): ContributionCalendar['weeks'] { +export function chunkDaysIntoWeeks(days?: ContributionDay[] | null): ContributionCalendar['weeks'] { + if (!days || !Array.isArray(days)) { + return []; + } const weeks: ContributionCalendar['weeks'] = []; let currentWeek: ContributionDay[] = []; for (const day of days) { - if (currentWeek.length > 0 && new Date(day.date).getUTCDay() === 0) { + if (!day || !day.date) continue; + + // Safety check for date parser + const parsedDate = new Date(day.date); + if (isNaN(parsedDate.getTime())) { + continue; + } + + if (currentWeek.length > 0 && parsedDate.getUTCDay() === 0) { weeks.push({ contributionDays: currentWeek }); currentWeek = []; } @@ -304,9 +390,19 @@ export function chunkDaysIntoWeeks(days: ContributionDay[]): ContributionCalenda /** * Processes a calendar to generate deep insights for "GitHub Wrapped" */ -export function calculateWrappedStats(calendar: ContributionCalendar) { - const weeks = calendar?.weeks || []; - const days = weeks.flatMap((w) => w?.contributionDays || []); +export function calculateWrappedStats(calendar?: ContributionCalendar | null) { + if (!calendar) { + return { + totalContributions: 0, + mostActiveDate: 'N/A', + highestDailyCount: 0, + busiestMonth: 'N/A', + weekendRatio: 0, + }; + } + + const weeks = calendar.weeks || []; + const days = weeks.flatMap((w) => w?.contributionDays || []).filter(Boolean); let mostActiveDay = { date: 'N/A', count: 0 }; const monthCounts: Record = {}; @@ -314,22 +410,30 @@ export function calculateWrappedStats(calendar: ContributionCalendar) { let weekdayCommits = 0; days.forEach((day) => { + if (!day || !day.date) return; + + // Safety check for date parser + const dateObj = new Date(day.date); + if (isNaN(dateObj.getTime())) { + return; + } + + const count = day.contributionCount || 0; // 1. Highest single day - if (day.contributionCount > mostActiveDay.count) { - mostActiveDay = { date: day.date, count: day.contributionCount }; + if (count > mostActiveDay.count) { + mostActiveDay = { date: day.date, count }; } // 2. Busiest month const month = day.date.substring(0, 7); // YYYY-MM - monthCounts[month] = (monthCounts[month] || 0) + day.contributionCount; + monthCounts[month] = (monthCounts[month] || 0) + count; // 3. Weekday vs Weekend grind - const dateObj = new Date(day.date); const dayOfWeek = dateObj.getUTCDay(); // 0 is Sunday, 6 is Saturday if (dayOfWeek === 0 || dayOfWeek === 6) { - weekendCommits += day.contributionCount; + weekendCommits += count; } else { - weekdayCommits += day.contributionCount; + weekdayCommits += count; } }); @@ -340,7 +444,7 @@ export function calculateWrappedStats(calendar: ContributionCalendar) { : Object.keys(monthCounts).reduce((a, b) => (monthCounts[a] > monthCounts[b] ? a : b)); return { - totalContributions: calendar.totalContributions, + totalContributions: calendar.totalContributions || 0, mostActiveDate: mostActiveDay.date, highestDailyCount: mostActiveDay.count, busiestMonth: busiestMonthStr,