blob: 36bef03a907d93460661f54f54cb6cfd48ff4399 [file] [log] [blame]
/*
******************************************************************************
* 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;
}
}