UNPKG

code996

Version:

通过分析 Git commit 的时间分布,计算出项目的'996指数'

145 lines 6.62 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WorkTimeAnalyzer = void 0; const end_hour_detector_1 = require("./end-hour-detector"); /** * 工作时间分析器 * 根据每日最早提交时间推算上班时间,并利用小时分布估计下班时间 */ class WorkTimeAnalyzer { /** * 检测工作时间 * @param hourData 按小时统计的提交数量 * @param dayData 按星期统计的提交数量(保留参数以兼容旧逻辑) * @param dailyFirstCommits 每日最早提交时间集合 */ static detectWorkingHours(hourData, dailyFirstCommits = []) { const filteredDailyCommits = this.filterValidDailyCommits(dailyFirstCommits); const sampleCount = filteredDailyCommits.length; const minutesSamples = filteredDailyCommits.map((item) => item.minutesFromMidnight); let detectionMethod = 'default'; let startRange; const defaultMinutes = 9 * 60; if (minutesSamples.length > 0) { const lowerQuantile = this.calculateQuantile(minutesSamples, 0.1, defaultMinutes); const upperQuantile = this.calculateQuantile(minutesSamples, 0.2, lowerQuantile + 60); startRange = this.buildStartHourRange(lowerQuantile, upperQuantile); detectionMethod = 'quantile-window'; } else { startRange = this.buildStartHourRange(defaultMinutes, defaultMinutes + 30); } const startHour = startRange.startHour; const standardEndHour = Math.min(startHour + this.STANDARD_WORK_HOURS, 24); const observedEndWindow = (0, end_hour_detector_1.detectEndHourWindow)(hourData, startHour, standardEndHour); const standardRange = this.buildEndHourRange(startHour, standardEndHour); const useObserved = observedEndWindow.method === 'backward-threshold'; const effectiveEndHour = useObserved ? observedEndWindow.endHour : standardEndHour; const effectiveRange = useObserved ? observedEndWindow.range : standardRange; const endDetectionMethod = useObserved ? 'backward-threshold' : 'standard-shift'; const confidence = this.estimateConfidence(sampleCount); return { startHour, endHour: effectiveEndHour, isReliable: confidence >= 60, sampleCount, detectionMethod, confidence, startHourRange: startRange, endHourRange: effectiveRange, endDetectionMethod, }; } /** * 根据识别结果判断某个整点是否属于工作时间 */ static isWorkingHour(hour, detection) { const hourStartMinutes = hour * 60; const startMinutes = detection.startHour * 60; const endMinutes = detection.endHour * 60; return hourStartMinutes >= startMinutes && hourStartMinutes < endMinutes; } /** * 过滤异常的每日最早提交数据(如凌晨噪点) */ static filterValidDailyCommits(dailyFirstCommits) { return dailyFirstCommits.filter((item) => { if (item.minutesFromMidnight < this.MIN_VALID_MINUTES || item.minutesFromMidnight > this.MAX_VALID_MINUTES) { return false; } const weekDay = new Date(`${item.date}T00:00:00Z`).getUTCDay(); return weekDay >= 1 && weekDay <= 5; }); } /** * 计算分钟数组的分位数,若样本不足则回退到给定的默认值 */ static calculateQuantile(samples, quantile, fallback) { if (!samples || samples.length === 0) { return fallback; } const sorted = [...samples].sort((a, b) => a - b); const index = Math.floor((sorted.length - 1) * quantile); const value = sorted[index]; if (value === undefined || Number.isNaN(value)) { return fallback; } return value; } /** * 将分钟数向下取整到最近的 30 分钟刻度 */ static roundDownToHalfHour(minutes) { const halfHourBlock = Math.floor(minutes / 30); return halfHourBlock * 30; } /** * 构建上班时间段,基于分位数生成最长 1 小时的范围 */ static buildStartHourRange(lowerMinutes, upperMinutes) { const boundedLower = Math.max(this.MIN_VALID_MINUTES, Math.min(lowerMinutes, this.MAX_VALID_MINUTES)); const boundedUpper = Math.max(this.MIN_VALID_MINUTES, Math.min(upperMinutes, this.MAX_VALID_MINUTES)); const sanitizedLower = this.roundDownToHalfHour(boundedLower); const sanitizedUpper = this.roundDownToHalfHour(Math.max(boundedUpper, sanitizedLower + 30)); const start = Math.min(sanitizedLower, sanitizedUpper); let end = Math.max(sanitizedUpper, start + 30); // 限制范围不超过 1 小时,且不晚于中午 12 点 end = Math.min(end, start + 60, this.MAX_VALID_MINUTES); return { startHour: start / 60, endHour: end / 60, }; } /** * 根据最早上班时间推导标准 9 小时工作日的下班时间段 */ static buildEndHourRange(startHour, endHour) { const startMinutes = this.roundDownToHalfHour(Math.max(startHour * 60, this.MIN_VALID_MINUTES)); const rawEndMinutes = Math.max(endHour * 60, startMinutes + this.STANDARD_WORK_HOURS * 60); const boundedEndMinutes = Math.min(rawEndMinutes, 24 * 60); const sanitizedEndMinutes = this.roundDownToHalfHour(boundedEndMinutes); const rangeEnd = sanitizedEndMinutes > 0 ? sanitizedEndMinutes : Math.min((startHour + 1) * 60, 24 * 60); const rangeStart = Math.max(startMinutes, rangeEnd - 60); return { startHour: rangeStart / 60, endHour: rangeEnd / 60, }; } /** * 根据样本数量估算可信度(百分比) * 使用渐近函数,无限趋近90%但永不达到 */ static estimateConfidence(sampleDays) { if (sampleDays <= 0) { return 0; } // 使用渐近函数:confidence = 90 * sampleDays / (sampleDays + 50) const confidence = (90 * sampleDays) / (sampleDays + 50); return Math.round(confidence); } } exports.WorkTimeAnalyzer = WorkTimeAnalyzer; WorkTimeAnalyzer.STANDARD_WORK_HOURS = 9; // 默认工作时长兜底 WorkTimeAnalyzer.MIN_VALID_MINUTES = 5 * 60; // 过滤非正常数据(早于5点视为无效) WorkTimeAnalyzer.MAX_VALID_MINUTES = 12 * 60; // 晚于中午12点视为无效 //# sourceMappingURL=work-time-analyzer.js.map