import { flow, pipe } from "fp-ts/function"; import * as E from "fp-ts/lib/Either"; import * as S from "fp-ts/lib/string"; import * as O from "fp-ts/lib/Option"; import * as R from "fp-ts/lib/ReadonlyArray"; export type Duration = number; export enum DurationUnit { MILLISECOND, SECOND, MINUTE, HOUR } const durationUnitMap: Record = { ms: DurationUnit.MILLISECOND, milliseconds: DurationUnit.MILLISECOND, sec: DurationUnit.SECOND, seconds: DurationUnit.SECOND, min: DurationUnit.MINUTE, minutes: DurationUnit.MINUTE, hr: DurationUnit.HOUR, hour: DurationUnit.HOUR, hours: DurationUnit.HOUR }; const getDurationUnit = (key: string): O.Option => O.fromNullable(durationUnitMap[key.toLowerCase()]); export const getMs = (duration: Duration): number => duration; export const getSeconds = (duration: Duration): number => duration / 1000; export const getMinutes = (duration: Duration): number => getSeconds(duration) / 60; export const getHours = (duration: Duration): number => getMinutes(duration) / 60; export const format = (duration: Duration): string => { const ms = getMs(duration) % 1000; const seconds = getSeconds(duration) % 60; const minutes = getMinutes(duration) % 60; const hours = getHours(duration); return ( [hours, minutes, seconds].map((x) => Math.floor(x).toString().padStart(2, "0")).join(":") + "." + ms.toString().padStart(3, "0") ); }; export interface DurationBuilder { readonly millis: number; readonly seconds: number; readonly minutes: number; readonly hours: number; } export const createDurationBuilder = (): DurationBuilder => ({ millis: 0, seconds: 0, minutes: 0, hours: 0 }); export type DurationBuilderField = (arg: T) => (builder: DurationBuilder) => DurationBuilder; export const withMillis: DurationBuilderField = (millis) => (builder) => ({ ...builder, millis }); export const withSeconds: DurationBuilderField = (seconds) => (builder) => ({ ...builder, seconds }); export const withMinutes: DurationBuilderField = (minutes) => (builder) => ({ ...builder, minutes }); export const withHours: DurationBuilderField = (hours) => (builder) => ({ ...builder, hours }); export const build = (builder: DurationBuilder): Duration => builder.millis + builder.seconds * 1000 + builder.minutes * 60 * 1000 + builder.hours * 60 * 60 * 1000; export const parse = (duration: string): E.Either => { const parts = pipe( duration, S.split(" "), R.map(S.trim), R.filter((part) => !S.isEmpty(part)) ); const valueUnitPairs = pipe( parts, R.mapWithIndex((i, part) => { const isUnit = i % 2 !== 0; if (!isUnit) return E.right(O.none); const value = Number(parts[i - 1]); if (isNaN(value)) return E.left(`bad value: "${parts[i - 1]}"`); const unit = getDurationUnit(part); if (O.isNone(unit)) return E.left(`unknown duration type: ${part}`); return E.right(O.some([unit.value, value] as [DurationUnit, number])); }), E.sequenceArray, E.map( flow( R.filter(O.isSome), R.map(({ value }) => value) ) ) ); return pipe( valueUnitPairs, E.flatMap( R.reduce(E.of(createDurationBuilder()), (builderEither, [unit, value]) => pipe( builderEither, E.chain((builder) => { switch (unit) { case DurationUnit.MILLISECOND: return E.right(withMillis(value)(builder)); case DurationUnit.SECOND: return E.right(withSeconds(value)(builder)); case DurationUnit.MINUTE: return E.right(withMinutes(value)(builder)); case DurationUnit.HOUR: return E.right(withHours(value)(builder)); default: return E.left(`unknown unit: ${unit}`); } }) ) ) ), E.map(build) ); };