klog.js
Version:
A JavaScript implementation of the Klog time tracking file format
115 lines (114 loc) • 4.22 kB
JavaScript
import { format } from "date-fns";
import { Duration } from "./duration.js";
import { Entry } from "./entry.js";
import { Range } from "./range.js";
import { Summary } from "./summary.js";
export var RecordDateFormat;
(function (RecordDateFormat) {
RecordDateFormat[RecordDateFormat["Dashes"] = 0] = "Dashes";
RecordDateFormat[RecordDateFormat["Slashes"] = 1] = "Slashes";
})(RecordDateFormat || (RecordDateFormat = {}));
// TODO: `normalise` to apply sweeping format changes throughout the tree (dash spacing, am/pm or 24 hour, explicit duration +, etc)
// TODO: method for getting warnings about record (e.g. overlapping time ranges). See what Klog handles.
// TODO: maybe always just use string/string[] in summaries rather than forcing the user to submit a Summary class.
/**
* A block of time entries for any given day.
*/
export class Record {
date;
entries;
summary;
shouldTotal;
dateFormat;
/** Create a new record. */
constructor(
/** The date of the record. */
date,
/** Entries belonging to the record. */
entries = [],
/** Optional summary or tags associated with the entire record. */
summary = null,
/** The expected duration all the entries in the record should sum up to. */
shouldTotal = null,
/** What dividers should be used for the date when using `toString` */
dateFormat = RecordDateFormat.Dashes) {
this.date = date;
this.entries = entries;
this.summary = summary;
this.shouldTotal = shouldTotal;
this.dateFormat = dateFormat;
}
/** @internal */
static fromAST = (node) => {
return new this(node.date, node.entries.map(Entry.fromAST), node.summary ? new Summary(node.summary) : null, node.shouldTotal ? Duration.fromMinutes(node.shouldTotal) : null);
};
/**
* The currently open entry, if any.
*/
get openEntry() {
return (this.entries.find((e) => e.value instanceof Range && e.value.open) || null);
}
/**
* The date string formatted according to the record's date format.
*/
get dateString() {
const dateSep = this.dateFormat === RecordDateFormat.Dashes ? "-" : "/";
return format(this.date, `yyyy${dateSep}MM${dateSep}dd`);
}
/**
* Calculate the difference between the record's actual total duration and the expected total duration.
*/
shouldTotalDiff() {
const actual = this.toDuration();
if (!this.shouldTotal)
return actual;
return actual.subtract(this.shouldTotal);
}
/**
* Start a new time entry.
* @param startTime - The time when the new entry starts.
* @param summary - The new entry's summary.
* @throws {Error} There is already an open entry in the record.
*/
start(startTime, summary = null) {
if (this.openEntry)
throw new Error("Records can only have one open range at a time");
this.entries.push(new Entry(new Range(startTime), summary));
}
/**
* End the currently open time entry, if any.
* @param endTime - The time the open range will end.
* @throws {Error} There are no open entries in the record.
*/
end(endTime) {
if (!this.openEntry)
throw new Error("Record does not have any currently open ranges");
this.openEntry.value.end = endTime;
}
/**
* Converts the total duration of all entries to minutes.
*/
toMinutes() {
return this.entries.map((e) => e.toMinutes()).reduce((a, b) => a + b, 0);
}
/**
* Converts the total duration of all entries to a duration.
*/
toDuration() {
return Duration.fromMinutes(this.toMinutes());
}
/**
* Render the record as a Klog string.
*/
toString() {
// TODO: support windows newlines
let headline = this.dateString;
if (this.shouldTotal?.toMinutes())
headline += ` (${this.shouldTotal}!)`;
const summary = this.summary
? this.summary.toString(null, false) + "\n"
: "";
const entries = this.entries.map((e) => e.toString()).join("\n");
return headline + "\n" + summary + entries;
}
}