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.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.NativeArray;
35 import org.htmlunit.corejs.javascript.Scriptable;
36 import org.htmlunit.javascript.HtmlUnitScriptable;
37 import org.htmlunit.javascript.JavaScriptEngine;
38 import org.htmlunit.javascript.RecursiveFunctionObject;
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
46
47
48
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 transient DateTimeFormatHelper formatter_;
59
60 static {
61 final String ddSlash = "\u200Edd\u200E/\u200EMM\u200E/\u200EYYYY";
62 final String ddDash = "\u200Edd\u200E-\u200EMM\u200E-\u200EYYYY";
63 final String ddDot = "\u200Edd\u200E.\u200EMM\u200E.\u200EYYYY";
64 final String ddDotDot = "\u200Edd\u200E.\u200EMM\u200E.\u200EYYYY\u200E.";
65 final String ddDotBlank = "\u200Edd\u200E. \u200EMM\u200E. \u200EYYYY";
66 final String ddDotBlankDot = "\u200Edd\u200E. \u200EMM\u200E. \u200EYYYY.";
67 final String mmSlash = "\u200EMM\u200E/\u200Edd\u200E/\u200EYYYY";
68 final String yyyySlash = "\u200EYYYY\u200E/\u200EMM\u200E/\u200Edd";
69 final String yyyyDash = "\u200EYYYY\u200E-\u200EMM\u200E-\u200Edd";
70 final String yyyyDotBlankDot = "\u200EYYYY\u200E. \u200EMM\u200E. \u200Edd.";
71
72 final Map<String, String> commonFormats = new HashMap<>();
73 commonFormats.put("", ddDot);
74 commonFormats.put("ar", "dd\u200F/MM\u200F/YYYY");
75 commonFormats.put("ar-SA", "d\u200F/M\u200F/YYYY هـ");
76 commonFormats.put("ban", mmSlash);
77 commonFormats.put("be", ddDot);
78 commonFormats.put("bg", ddDot + "\u200E \u0433.");
79 commonFormats.put("ca", ddSlash);
80 commonFormats.put("cs", ddDotBlank);
81 commonFormats.put("da", ddDot);
82 commonFormats.put("de", ddDot);
83 commonFormats.put("el", ddSlash);
84 commonFormats.put("en", mmSlash);
85 commonFormats.put("en-CA", yyyyDash);
86 commonFormats.put("en-NZ", ddSlash);
87 commonFormats.put("en-PA", ddSlash);
88 commonFormats.put("en-PR", ddSlash);
89 commonFormats.put("en-PH", mmSlash);
90 commonFormats.put("en-AU", ddSlash);
91 commonFormats.put("en-GB", ddSlash);
92 commonFormats.put("en-IE", ddSlash);
93 commonFormats.put("en-IN", ddSlash);
94 commonFormats.put("en-MT", ddSlash);
95 commonFormats.put("en-SG", ddSlash);
96 commonFormats.put("en-ZA", yyyySlash);
97 commonFormats.put("es", ddSlash);
98 commonFormats.put("es-CL", ddDash);
99 commonFormats.put("es-PA", mmSlash);
100 commonFormats.put("es-PR", mmSlash);
101 commonFormats.put("es-US", ddSlash);
102 commonFormats.put("et", ddDot);
103 commonFormats.put("fi", ddDot);
104 commonFormats.put("fr", ddSlash);
105 commonFormats.put("fr-CA", yyyyDash);
106 commonFormats.put("ga", ddSlash);
107 commonFormats.put("hi", ddSlash);
108 commonFormats.put("hr", ddDotBlankDot);
109 commonFormats.put("hu", yyyyDotBlankDot);
110 commonFormats.put("id", ddSlash);
111 commonFormats.put("in", ddSlash);
112 commonFormats.put("is", ddDot);
113 commonFormats.put("it", ddSlash);
114 commonFormats.put("iw", ddDot);
115 commonFormats.put("ja", yyyySlash);
116 commonFormats.put("ja-JP-u-ca-japanese", "'H'yy/MM/dd");
117 commonFormats.put("ko", yyyyDotBlankDot);
118 commonFormats.put("lt", yyyyDash);
119 commonFormats.put("lv", ddDotDot);
120 commonFormats.put("mk", ddDot);
121 commonFormats.put("ms", ddSlash);
122 commonFormats.put("mt", mmSlash);
123 commonFormats.put("nl", ddDash);
124 commonFormats.put("nl-BE", ddSlash);
125 commonFormats.put("pl", ddDot);
126 commonFormats.put("pt", ddSlash);
127 commonFormats.put("ro", ddDot);
128 commonFormats.put("ru", ddDot);
129 commonFormats.put("sk", ddDotBlank);
130 commonFormats.put("sl", ddDotBlank);
131 commonFormats.put("sq", ddDot);
132 commonFormats.put("sr", ddDotBlankDot);
133 commonFormats.put("sv", yyyyDash);
134 commonFormats.put("th", ddSlash);
135 commonFormats.put("tr", ddDot);
136 commonFormats.put("uk", ddDot);
137 commonFormats.put("vi", ddSlash);
138 commonFormats.put("zh", yyyySlash);
139 commonFormats.put("zh-HK", ddSlash);
140 commonFormats.put("zh-SG", "\u200EYYYY\u200E\u5E74\u200EMM\u200E\u6708\u200Edd\u200E\u65E5");
141 commonFormats.put("fr-CH", ddDot);
142
143 FF_FORMATS_.putAll(commonFormats);
144 FF_ESR_FORMATS_.putAll(commonFormats);
145
146 commonFormats.put("be", mmSlash);
147 commonFormats.put("ga", mmSlash);
148 commonFormats.put("is", mmSlash);
149 commonFormats.put("mk", mmSlash);
150
151 EDGE_FORMATS_.putAll(commonFormats);
152
153 CHROME_FORMATS_.putAll(commonFormats);
154 CHROME_FORMATS_.put("sq", mmSlash);
155 }
156
157
158
159
160 public DateTimeFormat() {
161 super();
162 }
163
164 private DateTimeFormat(final String[] locales, final BrowserVersion browserVersion) {
165 super();
166
167 final Map<String, String> formats;
168 if (browserVersion.isChrome()) {
169 formats = CHROME_FORMATS_;
170 }
171 else if (browserVersion.isEdge()) {
172 formats = EDGE_FORMATS_;
173 }
174 else if (browserVersion.isFirefoxESR()) {
175 formats = FF_ESR_FORMATS_;
176 }
177 else {
178 formats = FF_FORMATS_;
179 }
180
181 String locale = browserVersion.getBrowserLocale().toLanguageTag();
182 String pattern = getPattern(formats, locale);
183
184 for (final String l : locales) {
185 pattern = getPattern(formats, l);
186 if (pattern != null) {
187 locale = l;
188 }
189 }
190
191 if (pattern == null) {
192 pattern = formats.get("");
193 }
194
195 if (!locale.startsWith("ar")) {
196 pattern = pattern.replace("\u200E", "");
197 }
198
199 formatter_ = new DateTimeFormatHelper(locale, browserVersion, pattern);
200 }
201
202 private static String getPattern(final Map<String, String> formats, final String locale) {
203 if ("no-NO-NY".equals(locale)) {
204 throw JavaScriptEngine.rangeError("Invalid language tag: " + locale);
205 }
206 String pattern = formats.get(locale);
207 if (pattern == null && locale.indexOf('-') != -1) {
208 pattern = formats.get(locale.substring(0, locale.indexOf('-')));
209 }
210 return pattern;
211 }
212
213
214
215
216
217
218
219
220
221
222 @JsxConstructor
223 public static Scriptable jsConstructor(final Context cx, final Scriptable scope,
224 final Object[] args, final Function ctorObj, final boolean inNewExpr) {
225 final String[] locales;
226 if (args.length != 0) {
227 if (args[0] instanceof NativeArray) {
228 final NativeArray array = (NativeArray) args[0];
229 locales = new String[(int) array.getLength()];
230 for (int i = 0; i < locales.length; i++) {
231 locales[i] = JavaScriptEngine.toString(array.get(i));
232 }
233 }
234 else {
235 locales = new String[] {JavaScriptEngine.toString(args[0])};
236 }
237 }
238 else {
239 locales = new String[0];
240 }
241
242 final Window window = getWindow(ctorObj);
243 final DateTimeFormat format = new DateTimeFormat(locales, window.getBrowserVersion());
244 format.setParentScope(window);
245 format.setPrototype(((RecursiveFunctionObject) ctorObj).getClassPrototype());
246 return format;
247 }
248
249
250
251
252
253
254 @JsxFunction
255 public String format(final Object object) {
256 final Date date = (Date) Context.jsToJava(object, Date.class);
257 return formatter_.format(date, Context.getCurrentContext().getTimeZone().toZoneId());
258 }
259
260
261
262
263
264 @JsxFunction
265 public Scriptable resolvedOptions() {
266 final Context cx = Context.getCurrentContext();
267 final Scriptable options = cx.newObject(getParentScope());
268 options.put("timeZone", options, cx.getTimeZone().getID());
269
270 if (StringUtils.isEmpty(formatter_.locale_)) {
271 options.put("locale", options, cx.getLocale().toLanguageTag());
272 }
273 else {
274 options.put("locale", options, formatter_.locale_);
275 }
276 return options;
277 }
278
279
280
281
282 static final class DateTimeFormatHelper {
283
284 private final DateTimeFormatter formatter_;
285 private Chronology chronology_;
286 private String locale_;
287
288 DateTimeFormatHelper(final String locale, final BrowserVersion browserVersion, final String pattern) {
289 locale_ = locale;
290 if (locale.startsWith("ar")
291 && !"ar-DZ".equals(locale)
292 && !"ar-LY".equals(locale)
293 && !"ar-MA".equals(locale)
294 && !"ar-TN".equals(locale)) {
295 final DecimalStyle decimalStyle = DecimalStyle.STANDARD.withZeroDigit('\u0660');
296 formatter_ = DateTimeFormatter.ofPattern(pattern).withDecimalStyle(decimalStyle);
297 }
298 else {
299 formatter_ = DateTimeFormatter.ofPattern(pattern);
300 }
301
302 switch (locale) {
303 case "ja-JP-u-ca-japanese":
304 chronology_ = JapaneseChronology.INSTANCE;
305 break;
306
307 case "ar-SA":
308 chronology_ = HijrahChronology.INSTANCE;
309 break;
310
311 case "th":
312 case "th-TH":
313 chronology_ = ThaiBuddhistChronology.INSTANCE;
314 break;
315
316 default:
317 }
318 }
319
320
321
322
323
324
325
326 String format(final Date date, final ZoneId zoneId) {
327 TemporalAccessor zonedDateTime = date.toInstant().atZone(zoneId);
328 if (chronology_ != null) {
329 zonedDateTime = chronology_.date(zonedDateTime);
330 }
331 return formatter_.format(zonedDateTime);
332 }
333 }
334 }