1/* 2****************************************************************************** 3* Copyright (C) 2009-2011, International Business Machines Corporation and * 4* others. All Rights Reserved. * 5****************************************************************************** 6*/ 7 8package com.ibm.icu.impl.duration.impl; 9 10import java.util.Arrays; 11 12import com.ibm.icu.impl.duration.TimeUnit; 13import com.ibm.icu.impl.duration.impl.DataRecord.ECountVariant; 14import com.ibm.icu.impl.duration.impl.DataRecord.EDecimalHandling; 15import com.ibm.icu.impl.duration.impl.DataRecord.EFractionHandling; 16import com.ibm.icu.impl.duration.impl.DataRecord.EGender; 17import com.ibm.icu.impl.duration.impl.DataRecord.EHalfPlacement; 18import com.ibm.icu.impl.duration.impl.DataRecord.EHalfSupport; 19import com.ibm.icu.impl.duration.impl.DataRecord.ENumberSystem; 20import com.ibm.icu.impl.duration.impl.DataRecord.EPluralization; 21import com.ibm.icu.impl.duration.impl.DataRecord.EUnitVariant; 22import com.ibm.icu.impl.duration.impl.DataRecord.EZeroHandling; 23import com.ibm.icu.impl.duration.impl.DataRecord.ScopeData; 24 25 26/** 27 * PeriodFormatterData provides locale-specific data used to format 28 * relative dates and times, and convenience api to access it. 29 * 30 * An instance of PeriodFormatterData is usually created by requesting 31 * data for a given locale from an PeriodFormatterDataService. 32 */ 33public class PeriodFormatterData { 34 final DataRecord dr; 35 String localeName; 36 37 // debug 38 public static boolean trace = false; 39 40 public PeriodFormatterData(String localeName, DataRecord dr) { 41 this.dr = dr; 42 this.localeName = localeName; 43 if(localeName == null) { 44 throw new NullPointerException("localename is null"); 45 } 46// System.err.println("** localeName is " + localeName); 47 if (dr == null) { 48// Thread.dumpStack(); 49 throw new NullPointerException("data record is null"); 50 } 51 } 52 53 // none - chinese (all forms the same) 54 // plural - english, special form for 1 55 // dual - special form for 1 and 2 56 // paucal - russian, special form for 1, for 2-4 and n > 20 && n % 10 == 2-4 57 // rpt_dual_few - slovenian, special form for 1, 2, 3-4 and n as above 58 // hebrew, dual plus singular form for years > 11 59 // arabic, dual, plus singular form for all terms > 10 60 61 /** 62 * Return the pluralization format used by this locale. 63 * @return the pluralization format 64 */ 65 public int pluralization() { 66 return dr.pl; 67 } 68 69 /** 70 * Return true if zeros are allowed in the display. 71 * @return true if zeros should be allowed 72 */ 73 public boolean allowZero() { 74 return dr.allowZero; 75 } 76 77 public boolean weeksAloneOnly() { 78 return dr.weeksAloneOnly; 79 } 80 81 public int useMilliseconds() { 82 return dr.useMilliseconds; 83 } 84 85 /** 86 * Append the appropriate prefix to the string builder, depending on whether and 87 * how a limit and direction are to be displayed. 88 * 89 * @param tl how and whether to display the time limit 90 * @param td how and whether to display the time direction 91 * @param sb the string builder to which to append the text 92 * @return true if a following digit will require a digit prefix 93 */ 94 public boolean appendPrefix(int tl, int td, StringBuffer sb) { 95 if (dr.scopeData != null) { 96 int ix = tl * 3 + td; 97 ScopeData sd = dr.scopeData[ix]; 98 if (sd != null) { 99 String prefix = sd.prefix; 100 if (prefix != null) { 101 sb.append(prefix); 102 return sd.requiresDigitPrefix; 103 } 104 } 105 } 106 return false; 107 } 108 109 /** 110 * Append the appropriate suffix to the string builder, depending on whether and 111 * how a limit and direction are to be displayed. 112 * 113 * @param tl how and whether to display the time limit 114 * @param td how and whether to display the time direction 115 * @param sb the string builder to which to append the text 116 */ 117 public void appendSuffix(int tl, int td, StringBuffer sb) { 118 if (dr.scopeData != null) { 119 int ix = tl * 3 + td; 120 ScopeData sd = dr.scopeData[ix]; 121 if (sd != null) { 122 String suffix = sd.suffix; 123 if (suffix != null) { 124 if (trace) { 125 System.out.println("appendSuffix '" + suffix + "'"); 126 } 127 sb.append(suffix); 128 } 129 } 130 } 131 } 132 133 /** 134 * Append the count and unit to the string builder. 135 * 136 * @param unit the unit to append 137 * @param count the count of units, * 1000 138 * @param cv the format to use for displaying the count 139 * @param uv the format to use for displaying the unit 140 * @param useCountSep if false, force no separator between count and unit 141 * @param useDigitPrefix if true, use the digit prefix 142 * @param multiple true if there are multiple units in this string 143 * @param last true if this is the last unit 144 * @param wasSkipped true if the unit(s) before this were skipped 145 * @param sb the string builder to which to append the text 146 * @return true if will require skip marker 147 */ 148 @SuppressWarnings("fallthrough") 149 public boolean appendUnit(TimeUnit unit, int count, int cv, 150 int uv, boolean useCountSep, 151 boolean useDigitPrefix, boolean multiple, 152 boolean last, boolean wasSkipped, 153 StringBuffer sb) { 154 int px = unit.ordinal(); 155 156 boolean willRequireSkipMarker = false; 157 if (dr.requiresSkipMarker != null && dr.requiresSkipMarker[px] && 158 dr.skippedUnitMarker != null) { 159 if (!wasSkipped && last) { 160 sb.append(dr.skippedUnitMarker); 161 } 162 willRequireSkipMarker = true; 163 } 164 165 if (uv != EUnitVariant.PLURALIZED) { 166 boolean useMedium = uv == EUnitVariant.MEDIUM; 167 String[] names = useMedium ? dr.mediumNames : dr.shortNames; 168 if (names == null || names[px] == null) { 169 names = useMedium ? dr.shortNames : dr.mediumNames; 170 } 171 if (names != null && names[px] != null) { 172 appendCount(unit, false, false, count, cv, useCountSep, 173 names[px], last, sb); // omit suffix, ok? 174 return false; // omit skip marker 175 } 176 } 177 178 // check cv 179 if (cv == ECountVariant.HALF_FRACTION && dr.halfSupport != null) { 180 switch (dr.halfSupport[px]) { 181 case EHalfSupport.YES: break; 182 case EHalfSupport.ONE_PLUS: 183 if (count > 1000) { 184 break; 185 } 186 // else fall through to decimal 187 case EHalfSupport.NO: { 188 count = (count / 500) * 500; // round to 1/2 189 cv = ECountVariant.DECIMAL1; 190 } break; 191 } 192 } 193 194 String name = null; 195 int form = computeForm(unit, count, cv, multiple && last); 196 if (form == FORM_SINGULAR_SPELLED) { 197 if (dr.singularNames == null) { 198 form = FORM_SINGULAR; 199 name = dr.pluralNames[px][form]; 200 } else { 201 name = dr.singularNames[px]; 202 } 203 } else if (form == FORM_SINGULAR_NO_OMIT) { 204 name = dr.pluralNames[px][FORM_SINGULAR]; 205 } else if (form == FORM_HALF_SPELLED) { 206 name = dr.halfNames[px]; 207 } else { 208 try { 209 name = dr.pluralNames[px][form]; 210 } catch (NullPointerException e) { 211 System.out.println("Null Pointer in PeriodFormatterData["+localeName+"].au px: " + px + " form: " + form + " pn: " + Arrays.toString(dr.pluralNames)); 212 throw e; 213 } 214 } 215 if (name == null) { 216 form = FORM_PLURAL; 217 name = dr.pluralNames[px][form]; 218 } 219 220 boolean omitCount = 221 (form == FORM_SINGULAR_SPELLED || form == FORM_HALF_SPELLED) || 222 (dr.omitSingularCount && form == FORM_SINGULAR) || 223 (dr.omitDualCount && form == FORM_DUAL); 224 225 int suffixIndex = appendCount(unit, omitCount, useDigitPrefix, count, cv, 226 useCountSep, name, last, sb); 227 if (last && suffixIndex >= 0) { 228 String suffix = null; 229 if (dr.rqdSuffixes != null && suffixIndex < dr.rqdSuffixes.length) { 230 suffix = dr.rqdSuffixes[suffixIndex]; 231 } 232 if (suffix == null && dr.optSuffixes != null && 233 suffixIndex < dr.optSuffixes.length) { 234 suffix = dr.optSuffixes[suffixIndex]; 235 } 236 if (suffix != null) { 237 sb.append(suffix); 238 } 239 } 240 return willRequireSkipMarker; 241 } 242 243 /** 244 * Append a count to the string builder. 245 * 246 * @param unit the unit 247 * @param count the count 248 * @param cv the format to use for displaying the count 249 * @param useSep whether to use the count separator, if available 250 * @param name the term name 251 * @param last true if this is the last unit to be formatted 252 * @param sb the string builder to which to append the text 253 * @return index to use if might have required or optional suffix, or -1 if none required 254 */ 255 public int appendCount(TimeUnit unit, boolean omitCount, 256 boolean useDigitPrefix, 257 int count, int cv, boolean useSep, 258 String name, boolean last, StringBuffer sb) { 259 if (cv == ECountVariant.HALF_FRACTION && dr.halves == null) { 260 cv = ECountVariant.INTEGER; 261 } 262 263 if (!omitCount && useDigitPrefix && dr.digitPrefix != null) { 264 sb.append(dr.digitPrefix); 265 } 266 267 int index = unit.ordinal(); 268 switch (cv) { 269 case ECountVariant.INTEGER: { 270 if (!omitCount) { 271 appendInteger(count/1000, 1, 10, sb); 272 } 273 } break; 274 275 case ECountVariant.INTEGER_CUSTOM: { 276 int val = count / 1000; 277 // only custom names we have for now 278 if (unit == TimeUnit.MINUTE && 279 (dr.fiveMinutes != null || dr.fifteenMinutes != null)) { 280 if (val != 0 && val % 5 == 0) { 281 if (dr.fifteenMinutes != null && (val == 15 || val == 45)) { 282 val = val == 15 ? 1 : 3; 283 if (!omitCount) appendInteger(val, 1, 10, sb); 284 name = dr.fifteenMinutes; 285 index = 8; // hack 286 break; 287 } 288 if (dr.fiveMinutes != null) { 289 val = val / 5; 290 if (!omitCount) appendInteger(val, 1, 10, sb); 291 name = dr.fiveMinutes; 292 index = 9; // hack 293 break; 294 } 295 } 296 } 297 if (!omitCount) appendInteger(val, 1, 10, sb); 298 } break; 299 300 case ECountVariant.HALF_FRACTION: { 301 // 0, 1/2, 1, 1-1/2... 302 int v = count / 500; 303 if (v != 1) { 304 if (!omitCount) appendCountValue(count, 1, 0, sb); 305 } 306 if ((v & 0x1) == 1) { 307 // hack, using half name 308 if (v == 1 && dr.halfNames != null && dr.halfNames[index] != null) { 309 sb.append(name); 310 return last ? index : -1; 311 } 312 313 int solox = v == 1 ? 0 : 1; 314 if (dr.genders != null && dr.halves.length > 2) { 315 if (dr.genders[index] == EGender.F) { 316 solox += 2; 317 } 318 } 319 int hp = dr.halfPlacements == null 320 ? EHalfPlacement.PREFIX 321 : dr.halfPlacements[solox & 0x1]; 322 String half = dr.halves[solox]; 323 String measure = dr.measures == null ? null : dr.measures[index]; 324 switch (hp) { 325 case EHalfPlacement.PREFIX: 326 sb.append(half); 327 break; 328 case EHalfPlacement.AFTER_FIRST: { 329 if (measure != null) { 330 sb.append(measure); 331 sb.append(half); 332 if (useSep && !omitCount) { 333 sb.append(dr.countSep); 334 } 335 sb.append(name); 336 } else { // ignore sep completely 337 sb.append(name); 338 sb.append(half); 339 return last ? index : -1; // might use suffix 340 } 341 } return -1; // exit early 342 case EHalfPlacement.LAST: { 343 if (measure != null) { 344 sb.append(measure); 345 } 346 if (useSep && !omitCount) { 347 sb.append(dr.countSep); 348 } 349 sb.append(name); 350 sb.append(half); 351 } return last ? index : -1; // might use suffix 352 } 353 } 354 } break; 355 default: { 356 int decimals = 1; 357 switch (cv) { 358 case ECountVariant.DECIMAL2: decimals = 2; break; 359 case ECountVariant.DECIMAL3: decimals = 3; break; 360 default: break; 361 } 362 if (!omitCount) appendCountValue(count, 1, decimals, sb); 363 } break; 364 } 365 if (!omitCount && useSep) { 366 sb.append(dr.countSep); 367 } 368 if (!omitCount && dr.measures != null && index < dr.measures.length) { 369 String measure = dr.measures[index]; 370 if (measure != null) { 371 sb.append(measure); 372 } 373 } 374 sb.append(name); 375 return last ? index : -1; 376 } 377 378 /** 379 * Append a count value to the builder. 380 * 381 * @param count the count 382 * @param integralDigits the number of integer digits to display 383 * @param decimalDigits the number of decimal digits to display, <= 3 384 * @param sb the string builder to which to append the text 385 */ 386 public void appendCountValue(int count, int integralDigits, 387 int decimalDigits, StringBuffer sb) { 388 int ival = count / 1000; 389 if (decimalDigits == 0) { 390 appendInteger(ival, integralDigits, 10, sb); 391 return; 392 } 393 394 if (dr.requiresDigitSeparator && sb.length() > 0) { 395 sb.append(' '); 396 } 397 appendDigits(ival, integralDigits, 10, sb); 398 int dval = count % 1000; 399 if (decimalDigits == 1) { 400 dval /= 100; 401 } else if (decimalDigits == 2) { 402 dval /= 10; 403 } 404 sb.append(dr.decimalSep); 405 appendDigits(dval, decimalDigits, decimalDigits, sb); 406 if (dr.requiresDigitSeparator) { 407 sb.append(' '); 408 } 409 } 410 411 public void appendInteger(int num, int mindigits, int maxdigits, 412 StringBuffer sb) { 413 if (dr.numberNames != null && num < dr.numberNames.length) { 414 String name = dr.numberNames[num]; 415 if (name != null) { 416 sb.append(name); 417 return; 418 } 419 } 420 421 if (dr.requiresDigitSeparator && sb.length() > 0) { 422 sb.append(' '); 423 } 424 switch (dr.numberSystem) { 425 case ENumberSystem.DEFAULT: appendDigits(num, mindigits, maxdigits, sb); break; 426 case ENumberSystem.CHINESE_TRADITIONAL: sb.append( 427 Utils.chineseNumber(num, Utils.ChineseDigits.TRADITIONAL)); break; 428 case ENumberSystem.CHINESE_SIMPLIFIED: sb.append( 429 Utils.chineseNumber(num, Utils.ChineseDigits.SIMPLIFIED)); break; 430 case ENumberSystem.KOREAN: sb.append( 431 Utils.chineseNumber(num, Utils.ChineseDigits.KOREAN)); break; 432 } 433 if (dr.requiresDigitSeparator) { 434 sb.append(' '); 435 } 436 } 437 438 /** 439 * Append digits to the string builder, using this.zero for '0' etc. 440 * 441 * @param num the integer to append 442 * @param mindigits the minimum number of digits to append 443 * @param maxdigits the maximum number of digits to append 444 * @param sb the string builder to which to append the text 445 */ 446 public void appendDigits(long num, int mindigits, int maxdigits, 447 StringBuffer sb) { 448 char[] buf = new char[maxdigits]; 449 int ix = maxdigits; 450 while (ix > 0 && num > 0) { 451 buf[--ix] = (char)(dr.zero + (num % 10)); 452 num /= 10; 453 } 454 for (int e = maxdigits - mindigits; ix > e;) { 455 buf[--ix] = dr.zero; 456 } 457 sb.append(buf, ix, maxdigits - ix); 458 } 459 460 /** 461 * Append a marker for skipped units internal to a string. 462 * @param sb the string builder to which to append the text 463 */ 464 public void appendSkippedUnit(StringBuffer sb) { 465 if (dr.skippedUnitMarker != null) { 466 sb.append(dr.skippedUnitMarker); 467 } 468 } 469 470 /** 471 * Append the appropriate separator between units 472 * 473 * @param unit the unit to which to append the separator 474 * @param afterFirst true if this is the first unit formatted 475 * @param beforeLast true if this is the next-to-last unit to be formatted 476 * @param sb the string builder to which to append the text 477 * @return true if a prefix will be required before a following unit 478 */ 479 public boolean appendUnitSeparator(TimeUnit unit, boolean longSep, 480 boolean afterFirst, boolean beforeLast, 481 StringBuffer sb) { 482 // long seps 483 // false, false "...b', '...d" 484 // false, true "...', and 'c" 485 // true, false - "a', '...c" 486 // true, true - "a' and 'b" 487 if ((longSep && dr.unitSep != null) || dr.shortUnitSep != null) { 488 if (longSep && dr.unitSep != null) { 489 int ix = (afterFirst ? 2 : 0) + (beforeLast ? 1 : 0); 490 sb.append(dr.unitSep[ix]); 491 return dr.unitSepRequiresDP != null && dr.unitSepRequiresDP[ix]; 492 } 493 sb.append(dr.shortUnitSep); // todo: investigate whether DP is required 494 } 495 return false; 496 } 497 498 private static final int 499 FORM_PLURAL = 0, 500 FORM_SINGULAR = 1, 501 FORM_DUAL = 2, 502 FORM_PAUCAL = 3, 503 FORM_SINGULAR_SPELLED = 4, // following are not in the pluralization list 504 FORM_SINGULAR_NO_OMIT = 5, // a hack 505 FORM_HALF_SPELLED = 6; 506 507 private int computeForm(TimeUnit unit, int count, int cv, 508 boolean lastOfMultiple) { 509 // first check if a particular form is forced by the countvariant. if 510 // SO, just return that. otherwise convert the count to an integer 511 // and use pluralization rules to determine which form to use. 512 // careful, can't assume any forms but plural exist. 513 514 if (trace) { 515 System.err.println("pfd.cf unit: " + unit + " count: " + count + " cv: " + cv + " dr.pl: " + dr.pl); 516 Thread.dumpStack(); 517 } 518 if (dr.pl == EPluralization.NONE) { 519 return FORM_PLURAL; 520 } 521 // otherwise, assume we have at least a singular and plural form 522 523 int val = count/1000; 524 525 switch (cv) { 526 case ECountVariant.INTEGER: 527 case ECountVariant.INTEGER_CUSTOM: { 528 // do more analysis based on floor of count 529 } break; 530 case ECountVariant.HALF_FRACTION: { 531 switch (dr.fractionHandling) { 532 case EFractionHandling.FPLURAL: 533 return FORM_PLURAL; 534 535 case EFractionHandling.FSINGULAR_PLURAL_ANDAHALF: 536 case EFractionHandling.FSINGULAR_PLURAL: { 537 // if half-floor is 1/2, use singular 538 // else if half-floor is not integral, use plural 539 // else do more analysis 540 int v = count / 500; 541 if (v == 1) { 542 if (dr.halfNames != null && dr.halfNames[unit.ordinal()] != null) { 543 return FORM_HALF_SPELLED; 544 } 545 return FORM_SINGULAR_NO_OMIT; 546 } 547 if ((v & 0x1) == 1) { 548 if (dr.pl == EPluralization.ARABIC && v > 21) { // hack 549 return FORM_SINGULAR_NO_OMIT; 550 } 551 if (v == 3 && dr.pl == EPluralization.PLURAL && 552 dr.fractionHandling != EFractionHandling.FSINGULAR_PLURAL_ANDAHALF) { 553 return FORM_PLURAL; 554 } 555 } 556 557 // it will display like an integer, so do more analysis 558 } break; 559 560 case EFractionHandling.FPAUCAL: { 561 int v = count / 500; 562 if (v == 1 || v == 3) { 563 return FORM_PAUCAL; 564 } 565 // else use integral form 566 } break; 567 568 default: 569 throw new IllegalStateException(); 570 } 571 } break; 572 default: { // for all decimals 573 switch (dr.decimalHandling) { 574 case EDecimalHandling.DPLURAL: break; 575 case EDecimalHandling.DSINGULAR: return FORM_SINGULAR_NO_OMIT; 576 case EDecimalHandling.DSINGULAR_SUBONE: 577 if (count < 1000) { 578 return FORM_SINGULAR_NO_OMIT; 579 } 580 break; 581 case EDecimalHandling.DPAUCAL: 582 if (dr.pl == EPluralization.PAUCAL) { 583 return FORM_PAUCAL; 584 } 585 break; 586 default: 587 break; 588 } 589 return FORM_PLURAL; 590 } 591 } 592 593 // select among pluralization forms 594 if (trace && count == 0) { 595 System.err.println("EZeroHandling = " + dr.zeroHandling); 596 } 597 if (count == 0 && dr.zeroHandling == EZeroHandling.ZSINGULAR) { 598 return FORM_SINGULAR_SPELLED; 599 } 600 601 int form = FORM_PLURAL; 602 switch(dr.pl) { 603 case EPluralization.NONE: break; // never get here 604 case EPluralization.PLURAL: { 605 if (val == 1) { 606 form = FORM_SINGULAR_SPELLED; // defaults to form_singular if no spelled forms 607 } 608 } break; 609 case EPluralization.DUAL: { 610 if (val == 2) { 611 form = FORM_DUAL; 612 } else if (val == 1) { 613 form = FORM_SINGULAR; 614 } 615 } break; 616 case EPluralization.PAUCAL: { 617 int v = val; 618 v = v % 100; 619 if (v > 20) { 620 v = v % 10; 621 } 622 if (v == 1) { 623 form = FORM_SINGULAR; 624 } else if (v > 1 && v < 5) { 625 form = FORM_PAUCAL; 626 } 627 } break; 628 /* 629 case EPluralization.RPT_DUAL_FEW: { 630 int v = val; 631 if (v > 20) { 632 v = v % 10; 633 } 634 if (v == 1) { 635 form = FORM_SINGULAR; 636 } else if (v == 2) { 637 form = FORM_DUAL; 638 } else if (v > 2 && v < 5) { 639 form = FORM_PAUCAL; 640 } 641 } break; 642 */ 643 case EPluralization.HEBREW: { 644 if (val == 2) { 645 form = FORM_DUAL; 646 } else if (val == 1) { 647 if (lastOfMultiple) { 648 form = FORM_SINGULAR_SPELLED; 649 } else { 650 form = FORM_SINGULAR; 651 } 652 } else if (unit == TimeUnit.YEAR && val > 11) { 653 form = FORM_SINGULAR_NO_OMIT; 654 } 655 } break; 656 case EPluralization.ARABIC: { 657 if (val == 2) { 658 form = FORM_DUAL; 659 } else if (val == 1) { 660 form = FORM_SINGULAR; 661 } else if (val > 10) { 662 form = FORM_SINGULAR_NO_OMIT; 663 } 664 } break; 665 default: 666 System.err.println("dr.pl is " + dr.pl); 667 throw new IllegalStateException(); 668 } 669 670 return form; 671 } 672} 673