ts-time-utils
Version:
A comprehensive TypeScript utility library for time, dates, durations, and calendar operations with full tree-shaking support
383 lines (382 loc) • 11.7 kB
JavaScript
import { MILLISECONDS_PER_SECOND, MILLISECONDS_PER_MINUTE, MILLISECONDS_PER_HOUR, MILLISECONDS_PER_DAY, MILLISECONDS_PER_WEEK } from './constants.js';
/**
* Represents a duration of time with arithmetic and conversion capabilities
*/
export class Duration {
constructor(input) {
if (typeof input === 'number') {
this._milliseconds = input;
}
else if (typeof input === 'string') {
this._milliseconds = this.parseString(input);
}
else {
this._milliseconds = this.calculateMilliseconds(input);
}
}
/**
* Create Duration from milliseconds
*/
static fromMilliseconds(ms) {
return new Duration(ms);
}
/**
* Create Duration from seconds
*/
static fromSeconds(seconds) {
return new Duration(seconds * MILLISECONDS_PER_SECOND);
}
/**
* Create Duration from minutes
*/
static fromMinutes(minutes) {
return new Duration(minutes * MILLISECONDS_PER_MINUTE);
}
/**
* Create Duration from hours
*/
static fromHours(hours) {
return new Duration(hours * MILLISECONDS_PER_HOUR);
}
/**
* Create Duration from days
*/
static fromDays(days) {
return new Duration(days * MILLISECONDS_PER_DAY);
}
/**
* Create Duration from weeks
*/
static fromWeeks(weeks) {
return new Duration(weeks * MILLISECONDS_PER_WEEK);
}
/**
* Create Duration from a string (e.g., "1h 30m", "2.5 hours", "90 seconds")
*/
static fromString(str) {
return new Duration(str);
}
/**
* Create Duration between two dates
*/
static between(start, end) {
return new Duration(Math.abs(end.getTime() - start.getTime()));
}
/**
* Get duration in milliseconds
*/
get milliseconds() {
return this._milliseconds;
}
/**
* Get duration in seconds
*/
get seconds() {
return this._milliseconds / MILLISECONDS_PER_SECOND;
}
/**
* Get duration in minutes
*/
get minutes() {
return this._milliseconds / MILLISECONDS_PER_MINUTE;
}
/**
* Get duration in hours
*/
get hours() {
return this._milliseconds / MILLISECONDS_PER_HOUR;
}
/**
* Get duration in days
*/
get days() {
return this._milliseconds / MILLISECONDS_PER_DAY;
}
/**
* Get duration in weeks
*/
get weeks() {
return this._milliseconds / MILLISECONDS_PER_WEEK;
}
/**
* Add another duration
*/
add(other) {
const otherDuration = this.normalizeToDuration(other);
return new Duration(this._milliseconds + otherDuration._milliseconds);
}
/**
* Subtract another duration
*/
subtract(other) {
const otherDuration = this.normalizeToDuration(other);
return new Duration(Math.max(0, this._milliseconds - otherDuration._milliseconds));
}
/**
* Multiply duration by a factor
*/
multiply(factor) {
return new Duration(this._milliseconds * factor);
}
/**
* Divide duration by a factor
*/
divide(factor) {
if (factor === 0) {
throw new Error('Cannot divide duration by zero');
}
return new Duration(this._milliseconds / factor);
}
/**
* Get absolute duration (always positive)
*/
abs() {
return new Duration(Math.abs(this._milliseconds));
}
/**
* Negate duration
*/
negate() {
return new Duration(-this._milliseconds);
}
/**
* Check if duration equals another
*/
equals(other) {
const otherMs = typeof other === 'number' ? other : other._milliseconds;
return Math.abs(this._milliseconds - otherMs) < 1; // Allow for floating point precision
}
/**
* Check if duration is greater than another
*/
greaterThan(other) {
const otherMs = typeof other === 'number' ? other : other._milliseconds;
return this._milliseconds > otherMs;
}
/**
* Check if duration is less than another
*/
lessThan(other) {
const otherMs = typeof other === 'number' ? other : other._milliseconds;
return this._milliseconds < otherMs;
}
/**
* Compare with another duration (-1, 0, 1)
*/
compareTo(other) {
if (this._milliseconds < other._milliseconds)
return -1;
if (this._milliseconds > other._milliseconds)
return 1;
return 0;
}
/**
* Check if duration is zero
*/
isZero() {
return Math.abs(this._milliseconds) < 1;
}
/**
* Check if duration is positive
*/
isPositive() {
return this._milliseconds > 0;
}
/**
* Check if duration is negative
*/
isNegative() {
return this._milliseconds < 0;
}
/**
* Convert to human-readable string
*/
toString() {
if (this.isZero())
return '0ms';
const parts = [];
let remaining = Math.abs(this._milliseconds);
const units = [
{ name: 'd', value: MILLISECONDS_PER_DAY },
{ name: 'h', value: MILLISECONDS_PER_HOUR },
{ name: 'm', value: MILLISECONDS_PER_MINUTE },
{ name: 's', value: MILLISECONDS_PER_SECOND },
{ name: 'ms', value: 1 }
];
for (const unit of units) {
if (remaining >= unit.value) {
const count = Math.floor(remaining / unit.value);
parts.push(`${count}${unit.name}`);
remaining %= unit.value;
}
}
const result = parts.join(' ');
return this._milliseconds < 0 ? `-${result}` : result;
}
/**
* Convert to detailed object representation
*/
toObject() {
const ms = Math.abs(this._milliseconds);
const days = Math.floor(ms / MILLISECONDS_PER_DAY);
const hours = Math.floor((ms % MILLISECONDS_PER_DAY) / MILLISECONDS_PER_HOUR);
const minutes = Math.floor((ms % MILLISECONDS_PER_HOUR) / MILLISECONDS_PER_MINUTE);
const seconds = Math.floor((ms % MILLISECONDS_PER_MINUTE) / MILLISECONDS_PER_SECOND);
const milliseconds = ms % MILLISECONDS_PER_SECOND;
return {
years: 0, // Years/months require complex calendar calculations
months: 0,
weeks: Math.floor(days / 7),
days: days,
hours,
minutes,
seconds,
milliseconds
};
}
/**
* Convert to JSON representation
*/
toJSON() {
return this._milliseconds;
}
/**
* Create Duration from JSON
*/
static fromJSON(ms) {
return new Duration(ms);
}
calculateMilliseconds(input) {
let total = 0;
if (input.milliseconds)
total += input.milliseconds;
if (input.seconds)
total += input.seconds * MILLISECONDS_PER_SECOND;
if (input.minutes)
total += input.minutes * MILLISECONDS_PER_MINUTE;
if (input.hours)
total += input.hours * MILLISECONDS_PER_HOUR;
if (input.days)
total += input.days * MILLISECONDS_PER_DAY;
if (input.weeks)
total += input.weeks * MILLISECONDS_PER_WEEK;
// Approximate conversions for months and years
if (input.months)
total += input.months * MILLISECONDS_PER_DAY * 30.44; // Average month
if (input.years)
total += input.years * MILLISECONDS_PER_DAY * 365.25; // Average year
return total;
}
parseString(str) {
const normalized = str.toLowerCase().trim();
// Handle simple formats like "1000", "1000ms"
if (/^\d+(?:ms)?$/.test(normalized)) {
return parseInt(normalized.replace('ms', ''));
}
// Handle complex formats like "1h 30m 45s"
const patterns = [
{ regex: /(\d+(?:\.\d+)?)\s*y(?:ears?)?(?!\w)/g, multiplier: MILLISECONDS_PER_DAY * 365.25 },
{ regex: /(\d+(?:\.\d+)?)\s*mo(?:nths?)?(?!\w)/g, multiplier: MILLISECONDS_PER_DAY * 30.44 },
{ regex: /(\d+(?:\.\d+)?)\s*w(?:eeks?)?(?!\w)/g, multiplier: MILLISECONDS_PER_WEEK },
{ regex: /(\d+(?:\.\d+)?)\s*d(?:ays?)?(?!\w)/g, multiplier: MILLISECONDS_PER_DAY },
{ regex: /(\d+(?:\.\d+)?)\s*h(?:ours?)?(?!\w)/g, multiplier: MILLISECONDS_PER_HOUR },
{ regex: /(\d+(?:\.\d+)?)\s*min(?:utes?)?(?!\w)/g, multiplier: MILLISECONDS_PER_MINUTE },
{ regex: /(\d+(?:\.\d+)?)\s*m(?!s|o)(?!\w)/g, multiplier: MILLISECONDS_PER_MINUTE },
{ regex: /(\d+(?:\.\d+)?)\s*s(?:ec(?:onds?)?)?(?!\w)/g, multiplier: MILLISECONDS_PER_SECOND },
{ regex: /(\d+(?:\.\d+)?)\s*ms(?!\w)/g, multiplier: 1 }
];
let total = 0;
let hasMatch = false;
for (const pattern of patterns) {
let match;
while ((match = pattern.regex.exec(normalized)) !== null) {
total += parseFloat(match[1]) * pattern.multiplier;
hasMatch = true;
}
pattern.regex.lastIndex = 0; // Reset regex
}
if (!hasMatch) {
throw new Error(`Invalid duration format: ${str}`);
}
return total;
}
normalizeToDuration(input) {
if (input instanceof Duration) {
return input;
}
return new Duration(input);
}
}
/**
* Create a new Duration instance
*/
export function createDuration(input) {
return new Duration(input);
}
/**
* Check if a value is a valid duration
*/
export function isValidDuration(value) {
return value instanceof Duration;
}
/**
* Parse duration from various formats
*/
export function parseDurationString(input) {
return new Duration(input);
}
/**
* Format duration to human-readable string
*/
export function formatDurationString(duration, options) {
const d = typeof duration === 'number' ? new Duration(duration) : duration;
if (options?.long) {
const obj = d.toObject();
const parts = [];
const units = [
{ key: 'days', singular: 'day', plural: 'days' },
{ key: 'hours', singular: 'hour', plural: 'hours' },
{ key: 'minutes', singular: 'minute', plural: 'minutes' },
{ key: 'seconds', singular: 'second', plural: 'seconds' }
];
for (const unit of units) {
const value = obj[unit.key];
if (value > 0) {
parts.push(`${value} ${value === 1 ? unit.singular : unit.plural}`);
}
}
return parts.length > 0 ? parts.join(', ') : '0 seconds';
}
return d.toString();
}
/**
* Get the maximum duration from an array
*/
export function maxDuration(...durations) {
if (durations.length === 0)
return null;
return durations.reduce((max, current) => current.greaterThan(max) ? current : max);
}
/**
* Get the minimum duration from an array
*/
export function minDuration(...durations) {
if (durations.length === 0)
return null;
return durations.reduce((min, current) => current.lessThan(min) ? current : min);
}
/**
* Sum multiple durations
*/
export function sumDurations(...durations) {
return durations.reduce((sum, current) => sum.add(current), new Duration(0));
}
/**
* Get average duration from an array
*/
export function averageDuration(...durations) {
if (durations.length === 0)
return null;
const sum = sumDurations(...durations);
return sum.divide(durations.length);
}