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.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.javascript.HtmlUnitScriptable;
31  import org.htmlunit.javascript.JavaScriptEngine;
32  import org.htmlunit.javascript.configuration.JsxClass;
33  import org.htmlunit.javascript.configuration.JsxConstructor;
34  import org.htmlunit.javascript.configuration.JsxFunction;
35  import org.htmlunit.javascript.host.Window;
36  import org.htmlunit.util.StringUtils;
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          commonFormats.put("ar", ",.0");
78          commonFormats.put("ar-BH", "\u066c\u066b\u0660");
79          commonFormats.put("ar-EG", "\u066c\u066b\u0660");
80          commonFormats.put("ar-IQ", "\u066c\u066b\u0660");
81          commonFormats.put("ar-JO", "\u066c\u066b\u0660");
82          commonFormats.put("ar-KW", "\u066c\u066b\u0660");
83          commonFormats.put("ar-LB", "\u066c\u066b\u0660");
84          commonFormats.put("ar-OM", "\u066c\u066b\u0660");
85          commonFormats.put("ar-QA", "\u066c\u066b\u0660");
86          commonFormats.put("ar-SA", "\u066c\u066b\u0660");
87          commonFormats.put("ar-SD", "\u066c\u066b\u0660");
88          commonFormats.put("ar-SY", "\u066c\u066b\u0660");
89          commonFormats.put("ar-YE", "\u066c\u066b\u0660");
90  
91          FF_FORMATS_.putAll(commonFormats);
92          FF_ESR_FORMATS_.putAll(commonFormats);
93  
94          commonFormats.put("be", ",.");
95          commonFormats.put("en-ZA", ",.");
96          commonFormats.put("mk", ",.");
97          commonFormats.put("is", ",.");
98  
99          CHROME_FORMATS_.putAll(commonFormats);
100         CHROME_FORMATS_.put("sq", ",.");
101 
102         EDGE_FORMATS_.putAll(commonFormats);
103     }
104 
105     /**
106      * Default constructor.
107      */
108     public NumberFormat() {
109         super();
110     }
111 
112     private NumberFormat(final String[] locales, final BrowserVersion browserVersion) {
113         super();
114 
115         final Map<String, String> formats;
116         if (browserVersion.isChrome()) {
117             formats = CHROME_FORMATS_;
118         }
119         else if (browserVersion.isEdge()) {
120             formats = EDGE_FORMATS_;
121         }
122         else if (browserVersion.isFirefoxESR()) {
123             formats = FF_ESR_FORMATS_;
124         }
125         else {
126             formats = FF_FORMATS_;
127         }
128 
129         String locale = "";
130         String pattern = null;
131 
132         for (final String l : locales) {
133             pattern = getPattern(formats, l);
134             if (pattern != null) {
135                 locale = l;
136             }
137         }
138 
139         if (pattern == null) {
140             pattern = formats.get("");
141             if (locales.length > 0) {
142                 locale = locales[0];
143             }
144         }
145 
146         formatter_ = new NumberFormatHelper(locale, browserVersion, pattern);
147     }
148 
149     private static String getPattern(final Map<String, String> formats, final String locale) {
150         if ("no-NO-NY".equals(locale)) {
151             throw JavaScriptEngine.rangeError("Invalid language tag: " + locale);
152         }
153         String pattern = formats.get(locale);
154         if (pattern == null && locale.indexOf('-') != -1) {
155             pattern = formats.get(locale.substring(0, locale.indexOf('-')));
156         }
157         return pattern;
158     }
159 
160     /**
161      * JavaScript constructor.
162      * @param cx the current context
163      * @param scope the scope
164      * @param args the arguments to the WebSocket constructor
165      * @param ctorObj the function object
166      * @param inNewExpr Is new or not
167      * @return the java object to allow JavaScript to access
168      */
169     @JsxConstructor
170     public static Scriptable jsConstructor(final Context cx, final Scriptable scope,
171             final Object[] args, final Function ctorObj, final boolean inNewExpr) {
172         final String[] locales;
173         if (args.length == 0) {
174             locales = new String[] {""};
175         }
176         else {
177             if (args[0] instanceof NativeArray) {
178                 final NativeArray array = (NativeArray) args[0];
179                 locales = new String[(int) array.getLength()];
180                 for (int i = 0; i < locales.length; i++) {
181                     locales[i] = JavaScriptEngine.toString(array.get(i));
182                 }
183             }
184             else {
185                 locales = new String[] {JavaScriptEngine.toString(args[0])};
186             }
187         }
188         final Window window = getWindow(ctorObj);
189         final NumberFormat format = new NumberFormat(locales, window.getBrowserVersion());
190         format.setParentScope(window);
191         format.setPrototype(((FunctionObject) ctorObj).getClassPrototype());
192         return format;
193     }
194 
195     /**
196      * Formats a number according to the locale and formatting options of this Intl.NumberFormat object.
197      * @param object the JavaScript object to convert
198      * @return the dated formated
199      */
200     @JsxFunction
201     public String format(final Object object) {
202         final double number = JavaScriptEngine.toNumber(object);
203         return formatter_.format(number);
204     }
205 
206     /**
207      * @return A new object with properties reflecting the locale and date and time formatting options
208      *         computed during the initialization of the given {@code DateTimeFormat} object.
209      */
210     @JsxFunction
211     public Scriptable resolvedOptions() {
212         return Context.getCurrentContext().newObject(getParentScope());
213     }
214 
215     /**
216      * Helper.
217      */
218     static final class NumberFormatHelper {
219         private final DecimalFormat formatter_;
220 
221         NumberFormatHelper(final String localeName, final BrowserVersion browserVersion, final String pattern) {
222             Locale locale = browserVersion.getBrowserLocale();
223             if (!StringUtils.isEmptyOrNull(localeName)) {
224                 locale = Locale.forLanguageTag(localeName);
225             }
226 
227             final DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale);
228 
229             if (pattern.length() > 0) {
230                 final char groupingSeparator = pattern.charAt(0);
231                 if (groupingSeparator != ' ') {
232                     symbols.setGroupingSeparator(groupingSeparator);
233                 }
234 
235                 if (pattern.length() > 1) {
236                     final char decimalSeparator = pattern.charAt(1);
237                     if (decimalSeparator != ' ') {
238                         symbols.setDecimalSeparator(decimalSeparator);
239                     }
240 
241                     if (pattern.length() > 2) {
242                         final char zeroDigit = pattern.charAt(2);
243                         if (zeroDigit != ' ') {
244                             symbols.setZeroDigit(zeroDigit);
245                         }
246                     }
247                 }
248             }
249 
250             formatter_ = new DecimalFormat("#,##0.###", symbols);
251         }
252 
253         String format(final double number) {
254             return formatter_.format(number);
255         }
256     }
257 }