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-2011, International Business Machines Corporation and   *
6 * others. All Rights Reserved.                                               *
7 ******************************************************************************
8 */
9 
10 package com.ibm.icu.impl.duration;
11 
12 import java.util.TimeZone;
13 
14 import com.ibm.icu.impl.duration.impl.DataRecord;
15 import com.ibm.icu.impl.duration.impl.PeriodFormatterData;
16 import com.ibm.icu.impl.duration.impl.PeriodFormatterDataService;
17 
18 /**
19  * Default implementation of PeriodBuilderFactory.  This creates builders that
20  * use approximate durations.
21  */
22 class BasicPeriodBuilderFactory implements PeriodBuilderFactory {
23   private PeriodFormatterDataService ds;
24   private Settings settings;
25 
26   private static final short allBits = 0xff;
27 
BasicPeriodBuilderFactory(PeriodFormatterDataService ds)28   BasicPeriodBuilderFactory(PeriodFormatterDataService ds) {
29     this.ds = ds;
30     this.settings = new Settings();
31   }
32 
approximateDurationOf(TimeUnit unit)33   static long approximateDurationOf(TimeUnit unit) {
34     return TimeUnit.approxDurations[unit.ordinal];
35   }
36 
37   class Settings {
38     boolean inUse;
39     short uset = allBits;
40     TimeUnit maxUnit = TimeUnit.YEAR;
41     TimeUnit minUnit = TimeUnit.MILLISECOND;
42     int maxLimit;
43     int minLimit;
44     boolean allowZero = true;
45     boolean weeksAloneOnly;
46     boolean allowMillis = true;
47 
setUnits(int uset)48     Settings setUnits(int uset) {
49       if (this.uset == uset) {
50         return this;
51       }
52       Settings result = inUse ? copy() : this;
53 
54       result.uset = (short)uset;
55 
56       if ((uset & allBits) == allBits) {
57         result.uset = allBits;
58         result.maxUnit = TimeUnit.YEAR;
59         result.minUnit = TimeUnit.MILLISECOND;
60       } else {
61         int lastUnit = -1;
62         for (int i = 0; i < TimeUnit.units.length; ++i) {
63           if (0 != (uset & (1 << i))) {
64             if (lastUnit == -1) {
65               result.maxUnit = TimeUnit.units[i];
66             }
67             lastUnit = i;
68           }
69         }
70         if (lastUnit == -1) {
71             // currently empty, but this might be transient so no fail
72             result.minUnit = result.maxUnit = null;
73         } else {
74             result.minUnit = TimeUnit.units[lastUnit];
75         }
76       }
77 
78       return result;
79     }
80 
effectiveSet()81     short effectiveSet() {
82       if (allowMillis) {
83         return uset;
84       }
85       return (short)(uset & ~(1 << TimeUnit.MILLISECOND.ordinal));
86     }
87 
effectiveMinUnit()88     TimeUnit effectiveMinUnit() {
89         if (allowMillis || minUnit != TimeUnit.MILLISECOND) {
90             return minUnit;
91         }
92         // -1 to skip millisecond
93         for (int i = TimeUnit.units.length - 1; --i >= 0;) {
94             if (0 != (uset & (1 << i))) {
95                 return TimeUnit.units[i];
96             }
97         }
98         return TimeUnit.SECOND; // default for pathological case
99     }
100 
setMaxLimit(float maxLimit)101     Settings setMaxLimit(float maxLimit) {
102       int val = maxLimit <= 0 ? 0 : (int)(maxLimit*1000);
103       if (maxLimit == val) {
104         return this;
105       }
106       Settings result = inUse ? copy() : this;
107       result.maxLimit = val;
108       return result;
109     }
110 
setMinLimit(float minLimit)111     Settings setMinLimit(float minLimit) {
112       int val = minLimit <= 0 ? 0 : (int)(minLimit*1000);
113       if (minLimit == val) {
114         return this;
115       }
116       Settings result = inUse ? copy() : this;
117       result.minLimit = val;
118       return result;
119     }
120 
setAllowZero(boolean allow)121     Settings setAllowZero(boolean allow) {
122       if (this.allowZero == allow) {
123         return this;
124       }
125       Settings result = inUse ? copy() : this;
126       result.allowZero = allow;
127       return result;
128     }
129 
setWeeksAloneOnly(boolean weeksAlone)130     Settings setWeeksAloneOnly(boolean weeksAlone) {
131       if (this.weeksAloneOnly == weeksAlone) {
132         return this;
133       }
134       Settings result = inUse ? copy() : this;
135       result.weeksAloneOnly = weeksAlone;
136       return result;
137     }
138 
setAllowMilliseconds(boolean allowMillis)139     Settings setAllowMilliseconds(boolean allowMillis) {
140       if (this.allowMillis == allowMillis) {
141         return this;
142       }
143       Settings result = inUse ? copy() : this;
144       result.allowMillis = allowMillis;
145       return result;
146     }
147 
setLocale(String localeName)148     Settings setLocale(String localeName) {
149       PeriodFormatterData data = ds.get(localeName);
150       return this
151         .setAllowZero(data.allowZero())
152         .setWeeksAloneOnly(data.weeksAloneOnly())
153         .setAllowMilliseconds(data.useMilliseconds() != DataRecord.EMilliSupport.NO);
154     }
155 
setInUse()156     Settings setInUse() {
157       inUse = true;
158       return this;
159     }
160 
createLimited(long duration, boolean inPast)161     Period createLimited(long duration, boolean inPast) {
162       if (maxLimit > 0) {
163           long maxUnitDuration = approximateDurationOf(maxUnit);
164           if (duration * 1000 > maxLimit * maxUnitDuration) {
165               return Period.moreThan(maxLimit/1000f, maxUnit).inPast(inPast);
166           }
167       }
168 
169       if (minLimit > 0) {
170           TimeUnit emu = effectiveMinUnit();
171           long emud = approximateDurationOf(emu);
172           long eml = (emu == minUnit) ? minLimit :
173               Math.max(1000, (approximateDurationOf(minUnit) * minLimit) / emud);
174           if (duration * 1000 < eml * emud) {
175               return Period.lessThan(eml/1000f, emu).inPast(inPast);
176           }
177       }
178       return null;
179     }
180 
copy()181     public Settings copy() {
182         Settings result = new Settings();
183         result.inUse = inUse;
184         result.uset = uset;
185         result.maxUnit = maxUnit;
186         result.minUnit = minUnit;
187         result.maxLimit = maxLimit;
188         result.minLimit = minLimit;
189         result.allowZero = allowZero;
190         result.weeksAloneOnly = weeksAloneOnly;
191         result.allowMillis = allowMillis;
192         return result;
193     }
194   }
195 
196   @Override
setAvailableUnitRange(TimeUnit minUnit, TimeUnit maxUnit)197   public PeriodBuilderFactory setAvailableUnitRange(TimeUnit minUnit,
198                                                     TimeUnit maxUnit) {
199     int uset = 0;
200     for (int i = maxUnit.ordinal; i <= minUnit.ordinal; ++i) {
201         uset |= 1 << i;
202     }
203     if (uset == 0) {
204         throw new IllegalArgumentException("range " + minUnit + " to " + maxUnit + " is empty");
205     }
206     settings = settings.setUnits(uset);
207     return this;
208   }
209 
210   @Override
setUnitIsAvailable(TimeUnit unit, boolean available)211   public PeriodBuilderFactory setUnitIsAvailable(TimeUnit unit,
212                                                       boolean available) {
213     int uset = settings.uset;
214     if (available) {
215       uset |= 1 << unit.ordinal;
216     } else {
217       uset &= ~(1 << unit.ordinal);
218     }
219     settings = settings.setUnits(uset);
220     return this;
221   }
222 
223   @Override
setMaxLimit(float maxLimit)224   public PeriodBuilderFactory setMaxLimit(float maxLimit) {
225     settings = settings.setMaxLimit(maxLimit);
226     return this;
227   }
228 
229   @Override
setMinLimit(float minLimit)230   public PeriodBuilderFactory setMinLimit(float minLimit) {
231     settings = settings.setMinLimit(minLimit);
232     return this;
233   }
234 
235   @Override
setAllowZero(boolean allow)236   public PeriodBuilderFactory setAllowZero(boolean allow) {
237     settings = settings.setAllowZero(allow);
238     return this;
239   }
240 
241   @Override
setWeeksAloneOnly(boolean aloneOnly)242   public PeriodBuilderFactory setWeeksAloneOnly(boolean aloneOnly) {
243     settings = settings.setWeeksAloneOnly(aloneOnly);
244     return this;
245   }
246 
247   @Override
setAllowMilliseconds(boolean allow)248   public PeriodBuilderFactory setAllowMilliseconds(boolean allow) {
249     settings = settings.setAllowMilliseconds(allow);
250     return this;
251   }
252 
253   @Override
setLocale(String localeName)254   public PeriodBuilderFactory setLocale(String localeName) {
255     settings = settings.setLocale(localeName);
256     return this;
257   }
258 
259   @Override
setTimeZone(TimeZone timeZone)260   public PeriodBuilderFactory setTimeZone(TimeZone timeZone) {
261       // ignore this
262       return this;
263   }
264 
getSettings()265   private Settings getSettings() {
266     if (settings.effectiveSet() == 0) {
267       return null;
268     }
269     return settings.setInUse();
270   }
271 
272   /**
273    * Return a builder that represents relative time in terms of the single
274    * given TimeUnit
275    *
276    * @param unit the single TimeUnit with which to represent times
277    * @return a builder
278    */
279   @Override
getFixedUnitBuilder(TimeUnit unit)280   public PeriodBuilder getFixedUnitBuilder(TimeUnit unit) {
281     return FixedUnitBuilder.get(unit, getSettings());
282   }
283 
284   /**
285    * Return a builder that represents relative time in terms of the
286    * largest period less than or equal to the duration.
287    *
288    * @return a builder
289    */
290   @Override
getSingleUnitBuilder()291   public PeriodBuilder getSingleUnitBuilder() {
292     return SingleUnitBuilder.get(getSettings());
293   }
294 
295   /**
296    * Return a builder that formats the largest one or two periods,
297    * Starting with the largest period less than or equal to the duration.
298    * It formats two periods if the first period has a count &lt; 2
299    * and the next period has a count &gt;= 1.
300    *
301    * @return a builder
302    */
303   @Override
getOneOrTwoUnitBuilder()304   public PeriodBuilder getOneOrTwoUnitBuilder() {
305     return OneOrTwoUnitBuilder.get(getSettings());
306   }
307 
308   /**
309    * Return a builder that formats the given number of periods,
310    * starting with the largest period less than or equal to the
311    * duration.
312    *
313    * @return a builder
314    */
315   @Override
getMultiUnitBuilder(int periodCount)316   public PeriodBuilder getMultiUnitBuilder(int periodCount) {
317     return MultiUnitBuilder.get(periodCount, getSettings());
318   }
319 }
320 
321 abstract class PeriodBuilderImpl implements PeriodBuilder {
322 
323   protected BasicPeriodBuilderFactory.Settings settings;
324 
325   @Override
create(long duration)326   public Period create(long duration) {
327     return createWithReferenceDate(duration, System.currentTimeMillis());
328   }
329 
approximateDurationOf(TimeUnit unit)330   public long approximateDurationOf(TimeUnit unit) {
331     return BasicPeriodBuilderFactory.approximateDurationOf(unit);
332   }
333 
334   @Override
createWithReferenceDate(long duration, long referenceDate)335   public Period createWithReferenceDate(long duration, long referenceDate) {
336     boolean inPast = duration < 0;
337     if (inPast) {
338       duration = -duration;
339     }
340     Period ts = settings.createLimited(duration, inPast);
341     if (ts == null) {
342       ts = handleCreate(duration, referenceDate, inPast);
343       if (ts == null) {
344         ts = Period.lessThan(1, settings.effectiveMinUnit()).inPast(inPast);
345       }
346     }
347     return ts;
348   }
349 
350   @Override
351   public PeriodBuilder withTimeZone(TimeZone timeZone) {
352       // ignore the time zone
353       return this;
354   }
355 
356   @Override
357   public PeriodBuilder withLocale(String localeName) {
358     BasicPeriodBuilderFactory.Settings newSettings = settings.setLocale(localeName);
359     if (newSettings != settings) {
360       return withSettings(newSettings);
361     }
362     return this;
363   }
364 
365   protected abstract PeriodBuilder withSettings(BasicPeriodBuilderFactory.Settings settingsToUse);
366 
367   protected abstract Period handleCreate(long duration, long referenceDate,
368                                          boolean inPast);
369 
370   protected PeriodBuilderImpl(BasicPeriodBuilderFactory.Settings settings) {
371     this.settings = settings;
372   }
373 }
374 
375 class FixedUnitBuilder extends PeriodBuilderImpl {
376   private TimeUnit unit;
377 
378   public static FixedUnitBuilder get(TimeUnit unit, BasicPeriodBuilderFactory.Settings settingsToUse) {
379     if (settingsToUse != null && (settingsToUse.effectiveSet() & (1 << unit.ordinal)) != 0) {
380       return new FixedUnitBuilder(unit, settingsToUse);
381     }
382     return null;
383   }
384 
385   FixedUnitBuilder(TimeUnit unit, BasicPeriodBuilderFactory.Settings settings) {
386     super(settings);
387     this.unit = unit;
388   }
389 
390   @Override
391   protected PeriodBuilder withSettings(BasicPeriodBuilderFactory.Settings settingsToUse) {
392     return get(unit, settingsToUse);
393   }
394 
395   @Override
396   protected Period handleCreate(long duration, long referenceDate,
397                                 boolean inPast) {
398     if (unit == null) {
399       return null;
400     }
401     long unitDuration = approximateDurationOf(unit);
402     return Period.at((float)((double)duration/unitDuration), unit)
403         .inPast(inPast);
404   }
405 }
406 
407 class SingleUnitBuilder extends PeriodBuilderImpl {
408   SingleUnitBuilder(BasicPeriodBuilderFactory.Settings settings) {
409     super(settings);
410   }
411 
412   public static SingleUnitBuilder get(BasicPeriodBuilderFactory.Settings settings) {
413     if (settings == null) {
414       return null;
415     }
416     return new SingleUnitBuilder(settings);
417   }
418 
419   @Override
420   protected PeriodBuilder withSettings(BasicPeriodBuilderFactory.Settings settingsToUse) {
421     return SingleUnitBuilder.get(settingsToUse);
422   }
423 
424   @Override
425   protected Period handleCreate(long duration, long referenceDate,
426                                 boolean inPast) {
427     short uset = settings.effectiveSet();
428     for (int i = 0; i < TimeUnit.units.length; ++i) {
429       if (0 != (uset & (1 << i))) {
430         TimeUnit unit = TimeUnit.units[i];
431         long unitDuration = approximateDurationOf(unit);
432         if (duration >= unitDuration) {
433           return Period.at((float)((double)duration/unitDuration), unit)
434               .inPast(inPast);
435         }
436       }
437     }
438     return null;
439   }
440 }
441 
442 class OneOrTwoUnitBuilder extends PeriodBuilderImpl {
443   OneOrTwoUnitBuilder(BasicPeriodBuilderFactory.Settings settings) {
444     super(settings);
445   }
446 
447   public static OneOrTwoUnitBuilder get(BasicPeriodBuilderFactory.Settings settings) {
448     if (settings == null) {
449       return null;
450     }
451     return new OneOrTwoUnitBuilder(settings);
452   }
453 
454   @Override
455   protected PeriodBuilder withSettings(BasicPeriodBuilderFactory.Settings settingsToUse) {
456     return OneOrTwoUnitBuilder.get(settingsToUse);
457   }
458 
459   @Override
460   protected Period handleCreate(long duration, long referenceDate,
461                                 boolean inPast) {
462     Period period = null;
463     short uset = settings.effectiveSet();
464     for (int i = 0; i < TimeUnit.units.length; ++i) {
465       if (0 != (uset & (1 << i))) {
466         TimeUnit unit = TimeUnit.units[i];
467         long unitDuration = approximateDurationOf(unit);
468         if (duration >= unitDuration || period != null) {
469           double count = (double)duration/unitDuration;
470           if (period == null) {
471             if (count >= 2) {
472               period = Period.at((float)count, unit);
473               break;
474             }
475             period = Period.at(1, unit).inPast(inPast);
476             duration -= unitDuration;
477           } else {
478             if (count >= 1) {
479               period = period.and((float)count, unit);
480             }
481             break;
482           }
483         }
484       }
485     }
486     return period;
487   }
488 }
489 
490 class MultiUnitBuilder extends PeriodBuilderImpl {
491   private int nPeriods;
492 
493   MultiUnitBuilder(int nPeriods, BasicPeriodBuilderFactory.Settings settings) {
494     super(settings);
495     this.nPeriods = nPeriods;
496   }
497 
498   public static MultiUnitBuilder get(int nPeriods, BasicPeriodBuilderFactory.Settings settings) {
499     if (nPeriods > 0 && settings != null) {
500       return new MultiUnitBuilder(nPeriods, settings);
501     }
502     return null;
503   }
504 
505   @Override
506   protected PeriodBuilder withSettings(BasicPeriodBuilderFactory.Settings settingsToUse) {
507     return MultiUnitBuilder.get(nPeriods, settingsToUse);
508   }
509 
510   @Override
511   protected Period handleCreate(long duration, long referenceDate,
512                                 boolean inPast) {
513     Period period = null;
514     int n = 0;
515     short uset = settings.effectiveSet();
516     for (int i = 0; i < TimeUnit.units.length; ++i) {
517       if (0 != (uset & (1 << i))) {
518         TimeUnit unit = TimeUnit.units[i];
519         if (n == nPeriods) {
520           break;
521         }
522         long unitDuration = approximateDurationOf(unit);
523         if (duration >= unitDuration || n > 0) {
524           ++n;
525           double count = (double)duration / unitDuration;
526           if (n < nPeriods) {
527             count = Math.floor(count);
528             duration -= (long)(count * unitDuration);
529           }
530           if (period == null) {
531             period = Period.at((float)count, unit).inPast(inPast);
532           } else {
533             period = period.and((float)count, unit);
534           }
535         }
536       }
537     }
538     return period;
539   }
540 }
541 
542