PeriodFormatterData.java

// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 ******************************************************************************
 * Copyright (C) 2009-2011, International Business Machines Corporation and   *
 * others. All Rights Reserved.                                               *
 ******************************************************************************
 */

package com.ibm.icu.impl.duration.impl;

import com.ibm.icu.impl.duration.TimeUnit;
import com.ibm.icu.impl.duration.impl.DataRecord.ECountVariant;
import com.ibm.icu.impl.duration.impl.DataRecord.EDecimalHandling;
import com.ibm.icu.impl.duration.impl.DataRecord.EFractionHandling;
import com.ibm.icu.impl.duration.impl.DataRecord.EGender;
import com.ibm.icu.impl.duration.impl.DataRecord.EHalfPlacement;
import com.ibm.icu.impl.duration.impl.DataRecord.EHalfSupport;
import com.ibm.icu.impl.duration.impl.DataRecord.ENumberSystem;
import com.ibm.icu.impl.duration.impl.DataRecord.EPluralization;
import com.ibm.icu.impl.duration.impl.DataRecord.EUnitVariant;
import com.ibm.icu.impl.duration.impl.DataRecord.EZeroHandling;
import com.ibm.icu.impl.duration.impl.DataRecord.ScopeData;
import java.util.Arrays;

/**
 * PeriodFormatterData provides locale-specific data used to format relative dates and times, and
 * convenience api to access it.
 *
 * <p>An instance of PeriodFormatterData is usually created by requesting data for a given locale
 * from an PeriodFormatterDataService.
 */
public class PeriodFormatterData {
    final DataRecord dr;
    String localeName;

    // debug
    public static boolean trace = false;

    public PeriodFormatterData(String localeName, DataRecord dr) {
        this.dr = dr;
        this.localeName = localeName;
        if (localeName == null) {
            throw new NullPointerException("localename is null");
        }
        //    System.err.println("** localeName is " + localeName);
        if (dr == null) {
            //      Thread.dumpStack();
            throw new NullPointerException("data record is null");
        }
    }

    // none - chinese (all forms the same)
    // plural - english, special form for 1
    // dual - special form for 1 and 2
    // paucal - russian, special form for 1, for 2-4 and n > 20 && n % 10 == 2-4
    // rpt_dual_few - slovenian, special form for 1, 2, 3-4 and n as above
    // hebrew, dual plus singular form for years > 11
    // arabic, dual, plus singular form for all terms > 10

    /**
     * Return the pluralization format used by this locale.
     *
     * @return the pluralization format
     */
    public int pluralization() {
        return dr.pl;
    }

    /**
     * Return true if zeros are allowed in the display.
     *
     * @return true if zeros should be allowed
     */
    public boolean allowZero() {
        return dr.allowZero;
    }

    public boolean weeksAloneOnly() {
        return dr.weeksAloneOnly;
    }

    public int useMilliseconds() {
        return dr.useMilliseconds;
    }

    /**
     * Append the appropriate prefix to the string builder, depending on whether and how a limit and
     * direction are to be displayed.
     *
     * @param tl how and whether to display the time limit
     * @param td how and whether to display the time direction
     * @param sb the string builder to which to append the text
     * @return true if a following digit will require a digit prefix
     */
    public boolean appendPrefix(int tl, int td, StringBuffer sb) {
        if (dr.scopeData != null) {
            int ix = tl * 3 + td;
            ScopeData sd = dr.scopeData[ix];
            if (sd != null) {
                String prefix = sd.prefix;
                if (prefix != null) {
                    sb.append(prefix);
                    return sd.requiresDigitPrefix;
                }
            }
        }
        return false;
    }

    /**
     * Append the appropriate suffix to the string builder, depending on whether and how a limit and
     * direction are to be displayed.
     *
     * @param tl how and whether to display the time limit
     * @param td how and whether to display the time direction
     * @param sb the string builder to which to append the text
     */
    public void appendSuffix(int tl, int td, StringBuffer sb) {
        if (dr.scopeData != null) {
            int ix = tl * 3 + td;
            ScopeData sd = dr.scopeData[ix];
            if (sd != null) {
                String suffix = sd.suffix;
                if (suffix != null) {
                    if (trace) {
                        System.out.println("appendSuffix '" + suffix + "'");
                    }
                    sb.append(suffix);
                }
            }
        }
    }

    /**
     * Append the count and unit to the string builder.
     *
     * @param unit the unit to append
     * @param count the count of units, * 1000
     * @param cv the format to use for displaying the count
     * @param uv the format to use for displaying the unit
     * @param useCountSep if false, force no separator between count and unit
     * @param useDigitPrefix if true, use the digit prefix
     * @param multiple true if there are multiple units in this string
     * @param last true if this is the last unit
     * @param wasSkipped true if the unit(s) before this were skipped
     * @param sb the string builder to which to append the text
     * @return true if will require skip marker
     */
    @SuppressWarnings("fallthrough")
    public boolean appendUnit(
            TimeUnit unit,
            int count,
            int cv,
            int uv,
            boolean useCountSep,
            boolean useDigitPrefix,
            boolean multiple,
            boolean last,
            boolean wasSkipped,
            StringBuffer sb) {
        int px = unit.ordinal();

        boolean willRequireSkipMarker = false;
        if (dr.requiresSkipMarker != null
                && dr.requiresSkipMarker[px]
                && dr.skippedUnitMarker != null) {
            if (!wasSkipped && last) {
                sb.append(dr.skippedUnitMarker);
            }
            willRequireSkipMarker = true;
        }

        if (uv != EUnitVariant.PLURALIZED) {
            boolean useMedium = uv == EUnitVariant.MEDIUM;
            String[] names = useMedium ? dr.mediumNames : dr.shortNames;
            if (names == null || names[px] == null) {
                names = useMedium ? dr.shortNames : dr.mediumNames;
            }
            if (names != null && names[px] != null) {
                appendCount(
                        unit,
                        false,
                        false,
                        count,
                        cv,
                        useCountSep,
                        names[px],
                        last,
                        sb); // omit suffix, ok?
                return false; // omit skip marker
            }
        }

        // check cv
        if (cv == ECountVariant.HALF_FRACTION && dr.halfSupport != null) {
            switch (dr.halfSupport[px]) {
                case EHalfSupport.YES:
                    break;
                case EHalfSupport.ONE_PLUS:
                    if (count > 1000) {
                        break;
                    }
                // else fall through to decimal
                case EHalfSupport.NO:
                    {
                        count = (count / 500) * 500; // round to 1/2
                        cv = ECountVariant.DECIMAL1;
                    }
                    break;
            }
        }

        String name = null;
        int form = computeForm(unit, count, cv, multiple && last);
        if (form == FORM_SINGULAR_SPELLED) {
            if (dr.singularNames == null) {
                form = FORM_SINGULAR;
                name = dr.pluralNames[px][form];
            } else {
                name = dr.singularNames[px];
            }
        } else if (form == FORM_SINGULAR_NO_OMIT) {
            name = dr.pluralNames[px][FORM_SINGULAR];
        } else if (form == FORM_HALF_SPELLED) {
            name = dr.halfNames[px];
        } else {
            try {
                name = dr.pluralNames[px][form];
            } catch (NullPointerException e) {
                System.out.println(
                        "Null Pointer in PeriodFormatterData["
                                + localeName
                                + "].au px: "
                                + px
                                + " form: "
                                + form
                                + " pn: "
                                + Arrays.toString(dr.pluralNames));
                throw e;
            }
        }
        if (name == null) {
            form = FORM_PLURAL;
            name = dr.pluralNames[px][form];
        }

        boolean omitCount =
                (form == FORM_SINGULAR_SPELLED || form == FORM_HALF_SPELLED)
                        || (dr.omitSingularCount && form == FORM_SINGULAR)
                        || (dr.omitDualCount && form == FORM_DUAL);

        int suffixIndex =
                appendCount(
                        unit, omitCount, useDigitPrefix, count, cv, useCountSep, name, last, sb);
        if (last && suffixIndex >= 0) {
            String suffix = null;
            if (dr.rqdSuffixes != null && suffixIndex < dr.rqdSuffixes.length) {
                suffix = dr.rqdSuffixes[suffixIndex];
            }
            if (suffix == null && dr.optSuffixes != null && suffixIndex < dr.optSuffixes.length) {
                suffix = dr.optSuffixes[suffixIndex];
            }
            if (suffix != null) {
                sb.append(suffix);
            }
        }
        return willRequireSkipMarker;
    }

    /**
     * Append a count to the string builder.
     *
     * @param unit the unit
     * @param count the count
     * @param cv the format to use for displaying the count
     * @param useSep whether to use the count separator, if available
     * @param name the term name
     * @param last true if this is the last unit to be formatted
     * @param sb the string builder to which to append the text
     * @return index to use if might have required or optional suffix, or -1 if none required
     */
    public int appendCount(
            TimeUnit unit,
            boolean omitCount,
            boolean useDigitPrefix,
            int count,
            int cv,
            boolean useSep,
            String name,
            boolean last,
            StringBuffer sb) {
        if (cv == ECountVariant.HALF_FRACTION && dr.halves == null) {
            cv = ECountVariant.INTEGER;
        }

        if (!omitCount && useDigitPrefix && dr.digitPrefix != null) {
            sb.append(dr.digitPrefix);
        }

        int index = unit.ordinal();
        switch (cv) {
            case ECountVariant.INTEGER:
                {
                    if (!omitCount) {
                        appendInteger(count / 1000, 1, 10, sb);
                    }
                }
                break;

            case ECountVariant.INTEGER_CUSTOM:
                {
                    int val = count / 1000;
                    // only custom names we have for now
                    if (unit == TimeUnit.MINUTE
                            && (dr.fiveMinutes != null || dr.fifteenMinutes != null)) {
                        if (val != 0 && val % 5 == 0) {
                            if (dr.fifteenMinutes != null && (val == 15 || val == 45)) {
                                val = val == 15 ? 1 : 3;
                                if (!omitCount) appendInteger(val, 1, 10, sb);
                                name = dr.fifteenMinutes;
                                index = 8; // hack
                                break;
                            }
                            if (dr.fiveMinutes != null) {
                                val = val / 5;
                                if (!omitCount) appendInteger(val, 1, 10, sb);
                                name = dr.fiveMinutes;
                                index = 9; // hack
                                break;
                            }
                        }
                    }
                    if (!omitCount) appendInteger(val, 1, 10, sb);
                }
                break;

            case ECountVariant.HALF_FRACTION:
                {
                    // 0, 1/2, 1, 1-1/2...
                    int v = count / 500;
                    if (v != 1) {
                        if (!omitCount) appendCountValue(count, 1, 0, sb);
                    }
                    if ((v & 0x1) == 1) {
                        // hack, using half name
                        if (v == 1 && dr.halfNames != null && dr.halfNames[index] != null) {
                            sb.append(name);
                            return last ? index : -1;
                        }

                        int solox = v == 1 ? 0 : 1;
                        if (dr.genders != null && dr.halves.length > 2) {
                            if (dr.genders[index] == EGender.F) {
                                solox += 2;
                            }
                        }
                        int hp =
                                dr.halfPlacements == null
                                        ? EHalfPlacement.PREFIX
                                        : dr.halfPlacements[solox & 0x1];
                        String half = dr.halves[solox];
                        String measure = dr.measures == null ? null : dr.measures[index];
                        switch (hp) {
                            case EHalfPlacement.PREFIX:
                                sb.append(half);
                                break;
                            case EHalfPlacement.AFTER_FIRST:
                                {
                                    if (measure != null) {
                                        sb.append(measure);
                                        sb.append(half);
                                        if (useSep && !omitCount) {
                                            sb.append(dr.countSep);
                                        }
                                        sb.append(name);
                                    } else { // ignore sep completely
                                        sb.append(name);
                                        sb.append(half);
                                        return last ? index : -1; // might use suffix
                                    }
                                }
                                return -1; // exit early
                            case EHalfPlacement.LAST:
                                {
                                    if (measure != null) {
                                        sb.append(measure);
                                    }
                                    if (useSep && !omitCount) {
                                        sb.append(dr.countSep);
                                    }
                                    sb.append(name);
                                    sb.append(half);
                                }
                                return last ? index : -1; // might use suffix
                        }
                    }
                }
                break;
            default:
                {
                    int decimals = 1;
                    switch (cv) {
                        case ECountVariant.DECIMAL2:
                            decimals = 2;
                            break;
                        case ECountVariant.DECIMAL3:
                            decimals = 3;
                            break;
                        default:
                            break;
                    }
                    if (!omitCount) appendCountValue(count, 1, decimals, sb);
                }
                break;
        }
        if (!omitCount && useSep) {
            sb.append(dr.countSep);
        }
        if (!omitCount && dr.measures != null && index < dr.measures.length) {
            String measure = dr.measures[index];
            if (measure != null) {
                sb.append(measure);
            }
        }
        sb.append(name);
        return last ? index : -1;
    }

    /**
     * Append a count value to the builder.
     *
     * @param count the count
     * @param integralDigits the number of integer digits to display
     * @param decimalDigits the number of decimal digits to display, <= 3
     * @param sb the string builder to which to append the text
     */
    public void appendCountValue(
            int count, int integralDigits, int decimalDigits, StringBuffer sb) {
        int ival = count / 1000;
        if (decimalDigits == 0) {
            appendInteger(ival, integralDigits, 10, sb);
            return;
        }

        if (dr.requiresDigitSeparator && sb.length() > 0) {
            sb.append(' ');
        }
        appendDigits(ival, integralDigits, 10, sb);
        int dval = count % 1000;
        if (decimalDigits == 1) {
            dval /= 100;
        } else if (decimalDigits == 2) {
            dval /= 10;
        }
        sb.append(dr.decimalSep);
        appendDigits(dval, decimalDigits, decimalDigits, sb);
        if (dr.requiresDigitSeparator) {
            sb.append(' ');
        }
    }

    public void appendInteger(int num, int mindigits, int maxdigits, StringBuffer sb) {
        if (dr.numberNames != null && num < dr.numberNames.length) {
            String name = dr.numberNames[num];
            if (name != null) {
                sb.append(name);
                return;
            }
        }

        if (dr.requiresDigitSeparator && sb.length() > 0) {
            sb.append(' ');
        }
        switch (dr.numberSystem) {
            case ENumberSystem.DEFAULT:
                appendDigits(num, mindigits, maxdigits, sb);
                break;
            case ENumberSystem.CHINESE_TRADITIONAL:
                sb.append(Utils.chineseNumber(num, Utils.ChineseDigits.TRADITIONAL));
                break;
            case ENumberSystem.CHINESE_SIMPLIFIED:
                sb.append(Utils.chineseNumber(num, Utils.ChineseDigits.SIMPLIFIED));
                break;
            case ENumberSystem.KOREAN:
                sb.append(Utils.chineseNumber(num, Utils.ChineseDigits.KOREAN));
                break;
        }
        if (dr.requiresDigitSeparator) {
            sb.append(' ');
        }
    }

    /**
     * Append digits to the string builder, using this.zero for '0' etc.
     *
     * @param num the integer to append
     * @param mindigits the minimum number of digits to append
     * @param maxdigits the maximum number of digits to append
     * @param sb the string builder to which to append the text
     */
    public void appendDigits(long num, int mindigits, int maxdigits, StringBuffer sb) {
        char[] buf = new char[maxdigits];
        int ix = maxdigits;
        while (ix > 0 && num > 0) {
            buf[--ix] = (char) (dr.zero + (num % 10));
            num /= 10;
        }
        for (int e = maxdigits - mindigits; ix > e; ) {
            buf[--ix] = dr.zero;
        }
        sb.append(buf, ix, maxdigits - ix);
    }

    /**
     * Append a marker for skipped units internal to a string.
     *
     * @param sb the string builder to which to append the text
     */
    public void appendSkippedUnit(StringBuffer sb) {
        if (dr.skippedUnitMarker != null) {
            sb.append(dr.skippedUnitMarker);
        }
    }

    /**
     * Append the appropriate separator between units
     *
     * @param unit the unit to which to append the separator
     * @param afterFirst true if this is the first unit formatted
     * @param beforeLast true if this is the next-to-last unit to be formatted
     * @param sb the string builder to which to append the text
     * @return true if a prefix will be required before a following unit
     */
    public boolean appendUnitSeparator(
            TimeUnit unit,
            boolean longSep,
            boolean afterFirst,
            boolean beforeLast,
            StringBuffer sb) {
        // long seps
        // false, false "...b', '...d"
        // false, true  "...', and 'c"
        // true, false - "a', '...c"
        // true, true - "a' and 'b"
        if ((longSep && dr.unitSep != null) || dr.shortUnitSep != null) {
            if (longSep && dr.unitSep != null) {
                int ix = (afterFirst ? 2 : 0) + (beforeLast ? 1 : 0);
                sb.append(dr.unitSep[ix]);
                return dr.unitSepRequiresDP != null && dr.unitSepRequiresDP[ix];
            }
            sb.append(dr.shortUnitSep); // todo: investigate whether DP is required
        }
        return false;
    }

    private static final int FORM_PLURAL = 0,
            FORM_SINGULAR = 1,
            FORM_DUAL = 2,
            FORM_PAUCAL = 3,
            FORM_SINGULAR_SPELLED = 4, // following are not in the pluralization list
            FORM_SINGULAR_NO_OMIT = 5, // a hack
            FORM_HALF_SPELLED = 6;

    private int computeForm(TimeUnit unit, int count, int cv, boolean lastOfMultiple) {
        // first check if a particular form is forced by the countvariant.  if
        // SO, just return that.  otherwise convert the count to an integer
        // and use pluralization rules to determine which form to use.
        // careful, can't assume any forms but plural exist.

        if (trace) {
            System.err.println(
                    "pfd.cf unit: "
                            + unit
                            + " count: "
                            + count
                            + " cv: "
                            + cv
                            + " dr.pl: "
                            + dr.pl);
            Thread.dumpStack();
        }
        if (dr.pl == EPluralization.NONE) {
            return FORM_PLURAL;
        }
        // otherwise, assume we have at least a singular and plural form

        int val = count / 1000;

        switch (cv) {
            case ECountVariant.INTEGER:
            case ECountVariant.INTEGER_CUSTOM:
                {
                    // do more analysis based on floor of count
                }
                break;
            case ECountVariant.HALF_FRACTION:
                {
                    switch (dr.fractionHandling) {
                        case EFractionHandling.FPLURAL:
                            return FORM_PLURAL;

                        case EFractionHandling.FSINGULAR_PLURAL_ANDAHALF:
                        case EFractionHandling.FSINGULAR_PLURAL:
                            {
                                // if half-floor is 1/2, use singular
                                // else if half-floor is not integral, use plural
                                // else do more analysis
                                int v = count / 500;
                                if (v == 1) {
                                    if (dr.halfNames != null
                                            && dr.halfNames[unit.ordinal()] != null) {
                                        return FORM_HALF_SPELLED;
                                    }
                                    return FORM_SINGULAR_NO_OMIT;
                                }
                                if ((v & 0x1) == 1) {
                                    if (dr.pl == EPluralization.ARABIC && v > 21) { // hack
                                        return FORM_SINGULAR_NO_OMIT;
                                    }
                                    if (v == 3
                                            && dr.pl == EPluralization.PLURAL
                                            && dr.fractionHandling
                                                    != EFractionHandling
                                                            .FSINGULAR_PLURAL_ANDAHALF) {
                                        return FORM_PLURAL;
                                    }
                                }

                                // it will display like an integer, so do more analysis
                            }
                            break;

                        case EFractionHandling.FPAUCAL:
                            {
                                int v = count / 500;
                                if (v == 1 || v == 3) {
                                    return FORM_PAUCAL;
                                }
                                // else use integral form
                            }
                            break;

                        default:
                            throw new IllegalStateException();
                    }
                }
                break;
            default:
                { // for all decimals
                    switch (dr.decimalHandling) {
                        case EDecimalHandling.DPLURAL:
                            break;
                        case EDecimalHandling.DSINGULAR:
                            return FORM_SINGULAR_NO_OMIT;
                        case EDecimalHandling.DSINGULAR_SUBONE:
                            if (count < 1000) {
                                return FORM_SINGULAR_NO_OMIT;
                            }
                            break;
                        case EDecimalHandling.DPAUCAL:
                            if (dr.pl == EPluralization.PAUCAL) {
                                return FORM_PAUCAL;
                            }
                            break;
                        default:
                            break;
                    }
                    return FORM_PLURAL;
                }
        }

        // select among pluralization forms
        if (trace && count == 0) {
            System.err.println("EZeroHandling = " + dr.zeroHandling);
        }
        if (count == 0 && dr.zeroHandling == EZeroHandling.ZSINGULAR) {
            return FORM_SINGULAR_SPELLED;
        }

        int form = FORM_PLURAL;
        switch (dr.pl) {
            case EPluralization.NONE:
                break; // never get here
            case EPluralization.PLURAL:
                {
                    if (val == 1) {
                        form = FORM_SINGULAR_SPELLED; // defaults to form_singular if no spelled
                        // forms
                    }
                }
                break;
            case EPluralization.DUAL:
                {
                    if (val == 2) {
                        form = FORM_DUAL;
                    } else if (val == 1) {
                        form = FORM_SINGULAR;
                    }
                }
                break;
            case EPluralization.PAUCAL:
                {
                    int v = val;
                    v = v % 100;
                    if (v > 20) {
                        v = v % 10;
                    }
                    if (v == 1) {
                        form = FORM_SINGULAR;
                    } else if (v > 1 && v < 5) {
                        form = FORM_PAUCAL;
                    }
                }
                break;
            /*
            case EPluralization.RPT_DUAL_FEW: {
              int v = val;
              if (v > 20) {
                v = v % 10;
              }
              if (v == 1) {
                form = FORM_SINGULAR;
              } else if (v == 2) {
                form = FORM_DUAL;
              } else if (v > 2 && v < 5) {
                form = FORM_PAUCAL;
              }
            } break;
              */
            case EPluralization.HEBREW:
                {
                    if (val == 2) {
                        form = FORM_DUAL;
                    } else if (val == 1) {
                        if (lastOfMultiple) {
                            form = FORM_SINGULAR_SPELLED;
                        } else {
                            form = FORM_SINGULAR;
                        }
                    } else if (unit == TimeUnit.YEAR && val > 11) {
                        form = FORM_SINGULAR_NO_OMIT;
                    }
                }
                break;
            case EPluralization.ARABIC:
                {
                    if (val == 2) {
                        form = FORM_DUAL;
                    } else if (val == 1) {
                        form = FORM_SINGULAR;
                    } else if (val > 10) {
                        form = FORM_SINGULAR_NO_OMIT;
                    }
                }
                break;
            default:
                System.err.println("dr.pl is " + dr.pl);
                throw new IllegalStateException();
        }

        return form;
    }
}