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   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat">MDN Documentation</a>
49   */
50  @JsxClass
51  public class NumberFormat extends HtmlUnitScriptable {
52  
53      /** Symbol.toStringTag support. */
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      * Default constructor.
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      * JavaScript constructor.
174      * @param cx the current context
175      * @param scope the scope
176      * @param args the arguments to the NumberFormat constructor
177      * @param ctorObj the function object
178      * @param inNewExpr {@code true} if invoked with the {@code new} operator
179      * @return the Java object that JavaScript can access
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      * Formats a number according to the locale and formatting options of this Intl.NumberFormat object.
208      * @param object the JavaScript object to convert
209      * @return the formatted number
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      * Returns the resolved options for this Intl.NumberFormat object.
219      * @return a new object with properties reflecting the locale and number formatting options
220      *         computed during the initialization of the given {@code NumberFormat} object.
221      */
222     @JsxFunction
223     public Scriptable resolvedOptions() {
224         return JavaScriptEngine.newObject(getParentScope());
225     }
226 
227     /**
228      * Returns an array containing those of the provided locales that are supported
229      * without having to fall back to the default locale.
230      * @param localesArgument A string with a BCP 47 language tag, or an array of such strings
231      * @param options unused
232      * @return an array containing supported locales
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      * Helper.
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 }