View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
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   * A JavaScript object for Intl.NumberFormat.
43   *
44   * @author Ahmed Ashour
45   * @author Ronald Brill
46   * @author Lai Quang Duong
47   */
48  @JsxClass
49  public class NumberFormat extends HtmlUnitScriptable {
50  
51      /** Symbol.toStringTag support. */
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      * Default constructor.
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      * JavaScript constructor.
172      * @param cx the current context
173      * @param scope the scope
174      * @param args the arguments to the WebSocket constructor
175      * @param ctorObj the function object
176      * @param inNewExpr Is new or not
177      * @return the java object to allow JavaScript to access
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      * Formats a number according to the locale and formatting options of this Intl.NumberFormat object.
206      * @param object the JavaScript object to convert
207      * @return the dated formated
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      * @return A new object with properties reflecting the locale and date and time formatting options
217      *         computed during the initialization of the given {@code DateTimeFormat} object.
218      */
219     @JsxFunction
220     public Scriptable resolvedOptions() {
221         return JavaScriptEngine.newObject(getParentScope());
222     }
223 
224     /**
225      * Returns an array containing those of the provided locales that are supported
226      * without having to fall back to the default locale.
227      * @param localesArgument A string with a BCP 47 language tag, or an array of such strings
228      * @param options unused
229      * @return an array containing supported locales
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      * Helper.
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 }