| /* |
| ****************************************************************************** |
| * Copyright (C) 2009-2011, International Business Machines Corporation and * |
| * others. All Rights Reserved. * |
| ****************************************************************************** |
| */ |
| |
| package com.ibm.icu.impl.duration.impl; |
| |
| import java.util.Arrays; |
| |
| 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; |
| |
| |
| /** |
| * PeriodFormatterData provides locale-specific data used to format |
| * relative dates and times, and convenience api to access it. |
| * |
| * 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; |
| } |
| } |