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