Timestamp.ts raw

   1  import { InvalidTimestampError } from '../errors'
   2  
   3  /**
   4   * Value object representing a Unix timestamp (seconds since epoch).
   5   * Immutable, self-validating, with formatting utilities.
   6   */
   7  export class Timestamp {
   8    private constructor(private readonly _unix: number) {}
   9  
  10    /**
  11     * Create a Timestamp for the current time.
  12     */
  13    static now(): Timestamp {
  14      return new Timestamp(Math.floor(Date.now() / 1000))
  15    }
  16  
  17    /**
  18     * Create a Timestamp from a Unix timestamp (seconds).
  19     * @throws InvalidTimestampError if the value is negative
  20     */
  21    static fromUnix(unix: number): Timestamp {
  22      if (unix < 0 || !Number.isFinite(unix)) {
  23        throw new InvalidTimestampError(unix)
  24      }
  25      return new Timestamp(Math.floor(unix))
  26    }
  27  
  28    /**
  29     * Create a Timestamp from a Date object.
  30     */
  31    static fromDate(date: Date): Timestamp {
  32      const unix = Math.floor(date.getTime() / 1000)
  33      if (unix < 0) {
  34        throw new InvalidTimestampError(unix)
  35      }
  36      return new Timestamp(unix)
  37    }
  38  
  39    /**
  40     * Create a Timestamp from milliseconds since epoch.
  41     */
  42    static fromMillis(millis: number): Timestamp {
  43      return Timestamp.fromUnix(millis / 1000)
  44    }
  45  
  46    /**
  47     * Try to create a Timestamp. Returns null if invalid.
  48     */
  49    static tryFromUnix(unix: number): Timestamp | null {
  50      try {
  51        return Timestamp.fromUnix(unix)
  52      } catch {
  53        return null
  54      }
  55    }
  56  
  57    /** The Unix timestamp in seconds */
  58    get unix(): number {
  59      return this._unix
  60    }
  61  
  62    /** The timestamp as milliseconds since epoch */
  63    get millis(): number {
  64      return this._unix * 1000
  65    }
  66  
  67    /** Convert to a Date object */
  68    get date(): Date {
  69      return new Date(this._unix * 1000)
  70    }
  71  
  72    /** Check if this timestamp is before another */
  73    isBefore(other: Timestamp): boolean {
  74      return this._unix < other._unix
  75    }
  76  
  77    /** Check if this timestamp is after another */
  78    isAfter(other: Timestamp): boolean {
  79      return this._unix > other._unix
  80    }
  81  
  82    /** Check if this timestamp is in the future */
  83    isFuture(): boolean {
  84      return this._unix > Timestamp.now()._unix
  85    }
  86  
  87    /** Check if this timestamp is in the past */
  88    isPast(): boolean {
  89      return this._unix < Timestamp.now()._unix
  90    }
  91  
  92    /** Get the difference in seconds from another timestamp */
  93    secondsFrom(other: Timestamp): number {
  94      return this._unix - other._unix
  95    }
  96  
  97    /** Create a new Timestamp by adding seconds */
  98    addSeconds(seconds: number): Timestamp {
  99      return new Timestamp(this._unix + seconds)
 100    }
 101  
 102    /** Create a new Timestamp by adding minutes */
 103    addMinutes(minutes: number): Timestamp {
 104      return this.addSeconds(minutes * 60)
 105    }
 106  
 107    /** Create a new Timestamp by adding hours */
 108    addHours(hours: number): Timestamp {
 109      return this.addSeconds(hours * 3600)
 110    }
 111  
 112    /** Create a new Timestamp by adding days */
 113    addDays(days: number): Timestamp {
 114      return this.addSeconds(days * 86400)
 115    }
 116  
 117    /**
 118     * Format as a relative time string (e.g., "5m", "2h", "3d").
 119     */
 120    formatRelative(): string {
 121      const now = Timestamp.now()
 122      const diff = now._unix - this._unix
 123  
 124      if (diff < 0) {
 125        return 'in the future'
 126      }
 127      if (diff < 60) {
 128        return 'just now'
 129      }
 130      if (diff < 3600) {
 131        return `${Math.floor(diff / 60)}m`
 132      }
 133      if (diff < 86400) {
 134        return `${Math.floor(diff / 3600)}h`
 135      }
 136      if (diff < 604800) {
 137        return `${Math.floor(diff / 86400)}d`
 138      }
 139      if (diff < 2592000) {
 140        return `${Math.floor(diff / 604800)}w`
 141      }
 142      if (diff < 31536000) {
 143        return `${Math.floor(diff / 2592000)}mo`
 144      }
 145      return `${Math.floor(diff / 31536000)}y`
 146    }
 147  
 148    /**
 149     * Format as ISO 8601 string.
 150     */
 151    toISOString(): string {
 152      return this.date.toISOString()
 153    }
 154  
 155    /**
 156     * Format as locale date string.
 157     */
 158    toLocaleDateString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string {
 159      return this.date.toLocaleDateString(locales, options)
 160    }
 161  
 162    /**
 163     * Format as locale time string.
 164     */
 165    toLocaleTimeString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string {
 166      return this.date.toLocaleTimeString(locales, options)
 167    }
 168  
 169    /** Check equality with another Timestamp */
 170    equals(other: Timestamp): boolean {
 171      return this._unix === other._unix
 172    }
 173  
 174    /** Returns the Unix timestamp as a number */
 175    valueOf(): number {
 176      return this._unix
 177    }
 178  
 179    /** Returns the Unix timestamp as a string */
 180    toString(): string {
 181      return String(this._unix)
 182    }
 183  
 184    /** For JSON serialization */
 185    toJSON(): number {
 186      return this._unix
 187    }
 188  }
 189