From 6572f95389f1455aee67742f82fd499aa98ac9ee Mon Sep 17 00:00:00 2001 From: Marko Korhonen Date: Tue, 14 Nov 2023 17:03:18 +0200 Subject: [PATCH] Fix many issues in calculation and improve output formatting --- src/format.ts | 18 +++++++++- src/main.ts | 96 ++++++++++++++++++++++++++++++++++++--------------- src/parse.ts | 2 +- 3 files changed, 86 insertions(+), 30 deletions(-) diff --git a/src/format.ts b/src/format.ts index 2792924..3be9bb4 100644 --- a/src/format.ts +++ b/src/format.ts @@ -3,7 +3,23 @@ import { Duration } from 'dayjs/plugin/duration.js'; export const formatTimestamp = (timestamp: Dayjs): string => timestamp.format('YYYY-MM-DD HH:mm'); -export const formatDuration = (unLogged: Duration): string => unLogged.format('HH[ hours and ]mm [minutes]'); +export const formatTime = (time: Dayjs): string => time.format('HH:mm'); + +export const formatDuration = (duration: Duration, short?: boolean): string => { + if (duration.hours() === 0 && duration.minutes() === 0) { + return 'none'; + } + + const formatString = short + ? 'HH:mm' + : duration.hours() > 0 && duration.minutes() > 0 + ? `H [hour${duration.hours() > 1 ? 's' : ''} and] m [minute${duration.minutes() > 1 ? 's' : ''}]` + : duration.hours() > 0 + ? `H [hour${duration.hours() > 1 ? 's' : ''}]` + : `m [minute${duration.minutes() > 1 ? 's' : ''}]`; + + return duration.format(formatString); +}; export const getHoursRoundedStr = (duration: Duration) => `(${getHoursRounded(duration)} as hours rounded to next even 15 minutes)`; diff --git a/src/main.ts b/src/main.ts index 2cca12c..563d872 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,12 @@ import chalk from 'chalk'; import dayjs, { Dayjs } from 'dayjs'; import * as readline from 'readline/promises'; -import { formatDuration, formatTimestamp, getHoursRoundedStr } from './format.js'; -import { Duration } from 'dayjs/plugin/duration'; +import { formatDuration, formatTime, formatTimestamp, getHoursRoundedStr } from './format.js'; +import duration, { Duration } from 'dayjs/plugin/duration.js'; import { parseDuration, parseTimestamp } from './parse.js'; +dayjs.extend(duration); + const { log, error } = console; const defaultStartTime = '08:00'; const lunchBreakDuration = dayjs.duration({ minutes: 30 }); @@ -16,14 +18,18 @@ const main = async () => { output: process.stdout, }); - let started: Dayjs | undefined = undefined; + let startedAt: Dayjs | undefined = undefined; + const now = dayjs(); try { // Get work day duration let workDayDuration: Duration | undefined = undefined; const durationAnswer = await rl.question( - `How long is your work day today, excluding the lunch break? [${formatDuration(defaultWorkDayDuration)}] `, + `How long is your work day today, excluding the lunch break? [${formatDuration( + defaultWorkDayDuration, + true, + )}] `, ); if (durationAnswer !== '') { workDayDuration = parseDuration(durationAnswer); @@ -44,8 +50,8 @@ const main = async () => { // Calculate worked time const startTimeAnswer = await rl.question(`What time did you start work today? [${defaultStartTime}] `); if (startTimeAnswer !== '') { - started = parseTimestamp(startTimeAnswer); - if (!started.isValid()) { + startedAt = parseTimestamp(startTimeAnswer); + if (!startedAt.isValid()) { error( chalk.red( `Failed to parse ${startTimeAnswer} to time, using default start time ${defaultStartTime}`, @@ -54,54 +60,88 @@ const main = async () => { } } - if (!started?.isValid()) { - started = parseTimestamp(defaultStartTime); + if (!startedAt?.isValid()) { + startedAt = parseTimestamp(defaultStartTime); } - let stopped: Dayjs | undefined = undefined; + let stoppedWorking = false; + let stoppedAt: Dayjs | undefined = undefined; const stoppedAnswer = await rl.question( - "What time did you stop working (leave empty if you didn't stop yet)? ", + `What time did you stop working (default is current time if you didn't stop yet)? [${formatTime(now)}] `, ); if (stoppedAnswer === '') { - stopped = dayjs(); + stoppedAt = now; } else { - stopped = parseTimestamp(stoppedAnswer); - if (!stopped.isValid()) { + stoppedWorking = true; + stoppedAt = parseTimestamp(stoppedAnswer); + if (!stoppedAt.isValid()) { error(`Failed to parse ${stoppedAnswer} to time, using current time`); - stopped = dayjs(); + stoppedAt = dayjs(); } } - let worked = dayjs.duration(stopped.diff(started)); + if (stoppedAt.isSame(startedAt) || stoppedAt.isBefore(startedAt)) { + error( + chalk.red( + `Start time (${formatTime(startedAt)}) needs to be before stop time (${formatTime( + stoppedAt, + )}). Exiting`, + ), + ); + process.exit(1); + } + + let worked = dayjs.duration(stoppedAt.diff(startedAt)); if ((await rl.question('Did you have a lunch break? [Y/n] ')).toLowerCase() !== 'n') { worked = worked.subtract(lunchBreakDuration); } // Calculate unlogged time - let unLogged: Duration | undefined = undefined; let loggedAnswer = await rl.question('How many hours did you log already? [00:00] '); if (loggedAnswer === '') { loggedAnswer = '00:00'; } const logged = parseDuration(loggedAnswer); - const unLoggedDuration = workDayDuration.subtract(logged); - if (unLoggedDuration.asMinutes() > 0) { - unLogged = unLoggedDuration; + let unLogged: Duration | undefined = undefined; + + if (logged.asMinutes() === worked.asMinutes()) { + unLogged = worked.subtract(logged); + } else { + unLogged = worked.subtract(logged); } // Log result log(); - log('Started working at', formatTimestamp(started)); - log('Stopped working at', formatTimestamp(stopped)); - log('Total worked today:', chalk.green(formatDuration(worked)), chalk.yellow(getHoursRoundedStr(worked))); - log( - 'Unlogged today:', - unLogged - ? `${chalk.red(formatDuration(unLogged))} ${chalk.yellow(getHoursRoundedStr(unLogged))}` - : chalk.green('none'), - ); + log('Started working at:', formatTimestamp(startedAt)); + log((stoppedWorking ? 'Stopped working' : 'Hours calculated') + ' at:', formatTimestamp(stoppedAt)); + log('Worked today:', chalk.green(formatDuration(worked)), chalk.yellow(getHoursRoundedStr(worked))); + + if (unLogged.asMinutes() == 0) { + log('Unlogged today:', chalk.green('none')); + } else if (unLogged.asMinutes() > 0) { + log('Unlogged today:', chalk.red(formatDuration(unLogged)), chalk.yellow(getHoursRoundedStr(unLogged))); + } else if (unLogged.asMinutes() < 0) { + log( + chalk.red(`You have logged ${formatDuration(unLogged)} more than you worked today!`), + chalk.yellow(getHoursRoundedStr(unLogged)), + ); + } + + const workLeft = workDayDuration.subtract(worked); + const workLeftMinutes = workLeft.asMinutes(); + if (workLeftMinutes > 0) { + log('You still have to work', chalk.green(formatDuration(workLeft)), 'more today'); + } else if (workLeft.asMinutes() < 0) { + log( + 'You worked', + chalk.green( + formatDuration(dayjs.duration({ minutes: Math.round(workLeftMinutes * -1) })), + 'overtime today', + ), + ); + } } finally { rl.close(); } diff --git a/src/parse.ts b/src/parse.ts index bf3e4d8..a3cab22 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -5,7 +5,7 @@ import duration, { Duration } from 'dayjs/plugin/duration.js'; dayjs.extend(customParseFormat); dayjs.extend(duration); -export const parseTimestamp = (time: string): Dayjs => dayjs(time, 'H:mm', true); +export const parseTimestamp = (time: string): Dayjs => dayjs(time, 'HH:mm', true); export const parseDuration = (time: string): Duration => { const [hours, minutes] = time.split(':').map(Number);