View Javadoc
1   /*
2    * Copyright (c) 2002-2025 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.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   * A JavaScript object for {@code NumberFormat}.
40   *
41   * @author Ahmed Ashour
42   * @author Ronald Brill
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      * Default constructor.
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      * JavaScript constructor.
163      * @param cx the current context
164      * @param scope the scope
165      * @param args the arguments to the WebSocket constructor
166      * @param ctorObj the function object
167      * @param inNewExpr Is new or not
168      * @return the java object to allow JavaScript to access
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      * Formats a number according to the locale and formatting options of this Intl.NumberFormat object.
198      * @param object the JavaScript object to convert
199      * @return the dated formated
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      * @return A new object with properties reflecting the locale and date and time formatting options
209      *         computed during the initialization of the given {@code DateTimeFormat} object.
210      */
211     @JsxFunction
212     public Scriptable resolvedOptions() {
213         return Context.getCurrentContext().newObject(getParentScope());
214     }
215 
216     /**
217      * Helper.
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 }