UNPKG

@zubenelakrab/gitstats

Version:

Powerful Git repository analyzer with comprehensive statistics and insights

232 lines 9.04 kB
import { getDayOfWeek, getHourOfDay, toDateString, } from '../utils/date.js'; export class WorkPatternsAnalyzer { name = 'workpatterns-analyzer'; description = 'Analyzes work patterns and detects crunch time'; async analyze(commits, _config) { if (commits.length === 0) { return this.emptyStats(); } // Hourly and daily distributions const hourlyDistribution = new Array(24).fill(0); const dailyDistribution = new Array(7).fill(0); let nightCommits = 0; let weekendCommits = 0; // Daily commit counts for crunch detection const dailyCommits = new Map(); // Author patterns const authorData = new Map(); for (const commit of commits) { const hour = getHourOfDay(commit.date); const day = getDayOfWeek(commit.date); const dateKey = toDateString(commit.date); hourlyDistribution[hour]++; dailyDistribution[day]++; // Night owl (22:00 - 06:00) if (hour >= 22 || hour < 6) { nightCommits++; } // Weekend (Saturday = 6, Sunday = 0) if (day === 0 || day === 6) { weekendCommits++; } // Daily counts dailyCommits.set(dateKey, (dailyCommits.get(dateKey) || 0) + 1); // Author data const authorKey = commit.author.email.toLowerCase(); if (!authorData.has(authorKey)) { authorData.set(authorKey, { name: commit.author.name, email: commit.author.email, hours: [], days: [], commits: 0, }); } const author = authorData.get(authorKey); author.hours.push(hour); author.days.push(day); author.commits++; } // Find peak hour and day const peakHour = hourlyDistribution.indexOf(Math.max(...hourlyDistribution)); const peakDay = dailyDistribution.indexOf(Math.max(...dailyDistribution)); // Calculate percentages const nightOwlPercentage = (nightCommits / commits.length) * 100; const weekendPercentage = (weekendCommits / commits.length) * 100; // Detect crunch periods const crunchPeriods = this.detectCrunchPeriods(dailyCommits); // Calculate author patterns const authorPatterns = this.calculateAuthorPatterns(authorData); // Work-life balance score const workLifeBalance = this.calculateWorkLifeBalance(nightOwlPercentage, weekendPercentage, crunchPeriods); return { peakHour, peakDay, nightOwlPercentage, weekendPercentage, crunchPeriods, authorPatterns, hourlyDistribution, dailyDistribution, workLifeBalance, }; } detectCrunchPeriods(dailyCommits) { const periods = []; const entries = Array.from(dailyCommits.entries()).sort((a, b) => a[0].localeCompare(b[0])); if (entries.length < 7) return periods; // Calculate normal average const allCounts = entries.map(([, count]) => count); const normalAverage = allCounts.reduce((a, b) => a + b, 0) / allCounts.length; const threshold = normalAverage * 2; // 2x normal is considered crunch let crunchStart = null; let crunchCommits = 0; let crunchDays = 0; for (let i = 0; i < entries.length; i++) { const [date, count] = entries[i]; if (count >= threshold) { if (!crunchStart) { crunchStart = date; crunchCommits = 0; crunchDays = 0; } crunchCommits += count; crunchDays++; } else if (crunchStart) { // End of crunch period if (crunchDays >= 3) { // At least 3 days to be considered crunch const avgPerDay = crunchCommits / crunchDays; let severity; if (avgPerDay > normalAverage * 4) { severity = 'severe'; } else if (avgPerDay > normalAverage * 3) { severity = 'moderate'; } else { severity = 'mild'; } periods.push({ startDate: crunchStart, endDate: entries[i - 1][0], days: crunchDays, commits: crunchCommits, averageCommitsPerDay: avgPerDay, normalAverage, severity, }); } crunchStart = null; } } return periods.slice(0, 10); } calculateAuthorPatterns(authorData) { const patterns = []; for (const [, data] of authorData) { if (data.commits < 5) continue; // Count hours and days const hourCounts = new Array(24).fill(0); const dayCounts = new Array(7).fill(0); let nightCommits = 0; let weekendCommits = 0; for (const hour of data.hours) { hourCounts[hour]++; if (hour >= 22 || hour < 6) nightCommits++; } for (const day of data.days) { dayCounts[day]++; if (day === 0 || day === 6) weekendCommits++; } // Get top 3 hours and days const preferredHours = hourCounts .map((count, hour) => ({ hour, count })) .sort((a, b) => b.count - a.count) .slice(0, 3) .map(h => h.hour); const preferredDays = dayCounts .map((count, day) => ({ day, count })) .sort((a, b) => b.count - a.count) .slice(0, 3) .map(d => d.day); // Calculate scores const nightOwlScore = (nightCommits / data.commits) * 100; const weekendWarriorScore = (weekendCommits / data.commits) * 100; // Consistency (how spread out are the commits) const hourVariance = this.calculateVariance(hourCounts.filter(c => c > 0)); const consistencyScore = Math.min(100, hourVariance * 10); // Determine work style const avgHour = data.hours.reduce((a, b) => a + b, 0) / data.hours.length; let workStyle; if (nightOwlScore > 30) { workStyle = 'night-owl'; } else if (avgHour < 10) { workStyle = 'early-bird'; } else if (avgHour >= 9 && avgHour <= 17 && weekendWarriorScore < 10) { workStyle = 'nine-to-five'; } else { workStyle = 'flexible'; } patterns.push({ name: data.name, email: data.email, preferredHours, preferredDays, nightOwlScore, weekendWarriorScore, consistencyScore, workStyle, }); } return patterns.sort((a, b) => b.nightOwlScore - a.nightOwlScore); } calculateVariance(values) { if (values.length === 0) return 0; const mean = values.reduce((a, b) => a + b, 0) / values.length; const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length; return Math.sqrt(variance); } calculateWorkLifeBalance(nightOwlPercentage, weekendPercentage, crunchPeriods) { let score = 100; // Deduct for night work score -= nightOwlPercentage * 1.5; // Deduct for weekend work score -= weekendPercentage * 2; // Deduct for crunch periods for (const period of crunchPeriods) { if (period.severity === 'severe') score -= 10; else if (period.severity === 'moderate') score -= 5; else score -= 2; } return Math.max(0, Math.min(100, Math.round(score))); } emptyStats() { return { peakHour: 0, peakDay: 0, nightOwlPercentage: 0, weekendPercentage: 0, crunchPeriods: [], authorPatterns: [], hourlyDistribution: new Array(24).fill(0), dailyDistribution: new Array(7).fill(0), workLifeBalance: 100, }; } } export function createWorkPatternsAnalyzer() { return new WorkPatternsAnalyzer(); } //# sourceMappingURL=workpatterns-analyzer.js.map