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