yday
Version:
Git retrospective analysis tool - smart views of recent development work
362 lines (307 loc) • 13.3 kB
JavaScript
/**
* Timespan Determination Logic
*
* Step 1 of clean timeline architecture.
* Converts CLI options into precise date ranges.
*/
class TimespanAnalyzer {
/**
* Determine the exact timespan based on CLI options
* @param {Object} options - CLI options from commander
* @param {Date} currentDate - Current date (for testing)
* @returns {Object} Timespan object with type, dates, and description
*/
determineTimespan(options, currentDate = new Date()) {
// Handle specific day options (--last-tuesday, etc.)
if (this.hasSpecificDayOption(options)) {
return this.handleSpecificDay(options, currentDate);
}
// Handle explicit date ranges
if (options.days) {
return this.handleDaysOption(options.days, currentDate);
}
if (options.after || options.before) {
return this.handleDateRangeOptions(options, currentDate);
}
if (options.on !== undefined) {
return this.handleOnOption(options.on, currentDate);
}
if (options.today) {
return this.handleTodayOption(currentDate);
}
if (options.prev) {
return this.handlePrevWeek(currentDate);
}
// Default: smart yesterday logic
return this.handleSmartYesterday(currentDate);
}
/**
* Check if any specific day option is set
*/
hasSpecificDayOption(options) {
return options.lastMonday || options.lastTuesday || options.lastWednesday ||
options.lastThursday || options.lastFriday || options.lastSaturday ||
options.lastSunday || options.lastWorkday ||
options.prevMonday || options.prevTuesday || options.prevWednesday ||
options.prevThursday || options.prevFriday || options.prevSaturday ||
options.prevSunday;
}
/**
* Handle --last-tuesday, --last-monday, etc.
*/
handleSpecificDay(options, currentDate) {
// Handle prev-day options first (previous week)
if (options.prevMonday || options.prevTuesday || options.prevWednesday ||
options.prevThursday || options.prevFriday || options.prevSaturday ||
options.prevSunday) {
return this.handlePrevDay(options, currentDate);
}
let targetDay;
let description;
if (options.lastMonday) { targetDay = 1; description = 'Monday'; }
else if (options.lastTuesday) { targetDay = 2; description = 'Tuesday'; }
else if (options.lastWednesday) { targetDay = 3; description = 'Wednesday'; }
else if (options.lastThursday) { targetDay = 4; description = 'Thursday'; }
else if (options.lastFriday) { targetDay = 5; description = 'Friday'; }
else if (options.lastSaturday) { targetDay = 6; description = 'Saturday'; }
else if (options.lastSunday) { targetDay = 0; description = 'Sunday'; }
else if (options.lastWorkday) {
return this.handleLastWorkday(currentDate);
}
const targetDate = this.findLastOccurrenceOfDay(targetDay, currentDate);
// Create UTC dates to avoid timezone issues in tests
const startDate = new Date(Date.UTC(targetDate.getUTCFullYear(), targetDate.getUTCMonth(), targetDate.getUTCDate(), 0, 0, 0, 0));
const endDate = new Date(Date.UTC(targetDate.getUTCFullYear(), targetDate.getUTCMonth(), targetDate.getUTCDate(), 23, 59, 59, 999));
return {
type: 'single-day',
date: new Date(Date.UTC(targetDate.getUTCFullYear(), targetDate.getUTCMonth(), targetDate.getUTCDate(), 0, 0, 0, 0)),
startDate,
endDate,
description: `${description}, ${targetDate.toLocaleDateString('en-US', { month: 'long', day: 'numeric' })}`
};
}
/**
* Find the most recent occurrence of a specific day of week
* @param {number} targetDay - 0=Sunday, 1=Monday, ..., 6=Saturday
* @param {Date} currentDate - Reference date
* @returns {Date} The target date
*/
findLastOccurrenceOfDay(targetDay, currentDate) {
const current = new Date(currentDate);
const currentDay = current.getDay();
let daysBack;
if (currentDay === targetDay) {
// If it's the same day, go back a full week
daysBack = 7;
} else if (currentDay > targetDay) {
// Target day is earlier in the week
daysBack = currentDay - targetDay;
} else {
// Target day is in the previous week
daysBack = 7 - (targetDay - currentDay);
}
const targetDate = new Date(current);
targetDate.setDate(current.getDate() - daysBack);
return targetDate;
}
/**
* Find the previous week's occurrence of a specific day of week
* Always goes back at least 7 days to ensure it's in the previous week
* @param {number} targetDay - 0=Sunday, 1=Monday, ..., 6=Saturday
* @param {Date} currentDate - Reference date
* @returns {Date} The target date in the previous week
*/
findPrevWeekOccurrenceOfDay(targetDay, currentDate) {
const current = new Date(currentDate);
// Find the most recent occurrence of the target day (could be this week or last week)
const mostRecentTargetDay = this.findLastOccurrenceOfDay(targetDay, currentDate);
// Go back exactly 7 days from that to get the previous week's occurrence
const prevWeekTargetDay = new Date(mostRecentTargetDay);
prevWeekTargetDay.setDate(mostRecentTargetDay.getDate() - 7);
return prevWeekTargetDay;
}
/**
* Handle --last-workday (skip weekends)
*/
handleLastWorkday(currentDate) {
const current = new Date(currentDate);
const currentDay = current.getDay(); // 0=Sunday, 1=Monday, ..., 6=Saturday
let daysBack;
if (currentDay === 1) { // Monday
daysBack = 3; // Go to Friday
} else if (currentDay === 0) { // Sunday
daysBack = 2; // Go to Friday
} else {
daysBack = 1; // Go to previous day
}
const targetDate = new Date(current);
targetDate.setDate(current.getDate() - daysBack);
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
return {
type: 'single-day',
date: targetDate,
startDate: new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate(), 0, 0, 0, 0),
endDate: new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate(), 23, 59, 59, 999),
description: `${dayNames[targetDate.getDay()]}, ${targetDate.toLocaleDateString('en-US', { month: 'long', day: 'numeric' })}`
};
}
/**
* Handle --prev-tuesday, --prev-monday, etc. (previous week's occurrence)
*/
handlePrevDay(options, currentDate) {
let targetDay;
let description;
if (options.prevMonday) { targetDay = 1; description = 'Monday'; }
else if (options.prevTuesday) { targetDay = 2; description = 'Tuesday'; }
else if (options.prevWednesday) { targetDay = 3; description = 'Wednesday'; }
else if (options.prevThursday) { targetDay = 4; description = 'Thursday'; }
else if (options.prevFriday) { targetDay = 5; description = 'Friday'; }
else if (options.prevSaturday) { targetDay = 6; description = 'Saturday'; }
else if (options.prevSunday) { targetDay = 0; description = 'Sunday'; }
const targetDate = this.findPrevWeekOccurrenceOfDay(targetDay, currentDate);
// Create UTC dates to avoid timezone issues in tests
const startDate = new Date(Date.UTC(targetDate.getUTCFullYear(), targetDate.getUTCMonth(), targetDate.getUTCDate(), 0, 0, 0, 0));
const endDate = new Date(Date.UTC(targetDate.getUTCFullYear(), targetDate.getUTCMonth(), targetDate.getUTCDate(), 23, 59, 59, 999));
return {
type: 'prev-day',
date: new Date(Date.UTC(targetDate.getUTCFullYear(), targetDate.getUTCMonth(), targetDate.getUTCDate(), 0, 0, 0, 0)),
startDate,
endDate,
description: `previous ${description}, ${targetDate.toLocaleDateString('en-US', { month: 'long', day: 'numeric' })}`
};
}
/**
* Handle --days N option
*/
handleDaysOption(days, currentDate) {
// Calculate start date (N days ago)
const startTargetDate = new Date(currentDate);
startTargetDate.setDate(currentDate.getDate() - days + 1); // Include today
// Create UTC dates to avoid timezone issues
const startDate = new Date(Date.UTC(startTargetDate.getUTCFullYear(), startTargetDate.getUTCMonth(), startTargetDate.getUTCDate(), 0, 0, 0, 0));
const endDate = new Date(Date.UTC(currentDate.getUTCFullYear(), currentDate.getUTCMonth(), currentDate.getUTCDate(), 23, 59, 59, 999));
return {
type: 'multi-day',
startDate,
endDate,
days,
description: `last ${days} day${days === 1 ? '' : 's'}`
};
}
/**
* Handle --on N option (exactly N days ago)
*/
handleOnOption(daysAgo, currentDate) {
const targetDate = new Date(currentDate);
targetDate.setDate(currentDate.getDate() - daysAgo);
return {
type: 'single-day',
date: targetDate,
startDate: new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate(), 0, 0, 0, 0),
endDate: new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate(), 23, 59, 59, 999),
description: `${daysAgo} day${daysAgo === 1 ? '' : 's'} ago`
};
}
/**
* Handle --today option
*/
handleTodayOption(currentDate) {
return {
type: 'single-day',
date: currentDate,
startDate: new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate(), 0, 0, 0, 0),
endDate: new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate(), 23, 59, 59, 999),
description: 'today'
};
}
/**
* Handle --prev option (previous week)
*/
handlePrevWeek(currentDate) {
// Calculate the previous Monday-Sunday week
const today = new Date(currentDate);
const dayOfWeek = today.getDay(); // 0=Sunday, 1=Monday, ..., 6=Saturday
// Find this week's Monday
const daysFromMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
const thisMonday = new Date(today);
thisMonday.setDate(today.getDate() - daysFromMonday);
// Previous week is 7 days before this Monday
const prevMonday = new Date(thisMonday);
prevMonday.setDate(thisMonday.getDate() - 7);
// Previous week's Sunday
const prevSunday = new Date(prevMonday);
prevSunday.setDate(prevMonday.getDate() + 6);
return {
type: 'multi-day',
startDate: new Date(Date.UTC(prevMonday.getUTCFullYear(), prevMonday.getUTCMonth(), prevMonday.getUTCDate(), 0, 0, 0, 0)),
endDate: new Date(Date.UTC(prevSunday.getUTCFullYear(), prevSunday.getUTCMonth(), prevSunday.getUTCDate(), 23, 59, 59, 999)),
days: 7,
description: `previous week`
};
}
/**
* Handle default smart yesterday logic
*/
handleSmartYesterday(currentDate) {
const currentDay = currentDate.getDay(); // 0=Sunday, 1=Monday, ..., 6=Saturday
let targetDate;
if (currentDay === 1) { // Monday
// Show Friday's work
targetDate = new Date(currentDate);
targetDate.setDate(currentDate.getDate() - 3);
} else {
// Show yesterday's work
targetDate = new Date(currentDate);
targetDate.setDate(currentDate.getDate() - 1);
}
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
// Create UTC dates to avoid timezone issues
const startDate = new Date(Date.UTC(targetDate.getUTCFullYear(), targetDate.getUTCMonth(), targetDate.getUTCDate(), 0, 0, 0, 0));
const endDate = new Date(Date.UTC(targetDate.getUTCFullYear(), targetDate.getUTCMonth(), targetDate.getUTCDate(), 23, 59, 59, 999));
// Create appropriate description based on what day we're looking back to
let description;
if (currentDay === 1) { // Monday looking back to Friday
description = `since ${dayNames[targetDate.getDay()]}, ${targetDate.toLocaleDateString('en-US', { month: 'long', day: 'numeric' })}`;
} else {
description = `yesterday, ${dayNames[targetDate.getDay()]}, ${targetDate.toLocaleDateString('en-US', { month: 'long', day: 'numeric' })}`;
}
return {
type: 'smart-yesterday',
startDate,
endDate,
description
};
}
/**
* Handle --after and --before options
*/
handleDateRangeOptions(options, currentDate) {
let startDate, endDate;
if (options.after) {
startDate = new Date(options.after);
startDate.setHours(0, 0, 0, 0);
} else {
// Default to 30 days ago if only --before is specified
startDate = new Date(currentDate);
startDate.setDate(currentDate.getDate() - 30);
startDate.setHours(0, 0, 0, 0);
}
if (options.before) {
endDate = new Date(options.before);
endDate.setHours(23, 59, 59, 999);
} else {
// Default to today if only --after is specified
endDate = new Date(currentDate);
endDate.setHours(23, 59, 59, 999);
}
const daysDiff = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));
return {
type: 'date-range',
startDate,
endDate,
days: daysDiff,
description: `${startDate.toLocaleDateString()} to ${endDate.toLocaleDateString()}`
};
}
}
module.exports = TimespanAnalyzer;