UNPKG

veffect

Version:

powerful TypeScript validation library built on the robust foundation of Effect combining exceptional type safety, high performance, and developer experience. Taking inspiration from Effect's functional principles, VEffect delivers a balanced approach tha

743 lines (681 loc) 18 kB
/** * @since 2.0.0 */ import * as Equal from "./Equal.js" import type * as equivalence from "./Equivalence.js" import { dual } from "./Function.js" import * as Hash from "./Hash.js" import type { Inspectable } from "./Inspectable.js" import { NodeInspectSymbol } from "./Inspectable.js" import * as Option from "./Option.js" import * as order from "./Order.js" import type { Pipeable } from "./Pipeable.js" import { pipeArguments } from "./Pipeable.js" import { hasProperty, isBigInt, isNumber, isString } from "./Predicate.js" const TypeId: unique symbol = Symbol.for("effect/Duration") const bigint0 = BigInt(0) const bigint24 = BigInt(24) const bigint60 = BigInt(60) const bigint1e3 = BigInt(1_000) const bigint1e6 = BigInt(1_000_000) const bigint1e9 = BigInt(1_000_000_000) /** * @since 2.0.0 * @category symbol */ export type TypeId = typeof TypeId /** * @since 2.0.0 * @category models */ export interface Duration extends Equal.Equal, Pipeable, Inspectable { readonly [TypeId]: TypeId readonly value: DurationValue } /** * @since 2.0.0 * @category models */ export type DurationValue = | { _tag: "Millis"; millis: number } | { _tag: "Nanos"; nanos: bigint } | { _tag: "Infinity" } /** * @since 2.0.0 * @category models */ export type Unit = | "nano" | "nanos" | "micro" | "micros" | "milli" | "millis" | "second" | "seconds" | "minute" | "minutes" | "hour" | "hours" | "day" | "days" | "week" | "weeks" /** * @since 2.0.0 * @category models */ export type DurationInput = | Duration | number // millis | bigint // nanos | [seconds: number, nanos: number] | `${number} ${Unit}` const DURATION_REGEX = /^(-?\d+(?:\.\d+)?)\s+(nanos?|micros?|millis?|seconds?|minutes?|hours?|days?|weeks?)$/ /** * @since 2.0.0 */ export const decode = (input: DurationInput): Duration => { if (isDuration(input)) { return input } else if (isNumber(input)) { return millis(input) } else if (isBigInt(input)) { return nanos(input) } else if (Array.isArray(input)) { if (input.length === 2 && isNumber(input[0]) && isNumber(input[1])) { return nanos(BigInt(input[0]) * bigint1e9 + BigInt(input[1])) } } else if (isString(input)) { DURATION_REGEX.lastIndex = 0 // Reset the lastIndex before each use const match = DURATION_REGEX.exec(input) if (match) { const [_, valueStr, unit] = match const value = Number(valueStr) switch (unit) { case "nano": case "nanos": return nanos(BigInt(valueStr)) case "micro": case "micros": return micros(BigInt(valueStr)) case "milli": case "millis": return millis(value) case "second": case "seconds": return seconds(value) case "minute": case "minutes": return minutes(value) case "hour": case "hours": return hours(value) case "day": case "days": return days(value) case "week": case "weeks": return weeks(value) } } } throw new Error("Invalid DurationInput") } /** * @since 2.5.0 */ export const decodeUnknown: (u: unknown) => Option.Option<Duration> = Option.liftThrowable(decode) as any const zeroValue: DurationValue = { _tag: "Millis", millis: 0 } const infinityValue: DurationValue = { _tag: "Infinity" } const DurationProto: Omit<Duration, "value"> = { [TypeId]: TypeId, [Hash.symbol](this: Duration) { return Hash.cached(this, Hash.structure(this.value)) }, [Equal.symbol](this: Duration, that: unknown): boolean { return isDuration(that) && equals(this, that) }, toString(this: Duration) { return `Duration(${format(this)})` }, toJSON(this: Duration) { switch (this.value._tag) { case "Millis": return { _id: "Duration", _tag: "Millis", millis: this.value.millis } case "Nanos": return { _id: "Duration", _tag: "Nanos", hrtime: toHrTime(this) } case "Infinity": return { _id: "Duration", _tag: "Infinity" } } }, [NodeInspectSymbol]() { return this.toJSON() }, pipe() { return pipeArguments(this, arguments) } } as const const make = (input: number | bigint): Duration => { const duration = Object.create(DurationProto) if (isNumber(input)) { if (isNaN(input) || input <= 0) { duration.value = zeroValue } else if (!Number.isFinite(input)) { duration.value = infinityValue } else if (!Number.isInteger(input)) { duration.value = { _tag: "Nanos", nanos: BigInt(Math.round(input * 1_000_000)) } } else { duration.value = { _tag: "Millis", millis: input } } } else if (input <= bigint0) { duration.value = zeroValue } else { duration.value = { _tag: "Nanos", nanos: input } } return duration } /** * @since 2.0.0 * @category guards */ export const isDuration = (u: unknown): u is Duration => hasProperty(u, TypeId) /** * @since 2.0.0 * @category guards */ export const isFinite = (self: Duration): boolean => self.value._tag !== "Infinity" /** * @since 2.0.0 * @category constructors */ export const zero: Duration = make(0) /** * @since 2.0.0 * @category constructors */ export const infinity: Duration = make(Infinity) /** * @since 2.0.0 * @category constructors */ export const nanos = (nanos: bigint): Duration => make(nanos) /** * @since 2.0.0 * @category constructors */ export const micros = (micros: bigint): Duration => make(micros * bigint1e3) /** * @since 2.0.0 * @category constructors */ export const millis = (millis: number): Duration => make(millis) /** * @since 2.0.0 * @category constructors */ export const seconds = (seconds: number): Duration => make(seconds * 1000) /** * @since 2.0.0 * @category constructors */ export const minutes = (minutes: number): Duration => make(minutes * 60_000) /** * @since 2.0.0 * @category constructors */ export const hours = (hours: number): Duration => make(hours * 3_600_000) /** * @since 2.0.0 * @category constructors */ export const days = (days: number): Duration => make(days * 86_400_000) /** * @since 2.0.0 * @category constructors */ export const weeks = (weeks: number): Duration => make(weeks * 604_800_000) /** * @since 2.0.0 * @category getters */ export const toMillis = (self: DurationInput): number => { const _self = decode(self) switch (_self.value._tag) { case "Infinity": return Infinity case "Nanos": return Number(_self.value.nanos) / 1_000_000 case "Millis": return _self.value.millis } } /** * @since 2.0.0 * @category getters */ export const toSeconds = (self: DurationInput): number => toMillis(self) / 1_000 /** * Get the duration in nanoseconds as a bigint. * * If the duration is infinite, returns `Option.none()` * * @since 2.0.0 * @category getters */ export const toNanos = (self: DurationInput): Option.Option<bigint> => { const _self = decode(self) switch (_self.value._tag) { case "Infinity": return Option.none() case "Nanos": return Option.some(_self.value.nanos) case "Millis": return Option.some(BigInt(Math.round(_self.value.millis * 1_000_000))) } } /** * Get the duration in nanoseconds as a bigint. * * If the duration is infinite, it throws an error. * * @since 2.0.0 * @category getters */ export const unsafeToNanos = (self: DurationInput): bigint => { const _self = decode(self) switch (_self.value._tag) { case "Infinity": throw new Error("Cannot convert infinite duration to nanos") case "Nanos": return _self.value.nanos case "Millis": return BigInt(Math.round(_self.value.millis * 1_000_000)) } } /** * @since 2.0.0 * @category getters */ export const toHrTime = (self: DurationInput): [seconds: number, nanos: number] => { const _self = decode(self) switch (_self.value._tag) { case "Infinity": return [Infinity, 0] case "Nanos": return [ Number(_self.value.nanos / bigint1e9), Number(_self.value.nanos % bigint1e9) ] case "Millis": return [ Math.floor(_self.value.millis / 1000), Math.round((_self.value.millis % 1000) * 1_000_000) ] } } /** * @since 2.0.0 * @category pattern matching */ export const match: { <A, B>( options: { readonly onMillis: (millis: number) => A readonly onNanos: (nanos: bigint) => B } ): (self: DurationInput) => A | B <A, B>( self: DurationInput, options: { readonly onMillis: (millis: number) => A readonly onNanos: (nanos: bigint) => B } ): A | B } = dual(2, <A, B>( self: DurationInput, options: { readonly onMillis: (millis: number) => A readonly onNanos: (nanos: bigint) => B } ): A | B => { const _self = decode(self) switch (_self.value._tag) { case "Nanos": return options.onNanos(_self.value.nanos) case "Infinity": return options.onMillis(Infinity) case "Millis": return options.onMillis(_self.value.millis) } }) /** * @since 2.0.0 * @category pattern matching */ export const matchWith: { <A, B>( that: DurationInput, options: { readonly onMillis: (self: number, that: number) => A readonly onNanos: (self: bigint, that: bigint) => B } ): (self: DurationInput) => A | B <A, B>( self: DurationInput, that: DurationInput, options: { readonly onMillis: (self: number, that: number) => A readonly onNanos: (self: bigint, that: bigint) => B } ): A | B } = dual(3, <A, B>( self: DurationInput, that: DurationInput, options: { readonly onMillis: (self: number, that: number) => A readonly onNanos: (self: bigint, that: bigint) => B } ): A | B => { const _self = decode(self) const _that = decode(that) if (_self.value._tag === "Infinity" || _that.value._tag === "Infinity") { return options.onMillis( toMillis(_self), toMillis(_that) ) } else if (_self.value._tag === "Nanos" || _that.value._tag === "Nanos") { const selfNanos = _self.value._tag === "Nanos" ? _self.value.nanos : BigInt(Math.round(_self.value.millis * 1_000_000)) const thatNanos = _that.value._tag === "Nanos" ? _that.value.nanos : BigInt(Math.round(_that.value.millis * 1_000_000)) return options.onNanos(selfNanos, thatNanos) } return options.onMillis( _self.value.millis, _that.value.millis ) }) /** * @category instances * @since 2.0.0 */ export const Order: order.Order<Duration> = order.make((self, that) => matchWith(self, that, { onMillis: (self, that) => (self < that ? -1 : self > that ? 1 : 0), onNanos: (self, that) => (self < that ? -1 : self > that ? 1 : 0) }) ) /** * Checks if a `Duration` is between a `minimum` and `maximum` value. * * @category predicates * @since 2.0.0 */ export const between: { (options: { minimum: DurationInput maximum: DurationInput }): (self: DurationInput) => boolean (self: DurationInput, options: { minimum: DurationInput maximum: DurationInput }): boolean } = order.between(order.mapInput(Order, decode)) /** * @category instances * @since 2.0.0 */ export const Equivalence: equivalence.Equivalence<Duration> = (self, that) => matchWith(self, that, { onMillis: (self, that) => self === that, onNanos: (self, that) => self === that }) const _min = order.min(Order) /** * @since 2.0.0 */ export const min: { (that: DurationInput): (self: DurationInput) => Duration (self: DurationInput, that: DurationInput): Duration } = dual(2, (self: DurationInput, that: DurationInput): Duration => _min(decode(self), decode(that))) const _max = order.max(Order) /** * @since 2.0.0 */ export const max: { (that: DurationInput): (self: DurationInput) => Duration (self: DurationInput, that: DurationInput): Duration } = dual(2, (self: DurationInput, that: DurationInput): Duration => _max(decode(self), decode(that))) const _clamp = order.clamp(Order) /** * @since 2.0.0 */ export const clamp: { (options: { minimum: DurationInput maximum: DurationInput }): (self: DurationInput) => Duration (self: DurationInput, options: { minimum: DurationInput maximum: DurationInput }): Duration } = dual( 2, (self: DurationInput, options: { minimum: DurationInput maximum: DurationInput }): Duration => _clamp(decode(self), { minimum: decode(options.minimum), maximum: decode(options.maximum) }) ) /** * @since 2.4.19 * @category math */ export const divide: { (by: number): (self: DurationInput) => Option.Option<Duration> (self: DurationInput, by: number): Option.Option<Duration> } = dual( 2, (self: DurationInput, by: number): Option.Option<Duration> => match(self, { onMillis: (millis) => { if (by === 0 || isNaN(by) || !Number.isFinite(by)) { return Option.none() } return Option.some(make(millis / by)) }, onNanos: (nanos) => { if (isNaN(by) || by <= 0 || !Number.isFinite(by)) { return Option.none() } try { return Option.some(make(nanos / BigInt(by))) } catch (e) { return Option.none() } } }) ) /** * @since 2.4.19 * @category math */ export const unsafeDivide: { (by: number): (self: DurationInput) => Duration (self: DurationInput, by: number): Duration } = dual( 2, (self: DurationInput, by: number): Duration => match(self, { onMillis: (millis) => make(millis / by), onNanos: (nanos) => { if (isNaN(by) || by < 0 || Object.is(by, -0)) { return zero } else if (Object.is(by, 0) || !Number.isFinite(by)) { return infinity } return make(nanos / BigInt(by)) } }) ) /** * @since 2.0.0 * @category math */ export const times: { (times: number): (self: DurationInput) => Duration (self: DurationInput, times: number): Duration } = dual( 2, (self: DurationInput, times: number): Duration => match(self, { onMillis: (millis) => make(millis * times), onNanos: (nanos) => make(nanos * BigInt(times)) }) ) /** * @since 2.0.0 * @category math */ export const subtract: { (that: DurationInput): (self: DurationInput) => Duration (self: DurationInput, that: DurationInput): Duration } = dual( 2, (self: DurationInput, that: DurationInput): Duration => matchWith(self, that, { onMillis: (self, that) => make(self - that), onNanos: (self, that) => make(self - that) }) ) /** * @since 2.0.0 * @category math */ export const sum: { (that: DurationInput): (self: DurationInput) => Duration (self: DurationInput, that: DurationInput): Duration } = dual( 2, (self: DurationInput, that: DurationInput): Duration => matchWith(self, that, { onMillis: (self, that) => make(self + that), onNanos: (self, that) => make(self + that) }) ) /** * @since 2.0.0 * @category predicates */ export const lessThan: { (that: DurationInput): (self: DurationInput) => boolean (self: DurationInput, that: DurationInput): boolean } = dual( 2, (self: DurationInput, that: DurationInput): boolean => matchWith(self, that, { onMillis: (self, that) => self < that, onNanos: (self, that) => self < that }) ) /** * @since 2.0.0 * @category predicates */ export const lessThanOrEqualTo: { (that: DurationInput): (self: DurationInput) => boolean (self: DurationInput, that: DurationInput): boolean } = dual( 2, (self: DurationInput, that: DurationInput): boolean => matchWith(self, that, { onMillis: (self, that) => self <= that, onNanos: (self, that) => self <= that }) ) /** * @since 2.0.0 * @category predicates */ export const greaterThan: { (that: DurationInput): (self: DurationInput) => boolean (self: DurationInput, that: DurationInput): boolean } = dual( 2, (self: DurationInput, that: DurationInput): boolean => matchWith(self, that, { onMillis: (self, that) => self > that, onNanos: (self, that) => self > that }) ) /** * @since 2.0.0 * @category predicates */ export const greaterThanOrEqualTo: { (that: DurationInput): (self: DurationInput) => boolean (self: DurationInput, that: DurationInput): boolean } = dual( 2, (self: DurationInput, that: DurationInput): boolean => matchWith(self, that, { onMillis: (self, that) => self >= that, onNanos: (self, that) => self >= that }) ) /** * @since 2.0.0 * @category predicates */ export const equals: { (that: DurationInput): (self: DurationInput) => boolean (self: DurationInput, that: DurationInput): boolean } = dual(2, (self: DurationInput, that: DurationInput): boolean => Equivalence(decode(self), decode(that))) /** * Converts a `Duration` to a human readable string. * @since 2.0.0 * * @example * import * as Duration from "effect/Duration" * * Duration.format(Duration.millis(1000)) // "1s" * Duration.format(Duration.millis(1001)) // "1s 1ms" */ export const format = (self: DurationInput): string => { const duration = decode(self) const parts = [] if (duration.value._tag === "Infinity") { return "Infinity" } const nanos = unsafeToNanos(duration) if (nanos % bigint1e6) { parts.push(`${nanos % bigint1e6}ns`) } const ms = nanos / bigint1e6 if (ms % bigint1e3 !== bigint0) { parts.push(`${ms % bigint1e3}ms`) } const sec = ms / bigint1e3 if (sec % bigint60 !== bigint0) { parts.push(`${sec % bigint60}s`) } const min = sec / bigint60 if (min % bigint60 !== bigint0) { parts.push(`${min % bigint60}m`) } const hr = min / bigint60 if (hr % bigint24 !== bigint0) { parts.push(`${hr % bigint24}h`) } const days = hr / bigint24 if (days !== bigint0) { parts.push(`${days}d`) } return parts.reverse().join(" ") }