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) 2007-2010, International Business Machines Corporation and   *
6 * others. All Rights Reserved.                                               *
7 ******************************************************************************
8 */
9 
10 package com.ibm.icu.impl.duration;
11 
12 import com.ibm.icu.impl.duration.BasicPeriodFormatterFactory.Customizations;
13 import com.ibm.icu.impl.duration.impl.DataRecord.ECountVariant;
14 import com.ibm.icu.impl.duration.impl.DataRecord.EMilliSupport;
15 import com.ibm.icu.impl.duration.impl.DataRecord.ESeparatorVariant;
16 import com.ibm.icu.impl.duration.impl.DataRecord.ETimeDirection;
17 import com.ibm.icu.impl.duration.impl.DataRecord.ETimeLimit;
18 import com.ibm.icu.impl.duration.impl.PeriodFormatterData;
19 
20 /**
21  * Core implementation class for PeriodFormatter.
22  */
23 class BasicPeriodFormatter implements PeriodFormatter {
24   private BasicPeriodFormatterFactory factory;
25   private String localeName;
26   private PeriodFormatterData data;
27   private Customizations customs;
28 
BasicPeriodFormatter(BasicPeriodFormatterFactory factory, String localeName, PeriodFormatterData data, Customizations customs)29   BasicPeriodFormatter(BasicPeriodFormatterFactory factory,
30                        String localeName,
31                        PeriodFormatterData data,
32                        Customizations customs) {
33     this.factory = factory;
34     this.localeName = localeName;
35     this.data = data;
36     this.customs = customs;
37   }
38 
39   @Override
format(Period period)40 public String format(Period period) {
41     if (!period.isSet()) {
42       throw new IllegalArgumentException("period is not set");
43     }
44     return format(period.timeLimit, period.inFuture, period.counts);
45   }
46 
47   @Override
withLocale(String locName)48   public PeriodFormatter withLocale(String locName) {
49     if (!this.localeName.equals(locName)) {
50       PeriodFormatterData newData = factory.getData(locName);
51       return new BasicPeriodFormatter(factory, locName, newData,
52                                       customs);
53     }
54     return this;
55   }
56 
format(int tl, boolean inFuture, int[] counts)57   private String format(int tl, boolean inFuture, int[] counts) {
58     int mask = 0;
59     for (int i = 0; i < counts.length; ++i) {
60       if (counts[i] > 0) {
61         mask |= 1 << i;
62       }
63     }
64 
65     // if the data does not allow formatting of zero periods,
66     // remove these from consideration.  If the result has no
67     // periods set, return null to indicate we could not format
68     // the duration.
69     if (!data.allowZero()) {
70       for (int i = 0, m = 1; i < counts.length; ++i, m <<= 1) {
71         if ((mask & m) != 0 && counts[i] == 1) {
72           mask &= ~m;
73         }
74       }
75       if (mask == 0) {
76         return null;
77       }
78     }
79 
80     // if the data does not allow milliseconds but milliseconds are
81     // set, merge them with seconds and force display of seconds to
82     // decimal with 3 places.
83     boolean forceD3Seconds = false;
84     if (data.useMilliseconds() != EMilliSupport.YES &&
85         (mask & (1 << TimeUnit.MILLISECOND.ordinal)) != 0) {
86       int sx = TimeUnit.SECOND.ordinal;
87       int mx = TimeUnit.MILLISECOND.ordinal;
88       int sf = 1 << sx;
89       int mf = 1 << mx;
90       switch (data.useMilliseconds()) {
91         case EMilliSupport.WITH_SECONDS: {
92           // if there are seconds, merge with seconds, otherwise leave alone
93           if ((mask & sf) != 0) {
94             counts[sx] += (counts[mx]-1)/1000;
95             mask &= ~mf;
96             forceD3Seconds = true;
97           }
98         } break;
99         case EMilliSupport.NO: {
100           // merge with seconds, reset seconds before use just in case
101           if ((mask & sf) == 0) {
102             mask |= sf;
103             counts[sx] = 1;
104           }
105           counts[sx] += (counts[mx]-1)/1000;
106           mask &= ~mf;
107           forceD3Seconds = true;
108         } break;
109       }
110     }
111 
112     // get the first and last units that are set.
113     int first = 0;
114     int last = counts.length - 1;
115     while (first < counts.length && (mask & (1 << first)) == 0) ++first;
116     while (last > first && (mask & (1 << last)) == 0) --last;
117 
118     // determine if there is any non-zero unit
119     boolean isZero = true;
120     for (int i = first; i <= last; ++i) {
121       if (((mask & (1 << i)) != 0) &&  counts[i] > 1) {
122         isZero = false;
123         break;
124       }
125     }
126 
127     StringBuffer sb = new StringBuffer();
128 
129     // if we've been requested to not display a limit, or there are
130     // no non-zero units, do not display the limit.
131     if (!customs.displayLimit || isZero) {
132       tl = ETimeLimit.NOLIMIT;
133     }
134 
135     // if we've been requested to not display the direction, or there
136     // are no non-zero units, do not display the direction.
137     int td;
138     if (!customs.displayDirection || isZero) {
139       td = ETimeDirection.NODIRECTION;
140     } else {
141       td = inFuture ? ETimeDirection.FUTURE : ETimeDirection.PAST;
142     }
143 
144     // format the initial portion of the string before the units.
145     // record whether we need to use a digit prefix (because the
146     // initial portion forces it)
147     boolean useDigitPrefix = data.appendPrefix(tl, td, sb);
148 
149     // determine some formatting params and initial values
150     boolean multiple = first != last;
151     boolean wasSkipped = true; // no initial skip marker
152     boolean skipped = false;
153     boolean countSep = customs.separatorVariant != ESeparatorVariant.NONE;
154 
155     // loop for formatting the units
156     for (int i = first, j = i; i <= last; i = j) {
157       if (skipped) {
158         // we didn't format the previous unit
159         data.appendSkippedUnit(sb);
160         skipped = false;
161         wasSkipped = true;
162       }
163 
164       while (++j < last && (mask & (1 << j)) == 0) {
165         skipped = true; // skip
166       }
167 
168       TimeUnit unit = TimeUnit.units[i];
169       int count = counts[i] - 1;
170 
171       int cv = customs.countVariant;
172       if (i == last) {
173         if (forceD3Seconds) {
174           cv = ECountVariant.DECIMAL3;
175         }
176         // else leave unchanged
177       } else {
178         cv = ECountVariant.INTEGER;
179       }
180       boolean isLast = i == last;
181       boolean mustSkip = data.appendUnit(unit, count, cv, customs.unitVariant,
182                                          countSep, useDigitPrefix, multiple, isLast, wasSkipped, sb);
183       skipped |= mustSkip;
184       wasSkipped = false;
185 
186       if (customs.separatorVariant != ESeparatorVariant.NONE && j <= last) {
187         boolean afterFirst = i == first;
188         boolean beforeLast = j == last;
189         boolean fullSep = customs.separatorVariant == ESeparatorVariant.FULL;
190         useDigitPrefix = data.appendUnitSeparator(unit, fullSep, afterFirst, beforeLast, sb);
191       } else {
192         useDigitPrefix = false;
193       }
194     }
195     data.appendSuffix(tl, td, sb);
196 
197     return sb.toString();
198   }
199 }
200