1+ /*
2+ * Copyright (c) 2026 Eclipse Dirigible contributors
3+ *
4+ * All rights reserved. This program and the accompanying materials
5+ * are made available under the terms of the Eclipse Public License v2.0
6+ * which accompanies this distribution, and is available at
7+ * http://www.eclipse.org/legal/epl-v20.html
8+ *
9+ * SPDX-FileCopyrightText: Eclipse Dirigible contributors
10+ * SPDX-License-Identifier: EPL-2.0
11+ */
12+ class DateTimeUtil {
13+ /**
14+ * DateTime utility for formatting and getting relative time.
15+ * Uses native Intl APIs for localization.
16+ * @param {string } [locale] - Optional locale (e.g. "en", "bg", "de"). Defaults to runtime locale.
17+ */
18+ constructor ( locale = undefined ) {
19+ this . locale = locale ;
20+
21+ // Cache Intl formatters for performance
22+ this . _dtf = {
23+ default : new Intl . DateTimeFormat ( this . locale ) ,
24+ monthLong : new Intl . DateTimeFormat ( this . locale , { month : "long" } ) ,
25+ monthShort : new Intl . DateTimeFormat ( this . locale , { month : "short" } ) ,
26+ weekdayLong : new Intl . DateTimeFormat ( this . locale , { weekday : "long" } ) ,
27+ weekdayShort : new Intl . DateTimeFormat ( this . locale , { weekday : "short" } ) ,
28+ dayPeriod : new Intl . DateTimeFormat ( this . locale , { hour : "numeric" , hour12 : true } )
29+ } ;
30+
31+ this . _rtf = new Intl . RelativeTimeFormat ( this . locale , { numeric : "auto" } ) ;
32+ }
33+
34+ /**
35+ * Convert input into a valid Date object.
36+ * @private
37+ * @param {string|Date|number } input - ISO string, Date object, or timestamp.
38+ * @returns {Date }
39+ * @throws {Error } If the date is invalid.
40+ */
41+ toDate ( input ) {
42+ const date = input instanceof Date ? input : new Date ( input ) ;
43+ if ( isNaN ( date ) ) throw new Error ( "Invalid date" ) ;
44+ return date ;
45+ }
46+
47+ /**
48+ * Pad a number with leading zeros.
49+ * @private
50+ * @param {number } num
51+ * @returns {string }
52+ */
53+ pad ( num ) {
54+ return String ( num ) . padStart ( 2 , "0" ) ;
55+ }
56+
57+ /**
58+ * Format a date using either:
59+ * 1) Custom tokens
60+ * 2) Intl.DateTimeFormat options
61+ * 3) Locale default formatting (if no format is provided)
62+ *
63+ * ---
64+ * Supported tokens:
65+ * YYYY, YY - Year (2026, 26)
66+ * MMMM, MMM, MM, M - Month (January, Jan, 01, 1), localized
67+ * DD, D, dddd, ddd - Day (01, 1, Thursday, Thu), localized
68+ * HH, H, hh, h - Hour (09, 9, 09, 9), lowercase is for the 12 hour format
69+ * mm, m - Minute (07, 7)
70+ * ss, s - Second (01, 1)
71+ * A (AM/PM, localized)
72+ *
73+ * ---
74+ * Behavior:
75+ * - If `formatStr` is omitted → uses locale default format
76+ * - If `formatStr` is an object → uses Intl.DateTimeFormat options
77+ * - If `formatStr` is a string → uses token-based formatting
78+ *
79+ * @param {string|Date|number } input - Date input (ISO string, Date object, or timestamp)
80+ * @param {string|Object } [formatStr] - Format string OR Intl options
81+ * @returns {string }
82+ *
83+ * @example
84+ * du.format("2026-03-26", "YYYY-MM-DD") // "2026-03-26"
85+ *
86+ * @example
87+ * du.format("2026-03-26", "D MMMM YYYY") // "26 March 2026"
88+ *
89+ * @example
90+ * du.format("2026-03-26") // locale default (e.g. "3/26/2026")
91+ *
92+ * @example
93+ * du.format("2026-03-26", { dateStyle: "long" }) // "March 26, 2026"
94+ */
95+ format ( input , formatStr ) {
96+ const date = this . toDate ( input ) ;
97+
98+ // 👉 Default locale format
99+ if ( ! formatStr ) {
100+ return this . _dtf . default . format ( date ) ;
101+ }
102+
103+ // 👉 Intl options
104+ if ( typeof formatStr === "object" ) {
105+ return new Intl . DateTimeFormat ( this . locale , formatStr ) . format ( date ) ;
106+ }
107+
108+ const hours24 = date . getHours ( ) ;
109+ const hours12 = hours24 % 12 || 12 ;
110+
111+ const map = {
112+ YYYY : date . getFullYear ( ) ,
113+ YY : String ( date . getFullYear ( ) ) . slice ( - 2 ) ,
114+
115+ MMMM : this . _dtf . monthLong . format ( date ) ,
116+ MMM : this . _dtf . monthShort . format ( date ) ,
117+ MM : this . pad ( date . getMonth ( ) + 1 ) ,
118+ M : date . getMonth ( ) + 1 ,
119+
120+ DD : this . pad ( date . getDate ( ) ) ,
121+ D : date . getDate ( ) ,
122+
123+ dddd : this . _dtf . weekdayLong . format ( date ) ,
124+ ddd : this . _dtf . weekdayShort . format ( date ) ,
125+
126+ HH : this . pad ( hours24 ) ,
127+ H : hours24 ,
128+
129+ hh : this . pad ( hours12 ) ,
130+ h : hours12 ,
131+
132+ mm : this . pad ( date . getMinutes ( ) ) ,
133+ m : date . getMinutes ( ) ,
134+
135+ ss : this . pad ( date . getSeconds ( ) ) ,
136+ s : date . getSeconds ( ) ,
137+
138+ A : this . _dtf . dayPeriod
139+ . formatToParts ( date )
140+ . find ( p => p . type === "dayPeriod" ) ?. value || ""
141+ } ;
142+
143+ return formatStr . replace (
144+ / Y Y Y Y | Y Y | M M M M | M M M | M M | M | D D | D | d d d d | d d d | H H | H | h h | h | m m | m | s s | s | A / g,
145+ token => map [ token ]
146+ ) ;
147+ }
148+
149+ /**
150+ * Format a date relative to another date (e.g. "2 hours ago", "in 3 days").
151+ *
152+ * @param {string|Date|number } input - Target date
153+ * @param {string|Date|number } [base=new Date()] - Base date to compare against
154+ * @returns {string }
155+ *
156+ * @example
157+ * du.relative(Date.now() - 60000) // "1 minute ago"
158+ * du.relative(Date.now() + 86400000) // "in 1 day"
159+ */
160+ relative ( input , base = new Date ( ) ) {
161+ const date = this . toDate ( input ) ;
162+ const now = this . toDate ( base ) ;
163+
164+ const diffSec = Math . round ( ( date - now ) / 1000 ) ;
165+
166+ const units = [
167+ { limit : 60 , unit : "second" , value : diffSec } ,
168+ { limit : 60 , unit : "minute" , value : diffSec / 60 } ,
169+ { limit : 24 , unit : "hour" , value : diffSec / 3600 } ,
170+ { limit : 7 , unit : "day" , value : diffSec / 86400 } ,
171+ { limit : 4.345 , unit : "week" , value : diffSec / 604800 } ,
172+ { limit : 12 , unit : "month" , value : diffSec / 2629800 } ,
173+ { limit : Infinity , unit : "year" , value : diffSec / 31557600 }
174+ ] ;
175+
176+ for ( const u of units ) {
177+ if ( Math . abs ( u . value ) < u . limit ) {
178+ return this . _rtf . format ( Math . round ( u . value ) , u . unit ) ;
179+ }
180+ }
181+ }
182+ }
0 commit comments