View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.host.intl;
16  
17  import java.time.ZoneId;
18  import java.time.chrono.Chronology;
19  import java.time.chrono.HijrahChronology;
20  import java.time.chrono.JapaneseChronology;
21  import java.time.chrono.ThaiBuddhistChronology;
22  import java.time.format.DateTimeFormatter;
23  import java.time.format.DecimalStyle;
24  import java.time.temporal.TemporalAccessor;
25  import java.util.Date;
26  import java.util.HashMap;
27  import java.util.Map;
28  import java.util.concurrent.ConcurrentHashMap;
29  
30  import org.apache.commons.lang3.StringUtils;
31  import org.htmlunit.BrowserVersion;
32  import org.htmlunit.corejs.javascript.Context;
33  import org.htmlunit.corejs.javascript.Function;
34  import org.htmlunit.corejs.javascript.FunctionObject;
35  import org.htmlunit.corejs.javascript.NativeArray;
36  import org.htmlunit.corejs.javascript.Scriptable;
37  import org.htmlunit.javascript.HtmlUnitScriptable;
38  import org.htmlunit.javascript.JavaScriptEngine;
39  import org.htmlunit.javascript.configuration.JsxClass;
40  import org.htmlunit.javascript.configuration.JsxConstructor;
41  import org.htmlunit.javascript.configuration.JsxFunction;
42  import org.htmlunit.javascript.host.Window;
43  
44  /**
45   * A JavaScript object for {@code DateTimeFormat}.
46   *
47   * @author Ahmed Ashour
48   * @author Ronald Brill
49   */
50  @JsxClass
51  public class DateTimeFormat extends HtmlUnitScriptable {
52  
53      private static final ConcurrentHashMap<String, String> CHROME_FORMATS_ = new ConcurrentHashMap<>();
54      private static final ConcurrentHashMap<String, String> EDGE_FORMATS_ = new ConcurrentHashMap<>();
55      private static final ConcurrentHashMap<String, String> FF_FORMATS_ = new ConcurrentHashMap<>();
56      private static final ConcurrentHashMap<String, String> FF_ESR_FORMATS_ = new ConcurrentHashMap<>();
57  
58      private static final ConcurrentHashMap<String, Chronology> CHROME_CHRONOLOGIES_ = new ConcurrentHashMap<>();
59      private static final ConcurrentHashMap<String, Chronology> EDGE_CHRONOLOGIES_ = new ConcurrentHashMap<>();
60      private static final ConcurrentHashMap<String, Chronology> FF_CHRONOLOGIES_ = new ConcurrentHashMap<>();
61      private static final ConcurrentHashMap<String, Chronology> FF_ESR_CHRONOLOGIES_ = new ConcurrentHashMap<>();
62  
63      private transient DateTimeFormatHelper formatter_;
64  
65      static {
66          final String ddSlash = "\u200Edd\u200E/\u200EMM\u200E/\u200EYYYY";
67          final String ddDash = "\u200Edd\u200E-\u200EMM\u200E-\u200EYYYY";
68          final String ddDot = "\u200Edd\u200E.\u200EMM\u200E.\u200EYYYY";
69          final String ddDotDot = "\u200Edd\u200E.\u200EMM\u200E.\u200EYYYY\u200E.";
70          final String ddDotBlank = "\u200Edd\u200E. \u200EMM\u200E. \u200EYYYY";
71          final String ddDotBlankDot = "\u200Edd\u200E. \u200EMM\u200E. \u200EYYYY.";
72          final String mmSlash = "\u200EMM\u200E/\u200Edd\u200E/\u200EYYYY";
73          final String yyyySlash = "\u200EYYYY\u200E/\u200EMM\u200E/\u200Edd";
74          final String yyyyDash = "\u200EYYYY\u200E-\u200EMM\u200E-\u200Edd";
75          final String yyyyDotBlankDot = "\u200EYYYY\u200E. \u200EMM\u200E. \u200Edd.";
76  
77          final Map<String, String> commonFormats = new HashMap<>();
78          commonFormats.put("", ddDot);
79          commonFormats.put("ar", "dd\u200F/MM\u200F/YYYY");
80          commonFormats.put("ban", mmSlash);
81          commonFormats.put("be", ddDot);
82          commonFormats.put("bg", ddDot + "\u200E \u0433.");
83          commonFormats.put("ca", ddSlash);
84          commonFormats.put("cs", ddDotBlank);
85          commonFormats.put("da", ddDot);
86          commonFormats.put("de", ddDot);
87          commonFormats.put("el", ddSlash);
88          commonFormats.put("en", mmSlash);
89          commonFormats.put("en-CA", yyyyDash);
90          commonFormats.put("en-NZ", ddSlash);
91          commonFormats.put("en-PA", ddSlash);
92          commonFormats.put("en-PR", ddSlash);
93          commonFormats.put("en-PH", mmSlash);
94          commonFormats.put("en-AU", ddSlash);
95          commonFormats.put("en-GB", ddSlash);
96          commonFormats.put("en-IE", ddSlash);
97          commonFormats.put("en-IN", ddSlash);
98          commonFormats.put("en-MT", ddSlash);
99          commonFormats.put("en-SG", ddSlash);
100         commonFormats.put("en-ZA", yyyySlash);
101         commonFormats.put("es", ddSlash);
102         commonFormats.put("es-CL", ddDash);
103         commonFormats.put("es-PA", mmSlash);
104         commonFormats.put("es-PR", mmSlash);
105         commonFormats.put("es-US", ddSlash);
106         commonFormats.put("et", ddDot);
107         commonFormats.put("fi", ddDot);
108         commonFormats.put("fr", ddSlash);
109         commonFormats.put("fr-CA", yyyyDash);
110         commonFormats.put("ga", ddSlash);
111         commonFormats.put("hi", ddSlash);
112         commonFormats.put("hr", ddDotBlankDot);
113         commonFormats.put("hu", yyyyDotBlankDot);
114         commonFormats.put("id", ddSlash);
115         commonFormats.put("in", ddSlash);
116         commonFormats.put("is", ddDot);
117         commonFormats.put("it", ddSlash);
118         commonFormats.put("iw", ddDot);
119         commonFormats.put("ja", yyyySlash);
120         commonFormats.put("ja-JP-u-ca-japanese", "'H'yy/MM/dd");
121         commonFormats.put("ko", yyyyDotBlankDot);
122         commonFormats.put("lt", yyyyDash);
123         commonFormats.put("lv", ddDotDot);
124         commonFormats.put("mk", ddDot);
125         commonFormats.put("ms", ddSlash);
126         commonFormats.put("mt", mmSlash);
127         commonFormats.put("nl", ddDash);
128         commonFormats.put("nl-BE", ddSlash);
129         commonFormats.put("pl", ddDot);
130         commonFormats.put("pt", ddSlash);
131         commonFormats.put("ro", ddDot);
132         commonFormats.put("ru", ddDot);
133         commonFormats.put("sk", ddDotBlank);
134         commonFormats.put("sl", ddDotBlank);
135         commonFormats.put("sq", ddDot);
136         commonFormats.put("sr", ddDotBlankDot);
137         commonFormats.put("sv", yyyyDash);
138         commonFormats.put("th", ddSlash);
139         commonFormats.put("tr", ddDot);
140         commonFormats.put("uk", ddDot);
141         commonFormats.put("vi", ddSlash);
142         commonFormats.put("zh", yyyySlash);
143         commonFormats.put("zh-HK", ddSlash);
144         commonFormats.put("zh-SG", "\u200EYYYY\u200E\u5E74\u200EMM\u200E\u6708\u200Edd\u200E\u65E5");
145         commonFormats.put("fr-CH", ddDot);
146 
147         FF_FORMATS_.putAll(commonFormats);
148         FF_FORMATS_.put("mk", ddDot + "\u200E \u0433.");
149 
150         commonFormats.put("ar-SA", "d\u200F/M\u200F/YYYY هـ");
151         FF_ESR_FORMATS_.putAll(commonFormats);
152 
153         commonFormats.put("be", mmSlash);
154         commonFormats.put("ga", mmSlash);
155         commonFormats.put("is", mmSlash);
156         commonFormats.put("mk", mmSlash);
157 
158         EDGE_FORMATS_.putAll(commonFormats);
159 
160         CHROME_FORMATS_.putAll(commonFormats);
161         CHROME_FORMATS_.put("sq", mmSlash);
162 
163         final Map<String, Chronology> commonChronologies = new HashMap<>();
164         commonChronologies.put("ja-JP-u-ca-japanese", JapaneseChronology.INSTANCE);
165         commonChronologies.put("th", ThaiBuddhistChronology.INSTANCE);
166         commonChronologies.put("th-TH", ThaiBuddhistChronology.INSTANCE);
167 
168         FF_CHRONOLOGIES_.putAll(commonChronologies);
169 
170         commonChronologies.put("ar-SA", HijrahChronology.INSTANCE);
171         CHROME_CHRONOLOGIES_.putAll(commonChronologies);
172         EDGE_CHRONOLOGIES_.putAll(commonChronologies);
173         FF_ESR_CHRONOLOGIES_.putAll(commonChronologies);
174     }
175 
176     /**
177      * Default constructor.
178      */
179     public DateTimeFormat() {
180         super();
181     }
182 
183     private DateTimeFormat(final String[] locales, final BrowserVersion browserVersion) {
184         super();
185 
186         final Map<String, String> formats;
187         final Map<String, Chronology> chronologies;
188         if (browserVersion.isChrome()) {
189             formats = CHROME_FORMATS_;
190             chronologies = CHROME_CHRONOLOGIES_;
191         }
192         else if (browserVersion.isEdge()) {
193             formats = EDGE_FORMATS_;
194             chronologies = EDGE_CHRONOLOGIES_;
195         }
196         else if (browserVersion.isFirefoxESR()) {
197             formats = FF_ESR_FORMATS_;
198             chronologies = FF_ESR_CHRONOLOGIES_;
199         }
200         else {
201             formats = FF_FORMATS_;
202             chronologies = FF_CHRONOLOGIES_;
203         }
204 
205         String locale = browserVersion.getBrowserLocale().toLanguageTag();
206         String pattern = getPattern(formats, locale);
207 
208         for (final String l : locales) {
209             pattern = getPattern(formats, l);
210             if (pattern != null) {
211                 locale = l;
212             }
213         }
214 
215         if (pattern == null) {
216             pattern = formats.get("");
217         }
218 
219         if (!locale.startsWith("ar")) {
220             pattern = pattern.replace("\u200E", "");
221         }
222 
223         final Chronology chronology = getChronology(chronologies, locale);
224 
225         formatter_ = new DateTimeFormatHelper(locale, chronology, pattern);
226     }
227 
228     private static String getPattern(final Map<String, String> formats, String locale) {
229         if ("no-NO-NY".equals(locale)) {
230             throw JavaScriptEngine.rangeError("Invalid language tag: " + locale);
231         }
232         String pattern = formats.get(locale);
233         while (pattern == null && locale.indexOf('-') != -1) {
234             locale = locale.substring(0, locale.lastIndexOf('-'));
235             pattern = formats.get(locale);
236         }
237         return pattern;
238     }
239 
240     private static Chronology getChronology(final Map<String, Chronology> chronologies, String locale) {
241         Chronology chronology = chronologies.get(locale);
242         while (chronology == null && locale.indexOf('-') != -1) {
243             locale = locale.substring(0, locale.lastIndexOf('-'));
244             chronology = chronologies.get(locale);
245         }
246         return chronology;
247     }
248 
249     /**
250      * JavaScript constructor.
251      * @param cx the current context
252      * @param scope the scope
253      * @param args the arguments to the WebSocket constructor
254      * @param ctorObj the function object
255      * @param inNewExpr Is new or not
256      * @return the java object to allow JavaScript to access
257      */
258     @JsxConstructor
259     public static Scriptable jsConstructor(final Context cx, final Scriptable scope,
260             final Object[] args, final Function ctorObj, final boolean inNewExpr) {
261         final String[] locales;
262         if (args.length != 0) {
263             if (args[0] instanceof NativeArray) {
264                 final NativeArray array = (NativeArray) args[0];
265                 locales = new String[(int) array.getLength()];
266                 for (int i = 0; i < locales.length; i++) {
267                     locales[i] = JavaScriptEngine.toString(array.get(i));
268                 }
269             }
270             else {
271                 locales = new String[] {JavaScriptEngine.toString(args[0])};
272             }
273         }
274         else {
275             locales = new String[0];
276         }
277 
278         final Window window = getWindow(ctorObj);
279         final DateTimeFormat format = new DateTimeFormat(locales, window.getBrowserVersion());
280         format.setParentScope(window);
281         format.setPrototype(((FunctionObject) ctorObj).getClassPrototype());
282         return format;
283     }
284 
285     /**
286      * Formats a date according to the locale and formatting options of this {@code DateTimeFormat} object.
287      * @param object the JavaScript object to convert
288      * @return the dated formated
289      */
290     @JsxFunction
291     public String format(final Object object) {
292         final Date date = (Date) Context.jsToJava(object, Date.class);
293         return formatter_.format(date, Context.getCurrentContext().getTimeZone().toZoneId());
294     }
295 
296     /**
297      * @return A new object with properties reflecting the locale and date and time formatting options
298      *         computed during the initialization of the given {@code DateTimeFormat} object.
299      */
300     @JsxFunction
301     public Scriptable resolvedOptions() {
302         final Context cx = Context.getCurrentContext();
303         final Scriptable options = cx.newObject(getParentScope());
304         options.put("timeZone", options, cx.getTimeZone().getID());
305 
306         if (StringUtils.isEmpty(formatter_.locale_)) {
307             options.put("locale", options, cx.getLocale().toLanguageTag());
308         }
309         else {
310             options.put("locale", options, formatter_.locale_);
311         }
312         return options;
313     }
314 
315     /**
316      * Helper.
317      */
318     static final class DateTimeFormatHelper {
319 
320         private final DateTimeFormatter formatter_;
321         private Chronology chronology_;
322         private final String locale_;
323 
324         DateTimeFormatHelper(final String locale, final Chronology chronology, final String pattern) {
325             locale_ = locale;
326             chronology_ = chronology;
327 
328             if (locale.startsWith("ar")
329                     && !"ar-DZ".equals(locale)
330                     && !"ar-LY".equals(locale)
331                     && !"ar-MA".equals(locale)
332                     && !"ar-TN".equals(locale)) {
333                 final DecimalStyle decimalStyle = DecimalStyle.STANDARD.withZeroDigit('\u0660');
334                 formatter_ = DateTimeFormatter.ofPattern(pattern).withDecimalStyle(decimalStyle);
335             }
336             else {
337                 formatter_ = DateTimeFormatter.ofPattern(pattern);
338             }
339         }
340 
341         /**
342          * Formats a date according to the locale and formatting options of this {@code DateTimeFormat} object.
343          * @param date the JavaScript object to convert
344          * @param zoneId the current time zone id
345          * @return the dated formated
346          */
347         String format(final Date date, final ZoneId zoneId) {
348             TemporalAccessor zonedDateTime = date.toInstant().atZone(zoneId);
349             if (chronology_ != null) {
350                 zonedDateTime = chronology_.date(zonedDateTime);
351             }
352             return formatter_.format(zonedDateTime);
353         }
354     }
355 }