1 /*
  2  * Copyright (c) 1997, 2023, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 /**
 25  * @test
 26  * @bug 4122840 4135202 4408066 4838107 8008577
 27  * @summary test NumberFormat
 28  * @modules java.base/sun.util.resources
 29  *          jdk.localedata
 30  * @compile -XDignore.symbol.file NumberTest.java
 31  * @run junit/othervm -Djava.locale.providers=COMPAT,SPI NumberTest
 32  */
 33 
 34 import java.util.*;
 35 import java.text.*;
 36 import sun.util.resources.LocaleData;
 37 
 38 import org.junit.jupiter.api.Test;
 39 
 40 import static org.junit.jupiter.api.Assertions.fail;
 41 
 42 public class NumberTest
 43 {
 44     // Test pattern handling
 45     @Test
 46     public void TestPatterns()
 47     {
 48     DecimalFormatSymbols sym = DecimalFormatSymbols.getInstance(Locale.US);
 49     String pat[]    = { "#.#", "#.", ".#", "#" };
 50     String newpat[] = { "#0.#", "#0.", "#.0", "#" };
 51     String num[]    = { "0",   "0.", ".0", "0" };
 52     for (int i=0; i<pat.length; ++i)
 53     {
 54         DecimalFormat fmt = new DecimalFormat(pat[i], sym);
 55         String newp = fmt.toPattern();
 56         if (!newp.equals(newpat[i]))
 57         fail("FAIL: Pattern " + pat[i] + " should transmute to " + newpat[i] +
 58               "; " + newp + " seen instead");
 59 
 60         String s = fmt.format(0);
 61         if (!s.equals(num[i]))
 62         {
 63         fail("FAIL: Pattern " + pat[i] + " should format zero as " + num[i] +
 64               "; " + s + " seen instead");
 65         System.out.println("Min integer digits = " + fmt.getMinimumIntegerDigits());
 66         }
 67     }
 68     }
 69 
 70     // Test exponential pattern
 71     @Test
 72     public void TestExponential() {
 73         DecimalFormatSymbols sym = DecimalFormatSymbols.getInstance(Locale.US);
 74         String pat[] = { "0.####E0", "00.000E00", "##0.####E000", "0.###E0;[0.###E0]"  };
 75         double val[] = { 0.01234, 123456789, 1.23e300, -3.141592653e-271 };
 76         long lval[] = { 0, -1, 1, 123456789 };
 77         String valFormat[] = {
 78                 "1.234E-2", "1.2346E8", "1.23E300", "-3.1416E-271",
 79                 "12.340E-03", "12.346E07", "12.300E299", "-31.416E-272",
 80                 "12.34E-003", "123.4568E006", "1.23E300", "-314.1593E-273",
 81                 "1.234E-2", "1.235E8", "1.23E300", "[3.142E-271]"
 82         };
 83         String lvalFormat[] = {
 84                 "0E0", "-1E0", "1E0", "1.2346E8",
 85                 "00.000E00", "-10.000E-01", "10.000E-01", "12.346E07",
 86                 "0E000", "-1E000", "1E000", "123.4568E006",
 87                 "0E0", "[1E0]", "1E0", "1.235E8"
 88         };
 89         double valParse[] = {
 90                 0.01234, 123460000, 1.23E300, -3.1416E-271,
 91                 0.01234, 123460000, 1.23E300, -3.1416E-271,
 92                 0.01234, 123456800, 1.23E300, -3.141593E-271,
 93                 0.01234, 123500000, 1.23E300, -3.142E-271,
 94         };
 95         long lvalParse[] = {
 96                 0, -1, 1, 123460000,
 97                 0, -1, 1, 123460000,
 98                 0, -1, 1, 123456800,
 99                 0, -1, 1, 123500000,
100         };
101         int ival = 0, ilval = 0;
102         for (int p=0; p<pat.length; ++p) {
103             DecimalFormat fmt = new DecimalFormat(pat[p], sym);
104             System.out.println("Pattern \"" + pat[p] + "\" -toPattern-> \"" +
105                   fmt.toPattern() + '"');
106 
107             for (int v=0; v<val.length; ++v) {
108                 String s = fmt.format(val[v]);
109                 System.out.println(" Format " + val[v] + " -> " + escape(s));
110                 if (!s.equals(valFormat[v+ival])) {
111                     fail("FAIL: Expected " + valFormat[v+ival] +
112                           ", got " + s +
113                           ", pattern=" + fmt.toPattern());
114                 }
115 
116                 ParsePosition pos = new ParsePosition(0);
117                 Number a = fmt.parse(s, pos);
118                 if (pos.getIndex() == s.length()) {
119                     System.out.println(" Parse -> " + a);
120                     if (a.doubleValue() != valParse[v+ival]) {
121                         fail("FAIL: Expected " + valParse[v+ival] +
122                               ", got " + a.doubleValue() +
123                               ", pattern=" + fmt.toPattern());
124                     }
125                 } else {
126                     fail(" FAIL: Partial parse (" + pos.getIndex() +
127                           " chars) -> " + a);
128                 }
129             }
130             for (int v=0; v<lval.length; ++v) {
131                 String s = fmt.format(lval[v]);
132                 System.out.println(" Format " + lval[v] + "L -> " + escape(s));
133                 if (!s.equals(lvalFormat[v+ilval])) {
134                     fail("ERROR: Expected " + lvalFormat[v+ilval] +
135                           ", got " + s +
136                           ", pattern=" + fmt.toPattern());
137                 }
138 
139                 ParsePosition pos = new ParsePosition(0);
140                 Number a = fmt.parse(s, pos);
141                 if (pos.getIndex() == s.length()) {
142                     System.out.println(" Parse -> " + a);
143                     if (a.longValue() != lvalParse[v+ilval]) {
144                         fail("FAIL: Expected " + lvalParse[v+ilval] +
145                               ", got " + a +
146                               ", pattern=" + fmt.toPattern());
147                     }
148                 } else {
149                     fail(" FAIL: Partial parse (" + pos.getIndex() +
150                           " chars) -> " + a);
151                 }
152             }
153             ival += val.length;
154             ilval += lval.length;
155         }
156     }
157 
158     // Test the handling of quotes
159     @Test
160     public void TestQuotes()
161     {
162     String pat;
163     DecimalFormatSymbols sym = DecimalFormatSymbols.getInstance(Locale.US);
164     DecimalFormat fmt = new DecimalFormat(pat = "a'fo''o'b#", sym);
165     String s = fmt.format(123);
166     System.out.println("Pattern \"" + pat + "\"");
167     System.out.println(" Format 123 -> " + escape(s));
168     if (!s.equals("afo'ob123")) fail("FAIL: Expected afo'ob123");
169 
170     fmt = new DecimalFormat(pat = "a''b#", sym);
171     s = fmt.format(123);
172     System.out.println("Pattern \"" + pat + "\"");
173     System.out.println(" Format 123 -> " + escape(s));
174     if (!s.equals("a'b123")) fail("FAIL: Expected a'b123");
175     }
176 
177     // Test the use of the currency sign
178     @Test
179     public void TestCurrencySign()
180     {
181     DecimalFormatSymbols sym = DecimalFormatSymbols.getInstance(Locale.US);
182     DecimalFormat fmt = new DecimalFormat("\u00A4#,##0.00;-\u00A4#,##0.00", sym);
183     // Can't test this properly until currency API goes public
184     // DecimalFormatSymbols sym = fmt.getDecimalFormatSymbols();
185 
186     String s = fmt.format(1234.56);
187     System.out.println("Pattern \"" + fmt.toPattern() + "\"");
188     System.out.println(" Format " + 1234.56 + " -> " + escape(s));
189     if (!s.equals("$1,234.56")) fail("FAIL: Expected $1,234.56");
190     s = fmt.format(-1234.56);
191     System.out.println(" Format " + -1234.56 + " -> " + escape(s));
192     if (!s.equals("-$1,234.56")) fail("FAIL: Expected -$1,234.56");
193 
194     fmt = new DecimalFormat("\u00A4\u00A4 #,##0.00;\u00A4\u00A4 -#,##0.00", sym);
195     s = fmt.format(1234.56);
196     System.out.println("Pattern \"" + fmt.toPattern() + "\"");
197     System.out.println(" Format " + 1234.56 + " -> " + escape(s));
198     if (!s.equals("USD 1,234.56")) fail("FAIL: Expected USD 1,234.56");
199     s = fmt.format(-1234.56);
200     System.out.println(" Format " + -1234.56 + " -> " + escape(s));
201     if (!s.equals("USD -1,234.56")) fail("FAIL: Expected USD -1,234.56");
202     }
203     static String escape(String s)
204     {
205     StringBuffer buf = new StringBuffer();
206     char HEX[] = { '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' };
207     for (int i=0; i<s.length(); ++i)
208     {
209         char c = s.charAt(i);
210         if (c <= (char)0x7F) buf.append(c);
211         else
212         {
213         buf.append("\\U");
214         buf.append(HEX[(c & 0xF000) >> 12]);
215         buf.append(HEX[(c & 0x0F00) >> 8]);
216         buf.append(HEX[(c & 0x00F0) >> 4]);
217         buf.append(HEX[c & 0x000F]);
218         }
219     }
220     return buf.toString();
221     }
222 
223     // Test simple currency format
224     // Bug 4024941; this code used to throw a NumberFormat exception
225     @Test
226     public void TestCurrency() {
227         NumberFormat currencyFmt =
228                 NumberFormat.getCurrencyInstance(Locale.CANADA_FRENCH);
229         String s = currencyFmt.format(1.50);
230         System.out.println("Un pauvre ici a..........." + s);
231         if (!s.equals("1,50 $")) {
232             fail("FAIL: Expected 1,50 $; got " + s + "; "+ dumpFmt(currencyFmt));
233         }
234         currencyFmt = NumberFormat.getCurrencyInstance(Locale.GERMANY);
235         s = currencyFmt.format(1.50);
236         System.out.println("Un pauvre en Allemagne a.." + s);
237         if (!s.equals("1,50 \u20AC")) {
238             fail("FAIL: Expected 1,50 \u20AC; got " + s + "; " + dumpFmt(currencyFmt));
239         }
240         currencyFmt = NumberFormat.getCurrencyInstance(Locale.FRANCE);
241         s = currencyFmt.format(1.50);
242         System.out.println("Un pauvre en France a....." + s);
243         if (!s.equals("1,50 \u20AC")) {
244             fail("FAIL: Expected 1,50 \u20AC; got " + s + "; " + dumpFmt(currencyFmt));
245         }
246     }
247 
248     String dumpFmt(NumberFormat numfmt) {
249         DecimalFormat fmt = (DecimalFormat)numfmt;
250         StringBuffer buf = new StringBuffer();
251         buf.append("pattern \"");
252         buf.append(fmt.toPattern());
253         buf.append("\", currency \"");
254         buf.append(fmt.getDecimalFormatSymbols().getCurrencySymbol());
255         buf.append("\"");
256         return buf.toString();
257     }
258 
259     // Test numeric parsing
260     // Bug 4059870
261     @Test
262     public void TestParse()
263     {
264     String arg = "0";
265     java.text.DecimalFormat format = new java.text.DecimalFormat("00");
266     try {
267         Number n = format.parse(arg);
268         System.out.println("parse(" + arg + ") = " + n);
269         if (n.doubleValue() != 0.0) fail("FAIL: Expected 0");
270     } catch (Exception e) { fail("Exception caught: " + e); }
271     }
272 
273     // Test rounding
274     @Test
275     public void TestRounding487() {
276         NumberFormat nf = NumberFormat.getInstance(Locale.US);
277         roundingTest(nf, 0.00159999, 4, "0.0016");
278         roundingTest(nf, 0.00995,  4, "0.01");
279         roundingTest(nf, 12.7995,  3, "12.8");
280         roundingTest(nf, 12.4999,  0, "12");
281         roundingTest(nf, -19.5,  0, "-20");
282     }
283 
284     void roundingTest(NumberFormat nf, double x, int maxFractionDigits, String expected) {
285         nf.setMaximumFractionDigits(maxFractionDigits);
286         String out = nf.format(x);
287         System.out.println("" + x + " formats with " + maxFractionDigits + " fractional digits to " + out);
288         if (!out.equals(expected)) {
289             fail("FAIL: Expected " + expected + ", got " + out);
290         }
291     }
292 
293     /**
294      * Bug 4135202
295      * DecimalFormat should recognize not only Latin digits 0-9 (\u0030-\u0039)
296      * but also various other ranges of Unicode digits, such as Arabic
297      * digits \u0660-\u0669 and Devanagari digits \u0966-\u096F, to name
298      * a couple.
299      * @see java.lang.Character#isDigit(char)
300      */
301     @Test
302     public void TestUnicodeDigits() {
303         char[] zeros = {
304             0x0030, // ISO-LATIN-1 digits ('0' through '9')
305             0x0660, // Arabic-Indic digits
306             0x06F0, // Extended Arabic-Indic digits
307             0x0966, // Devanagari digits
308             0x09E6, // Bengali digits
309             0x0A66, // Gurmukhi digits
310             0x0AE6, // Gujarati digits
311             0x0B66, // Oriya digits
312             0x0BE6, // Tamil digits
313             0x0C66, // Telugu digits
314             0x0CE6, // Kannada digits
315             0x0D66, // Malayalam digits
316             0x0E50, // Thai digits
317             0x0ED0, // Lao digits
318             0x0F20, // Tibetan digits
319             0xFF10, // Fullwidth digits
320         };
321         NumberFormat format = NumberFormat.getInstance();
322         for (int i=0; i<zeros.length; ++i) {
323             char zero = zeros[i];
324             StringBuffer buf = new StringBuffer();
325             buf.append((char)(zero+3));
326             buf.append((char)(zero+1));
327             buf.append((char)(zero+4));
328             int n = -1;
329             try {
330                 n = format.parse(buf.toString()).intValue();
331             }
332             catch (ParseException e) { n = -2; }
333             if (n != 314)
334                 fail("Can't parse Unicode " + Integer.toHexString(zero) + " as digit (" + n + ")");
335             else
336                 System.out.println("Parse digit " + Integer.toHexString(zero) + " ok");
337         }
338     }
339 
340     /**
341      * Bug 4122840
342      * Make sure that the currency symbol is not hard-coded in any locale.
343      */
344     @Test
345     public void TestCurrencySubstitution() {
346         final String SYM = "<currency>";
347         final String INTL_SYM = "<intl.currency>";
348         Locale[] locales = NumberFormat.getAvailableLocales();
349         for (int i=0; i<locales.length; ++i) {
350             NumberFormat nf = NumberFormat.getCurrencyInstance(locales[i]);
351             if (nf instanceof DecimalFormat) {
352                 DecimalFormat df = (DecimalFormat)nf;
353                 String genericPos = df.format(1234.5678);
354                 String genericNeg = df.format(-1234.5678);
355                 DecimalFormatSymbols sym = df.getDecimalFormatSymbols();
356                 sym.setCurrencySymbol(SYM);
357                 sym.setInternationalCurrencySymbol(INTL_SYM);
358                 // We have to make a new DecimalFormat from scratch in order
359                 // to make the new symbols 'take'.  This may be a bug or
360                 // design flaw in DecimalFormat.
361                 String[] patterns = LocaleData.getBundle("sun.text.resources.FormatData", locales[i])
362                                               .getStringArray("NumberPatterns");
363                 df = new DecimalFormat(patterns[1 /*CURRENCYSTYLE*/], sym);
364                 String customPos = df.format(1234.5678);
365                 String customNeg = df.format(-1234.5678);
366                 if (genericPos.equals(customPos) || genericNeg.equals(customNeg)) {
367                     fail("FAIL: " + locales[i] +
368                           " not using currency symbol substitution: " + genericPos);
369                 }
370                 else {
371                     if (customPos.indexOf(SYM) >= 0) {
372                         if (customNeg.indexOf(INTL_SYM) >= 0)
373                             fail("Fail: Positive and negative patterns use different symbols");
374                         else
375                             System.out.println("Ok: " + locales[i] +
376                                   " uses currency symbol: " + genericPos +
377                                   ", " + customPos);
378                     }
379                     else if (customPos.indexOf(INTL_SYM) >= 0) {
380                         if (customNeg.indexOf(SYM) >= 0)
381                             fail("Fail: Positive and negative patterns use different symbols");
382                         else
383                             System.out.println("Ok: " + locales[i] +
384                                   " uses intl. currency symbol: " + genericPos +
385                                   ", " + customPos);
386                     }
387                     else {
388                         fail("FAIL: " + locales[i] +
389                               " contains no currency symbol (impossible!)");
390                     }
391                 }
392             }
393             else System.out.println("Skipping " + locales[i] + "; not a DecimalFormat");
394         }
395     }
396 
397     @Test
398     public void TestIntegerFormat() throws ParseException {
399         NumberFormat format = NumberFormat.getIntegerInstance(Locale.GERMANY);
400 
401         float[] formatInput = { 12345.67f, -12345.67f, -0, 0 };
402         String[] formatExpected = { "12.346", "-12.346", "0", "0" };
403 
404         for (int i = 0; i < formatInput.length; i++) {
405             String result = format.format(formatInput[i]);
406             if (!result.equals(formatExpected[i])) {
407                 fail("FAIL: Expected " + formatExpected[i] + ", got " + result);
408             }
409         }
410 
411         String[] parseInput = { "0", "-0", "12.345,67", "-12.345,67" };
412         float[] parseExpected = { 0, 0, 12345, -12345 };
413 
414         for (int i = 0; i < parseInput.length; i++) {
415             float result = format.parse(parseInput[i]).floatValue();
416             if (result != parseExpected[i]) {
417                 fail("FAIL: Expected " + parseExpected[i] + ", got " + result);
418             }
419         }
420     }
421 }