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.text.DecimalFormat;
18 import java.text.DecimalFormatSymbols;
19 import java.util.HashMap;
20 import java.util.Locale;
21 import java.util.Map;
22 import java.util.concurrent.ConcurrentHashMap;
23
24 import org.htmlunit.BrowserVersion;
25 import org.htmlunit.corejs.javascript.Context;
26 import org.htmlunit.corejs.javascript.Function;
27 import org.htmlunit.corejs.javascript.FunctionObject;
28 import org.htmlunit.corejs.javascript.NativeArray;
29 import org.htmlunit.corejs.javascript.Scriptable;
30 import org.htmlunit.corejs.javascript.VarScope;
31 import org.htmlunit.javascript.HtmlUnitScriptable;
32 import org.htmlunit.javascript.JavaScriptEngine;
33 import org.htmlunit.javascript.configuration.JsxClass;
34 import org.htmlunit.javascript.configuration.JsxConstructor;
35 import org.htmlunit.javascript.configuration.JsxFunction;
36 import org.htmlunit.javascript.configuration.JsxStaticFunction;
37 import org.htmlunit.javascript.configuration.JsxSymbolConstant;
38 import org.htmlunit.javascript.host.Window;
39 import org.htmlunit.util.StringUtils;
40
41
42
43
44
45
46
47
48
49
50 @JsxClass
51 public class NumberFormat extends HtmlUnitScriptable {
52
53
54 @JsxSymbolConstant
55 public static final String TO_STRING_TAG = "Intl.NumberFormat";
56
57 private static final ConcurrentHashMap<String, String> CHROME_FORMATS_ = new ConcurrentHashMap<>();
58 private static final ConcurrentHashMap<String, String> EDGE_FORMATS_ = new ConcurrentHashMap<>();
59 private static final ConcurrentHashMap<String, String> FF_FORMATS_ = new ConcurrentHashMap<>();
60 private static final ConcurrentHashMap<String, String> FF_ESR_FORMATS_ = new ConcurrentHashMap<>();
61
62 private transient NumberFormatHelper formatter_;
63
64 static {
65 final Map<String, String> commonFormats = new HashMap<>();
66 commonFormats.put("", "");
67 commonFormats.put("ar-DZ", ".,");
68 commonFormats.put("ar-LY", ".,");
69 commonFormats.put("ar-MA", ".,");
70 commonFormats.put("ar-TN", ".,");
71 commonFormats.put("id", ".,");
72 commonFormats.put("de-AT", "\u00a0");
73 commonFormats.put("de-CH", "'");
74 commonFormats.put("en-ZA", "\u00a0,");
75 commonFormats.put("es-CR", "\u00a0,");
76 commonFormats.put("fr-LU", ".,");
77 commonFormats.put("hi-IN", ",.0");
78 commonFormats.put("it-CH", "'");
79 commonFormats.put("pt-PT", "\u00a0,");
80 commonFormats.put("sq", "\u00a0,");
81
82 commonFormats.put("ar-AE", ",.0");
83 commonFormats.put("fr", "\u202f,");
84 commonFormats.put("fr-CA", "\u00a0,");
85
86 commonFormats.put("ar", ",.0");
87 commonFormats.put("ar-BH", "\u066c\u066b\u0660");
88 commonFormats.put("ar-EG", "\u066c\u066b\u0660");
89 commonFormats.put("ar-IQ", "\u066c\u066b\u0660");
90 commonFormats.put("ar-JO", "\u066c\u066b\u0660");
91 commonFormats.put("ar-KW", "\u066c\u066b\u0660");
92 commonFormats.put("ar-LB", "\u066c\u066b\u0660");
93 commonFormats.put("ar-OM", "\u066c\u066b\u0660");
94 commonFormats.put("ar-QA", "\u066c\u066b\u0660");
95 commonFormats.put("ar-SA", "\u066c\u066b\u0660");
96 commonFormats.put("ar-SD", "\u066c\u066b\u0660");
97 commonFormats.put("ar-SY", "\u066c\u066b\u0660");
98 commonFormats.put("ar-YE", "\u066c\u066b\u0660");
99
100 FF_FORMATS_.putAll(commonFormats);
101 FF_FORMATS_.put("fr-CH", "',");
102
103 FF_ESR_FORMATS_.putAll(commonFormats);
104 FF_ESR_FORMATS_.put("de-CH", "\u2019");
105 FF_ESR_FORMATS_.put("it-CH", "\u2019");
106
107 commonFormats.put("be", ",.");
108 commonFormats.put("mk", ",.");
109 commonFormats.put("is", ",.");
110
111 CHROME_FORMATS_.putAll(commonFormats);
112 CHROME_FORMATS_.put("sq", ",.");
113
114 EDGE_FORMATS_.putAll(commonFormats);
115 }
116
117
118
119
120 public NumberFormat() {
121 super();
122 }
123
124 private NumberFormat(final String[] locales, final BrowserVersion browserVersion) {
125 super();
126
127 final Map<String, String> formats;
128 if (browserVersion.isChrome()) {
129 formats = CHROME_FORMATS_;
130 }
131 else if (browserVersion.isEdge()) {
132 formats = EDGE_FORMATS_;
133 }
134 else if (browserVersion.isFirefoxESR()) {
135 formats = FF_ESR_FORMATS_;
136 }
137 else {
138 formats = FF_FORMATS_;
139 }
140
141 String locale = "";
142 String pattern = null;
143
144 for (final String l : locales) {
145 pattern = getPattern(formats, l);
146 if (pattern != null) {
147 locale = l;
148 }
149 }
150
151 if (pattern == null) {
152 pattern = formats.get("");
153 if (locales.length > 0) {
154 locale = locales[0];
155 }
156 }
157
158 formatter_ = new NumberFormatHelper(locale, browserVersion, pattern);
159 }
160
161 private static String getPattern(final Map<String, String> formats, final String locale) {
162 if ("no-NO-NY".equals(locale)) {
163 throw JavaScriptEngine.rangeError("Invalid language tag: " + locale);
164 }
165 String pattern = formats.get(locale);
166 if (pattern == null && locale.indexOf('-') != -1) {
167 pattern = formats.get(locale.substring(0, locale.indexOf('-')));
168 }
169 return pattern;
170 }
171
172
173
174
175
176
177
178
179
180
181 @JsxConstructor
182 public static Scriptable jsConstructor(final Context cx, final VarScope scope,
183 final Object[] args, final Function ctorObj, final boolean inNewExpr) {
184 final String[] locales;
185 if (args.length == 0) {
186 locales = new String[] {""};
187 }
188 else {
189 if (args[0] instanceof NativeArray array) {
190 locales = new String[(int) array.getLength()];
191 for (int i = 0; i < locales.length; i++) {
192 locales[i] = JavaScriptEngine.toString(array.get(i));
193 }
194 }
195 else {
196 locales = new String[] {JavaScriptEngine.toString(args[0])};
197 }
198 }
199 final Window window = getWindow(ctorObj);
200 final NumberFormat format = new NumberFormat(locales, window.getBrowserVersion());
201 format.setParentScope(getTopLevelScope(scope));
202 format.setPrototype(((FunctionObject) ctorObj).getClassPrototype());
203 return format;
204 }
205
206
207
208
209
210
211 @JsxFunction
212 public String format(final Object object) {
213 final double number = JavaScriptEngine.toNumber(object);
214 return formatter_.format(number);
215 }
216
217
218
219
220
221
222 @JsxFunction
223 public Scriptable resolvedOptions() {
224 return JavaScriptEngine.newObject(getParentScope());
225 }
226
227
228
229
230
231
232
233
234 @JsxStaticFunction
235 public static Scriptable supportedLocalesOf(final Scriptable localesArgument, final Scriptable options) {
236 return Intl.supportedLocalesOf(localesArgument);
237 }
238
239 @Override
240 public Object getDefaultValue(final Class<?> hint) {
241 if (String.class.equals(hint) || hint == null) {
242 return "[object Intl.NumberFormat]";
243 }
244 return super.getDefaultValue(hint);
245 }
246
247
248
249
250 static final class NumberFormatHelper {
251 private final DecimalFormat formatter_;
252
253 NumberFormatHelper(final String localeName, final BrowserVersion browserVersion, final String pattern) {
254 Locale locale = browserVersion.getBrowserLocale();
255 if (!StringUtils.isEmptyOrNull(localeName)) {
256 locale = Locale.forLanguageTag(localeName);
257 }
258
259 final DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale);
260
261 if (pattern.length() > 0) {
262 final char groupingSeparator = pattern.charAt(0);
263 if (groupingSeparator != ' ') {
264 symbols.setGroupingSeparator(groupingSeparator);
265 }
266
267 if (pattern.length() > 1) {
268 final char decimalSeparator = pattern.charAt(1);
269 if (decimalSeparator != ' ') {
270 symbols.setDecimalSeparator(decimalSeparator);
271 }
272
273 if (pattern.length() > 2) {
274 final char zeroDigit = pattern.charAt(2);
275 if (zeroDigit != ' ') {
276 symbols.setZeroDigit(zeroDigit);
277 }
278 }
279 }
280 }
281
282 formatter_ = new DecimalFormat("#,##0.###", symbols);
283 }
284
285 String format(final double number) {
286 return formatter_.format(number);
287 }
288 }
289 }