2,991 bytes · the TypeScript implementation · view raw
import { type Duration } from"./time_duration_types.ts";
const MAX_DIGITS = 6;
function notADuration(text: string): RangeError {
returnnew RangeError(`"${text}" is not a duration: write it as 1h30m, 90m or 01:30`);
}
function isDigit(ch: string): boolean {
return ch >= "0" && ch <= "9";
}
/** Read a run of digits from `at`; returns [value, next index], or null if there is none. */function readNumber(text: string, s: string, at: number): [number, number] | null {
let end = at;
while (end < s.length && isDigit(s[end])) end++;
if (end === at) returnnull;
if (end - at > MAX_DIGITS) throw notADuration(text);
return [Number(s.slice(at, end)), end];
}
function skipSpaces(s: string, at: number): number {
while (at < s.length && (s[at] === " " || s[at] === "\t")) at++;
return at;
}
/** One written duration, in minutes. */function parseOne(text: string): number {
if (typeof text !== "string") throw notADuration(String(text));
const s = text.slice(skipSpaces(text, 0)).replace(/[ \t]+$/, "");
const colon = s.indexOf(":");
if (colon >= 0) {
const hours = readNumber(text, s, 0);
if (hours === null || hours[1] !== colon) throw notADuration(text);
const minutes = readNumber(text, s, colon + 1);
if (minutes === null || minutes[1] !== s.length || minutes[1] - colon - 1 !== 2) throw notADuration(text);
if (minutes[0] >= 60) thrownew RangeError(`minutes must be under 60 when hours are given, in "${text}"`);
return hours[0] * 60 + minutes[0];
}
let at = 0;
let hours: number | null = null;
let minutes: number | null = null;
while (at < s.length) {
const read = readNumber(text, s, at);
if (read === null) throw notADuration(text);
at = skipSpaces(s, read[1]);
const unit = s[at];
if ((unit === "h" || unit === "H") && hours === null && minutes === null) hours = read[0];
elseif ((unit === "m" || unit === "M") && minutes === null) minutes = read[0];
elsethrow notADuration(text);
at = skipSpaces(s, at + 1);
}
if (hours === null && minutes === null) throw notADuration(text);
if (hours !== null && minutes !== null && minutes >= 60) {
thrownew RangeError(`minutes must be under 60 when hours are given, in "${text}"`);
}
return (hours ?? 0) * 60 + (minutes ?? 0);
}
/** * Parse each written duration, add them, and give the total in minutes and in * both written forms. The clock form does not wrap at 24 hours, because a * total of work is not a time of day. */exportfunction duration(parts: readonly string[]): Duration {
let total = 0;
for (const part of parts) total += parseOne(part);
const hours = Math.floor(total / 60);
const minutes = total - hours * 60;
let text: string;
if (hours > 0 && minutes > 0) text = `${hours}h${minutes}m`;
elseif (hours > 0) text = `${hours}h`;
else text = `${minutes}m`;
const clock = `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`;
return { minutes: total, text, clock };
}