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