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;
}
}