2 ******************************************************************************
3 * Copyright (C) 2009-2010, International Business Machines Corporation and *
4 * others. All Rights Reserved. *
5 ******************************************************************************
8 package com.ibm.icu.impl.duration.impl;
10 import com.ibm.icu.impl.duration.TimeUnit;
11 import com.ibm.icu.impl.duration.impl.DataRecord.ECountVariant;
12 import com.ibm.icu.impl.duration.impl.DataRecord.EDecimalHandling;
13 import com.ibm.icu.impl.duration.impl.DataRecord.EFractionHandling;
14 import com.ibm.icu.impl.duration.impl.DataRecord.EGender;
15 import com.ibm.icu.impl.duration.impl.DataRecord.EHalfPlacement;
16 import com.ibm.icu.impl.duration.impl.DataRecord.EHalfSupport;
17 import com.ibm.icu.impl.duration.impl.DataRecord.ENumberSystem;
18 import com.ibm.icu.impl.duration.impl.DataRecord.EPluralization;
19 import com.ibm.icu.impl.duration.impl.DataRecord.EUnitVariant;
20 import com.ibm.icu.impl.duration.impl.DataRecord.EZeroHandling;
21 import com.ibm.icu.impl.duration.impl.DataRecord.ScopeData;
25 * PeriodFormatterData provides locale-specific data used to format
26 * relative dates and times, and convenience api to access it.
28 * An instance of PeriodFormatterData is usually created by requesting
29 * data for a given locale from an PeriodFormatterDataService.
31 public class PeriodFormatterData {
36 public static boolean trace = false;
38 public PeriodFormatterData(String localeName, DataRecord dr) {
40 this.localeName = localeName;
41 if(localeName == null) {
42 throw new NullPointerException("localename is null");
44 // System.err.println("** localeName is " + localeName);
46 // Thread.dumpStack();
47 throw new NullPointerException("data record is null");
51 // none - chinese (all forms the same)
52 // plural - english, special form for 1
53 // dual - special form for 1 and 2
54 // paucal - russian, special form for 1, for 2-4 and n > 20 && n % 10 == 2-4
55 // rpt_dual_few - slovenian, special form for 1, 2, 3-4 and n as above
56 // hebrew, dual plus singular form for years > 11
57 // arabic, dual, plus singular form for all terms > 10
60 * Return the pluralization format used by this locale.
61 * @return the pluralization format
63 public int pluralization() {
68 * Return true if zeros are allowed in the display.
69 * @return true if zeros should be allowed
71 public boolean allowZero() {
75 public boolean weeksAloneOnly() {
76 return dr.weeksAloneOnly;
79 public int useMilliseconds() {
80 return dr.useMilliseconds;
84 * Append the appropriate prefix to the string builder, depending on whether and
85 * how a limit and direction are to be displayed.
87 * @param tl how and whether to display the time limit
88 * @param td how and whether to display the time direction
89 * @param sb the string builder to which to append the text
90 * @return true if a following digit will require a digit prefix
92 public boolean appendPrefix(int tl, int td, StringBuffer sb) {
93 if (dr.scopeData != null) {
95 ScopeData sd = dr.scopeData[ix];
97 String prefix = sd.prefix;
100 return sd.requiresDigitPrefix;
108 * Append the appropriate suffix to the string builder, depending on whether and
109 * how a limit and direction are to be displayed.
111 * @param tl how and whether to display the time limit
112 * @param td how and whether to display the time direction
113 * @param sb the string builder to which to append the text
115 public void appendSuffix(int tl, int td, StringBuffer sb) {
116 if (dr.scopeData != null) {
117 int ix = tl * 3 + td;
118 ScopeData sd = dr.scopeData[ix];
120 String suffix = sd.suffix;
121 if (suffix != null) {
123 System.out.println("appendSuffix '" + suffix + "'");
132 * Append the count and unit to the string builder.
134 * @param unit the unit to append
135 * @param count the count of units, * 1000
136 * @param cv the format to use for displaying the count
137 * @param uv the format to use for displaying the unit
138 * @param useCountSep if false, force no separator between count and unit
139 * @param useDigitPrefix if true, use the digit prefix
140 * @param multiple true if there are multiple units in this string
141 * @param last true if this is the last unit
142 * @param wasSkipped true if the unit(s) before this were skipped
143 * @param sb the string builder to which to append the text
144 * @return true if will require skip marker
146 @SuppressWarnings("fallthrough")
147 public boolean appendUnit(TimeUnit unit, int count, int cv,
148 int uv, boolean useCountSep,
149 boolean useDigitPrefix, boolean multiple,
150 boolean last, boolean wasSkipped,
152 int px = unit.ordinal();
154 boolean willRequireSkipMarker = false;
155 if (dr.requiresSkipMarker != null && dr.requiresSkipMarker[px] &&
156 dr.skippedUnitMarker != null) {
157 if (!wasSkipped && last) {
158 sb.append(dr.skippedUnitMarker);
160 willRequireSkipMarker = true;
163 if (uv != EUnitVariant.PLURALIZED) {
164 boolean useMedium = uv == EUnitVariant.MEDIUM;
165 String[] names = useMedium ? dr.mediumNames : dr.shortNames;
166 if (names == null || names[px] == null) {
167 names = useMedium ? dr.shortNames : dr.mediumNames;
169 if (names != null && names[px] != null) {
170 appendCount(unit, false, false, count, cv, useCountSep,
171 names[px], last, sb); // omit suffix, ok?
172 return false; // omit skip marker
177 if (cv == ECountVariant.HALF_FRACTION && dr.halfSupport != null) {
178 switch (dr.halfSupport[px]) {
179 case EHalfSupport.YES: break;
180 case EHalfSupport.ONE_PLUS:
184 // else fall through to decimal
185 case EHalfSupport.NO: {
186 count = (count / 500) * 500; // round to 1/2
187 cv = ECountVariant.DECIMAL1;
193 int form = computeForm(unit, count, cv, multiple && last);
194 if (form == FORM_SINGULAR_SPELLED) {
195 if (dr.singularNames == null) {
196 form = FORM_SINGULAR;
197 name = dr.pluralNames[px][form];
199 name = dr.singularNames[px];
201 } else if (form == FORM_SINGULAR_NO_OMIT) {
202 name = dr.pluralNames[px][FORM_SINGULAR];
203 } else if (form == FORM_HALF_SPELLED) {
204 name = dr.halfNames[px];
207 name = dr.pluralNames[px][form];
208 } catch (NullPointerException e) {
209 System.out.println("Null Pointer in PeriodFormatterData["+localeName+"].au px: " + px + " form: " + form + " pn: " + dr.pluralNames);
215 name = dr.pluralNames[px][form];
219 (form == FORM_SINGULAR_SPELLED || form == FORM_HALF_SPELLED) ||
220 (dr.omitSingularCount && form == FORM_SINGULAR) ||
221 (dr.omitDualCount && form == FORM_DUAL);
223 int suffixIndex = appendCount(unit, omitCount, useDigitPrefix, count, cv,
224 useCountSep, name, last, sb);
225 if (last && suffixIndex >= 0) {
226 String suffix = null;
227 if (dr.rqdSuffixes != null && suffixIndex < dr.rqdSuffixes.length) {
228 suffix = dr.rqdSuffixes[suffixIndex];
230 if (suffix == null && dr.optSuffixes != null &&
231 suffixIndex < dr.optSuffixes.length) {
232 suffix = dr.optSuffixes[suffixIndex];
234 if (suffix != null) {
238 return willRequireSkipMarker;
242 * Append a count to the string builder.
244 * @param unit the unit
245 * @param count the count
246 * @param cv the format to use for displaying the count
247 * @param useSep whether to use the count separator, if available
248 * @param name the term name
249 * @param last true if this is the last unit to be formatted
250 * @param sb the string builder to which to append the text
251 * @return index to use if might have required or optional suffix, or -1 if none required
253 public int appendCount(TimeUnit unit, boolean omitCount,
254 boolean useDigitPrefix,
255 int count, int cv, boolean useSep,
256 String name, boolean last, StringBuffer sb) {
257 if (cv == ECountVariant.HALF_FRACTION && dr.halves == null) {
258 cv = ECountVariant.INTEGER;
261 if (!omitCount && useDigitPrefix && dr.digitPrefix != null) {
262 sb.append(dr.digitPrefix);
265 int index = unit.ordinal();
267 case ECountVariant.INTEGER: {
269 appendInteger(count/1000, 1, 10, sb);
273 case ECountVariant.INTEGER_CUSTOM: {
274 int val = count / 1000;
275 // only custom names we have for now
276 if (unit == TimeUnit.MINUTE &&
277 (dr.fiveMinutes != null || dr.fifteenMinutes != null)) {
278 if (val != 0 && val % 5 == 0) {
279 if (dr.fifteenMinutes != null && (val == 15 || val == 45)) {
280 val = val == 15 ? 1 : 3;
281 if (!omitCount) appendInteger(val, 1, 10, sb);
282 name = dr.fifteenMinutes;
286 if (dr.fiveMinutes != null) {
288 if (!omitCount) appendInteger(val, 1, 10, sb);
289 name = dr.fiveMinutes;
295 if (!omitCount) appendInteger(val, 1, 10, sb);
298 case ECountVariant.HALF_FRACTION: {
299 // 0, 1/2, 1, 1-1/2...
302 if (!omitCount) appendCountValue(count, 1, 0, sb);
304 if ((v & 0x1) == 1) {
305 // hack, using half name
306 if (v == 1 && dr.halfNames != null && dr.halfNames[index] != null) {
308 return last ? index : -1;
311 int solox = v == 1 ? 0 : 1;
312 if (dr.genders != null && dr.halves.length > 2) {
313 if (dr.genders[index] == EGender.F) {
317 int hp = dr.halfPlacements == null
318 ? EHalfPlacement.PREFIX
319 : dr.halfPlacements[solox & 0x1];
320 String half = dr.halves[solox];
321 String measure = dr.measures == null ? null : dr.measures[index];
323 case EHalfPlacement.PREFIX:
326 case EHalfPlacement.AFTER_FIRST: {
327 if (measure != null) {
330 if (useSep && !omitCount) {
331 sb.append(dr.countSep);
334 } else { // ignore sep completely
337 return last ? index : -1; // might use suffix
339 } return -1; // exit early
340 case EHalfPlacement.LAST: {
341 if (measure != null) {
344 if (useSep && !omitCount) {
345 sb.append(dr.countSep);
349 } return last ? index : -1; // might use suffix
356 case ECountVariant.DECIMAL2: decimals = 2; break;
357 case ECountVariant.DECIMAL3: decimals = 3; break;
360 if (!omitCount) appendCountValue(count, 1, decimals, sb);
363 if (!omitCount && useSep) {
364 sb.append(dr.countSep);
366 if (!omitCount && dr.measures != null && index < dr.measures.length) {
367 String measure = dr.measures[index];
368 if (measure != null) {
373 return last ? index : -1;
377 * Append a count value to the builder.
379 * @param count the count
380 * @param integralDigits the number of integer digits to display
381 * @param decimalDigits the number of decimal digits to display, <= 3
382 * @param sb the string builder to which to append the text
384 public void appendCountValue(int count, int integralDigits,
385 int decimalDigits, StringBuffer sb) {
386 int ival = count / 1000;
387 if (decimalDigits == 0) {
388 appendInteger(ival, integralDigits, 10, sb);
392 if (dr.requiresDigitSeparator && sb.length() > 0) {
395 appendDigits(ival, integralDigits, 10, sb);
396 int dval = count % 1000;
397 if (decimalDigits == 1) {
399 } else if (decimalDigits == 2) {
402 sb.append(dr.decimalSep);
403 appendDigits(dval, decimalDigits, decimalDigits, sb);
404 if (dr.requiresDigitSeparator) {
409 public void appendInteger(int num, int mindigits, int maxdigits,
411 if (dr.numberNames != null && num < dr.numberNames.length) {
412 String name = dr.numberNames[num];
419 if (dr.requiresDigitSeparator && sb.length() > 0) {
422 switch (dr.numberSystem) {
423 case ENumberSystem.DEFAULT: appendDigits(num, mindigits, maxdigits, sb); break;
424 case ENumberSystem.CHINESE_TRADITIONAL: sb.append(
425 Utils.chineseNumber(num, Utils.ChineseDigits.TRADITIONAL)); break;
426 case ENumberSystem.CHINESE_SIMPLIFIED: sb.append(
427 Utils.chineseNumber(num, Utils.ChineseDigits.SIMPLIFIED)); break;
428 case ENumberSystem.KOREAN: sb.append(
429 Utils.chineseNumber(num, Utils.ChineseDigits.KOREAN)); break;
431 if (dr.requiresDigitSeparator) {
437 * Append digits to the string builder, using this.zero for '0' etc.
439 * @param num the integer to append
440 * @param mindigits the minimum number of digits to append
441 * @param maxdigits the maximum number of digits to append
442 * @param sb the string builder to which to append the text
444 public void appendDigits(long num, int mindigits, int maxdigits,
446 char[] buf = new char[maxdigits];
448 while (ix > 0 && num > 0) {
449 buf[--ix] = (char)(dr.zero + (num % 10));
452 for (int e = maxdigits - mindigits; ix > e;) {
455 sb.append(buf, ix, maxdigits - ix);
459 * Append a marker for skipped units internal to a string.
460 * @param sb the string builder to which to append the text
462 public void appendSkippedUnit(StringBuffer sb) {
463 if (dr.skippedUnitMarker != null) {
464 sb.append(dr.skippedUnitMarker);
469 * Append the appropriate separator between units
471 * @param unit the unit to which to append the separator
472 * @param afterFirst true if this is the first unit formatted
473 * @param beforeLast true if this is the next-to-last unit to be formatted
474 * @param sb the string builder to which to append the text
475 * @return true if a prefix will be required before a following unit
477 public boolean appendUnitSeparator(TimeUnit unit, boolean longSep,
478 boolean afterFirst, boolean beforeLast,
481 // false, false "...b', '...d"
482 // false, true "...', and 'c"
483 // true, false - "a', '...c"
484 // true, true - "a' and 'b"
485 if ((longSep && dr.unitSep != null) || dr.shortUnitSep != null) {
486 if (longSep && dr.unitSep != null) {
487 int ix = (afterFirst ? 2 : 0) + (beforeLast ? 1 : 0);
488 sb.append(dr.unitSep[ix]);
489 return dr.unitSepRequiresDP != null && dr.unitSepRequiresDP[ix];
491 sb.append(dr.shortUnitSep); // todo: investigate whether DP is required
496 private static final int
501 FORM_SINGULAR_SPELLED = 4, // following are not in the pluralization list
502 FORM_SINGULAR_NO_OMIT = 5, // a hack
503 FORM_HALF_SPELLED = 6;
505 private int computeForm(TimeUnit unit, int count, int cv,
506 boolean lastOfMultiple) {
507 // first check if a particular form is forced by the countvariant. if
508 // SO, just return that. otherwise convert the count to an integer
509 // and use pluralization rules to determine which form to use.
510 // careful, can't assume any forms but plural exist.
513 System.err.println("pfd.cf unit: " + unit + " count: " + count + " cv: " + cv + " dr.pl: " + dr.pl);
516 if (dr.pl == EPluralization.NONE) {
519 // otherwise, assume we have at least a singular and plural form
521 int val = count/1000;
524 case ECountVariant.INTEGER:
525 case ECountVariant.INTEGER_CUSTOM: {
526 // do more analysis based on floor of count
528 case ECountVariant.HALF_FRACTION: {
529 switch (dr.fractionHandling) {
530 case EFractionHandling.FPLURAL:
533 case EFractionHandling.FSINGULAR_PLURAL_ANDAHALF:
534 case EFractionHandling.FSINGULAR_PLURAL: {
535 // if half-floor is 1/2, use singular
536 // else if half-floor is not integral, use plural
537 // else do more analysis
540 if (dr.halfNames != null && dr.halfNames[unit.ordinal()] != null) {
541 return FORM_HALF_SPELLED;
543 return FORM_SINGULAR_NO_OMIT;
545 if ((v & 0x1) == 1) {
546 if (dr.pl == EPluralization.ARABIC && v > 21) { // hack
547 return FORM_SINGULAR_NO_OMIT;
549 if (v == 3 && dr.pl == EPluralization.PLURAL &&
550 dr.fractionHandling != EFractionHandling.FSINGULAR_PLURAL_ANDAHALF) {
555 // it will display like an integer, so do more analysis
558 case EFractionHandling.FPAUCAL: {
560 if (v == 1 || v == 3) {
563 // else use integral form
567 throw new IllegalStateException();
570 default: { // for all decimals
571 switch (dr.decimalHandling) {
572 case EDecimalHandling.DPLURAL: break;
573 case EDecimalHandling.DSINGULAR: return FORM_SINGULAR_NO_OMIT;
574 case EDecimalHandling.DSINGULAR_SUBONE:
576 return FORM_SINGULAR_NO_OMIT;
579 case EDecimalHandling.DPAUCAL:
580 if (dr.pl == EPluralization.PAUCAL) {
591 // select among pluralization forms
592 if (trace && count == 0) {
593 System.err.println("EZeroHandling = " + dr.zeroHandling);
595 if (count == 0 && dr.zeroHandling == EZeroHandling.ZSINGULAR) {
596 return FORM_SINGULAR_SPELLED;
599 int form = FORM_PLURAL;
601 case EPluralization.NONE: break; // never get here
602 case EPluralization.PLURAL: {
604 form = FORM_SINGULAR_SPELLED; // defaults to form_singular if no spelled forms
607 case EPluralization.DUAL: {
610 } else if (val == 1) {
611 form = FORM_SINGULAR;
614 case EPluralization.PAUCAL: {
621 form = FORM_SINGULAR;
622 } else if (v > 1 && v < 5) {
627 case EPluralization.RPT_DUAL_FEW: {
633 form = FORM_SINGULAR;
636 } else if (v > 2 && v < 5) {
641 case EPluralization.HEBREW: {
644 } else if (val == 1) {
645 if (lastOfMultiple) {
646 form = FORM_SINGULAR_SPELLED;
648 form = FORM_SINGULAR;
650 } else if (unit == TimeUnit.YEAR && val > 11) {
651 form = FORM_SINGULAR_NO_OMIT;
654 case EPluralization.ARABIC: {
657 } else if (val == 1) {
658 form = FORM_SINGULAR;
659 } else if (val > 10) {
660 form = FORM_SINGULAR_NO_OMIT;
664 System.err.println("dr.pl is " + dr.pl);
665 throw new IllegalStateException();