Duration

Kotlin-inspired time duration utilities for elegant time handling in TypeScript

Why Duration?

JavaScript's time handling is fragmented - milliseconds for setTimeout, seconds for performance metrics, various formats for display. Duration unifies time handling with a type-safe, expressive API that eliminates unit conversion bugs and makes time calculations readable.

import { Duration, seconds, minutes, hours } from 'kotlinify-ts/duration';

// Two ways to create durations
const timeout = Duration.seconds(30);
const delay = seconds(5);
const meetingLength = minutes(45);

// Chaining operations
const videoLength = hours(2).plus(minutes(35));
const apiTimeout = seconds(30);
const frameTime = milliseconds(16.67);

Creating Durations

Multiple ways to construct durations - choose the syntax that best fits your code style

import { Duration, seconds, minutes, hours, days, milliseconds, microseconds, nanoseconds } from 'kotlinify-ts/duration';

// Static factory methods
const boot = Duration.milliseconds(250);
const cache = Duration.minutes(5);
const session = Duration.hours(24);
const trial = Duration.days(30);

// Standalone functions
const animation = milliseconds(300);
const poll = seconds(10);
const lunch = minutes(60);
const workday = hours(8);

// Special values
const instant = Duration.zero();
const forever = Duration.infinite();

// Fractional durations
const quick = milliseconds(100);
const standard = seconds(30);
const meeting = hours(1.5);
const sprint = days(14);

Try it yourself:

// Create durations from numbers
const fiveSeconds = Duration.seconds(5);
const twoMinutes = Duration.minutes(2);
const oneHour = Duration.hours(1);

console.log('5 seconds:', fiveSeconds.toString());
console.log('2 minutes:', twoMinutes.toString());
console.log('1 hour:', oneHour.toString());

// Arithmetic operations
const total = fiveSeconds.plus(twoMinutes).plus(oneHour);
console.log('Total:', total.toString());
console.log('In seconds:', total.inWholeSeconds);
console.log('In minutes:', total.inWholeMinutes);

Arithmetic Operations

Perform calculations on durations with intuitive operators

import { Duration, seconds, minutes, hours } from 'kotlinify-ts/duration';

const start = minutes(10);
const extra = seconds(45);

// Addition and subtraction
const total = start.plus(extra);              // 10m 45s
const remaining = start.minus(seconds(120));   // 8m

// Scaling
const doubled = start.times(2);                // 20m
const half = start.div(2);                     // 5m

// Division between durations (ratio)
const ratio = hours(3).dividedBy(minutes(30)); // 6

// Negation and absolute value
const negative = start.unaryMinus();           // -10m
const positive = negative.absoluteValue();     // 10m

// Chaining operations
const complex = minutes(5)
  .plus(seconds(30))
  .times(2)
  .minus(minutes(3));                          // 8m

Unit Conversions

Convert between time units with precision control

import { Duration, DurationUnit } from 'kotlinify-ts/duration';

const duration = Duration.minutes(2.5);

// Whole units (truncated)
duration.inWholeSeconds;      // 150
duration.inWholeMilliseconds; // 150000
duration.inWholeMinutes;      // 2
duration.inWholeHours;        // 0

// Precise conversion with toDouble
duration.toDouble(DurationUnit.SECONDS);  // 150
duration.toDouble(DurationUnit.MINUTES);  // 2.5
duration.toDouble(DurationUnit.HOURS);    // 0.041666...

// Converting between different granularities
const precise = Duration.milliseconds(1234567);
precise.inWholeSeconds;       // 1234
precise.inWholeMinutes;       // 20
precise.toDouble(DurationUnit.SECONDS); // 1234.567
precise.toDouble(DurationUnit.MINUTES); // 20.576116...

Comparison and Sorting

Compare durations and check their properties

import { Duration, seconds, minutes } from 'kotlinify-ts/duration';

const short = seconds(30);
const medium = minutes(1);
const long = minutes(5);

// Comparison
short.compareTo(medium);  // -1 (less than)
medium.compareTo(short);  // 1 (greater than)
medium.compareTo(minutes(1)); // 0 (equal)

// Equality
medium.equals(seconds(60)); // true
short.equals(medium); // false

// Predicates
const negative = seconds(-10);
negative.isNegative(); // true
negative.isPositive(); // false

const infinite = Duration.infinite();
infinite.isInfinite(); // true
infinite.isFinite();   // false

// Sorting durations
const durations = [minutes(5), seconds(30), hours(1), minutes(2)];
durations.sort((a, b) => a.compareTo(b));
// [30s, 2m, 5m, 1h]

Formatting

Display durations in human-readable or standard formats

import { Duration, DurationUnit } from 'kotlinify-ts/duration';

// Default multi-component format
Duration.hours(2).plus(Duration.minutes(35)).toString();
// "2h 35m"

Duration.seconds(90.5).toString();
// "1m 30.5s"

Duration.milliseconds(1234).toString();
// "1.234s"

// Format with specific unit and decimals
const duration = Duration.seconds(125.456);
duration.toString(DurationUnit.SECONDS, 2);  // "125.46s"
duration.toString(DurationUnit.MINUTES, 1);  // "2.1m"
duration.toString(DurationUnit.HOURS, 3);    // "0.035h"

// Negative durations
Duration.minutes(-5).toString();             // "-5m"
Duration.hours(-2).plus(Duration.minutes(-30)).toString();
// "-(2h 30m)"

// ISO 8601 format
Duration.hours(2).plus(Duration.minutes(30)).toIsoString();
// "PT2H30M"

Duration.seconds(45.5).toIsoString();
// "PT45.5S"

Parsing

Parse duration strings from various formats

import { Duration } from 'kotlinify-ts/duration';

// Parse various formats
const d1 = Duration.parse("30s");           // 30 seconds
const d2 = Duration.parse("5m 30s");        // 5 minutes 30 seconds
const d3 = Duration.parse("2h 15m 45s");    // 2 hours 15 minutes 45 seconds
const d4 = Duration.parse("1.5h");          // 1.5 hours
const d5 = Duration.parse("-(1h 30m)");     // negative 1 hour 30 minutes

// Parse ISO 8601 format
const iso1 = Duration.parseIsoString("PT2H30M");     // 2 hours 30 minutes
const iso2 = Duration.parseIsoString("PT45.5S");     // 45.5 seconds
const iso3 = Duration.parseIsoString("-PT1H15M30S"); // negative duration

// Safe parsing with null fallback
const maybe = Duration.parseOrNull("invalid");       // null
const valid = Duration.parseOrNull("30s");          // Duration of 30s

const maybeIso = Duration.parseIsoStringOrNull("PT1X"); // null
const validIso = Duration.parseIsoStringOrNull("PT1H"); // Duration of 1h

// Special values
const inf = Duration.parse("Infinity");              // infinite duration
const negInf = Duration.parse("-Infinity");          // negative infinite

Component Decomposition

Break down durations into their constituent parts

import { Duration, hours, minutes, seconds } from 'kotlinify-ts/duration';

const duration = hours(2).plus(minutes(35)).plus(seconds(45));

// Decompose into components
duration.toComponents((days, hours, minutes, seconds, nanoseconds) => {
  console.log(`${days}d ${hours}h ${minutes}m ${seconds}s`);
  // "0d 2h 35m 45s"
  return { days, hours, minutes, seconds };
});

// Format for display
const formatted = duration.toComponents((d, h, m, s) => {
  const parts = [];
  if (d > 0) parts.push(`${d} day${d !== 1 ? 's' : ''}`);
  if (h > 0) parts.push(`${h} hour${h !== 1 ? 's' : ''}`);
  if (m > 0) parts.push(`${m} minute${m !== 1 ? 's' : ''}`);
  if (s > 0) parts.push(`${s} second${s !== 1 ? 's' : ''}`);
  return parts.join(', ');
});
// "2 hours, 35 minutes, 45 seconds"

// Calculate total seconds from components
const totalSeconds = duration.toComponents((d, h, m, s) =>
  d * 86400 + h * 3600 + m * 60 + s
);
// 9345

Real-World Examples

Practical applications of Duration in production code

import { Duration, seconds, minutes, milliseconds } from 'kotlinify-ts/duration';

// API retry with exponential backoff
async function retryWithBackoff<T>(
  operation: () => Promise<T>,
  maxAttempts = 3
): Promise<T> {
  let delay = milliseconds(100);

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await operation();
    } catch (error) {
      if (attempt === maxAttempts) throw error;

      console.log(`Retry in ${delay.toString()}`);
      await new Promise(resolve =>
        setTimeout(resolve, delay.inWholeMilliseconds)
      );

      delay = delay.times(2); // Exponential backoff
    }
  }
  throw new Error('Should not reach here');
}

// Video playback utilities
class VideoPlayer {
  private position = Duration.zero();
  private duration: Duration;

  constructor(durationSeconds: number) {
    this.duration = seconds(durationSeconds);
  }

  seek(to: Duration) {
    if (to.isNegative()) {
      this.position = Duration.zero();
    } else if (to.compareTo(this.duration) > 0) {
      this.position = this.duration;
    } else {
      this.position = to;
    }
  }

  skip(amount: Duration) {
    this.seek(this.position.plus(amount));
  }

  get remaining(): Duration {
    return this.duration.minus(this.position);
  }

  get progress(): number {
    return this.position.dividedBy(this.duration);
  }

  formatPosition(): string {
    return `${this.position.toString()} / ${this.duration.toString()}`;
  }
}

// Cache with TTL
class TimedCache<T> {
  private entries = new Map<string, { value: T; expires: number }>();

  set(key: string, value: T, ttl: Duration) {
    const expires = Date.now() + ttl.inWholeMilliseconds;
    this.entries.set(key, { value, expires });
  }

  get(key: string): T | undefined {
    const entry = this.entries.get(key);
    if (!entry) return undefined;

    if (Date.now() > entry.expires) {
      this.entries.delete(key);
      return undefined;
    }

    return entry.value;
  }
}

// Rate limiting
class RateLimiter {
  private requests: number[] = [];

  constructor(
    private maxRequests: number,
    private window: Duration
  ) {}

  canMakeRequest(): boolean {
    const now = Date.now();
    const windowStart = now - this.window.inWholeMilliseconds;

    this.requests = this.requests.filter(time => time > windowStart);

    if (this.requests.length < this.maxRequests) {
      this.requests.push(now);
      return true;
    }

    return false;
  }

  timeUntilNextRequest(): Duration {
    if (this.requests.length < this.maxRequests) {
      return Duration.zero();
    }

    const oldestRequest = Math.min(...this.requests);
    const availableAt = oldestRequest + this.window.inWholeMilliseconds;
    const waitMs = Math.max(0, availableAt - Date.now());

    return milliseconds(waitMs);
  }
}

// Usage
const limiter = new RateLimiter(10, minutes(1));
if (!limiter.canMakeRequest()) {
  const wait = limiter.timeUntilNextRequest();
  console.log(`Rate limited. Try again in ${wait.toString()}`);
}

Performance and Timing

Advanced patterns for benchmarking, animation, and timeout management

import { Duration, milliseconds } from 'kotlinify-ts/duration';

// Performance timing utility
class Timer {
  private startTime = performance.now();

  elapsed(): Duration {
    return milliseconds(performance.now() - this.startTime);
  }

  reset() {
    this.startTime = performance.now();
  }

  measure<T>(operation: () => T): [T, Duration] {
    const start = performance.now();
    const result = operation();
    const elapsed = milliseconds(performance.now() - start);
    return [result, elapsed];
  }
}

// Benchmark function performance
function benchmark(
  name: string,
  fn: () => void,
  iterations = 1000
): Duration {
  const timer = new Timer();

  for (let i = 0; i < iterations; i++) {
    fn();
  }

  const total = timer.elapsed();
  const average = total.div(iterations);

  console.log(`${name}:`);
  console.log(`  Total: ${total.toString()}`);
  console.log(`  Average: ${average.toString(DurationUnit.MICROSECONDS, 2)}`);

  return average;
}

// Animation timing
class Animation {
  private elapsed = Duration.zero();

  constructor(
    private duration: Duration,
    private easing: (t: number) => number = t => t
  ) {}

  update(deltaTime: Duration): boolean {
    this.elapsed = this.elapsed.plus(deltaTime);
    return !this.isComplete();
  }

  get progress(): number {
    const raw = Math.min(1, this.elapsed.dividedBy(this.duration));
    return this.easing(raw);
  }

  isComplete(): boolean {
    return this.elapsed.compareTo(this.duration) >= 0;
  }

  reset() {
    this.elapsed = Duration.zero();
  }
}

// Timeout manager
class TimeoutManager {
  private timeouts = new Map<string, {
    duration: Duration;
    callback: () => void;
    handle?: NodeJS.Timeout;
  }>();

  set(id: string, duration: Duration, callback: () => void) {
    this.clear(id);

    const handle = setTimeout(() => {
      callback();
      this.timeouts.delete(id);
    }, duration.inWholeMilliseconds);

    this.timeouts.set(id, { duration, callback, handle });
  }

  clear(id: string) {
    const timeout = this.timeouts.get(id);
    if (timeout?.handle) {
      clearTimeout(timeout.handle);
      this.timeouts.delete(id);
    }
  }

  clearAll() {
    for (const [id] of this.timeouts) {
      this.clear(id);
    }
  }
}

API Summary

Construction

  • Duration.seconds(n), Duration.minutes(n), Duration.hours(n), Duration.days(n) - static methods
  • Duration.milliseconds(n), Duration.microseconds(n), Duration.nanoseconds(n) - static methods
  • seconds(n), minutes(n), hours(n), days(n) - standalone functions
  • Duration.zero(), Duration.infinite() - special values

Conversion

  • inWholeSeconds, inWholeMinutes, inWholeHours, inWholeDays - truncated values
  • toDouble(unit) - precise conversion to any unit

Arithmetic

  • plus(other), minus(other) - add/subtract durations
  • times(scale), div(scale) - scale durations
  • dividedBy(other) - ratio between durations
  • unaryMinus(), absoluteValue() - sign operations

Comparison

  • compareTo(other) - compare durations (-1, 0, 1)
  • equals(other) - check equality
  • isNegative(), isPositive(), isInfinite(), isFinite() - predicates

Formatting & Parsing

  • toString() - multi-component format
  • toString(unit, decimals) - single unit format
  • toIsoString() - ISO 8601 format
  • Duration.parse(string) - parse duration string
  • Duration.parseIsoString(string) - parse ISO format
  • parseOrNull(), parseIsoStringOrNull() - safe parsing

Components

  • toComponents(action) - decompose into days, hours, minutes, seconds, nanoseconds

Next Steps