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
10package com.ibm.icu.impl.duration;
11
12import java.util.TimeZone;
13
14import com.ibm.icu.impl.duration.impl.DataRecord;
15import com.ibm.icu.impl.duration.impl.PeriodFormatterData;
16import com.ibm.icu.impl.duration.impl.PeriodFormatterDataService;
17
18/**
19 * Default implementation of PeriodBuilderFactory.  This creates builders that
20 * use approximate durations.
21 */
22class BasicPeriodBuilderFactory implements PeriodBuilderFactory {
23  private PeriodFormatterDataService ds;
24  private Settings settings;
25
26  private static final short allBits = 0xff;
27
28  BasicPeriodBuilderFactory(PeriodFormatterDataService ds) {
29    this.ds = ds;
30    this.settings = new Settings();
31  }
32
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
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
81    short effectiveSet() {
82      if (allowMillis) {
83        return uset;
84      }
85      return (short)(uset & ~(1 << TimeUnit.MILLISECOND.ordinal));
86    }
87
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
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
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
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
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
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
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
156    Settings setInUse() {
157      inUse = true;
158      return this;
159    }
160
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
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
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
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
224  public PeriodBuilderFactory setMaxLimit(float maxLimit) {
225    settings = settings.setMaxLimit(maxLimit);
226    return this;
227  }
228
229  @Override
230  public PeriodBuilderFactory setMinLimit(float minLimit) {
231    settings = settings.setMinLimit(minLimit);
232    return this;
233  }
234
235  @Override
236  public PeriodBuilderFactory setAllowZero(boolean allow) {
237    settings = settings.setAllowZero(allow);
238    return this;
239  }
240
241  @Override
242  public PeriodBuilderFactory setWeeksAloneOnly(boolean aloneOnly) {
243    settings = settings.setWeeksAloneOnly(aloneOnly);
244    return this;
245  }
246
247  @Override
248  public PeriodBuilderFactory setAllowMilliseconds(boolean allow) {
249    settings = settings.setAllowMilliseconds(allow);
250    return this;
251  }
252
253  @Override
254  public PeriodBuilderFactory setLocale(String localeName) {
255    settings = settings.setLocale(localeName);
256    return this;
257  }
258
259  @Override
260  public PeriodBuilderFactory setTimeZone(TimeZone timeZone) {
261      // ignore this
262      return this;
263  }
264
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
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
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
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
316  public PeriodBuilder getMultiUnitBuilder(int periodCount) {
317    return MultiUnitBuilder.get(periodCount, getSettings());
318  }
319}
320
321abstract class PeriodBuilderImpl implements PeriodBuilder {
322
323  protected BasicPeriodBuilderFactory.Settings settings;
324
325  @Override
326  public Period create(long duration) {
327    return createWithReferenceDate(duration, System.currentTimeMillis());
328  }
329
330  public long approximateDurationOf(TimeUnit unit) {
331    return BasicPeriodBuilderFactory.approximateDurationOf(unit);
332  }
333
334  @Override
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
375class 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
407class 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
442class 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
490class 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