diff --git a/src/commands/breakdown.ts b/src/commands/breakdown.ts index 3706551..5a8ee00 100644 --- a/src/commands/breakdown.ts +++ b/src/commands/breakdown.ts @@ -2,7 +2,7 @@ import sAgo from 's-ago' import weekday from 'weekday' import _uniq from 'lodash/uniq' import parseDate from 'time-speak' -import { eachDayOfInterval } from 'date-fns' +import { eachHourOfInterval, eachDayOfInterval } from 'date-fns' import DB from '../db' import log from '../log' @@ -63,6 +63,7 @@ const handler = (args: BreakdownCommandArguments) => { const resultsPerDay: Record = {} const resultsPerWeekday: Record = {} + const resultsPerHour: Record = {} const filteredSheets = U.getSheetsWithEntriesSinceDate(targetSheets, since) @@ -71,12 +72,40 @@ const handler = (args: BreakdownCommandArguments) => { entries.forEach((entry: TimeSheetEntry): void => { const { start, end } = entry - const days = eachDayOfInterval({ + const interval = { start, end: end === null ? new Date() : end - }) + } + + const days = eachDayOfInterval(interval) + const hours = eachHourOfInterval(interval).map((date: Date): number => + date.getHours() + ) days.forEach((date: Date): void => { + hours.forEach((hour: number): void => { + const hourStr = hour > 11 ? `${hour - 11}pm` : `${hour + 1}am` + const duration = U.getEntryDurationInHour(entry, date, hour) + + if (typeof resultsPerHour[hourStr] === 'undefined') { + resultsPerHour[hourStr] = { + date, + duration, + sheets: [sheet], + entries: [entry] + } + } else { + const resultEntry = resultsPerHour[hourStr] + + resultsPerHour[hourStr] = { + ...resultEntry, + duration: resultEntry.duration + duration, + entries: [...resultEntry.entries, entry], + sheets: _uniq([...resultEntry.sheets, sheet]) + } + } + }) + const dateKey = date.toLocaleDateString() const dateWeekday = weekday(date.getDay() + 1) const duration = U.getEntryDurationInDay(entry, date) @@ -120,10 +149,11 @@ const handler = (args: BreakdownCommandArguments) => { }) }) - const resultsPerDayItems = Object.values(resultsPerDay) + const dayResults = Object.values(resultsPerDay) const weekdayResults = Object.keys(resultsPerWeekday) + const hourResults = Object.keys(resultsPerHour) - if (resultsPerDayItems.length === 0) { + if (dayResults.length === 0) { throw new Error('No results found') } @@ -144,8 +174,10 @@ const handler = (args: BreakdownCommandArguments) => { const resultsPerDayOutputRows: string[][] = [] const resultsPerWeekdayOutputRows: string[][] = [] + const resultsPerHourOutputRows: string[][] = [] - resultsPerDayItems.forEach(({ date, duration, sheets, entries }): void => { + dayResults.sort(({ date: a }, { date: b }): number => (a > b ? 1 : -1)) + dayResults.forEach(({ date, duration, sheets, entries }): void => { const weekdayUI = `(${weekday(date.getDay() + 1)})` const dateUI = ago ? sAgo(date) : date.toLocaleDateString() const durationUI = U.getDurationLangString(duration, humanize) @@ -162,6 +194,7 @@ const handler = (args: BreakdownCommandArguments) => { ]) }) + weekdayResults.sort((a: string, b: string) => a.localeCompare(b)) weekdayResults.forEach((weekdayStr: string): void => { const result = resultsPerWeekday[weekdayStr] const { duration, sheets, entries } = result @@ -178,11 +211,43 @@ const handler = (args: BreakdownCommandArguments) => { ]) }) + hourResults.sort((a: string, b: string) => { + const aHour = a.includes('am') + ? +a.substring(0, a.length - 2) + : +a.substring(0, a.length - 2) + 12 + + const bHour = b.includes('am') + ? +b.substring(0, b.length - 2) + : +b.substring(0, b.length - 2) + 12 + + return aHour - bHour + }) + + hourResults.forEach((hourStr: string): void => { + const result = resultsPerHour[hourStr] + const { duration, sheets, entries } = result + const durationUI = U.getDurationLangString(duration, humanize) + const sheetCountUI = U.getPluralizedArrayLength(sheets, 'sheet') + const entryCountUI = U.getPluralizedArrayLength(entries, 'entry') + + resultsPerHourOutputRows.push([ + C.clHighlightRed(' *'), + C.clHighlight(hourStr), + C.clText(entryCountUI), + C.clText(sheetCountUI), + C.clDuration(durationUI) + ]) + }) + P.printJustifiedContent(resultsPerDayOutputRows) log('') log(C.clText(' = Totals per Week Day =')) log('') P.printJustifiedContent(resultsPerWeekdayOutputRows) + log('') + log(C.clText(' = Totals per Hour =')) + log('') + P.printJustifiedContent(resultsPerHourOutputRows) } export { handler } diff --git a/src/commands/list.ts b/src/commands/list.ts index ca51c33..a1bac53 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -65,7 +65,7 @@ const handler = (args: ListCommandArgs) => { const sinceDate = !_isEmpty(since) ? parseDate(since) : today - ? D.getStartDate() + ? D.getStartOfDayDate() : null const filteredSheets = diff --git a/src/commands/sheets.ts b/src/commands/sheets.ts index 3e65163..bd10b54 100644 --- a/src/commands/sheets.ts +++ b/src/commands/sheets.ts @@ -43,7 +43,7 @@ const handler = async (args: SheetsCommandArgs) => { const sinceDate = !_isEmpty(since) ? parseDate(since) : today - ? D.getStartDate() + ? D.getStartOfDayDate() : null const filteredSheets = diff --git a/src/commands/today.ts b/src/commands/today.ts index 9138a6e..61424dd 100644 --- a/src/commands/today.ts +++ b/src/commands/today.ts @@ -37,7 +37,7 @@ const handler = (args: TodayCommandArguments) => { const sheetsWithEntriesForToday = U.getSheetsWithEntriesSinceDate( sheets, - D.getStartDate() + D.getStartOfDayDate() ) if (sheetsWithEntriesForToday.length === 0) { diff --git a/src/commands/week.ts b/src/commands/week.ts index 831cc0a..f724c26 100644 --- a/src/commands/week.ts +++ b/src/commands/week.ts @@ -34,12 +34,12 @@ interface WeekCommandArguments { const DAY_MS = 24 * 60 * 60 * 1000 const LAST_WEEK_DATE = new Date( - +D.getStartDate(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)) + +D.getStartOfDayDate(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)) ) const getSheetsWithEntriesInLastWeek = (sheets: TimeSheet[]) => { - const startOfOneWeekAgo = D.getStartDate(LAST_WEEK_DATE) - const endOfToday = D.getEndDate() + const startOfOneWeekAgo = D.getStartOfDayDate(LAST_WEEK_DATE) + const endOfToday = D.getEndOfDayDate() return sheets .filter(({ entries }) => entries.length > 0) diff --git a/src/commands/yesterday.ts b/src/commands/yesterday.ts index 8709769..fc63f46 100644 --- a/src/commands/yesterday.ts +++ b/src/commands/yesterday.ts @@ -28,8 +28,8 @@ interface YesterdayCommandArguments { const isEntryForYesterday = (entry: TimeSheetEntry): boolean => { const { start, end } = entry const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000) - const startOfYesterday = D.getStartDate(yesterday) - const endOfYesterday = D.getEndDate(yesterday) + const startOfYesterday = D.getStartOfDayDate(yesterday) + const endOfYesterday = D.getEndOfDayDate(yesterday) return ( +start >= +startOfYesterday && (end === null || +end <= +endOfYesterday) diff --git a/src/dates/get_end_date.ts b/src/dates/get_end_of_day_date.ts similarity index 70% rename from src/dates/get_end_date.ts rename to src/dates/get_end_of_day_date.ts index 3a94425..a198404 100644 --- a/src/dates/get_end_date.ts +++ b/src/dates/get_end_of_day_date.ts @@ -1,6 +1,6 @@ import _isDate from 'lodash/isDate' -const getEndDate = (date?: Date): Date => { +const getEndOfDayDate = (date?: Date): Date => { const d = _isDate(date) ? new Date(+date) : new Date() d.setHours(23) @@ -11,4 +11,4 @@ const getEndDate = (date?: Date): Date => { return d } -export default getEndDate +export default getEndOfDayDate diff --git a/src/dates/get_end_of_hour_date.ts b/src/dates/get_end_of_hour_date.ts new file mode 100644 index 0000000..a1ab8fe --- /dev/null +++ b/src/dates/get_end_of_hour_date.ts @@ -0,0 +1,14 @@ +import _isDate from 'lodash/isDate' + +const getEndOfHourDate = (hour: number, date?: Date): Date => { + const d = _isDate(date) ? date : new Date() + + d.setHours(hour) + d.setMinutes(59) + d.setSeconds(59) + d.setMilliseconds(999) + + return d +} + +export default getEndOfHourDate diff --git a/src/dates/get_start_date.ts b/src/dates/get_start_of_day_date.ts similarity index 67% rename from src/dates/get_start_date.ts rename to src/dates/get_start_of_day_date.ts index 518d8ea..041d36b 100644 --- a/src/dates/get_start_date.ts +++ b/src/dates/get_start_of_day_date.ts @@ -1,6 +1,6 @@ import _isDate from 'lodash/isDate' -const getStartDate = (date?: Date): Date => { +const getStartOfDayDate = (date?: Date): Date => { const d = _isDate(date) ? date : new Date() d.setHours(0) @@ -11,4 +11,4 @@ const getStartDate = (date?: Date): Date => { return d } -export default getStartDate +export default getStartOfDayDate diff --git a/src/dates/get_start_of_hour_date.ts b/src/dates/get_start_of_hour_date.ts new file mode 100644 index 0000000..2316de2 --- /dev/null +++ b/src/dates/get_start_of_hour_date.ts @@ -0,0 +1,14 @@ +import _isDate from 'lodash/isDate' + +const getStartOfHourDate = (hour: number, date?: Date): Date => { + const d = _isDate(date) ? date : new Date() + + d.setHours(hour) + d.setMinutes(0) + d.setSeconds(0) + d.setMilliseconds(0) + + return d +} + +export default getStartOfHourDate diff --git a/src/dates/index.ts b/src/dates/index.ts index 1c0bfb0..93294aa 100644 --- a/src/dates/index.ts +++ b/src/dates/index.ts @@ -1,15 +1,19 @@ import getDaysMS from './get_days_ms' import getHoursMS from './get_hours_ms' -import getEndDate from './get_end_date' -import getStartDate from './get_start_date' import getPastDayDate from './get_past_day_date' +import getEndOfDayDate from './get_end_of_day_date' import getFutureDayDate from './get_future_day_date' +import getEndOfHourDate from './get_end_of_hour_date' +import getStartOfDayDate from './get_start_of_day_date' +import getStartOfHourDate from './get_start_of_hour_date' export { getDaysMS, getHoursMS, - getEndDate, - getStartDate, getPastDayDate, - getFutureDayDate + getEndOfDayDate, + getFutureDayDate, + getEndOfHourDate, + getStartOfDayDate, + getStartOfHourDate } diff --git a/src/entries/is_entry_today.ts b/src/entries/is_entry_today.ts index 1142e50..75801b6 100644 --- a/src/entries/is_entry_today.ts +++ b/src/entries/is_entry_today.ts @@ -3,8 +3,8 @@ import { type TimeSheetEntry } from '../types' const isEntryToday = (entry: TimeSheetEntry): boolean => { const { start, end } = entry - const startOfToday = D.getStartDate() - const endOfToday = D.getEndDate() + const startOfToday = D.getStartOfDayDate() + const endOfToday = D.getEndOfDayDate() return +start >= +startOfToday && (end === null || +end <= +endOfToday) } diff --git a/src/print/columns/get_sheet_entry_columns.ts b/src/print/columns/get_sheet_entry_columns.ts index 351ba3f..6ef1ab6 100644 --- a/src/print/columns/get_sheet_entry_columns.ts +++ b/src/print/columns/get_sheet_entry_columns.ts @@ -1,6 +1,7 @@ import ago from 's-ago' import colors from 'colors' import _isEmpty from 'lodash/isEmpty' +import _compact from 'lodash/compact' import * as C from '../../color' import * as U from '../../utils' @@ -30,22 +31,23 @@ const getSheetEntryColumns = ( end === null ? '' : C.clDate( - printDateAgo ? ago(start) : new Date(end).toLocaleDateString() + printDateAgo ? ago(end) : new Date(end).toLocaleDateString() ) - const dateUI = end === null ? startUI : endUI const sheetNamePrefix = typeof sheetName === 'undefined' || _isEmpty(sheetName) ? '' : `${C.clText('sheet')} ${C.clSheet(sheetName)}` - return [ + return _compact([ + ' ', sheetNamePrefix, `(${idUI})`, `[${durationUI}]`, - `started ${dateUI}`, + `started ${startUI}`, + end === null ? null : `ended ${endUI}`, descriptionUI - ].map((value: string): string => + ]).map((value: string): string => isActive ? colors.bold.underline(value) : value ) } diff --git a/src/print/justified_content.ts b/src/print/justified_content.ts index d12d032..5779a2b 100644 --- a/src/print/justified_content.ts +++ b/src/print/justified_content.ts @@ -4,7 +4,7 @@ import log from '../log' type ColumnWidths = Record -const DEFAULT_PADDING = 0 +const DEFAULT_PADDING = 1 const printJustifiedContent = ( rows: Array, diff --git a/src/tests/utils/dates/get_end_date.ts b/src/tests/utils/dates/get_end_date.ts index 2a5d67f..219ee71 100644 --- a/src/tests/utils/dates/get_end_date.ts +++ b/src/tests/utils/dates/get_end_date.ts @@ -1,12 +1,12 @@ /* eslint-env mocha */ import { expect } from 'chai' -import { getEndDate } from '../../../dates' +import { getEndOfDayDate } from '../../../dates' describe('utils:dates:get_end_date', () => { it('returns a date set to the end of the provided date', () => { const date = new Date() - const result = getEndDate(date) + const result = getEndOfDayDate(date) expect(result.getFullYear()).to.equal(date.getFullYear()) expect(result.getMonth()).to.equal(date.getMonth()) diff --git a/src/tests/utils/dates/get_start_date.ts b/src/tests/utils/dates/get_start_date.ts index c642c67..8a70928 100644 --- a/src/tests/utils/dates/get_start_date.ts +++ b/src/tests/utils/dates/get_start_date.ts @@ -1,12 +1,12 @@ /* eslint-env mocha */ import { expect } from 'chai' -import { getStartDate } from '../../../dates' +import { getStartOfDayDate } from '../../../dates' describe('utils:dates:get_start_date', () => { it('returns a date set to the start of the provided date', () => { const date = new Date() - const result = getStartDate(date) + const result = getStartOfDayDate(date) expect(result.getFullYear()).to.equal(date.getFullYear()) expect(result.getMonth()).to.equal(date.getMonth()) diff --git a/src/tests/utils/get_entry_duration_in_day.ts b/src/tests/utils/get_entry_duration_in_day.ts index 123113e..34cdd6c 100644 --- a/src/tests/utils/get_entry_duration_in_day.ts +++ b/src/tests/utils/get_entry_duration_in_day.ts @@ -5,7 +5,7 @@ import * as D from '../../dates' import { type TimeSheetEntry } from '../../types' import { getEntryDurationInDay } from '../../utils' -const YESTERDAY = D.getStartDate(D.getPastDayDate(1)) +const YESTERDAY = D.getStartOfDayDate(D.getPastDayDate(1)) const YESTERDAY_MS = +YESTERDAY describe('utils:get_entry_duration_in_day', () => { diff --git a/src/utils/get_entry_duration_in_day.ts b/src/utils/get_entry_duration_in_day.ts index 1a1daad..f4fac59 100644 --- a/src/utils/get_entry_duration_in_day.ts +++ b/src/utils/get_entry_duration_in_day.ts @@ -10,8 +10,8 @@ const getEntryDurationInDay = ( const { start, end } = entry const dayDate = _isFinite(day) ? new Date(day) : (day as Date) - const dayDateStart = D.getStartDate(dayDate) - const dayDateEnd = D.getEndDate(dayDate) + const dayDateStart = D.getStartOfDayDate(dayDate) + const dayDateEnd = D.getEndOfDayDate(dayDate) if (+start < +dayDateStart && end !== null && +end < +dayDateStart) { return 0 diff --git a/src/utils/get_entry_duration_in_hour.ts b/src/utils/get_entry_duration_in_hour.ts new file mode 100644 index 0000000..4ea1201 --- /dev/null +++ b/src/utils/get_entry_duration_in_hour.ts @@ -0,0 +1,34 @@ +import _isFinite from 'lodash/isFinite' + +import * as D from '../dates' +import { type TimeSheetEntry } from '../types' + +const getEntryDurationInHour = ( + entry: TimeSheetEntry, + date: Date | number, + hour: number +): number => { + const { start, end } = entry + + const hourDate = _isFinite(date) ? new Date(date) : (date as Date) + const hourDateStart = D.getStartOfHourDate(hour, hourDate) + const hourDateEnd = D.getEndOfHourDate(hour, hourDate) + + if (+start < +hourDateStart && end !== null && +end < +hourDateStart) { + return 0 + } else if (+start > +hourDateEnd) { + return 0 + } else if (+start < +hourDateStart && end !== null && +end > +hourDateStart) { + return +end - +hourDateStart + } else if (+start < +hourDateStart && end === null) { + return +hourDateEnd - +hourDateStart + } else if (+start > +hourDateStart && end === null) { + return +hourDateEnd - +start + } else if (+start > +hourDateStart && end !== null && +end < +hourDateEnd) { + return +end - +start + } + + return 0 +} + +export default getEntryDurationInHour diff --git a/src/utils/index.ts b/src/utils/index.ts index b8c5d36..dcd7708 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,6 +5,7 @@ import parseVariadicArg from './parse_variadic_arg' import getDurationLangString from './get_duration_lang_string' import getTotalSheetDuration from './get_total_sheet_duration' import getEntryDurationInDay from './get_entry_duration_in_day' +import getEntryDurationInHour from './get_entry_duration_in_hour' import getPluralizedArrayLength from './get_pluralized_array_length' import getSheetsWithEntriesSinceDate from './get_sheets_with_entries_since_date' @@ -16,6 +17,7 @@ export { getDurationLangString, getTotalSheetDuration, getEntryDurationInDay, + getEntryDurationInHour, getPluralizedArrayLength, getSheetsWithEntriesSinceDate }