1
2
3
4
5
6
7
8
9
10
11
12
13
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.JapaneseChronology;
20 import java.time.chrono.ThaiBuddhistChronology;
21 import java.time.format.DateTimeFormatter;
22 import java.time.format.DecimalStyle;
23 import java.time.temporal.TemporalAccessor;
24 import java.util.Date;
25 import java.util.HashMap;
26 import java.util.Map;
27 import java.util.concurrent.ConcurrentHashMap;
28
29 import org.htmlunit.BrowserVersion;
30 import org.htmlunit.corejs.javascript.Context;
31 import org.htmlunit.corejs.javascript.Function;
32 import org.htmlunit.corejs.javascript.FunctionObject;
33 import org.htmlunit.corejs.javascript.NativeArray;
34 import org.htmlunit.corejs.javascript.Scriptable;
35 import org.htmlunit.corejs.javascript.VarScope;
36 import org.htmlunit.javascript.HtmlUnitScriptable;
37 import org.htmlunit.javascript.JavaScriptEngine;
38 import org.htmlunit.javascript.configuration.JsxClass;
39 import org.htmlunit.javascript.configuration.JsxConstructor;
40 import org.htmlunit.javascript.configuration.JsxFunction;
41 import org.htmlunit.javascript.configuration.JsxStaticFunction;
42 import org.htmlunit.javascript.configuration.JsxSymbolConstant;
43 import org.htmlunit.javascript.host.Window;
44 import org.htmlunit.util.StringUtils;
45
46
47
48
49
50
51
52
53
54
55 @JsxClass
56 public class DateTimeFormat extends HtmlUnitScriptable {
57
58
59 @JsxSymbolConstant
60 public static final String TO_STRING_TAG = "Intl.DateTimeFormat";
61
62 private static final ConcurrentHashMap<String, String> CHROME_FORMATS_ = new ConcurrentHashMap<>();
63 private static final ConcurrentHashMap<String, String> EDGE_FORMATS_ = new ConcurrentHashMap<>();
64 private static final ConcurrentHashMap<String, String> FF_FORMATS_ = new ConcurrentHashMap<>();
65 private static final ConcurrentHashMap<String, String> FF_ESR_FORMATS_ = new ConcurrentHashMap<>();
66
67 private static final ConcurrentHashMap<String, Chronology> CHROME_CHRONOLOGIES_ = new ConcurrentHashMap<>();
68 private static final ConcurrentHashMap<String, Chronology> EDGE_CHRONOLOGIES_ = new ConcurrentHashMap<>();
69 private static final ConcurrentHashMap<String, Chronology> FF_CHRONOLOGIES_ = new ConcurrentHashMap<>();
70 private static final ConcurrentHashMap<String, Chronology> FF_ESR_CHRONOLOGIES_ = new ConcurrentHashMap<>();
71
72 private transient DateTimeFormatHelper formatter_;
73
74 static {
75 final String ddSlash = "\u200Edd\u200E/\u200EMM\u200E/\u200EYYYY";
76 final String ddDash = "\u200Edd\u200E-\u200EMM\u200E-\u200EYYYY";
77 final String ddDot = "\u200Edd\u200E.\u200EMM\u200E.\u200EYYYY";
78 final String ddDotDot = "\u200Edd\u200E.\u200EMM\u200E.\u200EYYYY\u200E.";
79 final String ddDotBlank = "\u200Edd\u200E. \u200EMM\u200E. \u200EYYYY";
80 final String ddDotBlankDot = "\u200Edd\u200E. \u200EMM\u200E. \u200EYYYY.";
81 final String mmSlash = "\u200EMM\u200E/\u200Edd\u200E/\u200EYYYY";
82 final String yyyySlash = "\u200EYYYY\u200E/\u200EMM\u200E/\u200Edd";
83 final String yyyyDash = "\u200EYYYY\u200E-\u200EMM\u200E-\u200Edd";
84 final String yyyyDotBlankDot = "\u200EYYYY\u200E. \u200EMM\u200E. \u200Edd.";
85
86 final Map<String, String> commonFormats = new HashMap<>();
87 commonFormats.put("", ddDot);
88 commonFormats.put("ar", "dd\u200F/MM\u200F/YYYY");
89 commonFormats.put("ban", mmSlash);
90 commonFormats.put("be", ddDot);
91 commonFormats.put("bg", ddDot + "\u200E \u0433.");
92 commonFormats.put("ca", ddSlash);
93 commonFormats.put("cs", ddDotBlank);
94 commonFormats.put("da", ddDot);
95 commonFormats.put("de", ddDot);
96 commonFormats.put("el", ddSlash);
97 commonFormats.put("en", mmSlash);
98 commonFormats.put("en-CA", yyyyDash);
99 commonFormats.put("en-NZ", ddSlash);
100 commonFormats.put("en-PA", ddSlash);
101 commonFormats.put("en-PR", ddSlash);
102 commonFormats.put("en-PH", mmSlash);
103 commonFormats.put("en-AU", ddSlash);
104 commonFormats.put("en-GB", ddSlash);
105 commonFormats.put("en-IE", ddSlash);
106 commonFormats.put("en-IN", ddSlash);
107 commonFormats.put("en-MT", ddSlash);
108 commonFormats.put("en-SG", ddSlash);
109 commonFormats.put("en-ZA", yyyySlash);
110 commonFormats.put("es", ddSlash);
111 commonFormats.put("es-CL", ddDash);
112 commonFormats.put("es-PA", mmSlash);
113 commonFormats.put("es-PR", mmSlash);
114 commonFormats.put("es-US", ddSlash);
115 commonFormats.put("et", ddDot);
116 commonFormats.put("fi", ddDot);
117 commonFormats.put("fr", ddSlash);
118 commonFormats.put("fr-CA", yyyyDash);
119 commonFormats.put("ga", ddSlash);
120 commonFormats.put("hi", ddSlash);
121 commonFormats.put("hr", ddDotBlankDot);
122 commonFormats.put("hu", yyyyDotBlankDot);
123 commonFormats.put("id", ddSlash);
124 commonFormats.put("in", ddSlash);
125 commonFormats.put("is", ddDot);
126 commonFormats.put("it", ddSlash);
127 commonFormats.put("iw", ddDot);
128 commonFormats.put("ja", yyyySlash);
129 commonFormats.put("ja-JP-u-ca-japanese", "'H'yy/MM/dd");
130 commonFormats.put("ko", yyyyDotBlankDot);
131 commonFormats.put("lt", yyyyDash);
132 commonFormats.put("lv", ddDotDot);
133 commonFormats.put("mk", ddDot + "\u200E \u0433.");
134 commonFormats.put("ms", ddSlash);
135 commonFormats.put("mt", mmSlash);
136 commonFormats.put("nl", ddDash);
137 commonFormats.put("nl-BE", ddSlash);
138 commonFormats.put("pl", ddDot);
139 commonFormats.put("pt", ddSlash);
140 commonFormats.put("ro", ddDot);
141 commonFormats.put("ru", ddDot);
142 commonFormats.put("sk", ddDotBlank);
143 commonFormats.put("sl", ddDotBlank);
144 commonFormats.put("sq", ddDot);
145 commonFormats.put("sr", ddDotBlankDot);
146 commonFormats.put("sv", yyyyDash);
147 commonFormats.put("th", ddSlash);
148 commonFormats.put("tr", ddDot);
149 commonFormats.put("uk", ddDot);
150 commonFormats.put("vi", ddSlash);
151 commonFormats.put("zh", yyyySlash);
152 commonFormats.put("zh-HK", ddSlash);
153 commonFormats.put("zh-SG", "\u200EYYYY\u200E\u5E74\u200EMM\u200E\u6708\u200Edd\u200E\u65E5");
154 commonFormats.put("fr-CH", ddDot);
155
156 FF_FORMATS_.putAll(commonFormats);
157 FF_ESR_FORMATS_.putAll(commonFormats);
158
159 commonFormats.put("be", mmSlash);
160 commonFormats.put("ga", mmSlash);
161 commonFormats.put("is", mmSlash);
162 commonFormats.put("mk", mmSlash);
163
164 EDGE_FORMATS_.putAll(commonFormats);
165
166 CHROME_FORMATS_.putAll(commonFormats);
167 CHROME_FORMATS_.put("sq", mmSlash);
168
169 final Map<String, Chronology> commonChronologies = new HashMap<>();
170 commonChronologies.put("ja-JP-u-ca-japanese", JapaneseChronology.INSTANCE);
171 commonChronologies.put("th", ThaiBuddhistChronology.INSTANCE);
172 commonChronologies.put("th-TH", ThaiBuddhistChronology.INSTANCE);
173
174 FF_CHRONOLOGIES_.putAll(commonChronologies);
175 FF_ESR_CHRONOLOGIES_.putAll(commonChronologies);
176 CHROME_CHRONOLOGIES_.putAll(commonChronologies);
177 EDGE_CHRONOLOGIES_.putAll(commonChronologies);
178 }
179
180
181
182
183 public DateTimeFormat() {
184 super();
185 }
186
187 private DateTimeFormat(final String[] locales, final BrowserVersion browserVersion) {
188 super();
189
190 final Map<String, String> formats;
191 final Map<String, Chronology> chronologies;
192 if (browserVersion.isChrome()) {
193 formats = CHROME_FORMATS_;
194 chronologies = CHROME_CHRONOLOGIES_;
195 }
196 else if (browserVersion.isEdge()) {
197 formats = EDGE_FORMATS_;
198 chronologies = EDGE_CHRONOLOGIES_;
199 }
200 else if (browserVersion.isFirefoxESR()) {
201 formats = FF_ESR_FORMATS_;
202 chronologies = FF_ESR_CHRONOLOGIES_;
203 }
204 else {
205 formats = FF_FORMATS_;
206 chronologies = FF_CHRONOLOGIES_;
207 }
208
209 String locale = browserVersion.getBrowserLocale().toLanguageTag();
210 String pattern = getPattern(formats, locale);
211
212 for (final String l : locales) {
213 pattern = getPattern(formats, l);
214 if (pattern != null) {
215 locale = l;
216 }
217 }
218
219 if (pattern == null) {
220 pattern = formats.get("");
221 }
222
223 if (!locale.startsWith("ar")) {
224 pattern = pattern.replace("\u200E", "");
225 }
226
227 final Chronology chronology = getChronology(chronologies, locale);
228
229 formatter_ = new DateTimeFormatHelper(locale, chronology, pattern);
230 }
231
232 private static String getPattern(final Map<String, String> formats, String locale) {
233 if ("no-NO-NY".equals(locale)) {
234 throw JavaScriptEngine.rangeError("Invalid language tag: " + locale);
235 }
236 String pattern = formats.get(locale);
237 while (pattern == null && locale.indexOf('-') != -1) {
238 locale = locale.substring(0, locale.lastIndexOf('-'));
239 pattern = formats.get(locale);
240 }
241 return pattern;
242 }
243
244 private static Chronology getChronology(final Map<String, Chronology> chronologies, String locale) {
245 Chronology chronology = chronologies.get(locale);
246 while (chronology == null && locale.indexOf('-') != -1) {
247 locale = locale.substring(0, locale.lastIndexOf('-'));
248 chronology = chronologies.get(locale);
249 }
250 return chronology;
251 }
252
253
254
255
256
257
258
259
260
261
262 @JsxConstructor
263 public static Scriptable jsConstructor(final Context cx, final VarScope scope,
264 final Object[] args, final Function ctorObj, final boolean inNewExpr) {
265 final String[] locales;
266 if (args.length != 0) {
267 if (args[0] instanceof NativeArray array) {
268 locales = new String[(int) array.getLength()];
269 for (int i = 0; i < locales.length; i++) {
270 locales[i] = JavaScriptEngine.toString(array.get(i));
271 }
272 }
273 else {
274 locales = new String[] {JavaScriptEngine.toString(args[0])};
275 }
276 }
277 else {
278 locales = new String[0];
279 }
280
281 final Window window = getWindow(ctorObj);
282 final DateTimeFormat format = new DateTimeFormat(locales, window.getBrowserVersion());
283 format.setParentScope(getTopLevelScope(scope));
284 format.setPrototype(((FunctionObject) ctorObj).getClassPrototype());
285 return format;
286 }
287
288
289
290
291
292
293 @JsxFunction
294 public String format(final Object object) {
295 final Date date = (Date) Context.jsToJava(object, Date.class);
296 return formatter_.format(date, Context.getCurrentContext().getTimeZone().toZoneId());
297 }
298
299
300
301
302
303
304 @JsxFunction
305 public Scriptable resolvedOptions() {
306 final Context cx = Context.getCurrentContext();
307 final Scriptable options = JavaScriptEngine.newObject(getParentScope());
308 options.put("timeZone", options, cx.getTimeZone().getID());
309
310 if (StringUtils.isEmptyOrNull(formatter_.locale_)) {
311 options.put("locale", options, cx.getLocale().toLanguageTag());
312 }
313 else {
314 options.put("locale", options, formatter_.locale_);
315 }
316 return options;
317 }
318
319
320
321
322
323
324
325
326 @JsxStaticFunction
327 public static Scriptable supportedLocalesOf(final Scriptable localesArgument, final Scriptable options) {
328 return Intl.supportedLocalesOf(localesArgument);
329 }
330
331 @Override
332 public Object getDefaultValue(final Class<?> hint) {
333 if (String.class.equals(hint) || hint == null) {
334 return "[object Intl.DateTimeFormat]";
335 }
336 return super.getDefaultValue(hint);
337 }
338
339
340
341
342 static final class DateTimeFormatHelper {
343
344 private final DateTimeFormatter formatter_;
345 private final Chronology chronology_;
346 private final String locale_;
347
348 DateTimeFormatHelper(final String locale, final Chronology chronology, final String pattern) {
349 locale_ = locale;
350 chronology_ = chronology;
351
352 if (locale.startsWith("ar")
353 && !"ar-DZ".equals(locale)
354 && !"ar-LY".equals(locale)
355 && !"ar-MA".equals(locale)
356 && !"ar-TN".equals(locale)) {
357 final DecimalStyle decimalStyle = DecimalStyle.STANDARD.withZeroDigit('\u0660');
358 formatter_ = DateTimeFormatter.ofPattern(pattern).withDecimalStyle(decimalStyle);
359 }
360 else {
361 formatter_ = DateTimeFormatter.ofPattern(pattern);
362 }
363 }
364
365
366
367
368
369
370
371 String format(final Date date, final ZoneId zoneId) {
372 TemporalAccessor zonedDateTime = date.toInstant().atZone(zoneId);
373 if (chronology_ != null) {
374 zonedDateTime = chronology_.date(zonedDateTime);
375 }
376 return formatter_.format(zonedDateTime);
377 }
378 }
379 }