uptime/src/util/duration.ts

140 lines
3.9 KiB
TypeScript

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)
);
};