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