chronos-ts
Version:
A comprehensive TypeScript library for date and time manipulation, inspired by Carbon PHP. Features immutable API, intervals, periods, timezones, and i18n support.
690 lines (689 loc) • 22.9 kB
JavaScript
"use strict";
/**
* ChronosInterval - Duration/Interval handling
* @module ChronosInterval
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChronosInterval = void 0;
const types_1 = require("../types");
const utils_1 = require("../utils");
const locales_1 = require("../locales");
// ============================================================================
// ChronosInterval Class
// ============================================================================
/**
* ChronosInterval - Represents a duration/interval of time
*
* @example
* ```typescript
* // Create intervals
* const interval = ChronosInterval.create({ days: 5, hours: 3 });
* const hours = ChronosInterval.hours(24);
* const fromISO = ChronosInterval.fromISO('P1Y2M3D');
*
* // Arithmetic
* const doubled = interval.multiply(2);
* const combined = interval.add(hours);
*
* // Formatting
* console.log(interval.forHumans()); // "5 days 3 hours"
* console.log(interval.toISO()); // "P5DT3H"
* ```
*/
class ChronosInterval {
// ============================================================================
// Constructor
// ============================================================================
constructor(duration = {}, inverted = false) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
this._years = (_a = duration.years) !== null && _a !== void 0 ? _a : 0;
this._months = (_b = duration.months) !== null && _b !== void 0 ? _b : 0;
this._weeks = (_c = duration.weeks) !== null && _c !== void 0 ? _c : 0;
this._days = (_d = duration.days) !== null && _d !== void 0 ? _d : 0;
this._hours = (_e = duration.hours) !== null && _e !== void 0 ? _e : 0;
this._minutes = (_f = duration.minutes) !== null && _f !== void 0 ? _f : 0;
this._seconds = Math.floor((_g = duration.seconds) !== null && _g !== void 0 ? _g : 0);
this._milliseconds =
(_h = duration.milliseconds) !== null && _h !== void 0 ? _h : (((_j = duration.seconds) !== null && _j !== void 0 ? _j : 0) % 1) * 1000;
this._locale = (0, locales_1.getLocale)('en');
this._inverted = inverted;
}
// ============================================================================
// Static Factory Methods
// ============================================================================
/**
* Create an interval from a duration object
*/
static create(duration) {
return new ChronosInterval(duration);
}
/**
* Create an interval from years
*/
static years(years) {
return new ChronosInterval({ years });
}
/**
* Create an interval from months
*/
static months(months) {
return new ChronosInterval({ months });
}
/**
* Create an interval from weeks
*/
static weeks(weeks) {
return new ChronosInterval({ weeks });
}
/**
* Create an interval from days
*/
static days(days) {
return new ChronosInterval({ days });
}
/**
* Create an interval from hours
*/
static hours(hours) {
return new ChronosInterval({ hours });
}
/**
* Create an interval from minutes
*/
static minutes(minutes) {
return new ChronosInterval({ minutes });
}
/**
* Create an interval from seconds
*/
static seconds(seconds) {
return new ChronosInterval({ seconds });
}
/**
* Create an interval from milliseconds
*/
static milliseconds(milliseconds) {
return new ChronosInterval({ milliseconds });
}
/**
* Create a single unit interval
*/
static unit(amount, unit) {
const normalizedUnit = (0, utils_1.normalizeUnit)(unit);
const duration = {};
switch (normalizedUnit) {
case 'millisecond':
duration.milliseconds = amount;
break;
case 'second':
duration.seconds = amount;
break;
case 'minute':
duration.minutes = amount;
break;
case 'hour':
duration.hours = amount;
break;
case 'day':
duration.days = amount;
break;
case 'week':
duration.weeks = amount;
break;
case 'month':
duration.months = amount;
break;
case 'quarter':
duration.months = amount * 3;
break;
case 'year':
duration.years = amount;
break;
case 'decade':
duration.years = amount * 10;
break;
case 'century':
duration.years = amount * 100;
break;
case 'millennium':
duration.years = amount * 1000;
break;
}
return new ChronosInterval(duration);
}
/**
* Create from ISO 8601 duration string (P1Y2M3DT4H5M6S)
*/
static fromISO(iso) {
const duration = (0, utils_1.parseISODuration)(iso);
return new ChronosInterval(duration);
}
/**
* Create from a human-readable string
*
* @example
* ```typescript
* ChronosInterval.fromString('2 days 3 hours')
* ChronosInterval.fromString('1 year, 6 months')
* ```
*/
static fromString(input) {
const duration = {};
const patterns = [
[/(\d+)\s*(?:years?|y)/i, 'years'],
[/(\d+)\s*(?:months?|mo)/i, 'months'],
[/(\d+)\s*(?:weeks?|w)/i, 'weeks'],
[/(\d+)\s*(?:days?|d)/i, 'days'],
[/(\d+)\s*(?:hours?|hrs?|h)/i, 'hours'],
[/(\d+)\s*(?:minutes?|mins?|m)(?!\w)/i, 'minutes'],
[/(\d+)\s*(?:seconds?|secs?|s)(?!\w)/i, 'seconds'],
[/(\d+)\s*(?:milliseconds?|ms)/i, 'milliseconds'],
];
for (const [pattern, key] of patterns) {
const match = input.match(pattern);
if (match) {
duration[key] = parseInt(match[1], 10);
}
}
return new ChronosInterval(duration);
}
/**
* Create a zero-length interval
*/
static zero() {
return new ChronosInterval();
}
/**
* Create an interval from the difference between two dates
*
* @example
* ```typescript
* const start = new Date('2024-01-01');
* const end = new Date('2024-03-15');
* const interval = ChronosInterval.between(start, end);
* ```
*/
static between(start, end) {
const startDate = start instanceof Date ? start : start.toDate();
const endDate = end instanceof Date ? end : end.toDate();
const diffMs = endDate.getTime() - startDate.getTime();
const inverted = diffMs < 0;
const absDiffMs = Math.abs(diffMs);
// Calculate approximate components
const days = Math.floor(absDiffMs / types_1.MILLISECONDS_PER_DAY);
const remainingMs = absDiffMs % types_1.MILLISECONDS_PER_DAY;
const hours = Math.floor(remainingMs / types_1.MILLISECONDS_PER_HOUR);
const remainingAfterHours = remainingMs % types_1.MILLISECONDS_PER_HOUR;
const minutes = Math.floor(remainingAfterHours / types_1.MILLISECONDS_PER_MINUTE);
const remainingAfterMinutes = remainingAfterHours % types_1.MILLISECONDS_PER_MINUTE;
const seconds = Math.floor(remainingAfterMinutes / types_1.MILLISECONDS_PER_SECOND);
const milliseconds = remainingAfterMinutes % types_1.MILLISECONDS_PER_SECOND;
return new ChronosInterval({ days, hours, minutes, seconds, milliseconds }, inverted);
}
// ============================================================================
// Getters
// ============================================================================
get years() {
return this._years;
}
get months() {
return this._months;
}
get weeks() {
return this._weeks;
}
get days() {
return this._days;
}
get hours() {
return this._hours;
}
get minutes() {
return this._minutes;
}
get seconds() {
return this._seconds;
}
get milliseconds() {
return this._milliseconds;
}
get inverted() {
return this._inverted;
}
// ============================================================================
// Total Calculations
// ============================================================================
/**
* Get total duration in a specific unit
*/
total(unit) {
const ms = this.totalMilliseconds();
const normalizedUnit = (0, utils_1.normalizeUnit)(unit);
switch (normalizedUnit) {
case 'millisecond':
return ms;
case 'second':
return ms / types_1.MILLISECONDS_PER_SECOND;
case 'minute':
return ms / types_1.MILLISECONDS_PER_MINUTE;
case 'hour':
return ms / types_1.MILLISECONDS_PER_HOUR;
case 'day':
return ms / types_1.MILLISECONDS_PER_DAY;
case 'week':
return ms / types_1.MILLISECONDS_PER_WEEK;
case 'month':
return ms / (types_1.AVERAGE_DAYS_PER_MONTH * types_1.MILLISECONDS_PER_DAY);
case 'year':
return ms / (types_1.AVERAGE_DAYS_PER_YEAR * types_1.MILLISECONDS_PER_DAY);
default:
return ms;
}
}
/**
* Get total duration in milliseconds
*/
totalMilliseconds() {
let ms = this._milliseconds;
ms += this._seconds * types_1.MILLISECONDS_PER_SECOND;
ms += this._minutes * types_1.MILLISECONDS_PER_MINUTE;
ms += this._hours * types_1.MILLISECONDS_PER_HOUR;
ms += this._days * types_1.MILLISECONDS_PER_DAY;
ms += this._weeks * types_1.MILLISECONDS_PER_WEEK;
ms += this._months * types_1.AVERAGE_DAYS_PER_MONTH * types_1.MILLISECONDS_PER_DAY;
ms += this._years * types_1.AVERAGE_DAYS_PER_YEAR * types_1.MILLISECONDS_PER_DAY;
return this._inverted ? -ms : ms;
}
/**
* Get total duration in seconds
*/
totalSeconds() {
return this.total('seconds');
}
/**
* Get total duration in minutes
*/
totalMinutes() {
return this.total('minutes');
}
/**
* Get total duration in hours
*/
totalHours() {
return this.total('hours');
}
/**
* Get total duration in days
*/
totalDays() {
return this.total('days');
}
/**
* Get total duration in weeks
*/
totalWeeks() {
return this.total('weeks');
}
/**
* Get total duration in months (approximate)
*/
totalMonths() {
return this.total('months');
}
/**
* Get total duration in years (approximate)
*/
totalYears() {
return this.total('years');
}
// ============================================================================
// Arithmetic Operations
// ============================================================================
/**
* Add another interval to this one
*/
add(other) {
const otherInterval = other instanceof ChronosInterval ? other : ChronosInterval.create(other);
return new ChronosInterval({
years: this._years + otherInterval._years,
months: this._months + otherInterval._months,
weeks: this._weeks + otherInterval._weeks,
days: this._days + otherInterval._days,
hours: this._hours + otherInterval._hours,
minutes: this._minutes + otherInterval._minutes,
seconds: this._seconds + otherInterval._seconds,
milliseconds: this._milliseconds + otherInterval._milliseconds,
});
}
/**
* Subtract another interval from this one
*/
subtract(other) {
const otherInterval = other instanceof ChronosInterval ? other : ChronosInterval.create(other);
return new ChronosInterval({
years: this._years - otherInterval._years,
months: this._months - otherInterval._months,
weeks: this._weeks - otherInterval._weeks,
days: this._days - otherInterval._days,
hours: this._hours - otherInterval._hours,
minutes: this._minutes - otherInterval._minutes,
seconds: this._seconds - otherInterval._seconds,
milliseconds: this._milliseconds - otherInterval._milliseconds,
});
}
/**
* Multiply the interval by a factor
*/
multiply(factor) {
return new ChronosInterval({
years: this._years * factor,
months: this._months * factor,
weeks: this._weeks * factor,
days: this._days * factor,
hours: this._hours * factor,
minutes: this._minutes * factor,
seconds: this._seconds * factor,
milliseconds: this._milliseconds * factor,
});
}
/**
* Divide the interval by a factor
*/
divide(factor) {
if (factor === 0) {
throw new Error('Cannot divide by zero');
}
return this.multiply(1 / factor);
}
/**
* Negate the interval
*/
negate() {
return new ChronosInterval({
years: this._years,
months: this._months,
weeks: this._weeks,
days: this._days,
hours: this._hours,
minutes: this._minutes,
seconds: this._seconds,
milliseconds: this._milliseconds,
}, !this._inverted);
}
/**
* Get absolute value of interval
*/
abs() {
return new ChronosInterval({
years: Math.abs(this._years),
months: Math.abs(this._months),
weeks: Math.abs(this._weeks),
days: Math.abs(this._days),
hours: Math.abs(this._hours),
minutes: Math.abs(this._minutes),
seconds: Math.abs(this._seconds),
milliseconds: Math.abs(this._milliseconds),
}, false);
}
// ============================================================================
// Comparison Methods
// ============================================================================
/**
* Check if equal to another interval
*/
equals(other) {
return this.totalMilliseconds() === other.totalMilliseconds();
}
/**
* Check if greater than another interval
*/
greaterThan(other) {
return this.totalMilliseconds() > other.totalMilliseconds();
}
/**
* Check if less than another interval
*/
lessThan(other) {
return this.totalMilliseconds() < other.totalMilliseconds();
}
/**
* Check if greater than or equal to another interval
*/
greaterThanOrEqual(other) {
return this.totalMilliseconds() >= other.totalMilliseconds();
}
/**
* Check if less than or equal to another interval
*/
lessThanOrEqual(other) {
return this.totalMilliseconds() <= other.totalMilliseconds();
}
/**
* Check if the interval is zero
*/
isZero() {
return this.totalMilliseconds() === 0;
}
/**
* Check if the interval is positive
*/
isPositive() {
return this.totalMilliseconds() > 0;
}
/**
* Check if the interval is negative
*/
isNegative() {
return this.totalMilliseconds() < 0;
}
// ============================================================================
// Normalization Methods
// ============================================================================
/**
* Cascade units to proper values (normalize overflow)
*
* @example
* 90 seconds becomes 1 minute 30 seconds
*/
cascade() {
let ms = Math.abs(this.totalMilliseconds());
const years = Math.floor(ms / (types_1.AVERAGE_DAYS_PER_YEAR * types_1.MILLISECONDS_PER_DAY));
ms %= types_1.AVERAGE_DAYS_PER_YEAR * types_1.MILLISECONDS_PER_DAY;
const months = Math.floor(ms / (types_1.AVERAGE_DAYS_PER_MONTH * types_1.MILLISECONDS_PER_DAY));
ms %= types_1.AVERAGE_DAYS_PER_MONTH * types_1.MILLISECONDS_PER_DAY;
const weeks = Math.floor(ms / types_1.MILLISECONDS_PER_WEEK);
ms %= types_1.MILLISECONDS_PER_WEEK;
const days = Math.floor(ms / types_1.MILLISECONDS_PER_DAY);
ms %= types_1.MILLISECONDS_PER_DAY;
const hours = Math.floor(ms / types_1.MILLISECONDS_PER_HOUR);
ms %= types_1.MILLISECONDS_PER_HOUR;
const minutes = Math.floor(ms / types_1.MILLISECONDS_PER_MINUTE);
ms %= types_1.MILLISECONDS_PER_MINUTE;
const seconds = Math.floor(ms / types_1.MILLISECONDS_PER_SECOND);
const milliseconds = ms % types_1.MILLISECONDS_PER_SECOND;
return new ChronosInterval({
years,
months,
weeks,
days,
hours,
minutes,
seconds,
milliseconds,
}, this._inverted);
}
/**
* Cascade without including weeks
*/
cascadeWithoutWeeks() {
const cascaded = this.cascade();
return new ChronosInterval({
years: cascaded._years,
months: cascaded._months,
days: cascaded._days + cascaded._weeks * 7,
hours: cascaded._hours,
minutes: cascaded._minutes,
seconds: cascaded._seconds,
milliseconds: cascaded._milliseconds,
}, this._inverted);
}
// ============================================================================
// Formatting Methods
// ============================================================================
/**
* Format as ISO 8601 duration
*/
toISO() {
return (0, utils_1.durationToISO)({
years: this._years,
months: this._months,
weeks: this._weeks,
days: this._days,
hours: this._hours,
minutes: this._minutes,
seconds: this._seconds + this._milliseconds / 1000,
});
}
/**
* Format for human reading
*
* @example
* ```typescript
* interval.forHumans() // "2 days 3 hours"
* interval.forHumans({ short: true }) // "2d 3h"
* interval.forHumans({ parts: 2 }) // "2 days 3 hours" (max 2 parts)
* ```
*/
forHumans(options = {}) {
const { short = false, parts = 7, join = ' ', conjunction } = options;
const cascaded = this.cascade();
const result = [];
const units = [
[cascaded._years, 'year'],
[cascaded._months, 'month'],
[cascaded._weeks, 'week'],
[cascaded._days, 'day'],
[cascaded._hours, 'hour'],
[cascaded._minutes, 'minute'],
[cascaded._seconds, 'second'],
];
for (const [value, unit] of units) {
if (value !== 0 && result.length < parts) {
const label = short ? unit[0] : ` ${(0, utils_1.pluralizeUnit)(unit, value)}`;
result.push(`${value}${label}`);
}
}
if (result.length === 0) {
return short ? '0s' : '0 seconds';
}
if (conjunction && result.length > 1) {
const last = result.pop();
return `${result.join(join)}${conjunction}${last}`;
}
return result.join(join);
}
/**
* Format using a format string
*
* Tokens:
* - %y: years
* - %m: months
* - %w: weeks
* - %d: days
* - %h: hours
* - %i: minutes
* - %s: seconds
* - %f: milliseconds
* - %R: +/- sign
* - %r: +/- or empty
*/
format(formatStr) {
const sign = this._inverted ? '-' : '+';
return formatStr
.replace(/%y/g, String(Math.abs(this._years)))
.replace(/%m/g, String(Math.abs(this._months)))
.replace(/%w/g, String(Math.abs(this._weeks)))
.replace(/%d/g, String(Math.abs(this._days)))
.replace(/%h/g, String(Math.abs(this._hours)))
.replace(/%H/g, (0, utils_1.padStart)(Math.abs(this._hours), 2))
.replace(/%i/g, String(Math.abs(this._minutes)))
.replace(/%I/g, (0, utils_1.padStart)(Math.abs(this._minutes), 2))
.replace(/%s/g, String(Math.abs(this._seconds)))
.replace(/%S/g, (0, utils_1.padStart)(Math.abs(this._seconds), 2))
.replace(/%f/g, String(Math.abs(this._milliseconds)))
.replace(/%R/g, sign)
.replace(/%r/g, this._inverted ? '-' : '');
}
// ============================================================================
// Conversion Methods
// ============================================================================
/**
* Convert to Duration object
*/
toDuration() {
return {
years: this._years,
months: this._months,
weeks: this._weeks,
days: this._days,
hours: this._hours,
minutes: this._minutes,
seconds: this._seconds,
milliseconds: this._milliseconds,
};
}
/**
* Convert to array
*/
toArray() {
return [
this._years,
this._months,
this._weeks,
this._days,
this._hours,
this._minutes,
this._seconds,
this._milliseconds,
];
}
/**
* Clone this interval
*/
clone() {
const cloned = new ChronosInterval(this.toDuration(), this._inverted);
cloned._locale = this._locale;
return cloned;
}
/**
* Set locale for this interval
*/
locale(code) {
const cloned = this.clone();
cloned._locale = (0, locales_1.getLocale)(code);
return cloned;
}
/**
* Get primitive value (total milliseconds)
*/
valueOf() {
return this.totalMilliseconds();
}
/**
* Convert to string
*/
toString() {
return this.forHumans();
}
/**
* Convert to JSON
*/
toJSON() {
return Object.assign(Object.assign({}, this.toDuration()), { iso: this.toISO() });
}
}
exports.ChronosInterval = ChronosInterval;
// ============================================================================
// Export Default
// ============================================================================
exports.default = ChronosInterval;