import Duration, {DurationLike, toDuration} from './duration.js'; import {ISO8601Result} from './parse.js'; import {PlainDateTime} from './plain-date-time.js'; import {RoundingMode} from './types.js'; import { TimeComponents, balanceTimeDuration, deltaTimeComponents, internalRoundTimeDuration, durationToTimeDuration, verifyTimeComponents, toTimeDuration, parseUnit, } from './utilities.js'; import {ZonedDateTime, pad} from './zoned-date-time.js'; const time = /(\d{2})(?::(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?))/; export type PlainTimeLike = PlainTime | ZonedDateTime | Partial | string; interface PlainTimeDeltaParameters { /// The largest unit of time to round to in the resulting duration, defaults to 'auto'. largestUnit?: 'auto' | 'hour' | 'minute' | 'second' | 'millisecond'; /// The smallest unit of time to round to in the resulting duration, defaults to 'millisecond', which means no rounding. smallestUnit?: 'hour' | 'minute' | 'second' | 'millisecond'; /// The granularity to round to, of the unit given by smallestUnit. Defaults to `1`. roundingIncrement?: number; /// Rounding method to use roundingMode?: RoundingMode; } interface PlainTimeRoundParameters { /// The smallest unit of time to round to in the resulting duration, defaults to 'millisecond', which means no rounding. smallestUnit: 'hour' | 'minute' | 'second' | 'millisecond'; /// The granularity to round to, of the unit given by smallestUnit. Defaults to `1`. roundingIncrement?: number; /// Rounding method to use roundingMode?: RoundingMode; } interface PlainTimeToStringParameters { /// The smallest unit of time to round to in the resulting duration, defaults to 'millisecond', which means no rounding. smallestUnit?: 'hour' | 'minute' | 'second' | 'millisecond'; /// Rounding method to use roundingMode?: RoundingMode; /// fractionalSecondDigits?: 'auto' | 0 | 1 | 2 | 3; } export function toPlainTime(plainTimeLike: PlainTimeLike) { return plainTimeLike instanceof PlainTime ? plainTimeLike : PlainTime.from(plainTimeLike); } export class PlainTime { readonly hour: number; readonly minute: number; readonly second: number; readonly millisecond: number; constructor(isoHour: number = 0, isoMinute: number = 0, isoSecond: number = 0, isoMillisecond: number = 0) { verifyTimeComponents(isoHour, isoMinute, isoSecond, isoMillisecond, 'reject'); this.hour = isoHour; this.minute = isoMinute; this.second = isoSecond; this.millisecond = isoMillisecond; } toLocaleString(locales?: string | string[], options: Intl.DateTimeFormatOptions = {}) { let resolvedOptions: Intl.DateTimeFormatOptions = 'timeStyle' in options ? {} : { hour: 'numeric', minute: '2-digit', second: '2-digit', }; let formatter = new Intl.DateTimeFormat(locales, { ...resolvedOptions, ...options, timeZone: 'UTC', }); return formatter.format(Date.UTC(0, 0, 0, this.hour, this.minute, this.second, this.millisecond)); } toString(options: PlainTimeToStringParameters = {}) { let smallestUnit = parseUnit(options.smallestUnit); let date = smallestUnit ? this.round({ smallestUnit, roundingMode: options.roundingMode ?? 'trunc', }) : this; let fractionalSecondDigits = options.fractionalSecondDigits ?? 'auto'; switch (smallestUnit) { case 'minute': return `${pad(date.hour)}:${pad(date.minute)}`; case 'second': return `${pad(date.hour)}:${pad(date.minute)}:${pad(date.second)}`; default: let time = `${pad(date.hour)}:${pad(date.minute)}:${pad(date.second)}`; if (options.fractionalSecondDigits === 0 || (fractionalSecondDigits === 'auto' && date.millisecond === 0)) { return time; } return `${time}.${pad(date.millisecond, 3)}`; } } toJSON() { return this.toString(); } add(durationLike: DurationLike): PlainTime { let duration = toDuration(durationLike); let selfDuration = toTimeDuration(this); let timeDuration = durationToTimeDuration(duration) + selfDuration; let {hours, minutes, seconds, milliseconds} = balanceTimeDuration(timeDuration, 'hour'); return new PlainTime(hours % 24, minutes, seconds, milliseconds); } subtract(durationLike: DurationLike): PlainTime { return this.add(toDuration(durationLike).negated()); } with(timeComponents: Partial, options: {overflow?: 'reject' | 'constrain'} = {}) { let components = verifyTimeComponents( timeComponents.hour ?? this.hour, timeComponents.minute ?? this.minute, timeComponents.second ?? this.second, timeComponents.millisecond ?? this.millisecond, options.overflow ?? 'constrain', ); return new PlainTime(components.hour, components.minute, components.second, components.millisecond); } until(other: PlainTimeLike, options: PlainTimeDeltaParameters = {}) { let timeComponents = deltaTimeComponents(this, toPlainTime(other)); let smallestUnit = parseUnit(options.smallestUnit, 'millisecond'); let largestUnit = parseUnit(options.largestUnit, 'hour'); let roundingIncrement = options.roundingIncrement || 1; let roundingMode = options.roundingMode ?? 'trunc'; if (largestUnit === 'auto') { largestUnit = 'hour'; } let duration = Duration.from(balanceTimeDuration(timeComponents, largestUnit)); if (smallestUnit !== 'millisecond' || roundingIncrement !== 1) { return duration.round({ largestUnit, smallestUnit, roundingIncrement, roundingMode, }); } return duration; } since(other: PlainTimeLike, options: PlainTimeDeltaParameters = {}) { return toPlainTime(other).until(this, options); } round(options: PlainTimeRoundParameters | PlainTimeRoundParameters['smallestUnit']) { let smallestUnit: PlainTimeRoundParameters['smallestUnit']; let roundingIncrement: Required; let roundingMode: Required; if (typeof options === 'string') { smallestUnit = parseUnit(options); roundingIncrement = 1; roundingMode = 'halfExpand'; } else { smallestUnit = parseUnit(options.smallestUnit); roundingIncrement = options.roundingIncrement ?? 1; roundingMode = options.roundingMode ?? 'halfExpand'; } let timeDuration = toTimeDuration(this); ({timeDuration} = internalRoundTimeDuration(0, timeDuration, roundingIncrement, roundingMode, smallestUnit)); let {hours, minutes, seconds, milliseconds} = balanceTimeDuration(timeDuration, 'hour'); return new PlainTime(hours % 24, minutes, seconds, milliseconds); } equals(other: PlainTimeLike) { let otherTime = toPlainTime(other); return ( this.hour === otherTime.hour && this.minute === otherTime.minute && this.second === otherTime.second && this.millisecond === otherTime.millisecond ); } valueOf() { throw new Error('Built-in arithmetic operators isn’t supported on PlainTime'); } static from(item: PlainTimeLike, options: {overflow?: 'reject' | 'constrain'} = {}): PlainTime { if (item instanceof PlainTime || item instanceof ZonedDateTime || item instanceof PlainDateTime) { return new PlainTime(item.hour, item.minute, item.second, item.millisecond); } else if (item && typeof item === 'object' && !Array.isArray(item)) { let {hour, minute, second, millisecond} = verifyTimeComponents( item.hour || 0, item.minute || 0, item.second || 0, item.millisecond || 0, options.overflow ?? 'constrain', ); return new PlainTime(hour, minute, second, millisecond); } else if (typeof item === 'string') { let result = ISO8601Result.parseDateTime(item); if (result && result.time) { return new PlainTime(result.time.hour, result.time.minute, result.time.second, result.time.millisecond); } let match = item.match(time); if (match) { let [rawHour, rawMinute, rawSecond, rawMillisecond] = match.slice(1); let [hour, minute, second, millisecond] = ISO8601Result.parseTimeGroups( rawHour, rawMinute, rawSecond, rawMillisecond, ); return new PlainTime(hour, minute, second, millisecond); } throw new RangeError('Not currently supported'); } throw new RangeError('Unknown type'); } static compare(one: PlainTimeLike, two: PlainTimeLike) { return Math.sign(toTimeDuration(toPlainTime(one)) - toTimeDuration(toPlainTime(two))); } }