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