Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
PojavLauncherTeam
GitHub Repository: PojavLauncherTeam/mobile
Path: blob/master/src/java.base/share/classes/java/util/Currency.java
41152 views
1
/*
2
* Copyright (c) 2000, 2021, 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. Oracle designates this
8
* particular file as subject to the "Classpath" exception as provided
9
* by Oracle in the LICENSE file that accompanied this code.
10
*
11
* This code is distributed in the hope that it will be useful, but WITHOUT
12
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14
* version 2 for more details (a copy is included in the LICENSE file that
15
* accompanied this code).
16
*
17
* You should have received a copy of the GNU General Public License version
18
* 2 along with this work; if not, write to the Free Software Foundation,
19
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20
*
21
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22
* or visit www.oracle.com if you need additional information or have any
23
* questions.
24
*/
25
26
package java.util;
27
28
import java.io.BufferedInputStream;
29
import java.io.DataInputStream;
30
import java.io.File;
31
import java.io.FileReader;
32
import java.io.InputStream;
33
import java.io.IOException;
34
import java.io.Serializable;
35
import java.security.AccessController;
36
import java.security.PrivilegedAction;
37
import java.text.ParseException;
38
import java.text.SimpleDateFormat;
39
import java.util.concurrent.ConcurrentHashMap;
40
import java.util.concurrent.ConcurrentMap;
41
import java.util.regex.Pattern;
42
import java.util.regex.Matcher;
43
import java.util.spi.CurrencyNameProvider;
44
import java.util.stream.Collectors;
45
46
import jdk.internal.util.StaticProperty;
47
import sun.util.locale.provider.CalendarDataUtility;
48
import sun.util.locale.provider.LocaleServiceProviderPool;
49
import sun.util.logging.PlatformLogger;
50
51
52
/**
53
* Represents a currency. Currencies are identified by their ISO 4217 currency
54
* codes. Visit the <a href="http://www.iso.org/iso/home/standards/currency_codes.htm">
55
* ISO web site</a> for more information.
56
* <p>
57
* The class is designed so that there's never more than one
58
* {@code Currency} instance for any given currency. Therefore, there's
59
* no public constructor. You obtain a {@code Currency} instance using
60
* the {@code getInstance} methods.
61
* <p>
62
* Users can supersede the Java runtime currency data by means of the system
63
* property {@systemProperty java.util.currency.data}. If this system property is
64
* defined then its value is the location of a properties file, the contents of
65
* which are key/value pairs of the ISO 3166 country codes and the ISO 4217
66
* currency data respectively. The value part consists of three ISO 4217 values
67
* of a currency, i.e., an alphabetic code, a numeric code, and a minor unit.
68
* Those three ISO 4217 values are separated by commas.
69
* The lines which start with '#'s are considered comment lines. An optional UTC
70
* timestamp may be specified per currency entry if users need to specify a
71
* cutover date indicating when the new data comes into effect. The timestamp is
72
* appended to the end of the currency properties and uses a comma as a separator.
73
* If a UTC datestamp is present and valid, the JRE will only use the new currency
74
* properties if the current UTC date is later than the date specified at class
75
* loading time. The format of the timestamp must be of ISO 8601 format :
76
* {@code 'yyyy-MM-dd'T'HH:mm:ss'}. For example,
77
* <p>
78
* <code>
79
* #Sample currency properties<br>
80
* JP=JPZ,999,0
81
* </code>
82
* <p>
83
* will supersede the currency data for Japan. If JPZ is one of the existing
84
* ISO 4217 currency code referred by other countries, the existing
85
* JPZ currency data is updated with the given numeric code and minor
86
* unit value.
87
*
88
* <p>
89
* <code>
90
* #Sample currency properties with cutover date<br>
91
* JP=JPZ,999,0,2014-01-01T00:00:00
92
* </code>
93
* <p>
94
* will supersede the currency data for Japan if {@code Currency} class is loaded after
95
* 1st January 2014 00:00:00 GMT.
96
* <p>
97
* Where syntactically malformed entries are encountered, the entry is ignored
98
* and the remainder of entries in file are processed. For instances where duplicate
99
* country code entries exist, the behavior of the Currency information for that
100
* {@code Currency} is undefined and the remainder of entries in file are processed.
101
* <p>
102
* If multiple property entries with same currency code but different numeric code
103
* and/or minor unit are encountered, those entries are ignored and the remainder
104
* of entries in file are processed.
105
*
106
* <p>
107
* It is recommended to use {@link java.math.BigDecimal} class while dealing
108
* with {@code Currency} or monetary values as it provides better handling of floating
109
* point numbers and their operations.
110
*
111
* @see java.math.BigDecimal
112
* @since 1.4
113
*/
114
@SuppressWarnings("removal")
115
public final class Currency implements Serializable {
116
117
@java.io.Serial
118
private static final long serialVersionUID = -158308464356906721L;
119
120
/**
121
* ISO 4217 currency code for this currency.
122
*
123
* @serial
124
*/
125
private final String currencyCode;
126
127
/**
128
* Default fraction digits for this currency.
129
* Set from currency data tables.
130
*/
131
private final transient int defaultFractionDigits;
132
133
/**
134
* ISO 4217 numeric code for this currency.
135
* Set from currency data tables.
136
*/
137
private final transient int numericCode;
138
139
140
// class data: instance map
141
142
private static ConcurrentMap<String, Currency> instances = new ConcurrentHashMap<>(7);
143
private static HashSet<Currency> available;
144
145
// Class data: currency data obtained from currency.data file.
146
// Purpose:
147
// - determine valid country codes
148
// - determine valid currency codes
149
// - map country codes to currency codes
150
// - obtain default fraction digits for currency codes
151
//
152
// sc = special case; dfd = default fraction digits
153
// Simple countries are those where the country code is a prefix of the
154
// currency code, and there are no known plans to change the currency.
155
//
156
// table formats:
157
// - mainTable:
158
// - maps country code to 32-bit int
159
// - 26*26 entries, corresponding to [A-Z]*[A-Z]
160
// - \u007F -> not valid country
161
// - bits 20-31: unused
162
// - bits 10-19: numeric code (0 to 1023)
163
// - bit 9: 1 - special case, bits 0-4 indicate which one
164
// 0 - simple country, bits 0-4 indicate final char of currency code
165
// - bits 5-8: fraction digits for simple countries, 0 for special cases
166
// - bits 0-4: final char for currency code for simple country, or ID of special case
167
// - special case IDs:
168
// - 0: country has no currency
169
// - other: index into specialCasesList
170
171
static int formatVersion;
172
static int dataVersion;
173
static int[] mainTable;
174
static List<SpecialCaseEntry> specialCasesList;
175
static List<OtherCurrencyEntry> otherCurrenciesList;
176
177
// handy constants - must match definitions in GenerateCurrencyData
178
// magic number
179
private static final int MAGIC_NUMBER = 0x43757244;
180
// number of characters from A to Z
181
private static final int A_TO_Z = ('Z' - 'A') + 1;
182
// entry for invalid country codes
183
private static final int INVALID_COUNTRY_ENTRY = 0x0000007F;
184
// entry for countries without currency
185
private static final int COUNTRY_WITHOUT_CURRENCY_ENTRY = 0x00000200;
186
// mask for simple case country entries
187
private static final int SIMPLE_CASE_COUNTRY_MASK = 0x00000000;
188
// mask for simple case country entry final character
189
private static final int SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK = 0x0000001F;
190
// mask for simple case country entry default currency digits
191
private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK = 0x000001E0;
192
// shift count for simple case country entry default currency digits
193
private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT = 5;
194
// maximum number for simple case country entry default currency digits
195
private static final int SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS = 9;
196
// mask for special case country entries
197
private static final int SPECIAL_CASE_COUNTRY_MASK = 0x00000200;
198
// mask for special case country index
199
private static final int SPECIAL_CASE_COUNTRY_INDEX_MASK = 0x0000001F;
200
// delta from entry index component in main table to index into special case tables
201
private static final int SPECIAL_CASE_COUNTRY_INDEX_DELTA = 1;
202
// mask for distinguishing simple and special case countries
203
private static final int COUNTRY_TYPE_MASK = SIMPLE_CASE_COUNTRY_MASK | SPECIAL_CASE_COUNTRY_MASK;
204
// mask for the numeric code of the currency
205
private static final int NUMERIC_CODE_MASK = 0x000FFC00;
206
// shift count for the numeric code of the currency
207
private static final int NUMERIC_CODE_SHIFT = 10;
208
209
// Currency data format version
210
private static final int VALID_FORMAT_VERSION = 3;
211
212
static {
213
AccessController.doPrivileged(new PrivilegedAction<>() {
214
@Override
215
public Void run() {
216
try {
217
try (InputStream in = getClass().getResourceAsStream("/java/util/currency.data")) {
218
if (in == null) {
219
throw new InternalError("Currency data not found");
220
}
221
DataInputStream dis = new DataInputStream(new BufferedInputStream(in));
222
if (dis.readInt() != MAGIC_NUMBER) {
223
throw new InternalError("Currency data is possibly corrupted");
224
}
225
formatVersion = dis.readInt();
226
if (formatVersion != VALID_FORMAT_VERSION) {
227
throw new InternalError("Currency data format is incorrect");
228
}
229
dataVersion = dis.readInt();
230
mainTable = readIntArray(dis, A_TO_Z * A_TO_Z);
231
int scCount = dis.readInt();
232
specialCasesList = readSpecialCases(dis, scCount);
233
int ocCount = dis.readInt();
234
otherCurrenciesList = readOtherCurrencies(dis, ocCount);
235
}
236
} catch (IOException e) {
237
throw new InternalError(e);
238
}
239
240
// look for the properties file for overrides
241
String propsFile = System.getProperty("java.util.currency.data");
242
if (propsFile == null) {
243
propsFile = StaticProperty.javaHome() + File.separator + "lib" +
244
File.separator + "currency.properties";
245
}
246
try {
247
File propFile = new File(propsFile);
248
if (propFile.exists()) {
249
Properties props = new Properties();
250
try (FileReader fr = new FileReader(propFile)) {
251
props.load(fr);
252
}
253
Pattern propertiesPattern =
254
Pattern.compile("([A-Z]{3})\\s*,\\s*(\\d{3})\\s*,\\s*" +
255
"(\\d+)\\s*,?\\s*(\\d{4}-\\d{2}-\\d{2}T\\d{2}:" +
256
"\\d{2}:\\d{2})?");
257
List<CurrencyProperty> currencyEntries
258
= getValidCurrencyData(props, propertiesPattern);
259
currencyEntries.forEach(Currency::replaceCurrencyData);
260
}
261
} catch (IOException e) {
262
CurrencyProperty.info("currency.properties is ignored"
263
+ " because of an IOException", e);
264
}
265
return null;
266
}
267
});
268
}
269
270
/**
271
* Constants for retrieving localized names from the name providers.
272
*/
273
private static final int SYMBOL = 0;
274
private static final int DISPLAYNAME = 1;
275
276
277
/**
278
* Constructs a {@code Currency} instance. The constructor is private
279
* so that we can insure that there's never more than one instance for a
280
* given currency.
281
*/
282
private Currency(String currencyCode, int defaultFractionDigits, int numericCode) {
283
this.currencyCode = currencyCode;
284
this.defaultFractionDigits = defaultFractionDigits;
285
this.numericCode = numericCode;
286
}
287
288
/**
289
* Returns the {@code Currency} instance for the given currency code.
290
*
291
* @param currencyCode the ISO 4217 code of the currency
292
* @return the {@code Currency} instance for the given currency code
293
* @throws NullPointerException if {@code currencyCode} is null
294
* @throws IllegalArgumentException if {@code currencyCode} is not
295
* a supported ISO 4217 code.
296
*/
297
public static Currency getInstance(String currencyCode) {
298
return getInstance(currencyCode, Integer.MIN_VALUE, 0);
299
}
300
301
private static Currency getInstance(String currencyCode, int defaultFractionDigits,
302
int numericCode) {
303
// Try to look up the currency code in the instances table.
304
// This does the null pointer check as a side effect.
305
// Also, if there already is an entry, the currencyCode must be valid.
306
Currency instance = instances.get(currencyCode);
307
if (instance != null) {
308
return instance;
309
}
310
311
if (defaultFractionDigits == Integer.MIN_VALUE) {
312
// Currency code not internally generated, need to verify first
313
// A currency code must have 3 characters and exist in the main table
314
// or in the list of other currencies.
315
boolean found = false;
316
if (currencyCode.length() != 3) {
317
throw new IllegalArgumentException();
318
}
319
char char1 = currencyCode.charAt(0);
320
char char2 = currencyCode.charAt(1);
321
int tableEntry = getMainTableEntry(char1, char2);
322
if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK
323
&& tableEntry != INVALID_COUNTRY_ENTRY
324
&& currencyCode.charAt(2) - 'A' == (tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK)) {
325
defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;
326
numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT;
327
found = true;
328
} else { //special case
329
int[] fractionAndNumericCode = SpecialCaseEntry.findEntry(currencyCode);
330
if (fractionAndNumericCode != null) {
331
defaultFractionDigits = fractionAndNumericCode[0];
332
numericCode = fractionAndNumericCode[1];
333
found = true;
334
}
335
}
336
337
if (!found) {
338
OtherCurrencyEntry ocEntry = OtherCurrencyEntry.findEntry(currencyCode);
339
if (ocEntry == null) {
340
throw new IllegalArgumentException();
341
}
342
defaultFractionDigits = ocEntry.fraction;
343
numericCode = ocEntry.numericCode;
344
}
345
}
346
347
Currency currencyVal =
348
new Currency(currencyCode, defaultFractionDigits, numericCode);
349
instance = instances.putIfAbsent(currencyCode, currencyVal);
350
return (instance != null ? instance : currencyVal);
351
}
352
353
/**
354
* Returns the {@code Currency} instance for the country of the
355
* given locale. The language and variant components of the locale
356
* are ignored. The result may vary over time, as countries change their
357
* currencies. For example, for the original member countries of the
358
* European Monetary Union, the method returns the old national currencies
359
* until December 31, 2001, and the Euro from January 1, 2002, local time
360
* of the respective countries.
361
* <p>
362
* If the specified {@code locale} contains "cu" and/or "rg"
363
* <a href="./Locale.html#def_locale_extension">Unicode extensions</a>,
364
* the instance returned from this method reflects
365
* the values specified with those extensions. If both "cu" and "rg" are
366
* specified, the currency from the "cu" extension supersedes the implicit one
367
* from the "rg" extension.
368
* <p>
369
* The method returns {@code null} for territories that don't
370
* have a currency, such as Antarctica.
371
*
372
* @param locale the locale for whose country a {@code Currency}
373
* instance is needed
374
* @return the {@code Currency} instance for the country of the given
375
* locale, or {@code null}
376
* @throws NullPointerException if {@code locale}
377
* is {@code null}
378
* @throws IllegalArgumentException if the country of the given {@code locale}
379
* is not a supported ISO 3166 country code.
380
*/
381
public static Currency getInstance(Locale locale) {
382
// check for locale overrides
383
String override = locale.getUnicodeLocaleType("cu");
384
if (override != null) {
385
try {
386
return getInstance(override.toUpperCase(Locale.ROOT));
387
} catch (IllegalArgumentException iae) {
388
// override currency is invalid. Fall through.
389
}
390
}
391
392
String country = CalendarDataUtility.findRegionOverride(locale).getCountry();
393
394
if (country == null || !country.matches("^[a-zA-Z]{2}$")) {
395
throw new IllegalArgumentException();
396
}
397
398
char char1 = country.charAt(0);
399
char char2 = country.charAt(1);
400
int tableEntry = getMainTableEntry(char1, char2);
401
if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK
402
&& tableEntry != INVALID_COUNTRY_ENTRY) {
403
char finalChar = (char) ((tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK) + 'A');
404
int defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;
405
int numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT;
406
StringBuilder sb = new StringBuilder(country);
407
sb.append(finalChar);
408
return getInstance(sb.toString(), defaultFractionDigits, numericCode);
409
} else {
410
// special cases
411
if (tableEntry == INVALID_COUNTRY_ENTRY) {
412
throw new IllegalArgumentException();
413
}
414
if (tableEntry == COUNTRY_WITHOUT_CURRENCY_ENTRY) {
415
return null;
416
} else {
417
int index = SpecialCaseEntry.toIndex(tableEntry);
418
SpecialCaseEntry scEntry = specialCasesList.get(index);
419
if (scEntry.cutOverTime == Long.MAX_VALUE
420
|| System.currentTimeMillis() < scEntry.cutOverTime) {
421
return getInstance(scEntry.oldCurrency,
422
scEntry.oldCurrencyFraction,
423
scEntry.oldCurrencyNumericCode);
424
} else {
425
return getInstance(scEntry.newCurrency,
426
scEntry.newCurrencyFraction,
427
scEntry.newCurrencyNumericCode);
428
}
429
}
430
}
431
}
432
433
/**
434
* Gets the set of available currencies. The returned set of currencies
435
* contains all of the available currencies, which may include currencies
436
* that represent obsolete ISO 4217 codes. The set can be modified
437
* without affecting the available currencies in the runtime.
438
*
439
* @return the set of available currencies. If there is no currency
440
* available in the runtime, the returned set is empty.
441
* @since 1.7
442
*/
443
public static Set<Currency> getAvailableCurrencies() {
444
synchronized(Currency.class) {
445
if (available == null) {
446
available = new HashSet<>(256);
447
448
// Add simple currencies first
449
for (char c1 = 'A'; c1 <= 'Z'; c1 ++) {
450
for (char c2 = 'A'; c2 <= 'Z'; c2 ++) {
451
int tableEntry = getMainTableEntry(c1, c2);
452
if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK
453
&& tableEntry != INVALID_COUNTRY_ENTRY) {
454
char finalChar = (char) ((tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK) + 'A');
455
int defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;
456
int numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT;
457
StringBuilder sb = new StringBuilder();
458
sb.append(c1);
459
sb.append(c2);
460
sb.append(finalChar);
461
available.add(getInstance(sb.toString(), defaultFractionDigits, numericCode));
462
} else if ((tableEntry & COUNTRY_TYPE_MASK) == SPECIAL_CASE_COUNTRY_MASK
463
&& tableEntry != INVALID_COUNTRY_ENTRY
464
&& tableEntry != COUNTRY_WITHOUT_CURRENCY_ENTRY) {
465
int index = SpecialCaseEntry.toIndex(tableEntry);
466
SpecialCaseEntry scEntry = specialCasesList.get(index);
467
468
if (scEntry.cutOverTime == Long.MAX_VALUE
469
|| System.currentTimeMillis() < scEntry.cutOverTime) {
470
available.add(getInstance(scEntry.oldCurrency,
471
scEntry.oldCurrencyFraction,
472
scEntry.oldCurrencyNumericCode));
473
} else {
474
available.add(getInstance(scEntry.newCurrency,
475
scEntry.newCurrencyFraction,
476
scEntry.newCurrencyNumericCode));
477
}
478
}
479
}
480
}
481
482
// Now add other currencies
483
for (OtherCurrencyEntry entry : otherCurrenciesList) {
484
available.add(getInstance(entry.currencyCode));
485
}
486
}
487
}
488
489
@SuppressWarnings("unchecked")
490
Set<Currency> result = (Set<Currency>) available.clone();
491
return result;
492
}
493
494
/**
495
* Gets the ISO 4217 currency code of this currency.
496
*
497
* @return the ISO 4217 currency code of this currency.
498
*/
499
public String getCurrencyCode() {
500
return currencyCode;
501
}
502
503
/**
504
* Gets the symbol of this currency for the default
505
* {@link Locale.Category#DISPLAY DISPLAY} locale.
506
* For example, for the US Dollar, the symbol is "$" if the default
507
* locale is the US, while for other locales it may be "US$". If no
508
* symbol can be determined, the ISO 4217 currency code is returned.
509
* <p>
510
* If the default {@link Locale.Category#DISPLAY DISPLAY} locale
511
* contains "rg" (region override)
512
* <a href="./Locale.html#def_locale_extension">Unicode extension</a>,
513
* the symbol returned from this method reflects
514
* the value specified with that extension.
515
* <p>
516
* This is equivalent to calling
517
* {@link #getSymbol(Locale)
518
* getSymbol(Locale.getDefault(Locale.Category.DISPLAY))}.
519
*
520
* @return the symbol of this currency for the default
521
* {@link Locale.Category#DISPLAY DISPLAY} locale
522
*/
523
public String getSymbol() {
524
return getSymbol(Locale.getDefault(Locale.Category.DISPLAY));
525
}
526
527
/**
528
* Gets the symbol of this currency for the specified locale.
529
* For example, for the US Dollar, the symbol is "$" if the specified
530
* locale is the US, while for other locales it may be "US$". If no
531
* symbol can be determined, the ISO 4217 currency code is returned.
532
* <p>
533
* If the specified {@code locale} contains "rg" (region override)
534
* <a href="./Locale.html#def_locale_extension">Unicode extension</a>,
535
* the symbol returned from this method reflects
536
* the value specified with that extension.
537
*
538
* @param locale the locale for which a display name for this currency is
539
* needed
540
* @return the symbol of this currency for the specified locale
541
* @throws NullPointerException if {@code locale} is null
542
*/
543
public String getSymbol(Locale locale) {
544
LocaleServiceProviderPool pool =
545
LocaleServiceProviderPool.getPool(CurrencyNameProvider.class);
546
locale = CalendarDataUtility.findRegionOverride(locale);
547
String symbol = pool.getLocalizedObject(
548
CurrencyNameGetter.INSTANCE,
549
locale, currencyCode, SYMBOL);
550
if (symbol != null) {
551
return symbol;
552
}
553
554
// use currency code as symbol of last resort
555
return currencyCode;
556
}
557
558
/**
559
* Gets the default number of fraction digits used with this currency.
560
* Note that the number of fraction digits is the same as ISO 4217's
561
* minor unit for the currency.
562
* For example, the default number of fraction digits for the Euro is 2,
563
* while for the Japanese Yen it's 0.
564
* In the case of pseudo-currencies, such as IMF Special Drawing Rights,
565
* -1 is returned.
566
*
567
* @return the default number of fraction digits used with this currency
568
*/
569
public int getDefaultFractionDigits() {
570
return defaultFractionDigits;
571
}
572
573
/**
574
* Returns the ISO 4217 numeric code of this currency.
575
*
576
* @return the ISO 4217 numeric code of this currency
577
* @since 1.7
578
*/
579
public int getNumericCode() {
580
return numericCode;
581
}
582
583
/**
584
* Returns the 3 digit ISO 4217 numeric code of this currency as a {@code String}.
585
* Unlike {@link #getNumericCode()}, which returns the numeric code as {@code int},
586
* this method always returns the numeric code as a 3 digit string.
587
* e.g. a numeric value of 32 would be returned as "032",
588
* and a numeric value of 6 would be returned as "006".
589
*
590
* @return the 3 digit ISO 4217 numeric code of this currency as a {@code String}
591
* @since 9
592
*/
593
public String getNumericCodeAsString() {
594
/* numeric code could be returned as a 3 digit string simply by using
595
String.format("%03d",numericCode); which uses regex to parse the format,
596
"%03d" in this case. Parsing a regex gives an extra performance overhead,
597
so String.format() approach is avoided in this scenario.
598
*/
599
if (numericCode < 100) {
600
StringBuilder sb = new StringBuilder();
601
sb.append('0');
602
if (numericCode < 10) {
603
sb.append('0');
604
}
605
return sb.append(numericCode).toString();
606
}
607
return String.valueOf(numericCode);
608
}
609
610
/**
611
* Gets the name that is suitable for displaying this currency for
612
* the default {@link Locale.Category#DISPLAY DISPLAY} locale.
613
* If there is no suitable display name found
614
* for the default locale, the ISO 4217 currency code is returned.
615
* <p>
616
* This is equivalent to calling
617
* {@link #getDisplayName(Locale)
618
* getDisplayName(Locale.getDefault(Locale.Category.DISPLAY))}.
619
*
620
* @return the display name of this currency for the default
621
* {@link Locale.Category#DISPLAY DISPLAY} locale
622
* @since 1.7
623
*/
624
public String getDisplayName() {
625
return getDisplayName(Locale.getDefault(Locale.Category.DISPLAY));
626
}
627
628
/**
629
* Gets the name that is suitable for displaying this currency for
630
* the specified locale. If there is no suitable display name found
631
* for the specified locale, the ISO 4217 currency code is returned.
632
*
633
* @param locale the locale for which a display name for this currency is
634
* needed
635
* @return the display name of this currency for the specified locale
636
* @throws NullPointerException if {@code locale} is null
637
* @since 1.7
638
*/
639
public String getDisplayName(Locale locale) {
640
LocaleServiceProviderPool pool =
641
LocaleServiceProviderPool.getPool(CurrencyNameProvider.class);
642
String result = pool.getLocalizedObject(
643
CurrencyNameGetter.INSTANCE,
644
locale, currencyCode, DISPLAYNAME);
645
if (result != null) {
646
return result;
647
}
648
649
// use currency code as symbol of last resort
650
return currencyCode;
651
}
652
653
/**
654
* Returns the ISO 4217 currency code of this currency.
655
*
656
* @return the ISO 4217 currency code of this currency
657
*/
658
@Override
659
public String toString() {
660
return currencyCode;
661
}
662
663
/**
664
* Resolves instances being deserialized to a single instance per currency.
665
*/
666
@java.io.Serial
667
private Object readResolve() {
668
return getInstance(currencyCode);
669
}
670
671
/**
672
* Gets the main table entry for the country whose country code consists
673
* of char1 and char2.
674
*/
675
private static int getMainTableEntry(char char1, char char2) {
676
if (char1 < 'A' || char1 > 'Z' || char2 < 'A' || char2 > 'Z') {
677
throw new IllegalArgumentException();
678
}
679
return mainTable[(char1 - 'A') * A_TO_Z + (char2 - 'A')];
680
}
681
682
/**
683
* Sets the main table entry for the country whose country code consists
684
* of char1 and char2.
685
*/
686
private static void setMainTableEntry(char char1, char char2, int entry) {
687
if (char1 < 'A' || char1 > 'Z' || char2 < 'A' || char2 > 'Z') {
688
throw new IllegalArgumentException();
689
}
690
mainTable[(char1 - 'A') * A_TO_Z + (char2 - 'A')] = entry;
691
}
692
693
/**
694
* Obtains a localized currency names from a CurrencyNameProvider
695
* implementation.
696
*/
697
private static class CurrencyNameGetter
698
implements LocaleServiceProviderPool.LocalizedObjectGetter<CurrencyNameProvider,
699
String> {
700
private static final CurrencyNameGetter INSTANCE = new CurrencyNameGetter();
701
702
@Override
703
public String getObject(CurrencyNameProvider currencyNameProvider,
704
Locale locale,
705
String key,
706
Object... params) {
707
assert params.length == 1;
708
int type = (Integer)params[0];
709
710
switch(type) {
711
case SYMBOL:
712
return currencyNameProvider.getSymbol(key, locale);
713
case DISPLAYNAME:
714
return currencyNameProvider.getDisplayName(key, locale);
715
default:
716
assert false; // shouldn't happen
717
}
718
719
return null;
720
}
721
}
722
723
private static int[] readIntArray(DataInputStream dis, int count) throws IOException {
724
int[] ret = new int[count];
725
for (int i = 0; i < count; i++) {
726
ret[i] = dis.readInt();
727
}
728
729
return ret;
730
}
731
732
private static List<SpecialCaseEntry> readSpecialCases(DataInputStream dis,
733
int count)
734
throws IOException {
735
736
List<SpecialCaseEntry> list = new ArrayList<>(count);
737
long cutOverTime;
738
String oldCurrency;
739
String newCurrency;
740
int oldCurrencyFraction;
741
int newCurrencyFraction;
742
int oldCurrencyNumericCode;
743
int newCurrencyNumericCode;
744
745
for (int i = 0; i < count; i++) {
746
cutOverTime = dis.readLong();
747
oldCurrency = dis.readUTF();
748
newCurrency = dis.readUTF();
749
oldCurrencyFraction = dis.readInt();
750
newCurrencyFraction = dis.readInt();
751
oldCurrencyNumericCode = dis.readInt();
752
newCurrencyNumericCode = dis.readInt();
753
SpecialCaseEntry sc = new SpecialCaseEntry(cutOverTime,
754
oldCurrency, newCurrency,
755
oldCurrencyFraction, newCurrencyFraction,
756
oldCurrencyNumericCode, newCurrencyNumericCode);
757
list.add(sc);
758
}
759
return list;
760
}
761
762
private static List<OtherCurrencyEntry> readOtherCurrencies(DataInputStream dis,
763
int count)
764
throws IOException {
765
766
List<OtherCurrencyEntry> list = new ArrayList<>(count);
767
String currencyCode;
768
int fraction;
769
int numericCode;
770
771
for (int i = 0; i < count; i++) {
772
currencyCode = dis.readUTF();
773
fraction = dis.readInt();
774
numericCode = dis.readInt();
775
OtherCurrencyEntry oc = new OtherCurrencyEntry(currencyCode,
776
fraction,
777
numericCode);
778
list.add(oc);
779
}
780
return list;
781
}
782
783
/**
784
* Parse currency data found in the properties file (that
785
* java.util.currency.data designates) to a List of CurrencyProperty
786
* instances. Also, remove invalid entries and the multiple currency
787
* code inconsistencies.
788
*
789
* @param props properties containing currency data
790
* @param pattern regex pattern for the properties entry
791
* @return list of parsed property entries
792
*/
793
private static List<CurrencyProperty> getValidCurrencyData(Properties props,
794
Pattern pattern) {
795
796
Set<String> keys = props.stringPropertyNames();
797
List<CurrencyProperty> propertyEntries = new ArrayList<>();
798
799
// remove all invalid entries and parse all valid currency properties
800
// entries to a group of CurrencyProperty, classified by currency code
801
Map<String, List<CurrencyProperty>> currencyCodeGroup = keys.stream()
802
.map(k -> CurrencyProperty
803
.getValidEntry(k.toUpperCase(Locale.ROOT),
804
props.getProperty(k).toUpperCase(Locale.ROOT),
805
pattern)).flatMap(o -> o.stream())
806
.collect(Collectors.groupingBy(entry -> entry.currencyCode));
807
808
// check each group for inconsistencies
809
currencyCodeGroup.forEach((curCode, list) -> {
810
boolean inconsistent = CurrencyProperty
811
.containsInconsistentInstances(list);
812
if (inconsistent) {
813
list.forEach(prop -> CurrencyProperty.info("The property"
814
+ " entry for " + prop.country + " is inconsistent."
815
+ " Ignored.", null));
816
} else {
817
propertyEntries.addAll(list);
818
}
819
});
820
821
return propertyEntries;
822
}
823
824
/**
825
* Replaces currency data found in the properties file that
826
* java.util.currency.data designates. This method is invoked for
827
* each valid currency entry.
828
*
829
* @param prop CurrencyProperty instance of the valid property entry
830
*/
831
private static void replaceCurrencyData(CurrencyProperty prop) {
832
833
834
String ctry = prop.country;
835
String code = prop.currencyCode;
836
int numeric = prop.numericCode;
837
int fraction = prop.fraction;
838
int entry = numeric << NUMERIC_CODE_SHIFT;
839
840
int index = SpecialCaseEntry.indexOf(code, fraction, numeric);
841
842
843
// If a new entry changes the numeric code/dfd of an existing
844
// currency code, update it in the sc list at the respective
845
// index and also change it in the other currencies list and
846
// main table (if that currency code is also used as a
847
// simple case).
848
849
// If all three components do not match with the new entry,
850
// but the currency code exists in the special case list
851
// update the sc entry with the new entry
852
int scCurrencyCodeIndex = -1;
853
if (index == -1) {
854
scCurrencyCodeIndex = SpecialCaseEntry.currencyCodeIndex(code);
855
if (scCurrencyCodeIndex != -1) {
856
//currency code exists in sc list, then update the old entry
857
specialCasesList.set(scCurrencyCodeIndex,
858
new SpecialCaseEntry(code, fraction, numeric));
859
860
// also update the entry in other currencies list
861
OtherCurrencyEntry oe = OtherCurrencyEntry.findEntry(code);
862
if (oe != null) {
863
int oIndex = otherCurrenciesList.indexOf(oe);
864
otherCurrenciesList.set(oIndex, new OtherCurrencyEntry(
865
code, fraction, numeric));
866
}
867
}
868
}
869
870
/* If a country switches from simple case to special case or
871
* one special case to other special case which is not present
872
* in the sc arrays then insert the new entry in special case arrays.
873
* If an entry with given currency code exists, update with the new
874
* entry.
875
*/
876
if (index == -1 && (ctry.charAt(0) != code.charAt(0)
877
|| ctry.charAt(1) != code.charAt(1))) {
878
879
if(scCurrencyCodeIndex == -1) {
880
specialCasesList.add(new SpecialCaseEntry(code, fraction,
881
numeric));
882
index = specialCasesList.size() - 1;
883
} else {
884
index = scCurrencyCodeIndex;
885
}
886
887
// update the entry in main table if it exists as a simple case
888
updateMainTableEntry(code, fraction, numeric);
889
}
890
891
if (index == -1) {
892
// simple case
893
entry |= (fraction << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT)
894
| (code.charAt(2) - 'A');
895
} else {
896
// special case
897
entry = SPECIAL_CASE_COUNTRY_MASK
898
| (index + SPECIAL_CASE_COUNTRY_INDEX_DELTA);
899
}
900
setMainTableEntry(ctry.charAt(0), ctry.charAt(1), entry);
901
}
902
903
// update the entry in maintable for any simple case found, if a new
904
// entry as a special case updates the entry in sc list with
905
// existing currency code
906
private static void updateMainTableEntry(String code, int fraction,
907
int numeric) {
908
// checking the existence of currency code in mainTable
909
int tableEntry = getMainTableEntry(code.charAt(0), code.charAt(1));
910
int entry = numeric << NUMERIC_CODE_SHIFT;
911
if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK
912
&& tableEntry != INVALID_COUNTRY_ENTRY
913
&& code.charAt(2) - 'A' == (tableEntry
914
& SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK)) {
915
916
int numericCode = (tableEntry & NUMERIC_CODE_MASK)
917
>> NUMERIC_CODE_SHIFT;
918
int defaultFractionDigits = (tableEntry
919
& SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK)
920
>> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;
921
if (numeric != numericCode || fraction != defaultFractionDigits) {
922
// update the entry in main table
923
entry |= (fraction << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT)
924
| (code.charAt(2) - 'A');
925
setMainTableEntry(code.charAt(0), code.charAt(1), entry);
926
}
927
}
928
}
929
930
/* Used to represent a special case currency entry
931
* - cutOverTime: cut-over time in millis as returned by
932
* System.currentTimeMillis for special case countries that are changing
933
* currencies; Long.MAX_VALUE for countries that are not changing currencies
934
* - oldCurrency: old currencies for special case countries
935
* - newCurrency: new currencies for special case countries that are
936
* changing currencies; null for others
937
* - oldCurrencyFraction: default fraction digits for old currencies
938
* - newCurrencyFraction: default fraction digits for new currencies, 0 for
939
* countries that are not changing currencies
940
* - oldCurrencyNumericCode: numeric code for old currencies
941
* - newCurrencyNumericCode: numeric code for new currencies, 0 for countries
942
* that are not changing currencies
943
*/
944
private static class SpecialCaseEntry {
945
946
private final long cutOverTime;
947
private final String oldCurrency;
948
private final String newCurrency;
949
private final int oldCurrencyFraction;
950
private final int newCurrencyFraction;
951
private final int oldCurrencyNumericCode;
952
private final int newCurrencyNumericCode;
953
954
private SpecialCaseEntry(long cutOverTime, String oldCurrency, String newCurrency,
955
int oldCurrencyFraction, int newCurrencyFraction,
956
int oldCurrencyNumericCode, int newCurrencyNumericCode) {
957
this.cutOverTime = cutOverTime;
958
this.oldCurrency = oldCurrency;
959
this.newCurrency = newCurrency;
960
this.oldCurrencyFraction = oldCurrencyFraction;
961
this.newCurrencyFraction = newCurrencyFraction;
962
this.oldCurrencyNumericCode = oldCurrencyNumericCode;
963
this.newCurrencyNumericCode = newCurrencyNumericCode;
964
}
965
966
private SpecialCaseEntry(String currencyCode, int fraction,
967
int numericCode) {
968
this(Long.MAX_VALUE, currencyCode, "", fraction, 0, numericCode, 0);
969
}
970
971
//get the index of the special case entry
972
private static int indexOf(String code, int fraction, int numeric) {
973
int size = specialCasesList.size();
974
for (int index = 0; index < size; index++) {
975
SpecialCaseEntry scEntry = specialCasesList.get(index);
976
if (scEntry.oldCurrency.equals(code)
977
&& scEntry.oldCurrencyFraction == fraction
978
&& scEntry.oldCurrencyNumericCode == numeric
979
&& scEntry.cutOverTime == Long.MAX_VALUE) {
980
return index;
981
}
982
}
983
return -1;
984
}
985
986
// get the fraction and numericCode of the sc currencycode
987
private static int[] findEntry(String code) {
988
int[] fractionAndNumericCode = null;
989
int size = specialCasesList.size();
990
for (int index = 0; index < size; index++) {
991
SpecialCaseEntry scEntry = specialCasesList.get(index);
992
if (scEntry.oldCurrency.equals(code) && (scEntry.cutOverTime == Long.MAX_VALUE
993
|| System.currentTimeMillis() < scEntry.cutOverTime)) {
994
//consider only when there is no new currency or cutover time is not passed
995
fractionAndNumericCode = new int[2];
996
fractionAndNumericCode[0] = scEntry.oldCurrencyFraction;
997
fractionAndNumericCode[1] = scEntry.oldCurrencyNumericCode;
998
break;
999
} else if (scEntry.newCurrency.equals(code)
1000
&& System.currentTimeMillis() >= scEntry.cutOverTime) {
1001
//consider only if the cutover time is passed
1002
fractionAndNumericCode = new int[2];
1003
fractionAndNumericCode[0] = scEntry.newCurrencyFraction;
1004
fractionAndNumericCode[1] = scEntry.newCurrencyNumericCode;
1005
break;
1006
}
1007
}
1008
return fractionAndNumericCode;
1009
}
1010
1011
// get the index based on currency code
1012
private static int currencyCodeIndex(String code) {
1013
int size = specialCasesList.size();
1014
for (int index = 0; index < size; index++) {
1015
SpecialCaseEntry scEntry = specialCasesList.get(index);
1016
if (scEntry.oldCurrency.equals(code) && (scEntry.cutOverTime == Long.MAX_VALUE
1017
|| System.currentTimeMillis() < scEntry.cutOverTime)) {
1018
//consider only when there is no new currency or cutover time is not passed
1019
return index;
1020
} else if (scEntry.newCurrency.equals(code)
1021
&& System.currentTimeMillis() >= scEntry.cutOverTime) {
1022
//consider only if the cutover time is passed
1023
return index;
1024
}
1025
}
1026
return -1;
1027
}
1028
1029
1030
// convert the special case entry to sc arrays index
1031
private static int toIndex(int tableEntry) {
1032
return (tableEntry & SPECIAL_CASE_COUNTRY_INDEX_MASK) - SPECIAL_CASE_COUNTRY_INDEX_DELTA;
1033
}
1034
1035
}
1036
1037
/* Used to represent Other currencies
1038
* - currencyCode: currency codes that are not the main currency
1039
* of a simple country
1040
* - otherCurrenciesDFD: decimal format digits for other currencies
1041
* - otherCurrenciesNumericCode: numeric code for other currencies
1042
*/
1043
private static class OtherCurrencyEntry {
1044
1045
private final String currencyCode;
1046
private final int fraction;
1047
private final int numericCode;
1048
1049
private OtherCurrencyEntry(String currencyCode, int fraction,
1050
int numericCode) {
1051
this.currencyCode = currencyCode;
1052
this.fraction = fraction;
1053
this.numericCode = numericCode;
1054
}
1055
1056
//get the instance of the other currency code
1057
private static OtherCurrencyEntry findEntry(String code) {
1058
int size = otherCurrenciesList.size();
1059
for (int index = 0; index < size; index++) {
1060
OtherCurrencyEntry ocEntry = otherCurrenciesList.get(index);
1061
if (ocEntry.currencyCode.equalsIgnoreCase(code)) {
1062
return ocEntry;
1063
}
1064
}
1065
return null;
1066
}
1067
1068
}
1069
1070
1071
/*
1072
* Used to represent an entry of the properties file that
1073
* java.util.currency.data designates
1074
*
1075
* - country: country representing the currency entry
1076
* - currencyCode: currency code
1077
* - fraction: default fraction digit
1078
* - numericCode: numeric code
1079
* - date: cutover date
1080
*/
1081
private static class CurrencyProperty {
1082
private final String country;
1083
private final String currencyCode;
1084
private final int fraction;
1085
private final int numericCode;
1086
private final String date;
1087
1088
private CurrencyProperty(String country, String currencyCode,
1089
int fraction, int numericCode, String date) {
1090
this.country = country;
1091
this.currencyCode = currencyCode;
1092
this.fraction = fraction;
1093
this.numericCode = numericCode;
1094
this.date = date;
1095
}
1096
1097
/**
1098
* Check the valid currency data and create/return an Optional instance
1099
* of CurrencyProperty
1100
*
1101
* @param ctry country representing the currency data
1102
* @param curData currency data of the given {@code ctry}
1103
* @param pattern regex pattern for the properties entry
1104
* @return Optional containing CurrencyProperty instance, If valid;
1105
* empty otherwise
1106
*/
1107
private static Optional<CurrencyProperty> getValidEntry(String ctry,
1108
String curData,
1109
Pattern pattern) {
1110
1111
CurrencyProperty prop = null;
1112
1113
if (ctry.length() != 2) {
1114
// Invalid country code. Ignore the entry.
1115
} else {
1116
1117
prop = parseProperty(ctry, curData, pattern);
1118
// if the property entry failed any of the below checked
1119
// criteria it is ignored
1120
if (prop == null
1121
|| (prop.date == null && curData.chars()
1122
.map(c -> c == ',' ? 1 : 0).sum() >= 3)) {
1123
// format is not recognized. ignore the data if date
1124
// string is null and we've 4 values, bad date value
1125
prop = null;
1126
} else if (prop.fraction
1127
> SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS) {
1128
prop = null;
1129
} else {
1130
try {
1131
if (prop.date != null
1132
&& !isPastCutoverDate(prop.date)) {
1133
prop = null;
1134
}
1135
} catch (ParseException ex) {
1136
prop = null;
1137
}
1138
}
1139
}
1140
1141
if (prop == null) {
1142
info("The property entry for " + ctry + " is invalid."
1143
+ " Ignored.", null);
1144
}
1145
1146
return Optional.ofNullable(prop);
1147
}
1148
1149
/*
1150
* Parse properties entry and return CurrencyProperty instance
1151
*/
1152
private static CurrencyProperty parseProperty(String ctry,
1153
String curData, Pattern pattern) {
1154
Matcher m = pattern.matcher(curData);
1155
if (!m.find()) {
1156
return null;
1157
} else {
1158
return new CurrencyProperty(ctry, m.group(1),
1159
Integer.parseInt(m.group(3)),
1160
Integer.parseInt(m.group(2)), m.group(4));
1161
}
1162
}
1163
1164
/**
1165
* Checks if the given list contains multiple inconsistent currency instances
1166
*/
1167
private static boolean containsInconsistentInstances(
1168
List<CurrencyProperty> list) {
1169
int numCode = list.get(0).numericCode;
1170
int fractionDigit = list.get(0).fraction;
1171
return list.stream().anyMatch(prop -> prop.numericCode != numCode
1172
|| prop.fraction != fractionDigit);
1173
}
1174
1175
private static boolean isPastCutoverDate(String s)
1176
throws ParseException {
1177
SimpleDateFormat format = new SimpleDateFormat(
1178
"yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT);
1179
format.setTimeZone(TimeZone.getTimeZone("UTC"));
1180
format.setLenient(false);
1181
long time = format.parse(s.trim()).getTime();
1182
return System.currentTimeMillis() > time;
1183
1184
}
1185
1186
private static void info(String message, Throwable t) {
1187
PlatformLogger logger = PlatformLogger
1188
.getLogger("java.util.Currency");
1189
if (logger.isLoggable(PlatformLogger.Level.INFO)) {
1190
if (t != null) {
1191
logger.info(message, t);
1192
} else {
1193
logger.info(message);
1194
}
1195
}
1196
}
1197
1198
}
1199
1200
}
1201
1202