Merge pull request #13 from jacobtread/feat-copy-duration-formats

feat: copy duration formats settings
This commit is contained in:
Jacob 2024-08-04 17:48:15 +12:00 committed by GitHub
commit b0e052d9a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 157 additions and 32 deletions

View file

@ -4,7 +4,7 @@ import { Timekeep } from "@/schema";
import React, { useState, useEffect } from "react";
import { useSettings } from "@/contexts/use-settings-context";
import { useTimekeepStore } from "@/contexts/use-timekeep-store";
import { formatDuration, formatDurationHoursTrunc } from "@/utils";
import { formatDurationLong, formatDurationShort } from "@/utils";
import {
isKeepRunning,
getRunningEntry,
@ -36,10 +36,10 @@ function getTimingState(timekeep: Timekeep): TimingState {
return {
running: runningEntry !== null,
current: formatDuration(current),
currentShort: formatDurationHoursTrunc(current),
total: formatDuration(total),
totalShort: formatDurationHoursTrunc(total),
current: formatDurationLong(current),
currentShort: formatDurationShort(current),
total: formatDurationLong(total),
totalShort: formatDurationShort(total),
};
}

View file

@ -1,6 +1,6 @@
import moment from "moment";
import { TimeEntry } from "@/schema";
import { formatDuration } from "@/utils";
import { formatDurationLong } from "@/utils";
import React, { useState, useEffect } from "react";
import { isEntryRunning, getEntryDuration } from "@/timekeep";
@ -46,5 +46,5 @@ export default function TimesheetRowDuration({ entry }: Props) {
function getFormattedDuration(entry: TimeEntry): string {
const currentTime = moment();
const duration = getEntryDuration(entry, currentTime);
return formatDuration(duration);
return formatDurationLong(duration);
}

View file

@ -6,8 +6,8 @@ import { TimekeepSettings } from "@/settings";
import { Page, View, Text, Document, StyleSheet } from "@/pdf";
import {
formatPdfDate,
formatDuration,
formatDurationHoursTrunc,
formatDurationLong,
formatDurationShort,
} from "@/utils";
import TimesheetPdfTable from "./TimesheetPdfTable";
@ -83,8 +83,8 @@ export default function TimesheetPdf({
const duration = getTotalDuration(data.entries, currentTime);
const currentDate = formatPdfDate(currentTime, settings);
const totalDuration = formatDuration(duration);
const totalDurationShort = formatDurationHoursTrunc(duration);
const totalDuration = formatDurationLong(duration);
const totalDurationShort = formatDurationShort(duration);
// Individual field within the details section
const DetailField = ({ name, value }: { name: string; value: string }) => (

View file

@ -4,7 +4,7 @@ import { getEntryDuration } from "@/timekeep";
import { TimekeepSettings } from "@/settings";
import { Timekeep, TimeEntry } from "@/schema";
import { View, Text, StyleSheet } from "@/pdf";
import { formatDuration, formatPdfRowDate } from "@/utils";
import { formatPdfRowDate, formatDurationLong } from "@/utils";
type Props = {
data: Timekeep;
@ -142,7 +142,7 @@ type RowProps = {
function TimesheetPdfTableRow({ entry, currentTime, settings }: RowProps) {
const duration = getEntryDuration(entry, currentTime);
const durationFormatted = formatDuration(duration);
const durationFormatted = formatDurationLong(duration);
// Render start and end timing for individual entries
const renderTiming = entry.startTime !== null && (

View file

@ -53,7 +53,10 @@ export function createRawTableEntries(
// Include duration for entries that are finished
(entry.startTime !== null && entry.endTime !== null) ||
entry.subEntries !== null
? formatDuration(getEntryDuration(entry, currentTime))
? formatDuration(
settings.exportDurationFormat,
getEntryDuration(entry, currentTime)
)
: "",
],
];

View file

@ -1,8 +1,8 @@
import type { Moment } from "moment";
import { formatDuration } from "@/utils";
import { getTotalDuration } from "@/timekeep";
import { TimekeepSettings } from "@/settings";
import { Timekeep, TimeEntry } from "@/schema";
import { DurationFormat, TimekeepSettings } from "@/settings";
import { RawTableRow, TOTAL_COLUMNS, createRawTable } from "@/export";
/**
@ -22,8 +22,13 @@ function createHeader(): RawTableRow {
* @param currentTime The current time to use for unfinished entries
* @returns The created row
*/
function createFooter(entries: TimeEntry[], currentTime: Moment): RawTableRow {
function createFooter(
entries: TimeEntry[],
currentTime: Moment,
durationFormat: DurationFormat
): RawTableRow {
const total: string = formatDuration(
durationFormat,
getTotalDuration(entries, currentTime)
);
return ["**Total**", "", "", `**${total}**`];
@ -68,7 +73,11 @@ export function createMarkdownTable(
// Markdown raw table contents
...createRawTable(timekeep.entries, settings, currentTime),
// Markdown footer row
createFooter(timekeep.entries, currentTime),
createFooter(
timekeep.entries,
currentTime,
settings.exportDurationFormat
),
];
// Array of indexes for all the columns (0 - TOTAL_COLUMNS)

View file

@ -2,6 +2,7 @@ import { Store } from "@/store";
import TimekeepPlugin from "@/main";
import { App, Setting, PluginSettingTab } from "obsidian";
import {
DurationFormat,
defaultSettings,
TimekeepSettings,
PdfExportBehavior,
@ -244,5 +245,27 @@ export class TimekeepSettingsTab extends PluginSettingTab {
}));
});
});
new Setting(this.containerEl)
.setName("CSV/Markdown duration format")
.setDesc("Format to show durations as when copying as CSV/Markdown")
.addDropdown((t) => {
t.addOptions({
[DurationFormat.LONG]:
"Long - Format including all units (1h 30m 25s)",
[DurationFormat.SHORT]:
"Short - Format just including hours (1.5h)",
[DurationFormat.DECIMAL]:
"Decimal - Short format without units (1.5)",
});
t.setValue(String(settings.exportDurationFormat));
t.onChange((v) => {
this.settingsStore.setState((currentValue) => ({
...currentValue,
exportDurationFormat: v as DurationFormat,
}));
});
});
}
}

View file

@ -7,6 +7,15 @@ export enum PdfExportBehavior {
OPEN_FILE = "OPEN_FILE",
}
export enum DurationFormat {
// Format including all units (1h 30m 25s)
LONG = "LONG",
// Format just including hours (1.5h)
SHORT = "SHORT",
// Short format without units (1.5)
DECIMAL = "DECIMAL",
}
export interface TimekeepSettings {
csvDelimiter: string;
csvTitle: boolean;
@ -20,6 +29,7 @@ export interface TimekeepSettings {
reverseSegmentOrder: boolean;
timestampFormat: string;
showDecimalHours: boolean;
exportDurationFormat: DurationFormat;
}
export const defaultSettings: TimekeepSettings = {
@ -36,4 +46,5 @@ export const defaultSettings: TimekeepSettings = {
reverseSegmentOrder: false,
limitTableSize: true,
showDecimalHours: true,
exportDurationFormat: DurationFormat.SHORT,
};

View file

@ -1,14 +1,16 @@
import moment from "moment";
import { defaultSettings, TimekeepSettings } from "@/settings";
import { DurationFormat, defaultSettings, TimekeepSettings } from "@/settings";
import {
formatPdfDate,
formatDuration,
formatTimestamp,
formatPdfRowDate,
formatDurationLong,
formatDurationShort,
formatDurationDecimal,
parseEditableTimestamp,
formatEditableTimestamp,
formatDurationHoursTrunc,
} from "./time";
it("should format time", () => {
@ -59,7 +61,7 @@ describe("format duration", () => {
])(
'for duration "%s" should expected formatted "%s"',
(input, expected) => {
const output = formatDuration(input);
const output = formatDurationLong(input);
expect(output).toBe(expected);
}
@ -75,7 +77,52 @@ describe("format duration short", () => {
])(
'for duration "%s" should expected formatted "%s"',
(input, expected) => {
const output = formatDurationHoursTrunc(input);
const output = formatDurationShort(input);
expect(output).toBe(expected);
}
);
});
describe("format duration decimal", () => {
test.each([
[1000 * 60 * 60 * 2, "2.00"],
[1000 * 60 * 60 * 25.25, "25.25"],
[1000 * 60 * 60 * 25.255, "25.25"],
[1000 * 60 * 60 * 50.5, "50.50"],
])(
'for duration "%s" should expected formatted "%s"',
(input, expected) => {
const output = formatDurationDecimal(input);
expect(output).toBe(expected);
}
);
});
describe("format duration with format", () => {
test.each([
[DurationFormat.LONG, 1000, "1s"],
[DurationFormat.LONG, 1000 * 12, "12s"],
[DurationFormat.LONG, 1000 * 60, "1m 0s"],
[DurationFormat.LONG, 1000 * 60 * 2, "2m 0s"],
[DurationFormat.LONG, 1000 * 60 * 60 * 2, "2h 0s"],
[DurationFormat.LONG, 1000 * 60 * 60 * 2.5, "2h 30m 0s"],
[DurationFormat.LONG, 1000 * 60 * 60 * 2.505, "2h 30m 18s"],
[DurationFormat.SHORT, 1000 * 60 * 60 * 2, "2.00h"],
[DurationFormat.SHORT, 1000 * 60 * 60 * 25.25, "25.25h"],
[DurationFormat.SHORT, 1000 * 60 * 60 * 25.255, "25.25h"],
[DurationFormat.SHORT, 1000 * 60 * 60 * 50.5, "50.50h"],
[DurationFormat.DECIMAL, 1000 * 60 * 60 * 2, "2.00"],
[DurationFormat.DECIMAL, 1000 * 60 * 60 * 25.25, "25.25"],
[DurationFormat.DECIMAL, 1000 * 60 * 60 * 25.255, "25.25"],
[DurationFormat.DECIMAL, 1000 * 60 * 60 * 50.5, "50.50"],
])(
'for duration "%s" should expected formatted "%s"',
(format, input, expected) => {
const output = formatDuration(format, input);
expect(output).toBe(expected);
}

View file

@ -1,5 +1,5 @@
import moment, { Moment } from "moment";
import { TimekeepSettings } from "@/settings";
import { DurationFormat, TimekeepSettings } from "@/settings";
/**
* Formats a timestamp for tables and generated output
@ -44,16 +44,37 @@ export function parseEditableTimestamp(
return moment(formatted, settings.editableTimestampFormat, true);
}
/**
* Formats a duration using the provided duration format
*
* @param format The format to use
* @param durationMS The duration to format
* @returns The formatted duration
*/
export function formatDuration(
format: DurationFormat,
durationMS: number
): string {
switch (format) {
case DurationFormat.LONG:
return formatDurationLong(durationMS);
case DurationFormat.SHORT:
return formatDurationShort(durationMS);
case DurationFormat.DECIMAL:
return formatDurationDecimal(durationMS);
}
}
/**
* Formats the provided duration in the form
* of hours, minutes, and seconds
*
* @param totalTime The duration to format
* @param durationMS The duration to format in milliseconds
* @returns The formatted duration
*/
export function formatDuration(totalTime: number): string {
export function formatDurationLong(durationMS: number): string {
let ret = "";
const duration = moment.duration(totalTime);
const duration = moment.duration(durationMS);
const hours = Math.floor(duration.asHours());
if (hours > 0) ret += hours + "h ";
@ -64,19 +85,30 @@ export function formatDuration(totalTime: number): string {
}
/**
* Formats a duration in the form of hours only
* minutes will be counted a portions of an hour
* (i.e 1h 30m will be 1.5h)
* Same as {@see formatDurationDecimal} but with a "h" suffix
* indicating its hours
*
* @param totalTime The duration to format
* @param durationMS The duration to format in milliseconds
* @returns The formatted duration
*/
export function formatDurationHoursTrunc(totalTime: number): string {
const duration = moment.duration(totalTime);
export function formatDurationShort(durationMS: number): string {
return formatDurationDecimal(durationMS) + "h";
}
/**
* Formats a duration in the form of hours only
* minutes will be counted a portions of an hour
* (i.e 1h 30m will be 1.5)
*
* @param durationMS The duration to format
* @returns The formatted duration
*/
export function formatDurationDecimal(durationMS: number): string {
const duration = moment.duration(durationMS);
const hours = duration.asHours();
return hours.toFixed(2) + "h";
return hours.toFixed(2);
}
/**