All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.icu.text.CompactDecimalDataCache Maven / Gradle / Ivy

There is a newer version: 2.12.15
Show newest version
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
/*
 *******************************************************************************
 * Copyright (C) 2012-2016, International Business Machines Corporation and
 * others. All Rights Reserved.
 *******************************************************************************
 */
package com.ibm.icu.text;

import java.util.HashMap;
import java.util.Map;
import java.util.MissingResourceException;

import com.ibm.icu.impl.ICUCache;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.SimpleCache;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.text.DecimalFormat.Unit;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;

/**
 * A cache containing data by locale for {@link CompactDecimalFormat}
 *
 * @author Travis Keep
 */
class CompactDecimalDataCache {

    private static final String SHORT_STYLE = "short";
    private static final String LONG_STYLE = "long";
    private static final String SHORT_CURRENCY_STYLE = "shortCurrency";
    private static final String NUMBER_ELEMENTS = "NumberElements";
    private static final String PATTERNS_LONG = "patternsLong";
    private static final String PATTERNS_SHORT = "patternsShort";
    private static final String DECIMAL_FORMAT = "decimalFormat";
    private static final String CURRENCY_FORMAT = "currencyFormat";
    private static final String LATIN_NUMBERING_SYSTEM = "latn";

    private static enum PatternsTableKey { PATTERNS_LONG, PATTERNS_SHORT };
    private static enum FormatsTableKey { DECIMAL_FORMAT, CURRENCY_FORMAT };

    public static final String OTHER = "other";

    /**
     * We can specify prefixes or suffixes for values with up to 15 digits,
     * less than 10^15.
     */
    static final int MAX_DIGITS = 15;

    private final ICUCache cache =
            new SimpleCache();

    /**
     * Data contains the compact decimal data for a particular locale. Data consists
     * of one array and two hashmaps. The index of the divisors array as well
     * as the arrays stored in the values of the two hashmaps correspond
     * to log10 of the number being formatted, so when formatting 12,345, the 4th
     * index of the arrays should be used. Divisors contain the number to divide
     * by before doing formatting. In the case of english, divisors[4]
     * is 1000.  So to format 12,345, divide by 1000 to get 12. Then use
     * PluralRules with the current locale to figure out which of the 6 plural variants
     * 12 matches: "zero", "one", "two", "few", "many", or "other." Prefixes and
     * suffixes are maps whose key is the plural variant and whose values are
     * arrays of strings with indexes corresponding to log10 of the original number.
     * these arrays contain the prefix or suffix to use.
     *
     * Each array in data is 15 in length, and every index is filled.
     *
     * @author Travis Keep
     *
     */
    static class Data {
        long[] divisors;
        Map units;
        boolean fromFallback;

        Data(long[] divisors, Map units)
        {
            this.divisors = divisors;
            this.units = units;
        }

        public boolean isEmpty() {
            return units == null || units.isEmpty();
        }
    }

    /**
     * DataBundle contains compact decimal data for all the styles in a particular
     * locale. Currently available styles are short and long for decimals, and
     * short only for currencies.
     *
     * @author Travis Keep
     */
    static class DataBundle {
        Data shortData;
        Data longData;
        Data shortCurrencyData;

        private DataBundle(Data shortData, Data longData, Data shortCurrencyData) {
            this.shortData = shortData;
            this.longData = longData;
            this.shortCurrencyData = shortCurrencyData;
        }

        private static DataBundle createEmpty() {
            return new DataBundle(
                new Data(new long[MAX_DIGITS], new HashMap()),
                new Data(new long[MAX_DIGITS], new HashMap()),
                new Data(new long[MAX_DIGITS], new HashMap())
            );
        }
    }

    /**
     * Sink for enumerating all of the compact decimal format patterns.
     *
     * More specific bundles (en_GB) are enumerated before their parents (en_001, en, root):
     * Only store a value if it is still missing, that is, it has not been overridden.
     */
    private static final class CompactDecimalDataSink extends UResource.Sink {

        private DataBundle dataBundle; // Where to save values when they are read
        private ULocale locale; // The locale we are traversing (for exception messages)
        private boolean isLatin; // Whether or not we are traversing the Latin table
        private boolean isFallback; // Whether or not we are traversing the Latin table as fallback

        /*
         * NumberElements{              <-- top (numbering system table)
         *  latn{                       <-- patternsTable (one per numbering system)
         *    patternsLong{             <-- formatsTable (one per pattern)
         *      decimalFormat{          <-- powersOfTenTable (one per format)
         *        1000{                 <-- pluralVariantsTable (one per power of ten)
         *          one{"0 thousand"}   <-- plural variant and template
         */

        public CompactDecimalDataSink(DataBundle dataBundle, ULocale locale) {
            this.dataBundle = dataBundle;
            this.locale = locale;
        }

        @Override
        public void put(UResource.Key key, UResource.Value value, boolean isRoot) {
            // SPECIAL CASE: Don't consume root in the non-Latin numbering system
            if (isRoot && !isLatin) { return; }

            UResource.Table patternsTable = value.getTable();
            for (int i1 = 0; patternsTable.getKeyAndValue(i1, key, value); ++i1) {

                // patterns table: check for patternsShort or patternsLong
                PatternsTableKey patternsTableKey;
                if (key.contentEquals(PATTERNS_SHORT)) {
                    patternsTableKey = PatternsTableKey.PATTERNS_SHORT;
                } else if (key.contentEquals(PATTERNS_LONG)) {
                    patternsTableKey = PatternsTableKey.PATTERNS_LONG;
                } else {
                    continue;
                }

                // traverse into the table of formats
                UResource.Table formatsTable = value.getTable();
                for (int i2 = 0; formatsTable.getKeyAndValue(i2, key, value); ++i2) {

                    // formats table: check for decimalFormat or currencyFormat
                    FormatsTableKey formatsTableKey;
                    if (key.contentEquals(DECIMAL_FORMAT)) {
                        formatsTableKey = FormatsTableKey.DECIMAL_FORMAT;
                    } else if (key.contentEquals(CURRENCY_FORMAT)) {
                        formatsTableKey = FormatsTableKey.CURRENCY_FORMAT;
                    } else {
                        continue;
                    }

                    // Set the current style and destination based on the lvl1 and lvl2 keys
                    String style = null;
                    Data destination = null;
                    if (patternsTableKey == PatternsTableKey.PATTERNS_LONG
                            && formatsTableKey == FormatsTableKey.DECIMAL_FORMAT) {
                        style = LONG_STYLE;
                        destination = dataBundle.longData;
                    } else if (patternsTableKey == PatternsTableKey.PATTERNS_SHORT
                            && formatsTableKey == FormatsTableKey.DECIMAL_FORMAT) {
                        style = SHORT_STYLE;
                        destination = dataBundle.shortData;
                    } else if (patternsTableKey == PatternsTableKey.PATTERNS_SHORT
                            && formatsTableKey == FormatsTableKey.CURRENCY_FORMAT) {
                        style = SHORT_CURRENCY_STYLE;
                        destination = dataBundle.shortCurrencyData;
                    } else {
                        // Silently ignore this case
                        continue;
                    }

                    // SPECIAL CASE: RULES FOR WHETHER OR NOT TO CONSUME THIS TABLE:
                    //   1) Don't consume longData if shortData was consumed from the non-Latin
                    //      locale numbering system
                    //   2) Don't consume longData for the first time if this is the root bundle and
                    //      shortData is already populated from a more specific locale. Note that if
                    //      both longData and shortData are both only in root, longData will be
                    //      consumed since it is alphabetically before shortData in the bundle.
                    if (isFallback
                            && style == LONG_STYLE
                            && !dataBundle.shortData.isEmpty()
                            && !dataBundle.shortData.fromFallback) {
                        continue;
                    }
                    if (isRoot
                            && style == LONG_STYLE
                            && dataBundle.longData.isEmpty()
                            && !dataBundle.shortData.isEmpty()) {
                        continue;
                    }

                    // Set the "fromFallback" flag on the data object
                    destination.fromFallback = isFallback;

                    // traverse into the table of powers of ten
                    UResource.Table powersOfTenTable = value.getTable();
                    for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) {

                        // This value will always be some even power of 10. e.g 10000.
                        long power10 = Long.parseLong(key.toString());
                        int log10Value = (int) Math.log10(power10);

                        // Silently ignore divisors that are too big.
                        if (log10Value >= MAX_DIGITS) continue;

                        // Iterate over the plural variants ("one", "other", etc)
                        UResource.Table pluralVariantsTable = value.getTable();
                        for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) {
                            // TODO: Use StandardPlural rather than String.
                            String pluralVariant = key.toString();
                            String template = value.toString();

                            // Copy the data into the in-memory data bundle (do not overwrite
                            // existing values)
                            int numZeros = populatePrefixSuffix(
                                    pluralVariant, log10Value, template, locale, style, destination, false);

                            // If populatePrefixSuffix returns -1, it means that this key has been
                            // encountered already.
                            if (numZeros < 0) {
                                continue;
                            }

                            // Set the divisor, which is based on the number of zeros in the template
                            // string.  If the divisor from here is different from the one previously
                            // stored, it means that the number of zeros in different plural variants
                            // differs; throw an exception.
                            long divisor = calculateDivisor(power10, numZeros);
                            if (destination.divisors[log10Value] != 0L
                                    && destination.divisors[log10Value] != divisor) {
                                throw new IllegalArgumentException("Plural variant '" + pluralVariant
                                        + "' template '" + template
                                        + "' for 10^" + log10Value
                                        + " has wrong number of zeros in " + localeAndStyle(locale, style));
                            }
                            destination.divisors[log10Value] = divisor;
                        }
                    }
                }
            }
        }
    }

    /**
     * Fetch data for a particular locale. Clients must not modify any part of the returned data. Portions of returned
     * data may be shared so modifying it will have unpredictable results.
     */
    DataBundle get(ULocale locale) {
        DataBundle result = cache.get(locale);
        if (result == null) {
            result = load(locale);
            cache.put(locale, result);
        }
        return result;
    }

    private static DataBundle load(ULocale ulocale) throws MissingResourceException {
        DataBundle dataBundle = DataBundle.createEmpty();
        String nsName = NumberingSystem.getInstance(ulocale).getName();
        ICUResourceBundle r = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,
                ulocale);
        CompactDecimalDataSink sink = new CompactDecimalDataSink(dataBundle, ulocale);
        sink.isFallback = false;

        // First load the number elements data from nsName if nsName is not Latin.
        if (!nsName.equals(LATIN_NUMBERING_SYSTEM)) {
            sink.isLatin = false;

            try {
                r.getAllItemsWithFallback(NUMBER_ELEMENTS + "/" + nsName, sink);
            } catch (MissingResourceException e) {
                // Silently ignore and use Latin
            }

            // Set the "isFallback" flag for when we read Latin
            sink.isFallback = true;
        }

        // Now load Latin, which will fill in things that were left out from above.
        sink.isLatin = true;
        r.getAllItemsWithFallback(NUMBER_ELEMENTS + "/" + LATIN_NUMBERING_SYSTEM, sink);

        // If longData is empty, default it to be equal to shortData
        if (dataBundle.longData.isEmpty()) {
            dataBundle.longData = dataBundle.shortData;
        }

        // Check for "other" variants in each of the three data classes
        checkForOtherVariants(dataBundle.longData, ulocale, LONG_STYLE);
        checkForOtherVariants(dataBundle.shortData, ulocale, SHORT_STYLE);
        checkForOtherVariants(dataBundle.shortCurrencyData, ulocale, SHORT_CURRENCY_STYLE);

        // Resolve missing elements
        fillInMissing(dataBundle.longData);
        fillInMissing(dataBundle.shortData);
        fillInMissing(dataBundle.shortCurrencyData);

        // Return the data bundle
        return dataBundle;
    }


    /**
     * Populates prefix and suffix information for a particular plural variant
     * and index (log10 value).
     * @param pluralVariant e.g "one", "other"
     * @param idx the index (log10 value of the number) 0 <= idx < MAX_DIGITS
     * @param template e.g "00K"
     * @param locale the locale
     * @param style the style
     * @param destination Extracted prefix and suffix stored here.
     * @return number of zeros found before any decimal point in template, or -1 if it was not saved.
     */
    private static int populatePrefixSuffix(
            String pluralVariant, int idx, String template, ULocale locale, String style,
            Data destination, boolean overwrite) {
        int firstIdx = template.indexOf("0");
        int lastIdx = template.lastIndexOf("0");
        if (firstIdx == -1) {
            throw new IllegalArgumentException(
                "Expect at least one zero in template '" + template +
                "' for variant '" +pluralVariant + "' for 10^" + idx +
                " in " + localeAndStyle(locale, style));
        }
        String prefix = template.substring(0, firstIdx);
        String suffix = template.substring(lastIdx + 1);

        // Save the unit, and return -1 if it was not saved
        boolean saved = saveUnit(new DecimalFormat.Unit(prefix, suffix), pluralVariant, idx, destination.units, overwrite);
        if (!saved) {
            return -1;
        }

        // If there is effectively no prefix or suffix, ignore the actual
        // number of 0's and act as if the number of 0's matches the size
        // of the number
        if (prefix.trim().length() == 0 && suffix.trim().length() == 0) {
          return idx + 1;
        }

        // Calculate number of zeros before decimal point.
        int i = firstIdx + 1;
        while (i <= lastIdx && template.charAt(i) == '0') {
            i++;
        }
        return i - firstIdx;
    }

    /**
     * Calculate a divisor based on the magnitude and number of zeros in the
     * template string.
     * @param power10
     * @param numZeros
     * @return
     */
    private static long calculateDivisor(long power10, int numZeros) {
        // We craft our divisor such that when we divide by it, we get a
        // number with the same number of digits as zeros found in the
        // plural variant templates. If our magnitude is 10000 and we have
        // two 0's in our plural variants, then we want a divisor of 1000.
        // Note that if we have 43560 which is of same magnitude as 10000.
        // When we divide by 1000 we a quotient which rounds to 44 (2 digits)
        long divisor = power10;
        for (int i = 1; i < numZeros; i++) {
            divisor /= 10;
        }
        return divisor;
    }


    /**
     * Returns locale and style. Used to form useful messages in thrown exceptions.
     *
     * Note: This is not covered by unit tests since no exceptions are thrown on the default CLDR data.  It is too
     * cumbersome to cover via reflection.
     *
     * @param locale the locale
     * @param style the style
     */
    private static String localeAndStyle(ULocale locale, String style) {
        return "locale '" + locale + "' style '" + style + "'";
    }

    /**
     * Checks to make sure that an "other" variant is present in all powers of 10.
     * @param data
     */
    private static void checkForOtherVariants(Data data, ULocale locale, String style) {
        DecimalFormat.Unit[] otherByBase = data.units.get(OTHER);

        if (otherByBase == null) {
            throw new IllegalArgumentException("No 'other' plural variants defined in "
                    + localeAndStyle(locale, style));
        }

        // Check all other plural variants, and make sure that if any of them are populated, then
        // other is also populated
        for (Map.Entry entry : data.units.entrySet()) {
            if (entry.getKey() == OTHER) continue;
            DecimalFormat.Unit[] variantByBase = entry.getValue();
            for (int log10Value = 0; log10Value < MAX_DIGITS; log10Value++) {
                if (variantByBase[log10Value] != null && otherByBase[log10Value] == null) {
                    throw new IllegalArgumentException(
                            "No 'other' plural variant defined for 10^" + log10Value
                            + " but a '" + entry.getKey() + "' variant is defined"
                            + " in " +localeAndStyle(locale, style));
                }
            }
        }
    }

    /**
     * After reading information from resource bundle into a Data object, there
     * is guarantee that it is complete.
     *
     * This method fixes any incomplete data it finds within result.
     * It looks at each log10 value applying the two rules.
     *   

* If no prefix is defined for the "other" variant, use the divisor, prefixes and * suffixes for all defined variants from the previous log10. For log10 = 0, * use all empty prefixes and suffixes and a divisor of 1. *

* Otherwise, examine each plural variant defined for the given log10 value. * If it has no prefix and suffix for a particular variant, use the one from the * "other" variant. *

* * @param result this instance is fixed in-place. */ private static void fillInMissing(Data result) { // Initially we assume that previous divisor is 1 with no prefix or suffix. long lastDivisor = 1L; for (int i = 0; i < result.divisors.length; i++) { if (result.units.get(OTHER)[i] == null) { result.divisors[i] = lastDivisor; copyFromPreviousIndex(i, result.units); } else { lastDivisor = result.divisors[i]; propagateOtherToMissing(i, result.units); } } } private static void propagateOtherToMissing( int idx, Map units) { DecimalFormat.Unit otherVariantValue = units.get(OTHER)[idx]; for (DecimalFormat.Unit[] byBase : units.values()) { if (byBase[idx] == null) { byBase[idx] = otherVariantValue; } } } private static void copyFromPreviousIndex(int idx, Map units) { for (DecimalFormat.Unit[] byBase : units.values()) { if (idx == 0) { byBase[idx] = DecimalFormat.NULL_UNIT; } else { byBase[idx] = byBase[idx - 1]; } } } private static boolean saveUnit( DecimalFormat.Unit unit, String pluralVariant, int idx, Map units, boolean overwrite) { DecimalFormat.Unit[] byBase = units.get(pluralVariant); if (byBase == null) { byBase = new DecimalFormat.Unit[MAX_DIGITS]; units.put(pluralVariant, byBase); } // Don't overwrite a pre-existing value unless the "overwrite" flag is true. if (!overwrite && byBase[idx] != null) { return false; } // Save the value and return byBase[idx] = unit; return true; } /** * Fetches a prefix or suffix given a plural variant and log10 value. If it * can't find the given variant, it falls back to "other". * @param prefixOrSuffix the prefix or suffix map * @param variant the plural variant * @param base log10 value. 0 <= base < MAX_DIGITS. * @return the prefix or suffix. */ static DecimalFormat.Unit getUnit( Map units, String variant, int base) { DecimalFormat.Unit[] byBase = units.get(variant); if (byBase == null) { byBase = units.get(CompactDecimalDataCache.OTHER); } return byBase[base]; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy