diff options
Diffstat (limited to 'src/duration.ts')
-rw-r--r-- | src/duration.ts | 155 |
1 files changed, 155 insertions, 0 deletions
diff --git a/src/duration.ts b/src/duration.ts new file mode 100644 index 0000000..3d1a44c --- /dev/null +++ b/src/duration.ts @@ -0,0 +1,155 @@ +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<string, DurationUnit> = { + 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<DurationUnit> => + 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<T> = ( + arg: T, +) => (builder: DurationBuilder) => DurationBuilder; + +export const withMillis: DurationBuilderField<number> = + (millis) => (builder) => ({ + ...builder, + millis, + }); + +export const withSeconds: DurationBuilderField<number> = + (seconds) => (builder) => ({ + ...builder, + seconds, + }); + +export const withMinutes: DurationBuilderField<number> = + (minutes) => (builder) => ({ + ...builder, + minutes, + }); + +export const withHours: DurationBuilderField<number> = + (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<string, Duration> => { + 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<string, DurationBuilder>(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), + ); +}; |