551955d458c4e17d034d2f0b79b13bd3f50f4e40
[ccsdk/features.git] /
1 /*******************************************************************************
2  * ============LICENSE_START========================================================================
3  * ONAP : ccsdk feature sdnr wt
4  * =================================================================================================
5  * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved.
6  * =================================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
8  * in compliance with the License. You may obtain a copy of the License at
9  * 
10  * http://www.apache.org/licenses/LICENSE-2.0
11  * 
12  * Unless required by applicable law or agreed to in writing, software distributed under the License
13  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
14  * or implied. See the License for the specific language governing permissions and limitations under
15  * the License.
16  * ============LICENSE_END==========================================================================
17  ******************************************************************************/
18 /*
19  *  SoSy-Lab Common is a library of useful utilities.
20  *  This file is part of SoSy-Lab Common.
21  *
22  *  Copyright (C) 2007-2015  Dirk Beyer
23  *  All rights reserved.
24  *
25  *  Licensed under the Apache License, Version 2.0 (the "License");
26  *  you may not use this file except in compliance with the License.
27  *  You may obtain a copy of the License at
28  *
29  *      http://www.apache.org/licenses/LICENSE-2.0
30  *
31  *  Unless required by applicable law or agreed to in writing, software
32  *  distributed under the License is distributed on an "AS IS" BASIS,
33  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
34  *  See the License for the specific language governing permissions and
35  *  limitations under the License.
36  */
37 package org.onap.ccsdk.features.sdnr.wt.devicemanager.base.internalTypes;
38
39 import static com.google.common.base.Preconditions.checkArgument;
40 import static com.google.common.base.Preconditions.checkNotNull;
41 import static java.util.concurrent.TimeUnit.DAYS;
42 import static java.util.concurrent.TimeUnit.HOURS;
43 import static java.util.concurrent.TimeUnit.MICROSECONDS;
44 import static java.util.concurrent.TimeUnit.MILLISECONDS;
45 import static java.util.concurrent.TimeUnit.MINUTES;
46 import static java.util.concurrent.TimeUnit.NANOSECONDS;
47 import static java.util.concurrent.TimeUnit.SECONDS;
48
49 import com.google.common.annotations.VisibleForTesting;
50 import com.google.common.base.Ascii;
51 import com.google.common.collect.EnumHashBiMap;
52 import com.google.common.collect.ImmutableSortedSet;
53 import com.google.common.collect.Lists;
54 import com.google.common.collect.Ordering;
55 import com.google.common.math.LongMath;
56 import com.google.common.primitives.Longs;
57
58 import java.io.Serializable;
59 import java.util.Arrays;
60 import java.util.EnumSet;
61 import java.util.Iterator;
62 import java.util.List;
63 import java.util.Locale;
64 import java.util.concurrent.TimeUnit;
65 import java.util.function.Function;
66 import java.util.regex.Matcher;
67 import java.util.regex.Pattern;
68 import javax.annotation.CheckReturnValue;
69 import javax.annotation.Nullable;
70
71 /**
72  * This is an immutable representation of some time span, using a {@link TimeUnit} and a value.
73  *
74  * <p>The value may be positive or negative. All operations check for overflows and underflows, the
75  * behavior on overflow and underflow differs and is documented for each method.
76  *
77  * <p>Two instances are considered equal if they represent the exact same time span regardless of
78  * their unit, for example, 60s and 1min are considered equal.
79  */
80
81 public final class TimeSpan implements Comparable<TimeSpan>, Serializable {
82
83   private static final long serialVersionUID = -4013592312989551009L;
84
85   private static final ImmutableSortedSet<TimeUnit> ALL_UNITS =
86       ImmutableSortedSet.copyOf(EnumSet.allOf(TimeUnit.class));
87
88   static {
89     assert ALL_UNITS.higher(SECONDS).equals(MINUTES); // assert expected order of set
90   }
91
92   private static final EnumHashBiMap<TimeUnit, String> TIME_UNITS =
93       EnumHashBiMap.create(TimeUnit.class);
94
95   static {
96     TIME_UNITS.put(NANOSECONDS, "ns");
97     TIME_UNITS.put(MICROSECONDS, "µs");
98     TIME_UNITS.put(MILLISECONDS, "ms");
99     TIME_UNITS.put(SECONDS, "s");
100     TIME_UNITS.put(MINUTES, "min");
101     TIME_UNITS.put(HOURS, "h");
102     TIME_UNITS.put(DAYS, "d");
103   }
104
105   private static final Pattern ONLY_DIGITS = Pattern.compile(" *([0-9]+) *");
106
107   private enum CharType {
108     BEGIN,
109     END,
110     LETTER,
111     DIGIT,
112     WHITESPACE
113   }
114
115   private final long span;
116   private final TimeUnit unit;
117
118   private TimeSpan(long pSpan, TimeUnit pUnit) {
119     span = pSpan;
120     unit = checkNotNull(pUnit);
121   }
122
123   public static TimeSpan of(long pSpan, TimeUnit pUnit) {
124     return new TimeSpan(pSpan, pUnit);
125   }
126
127   public static TimeSpan ofSeconds(long pSeconds) {
128     return new TimeSpan(pSeconds, SECONDS);
129   }
130
131   public static TimeSpan ofMillis(long pMillis) {
132     return new TimeSpan(pMillis, MILLISECONDS);
133   }
134
135   public static TimeSpan ofNanos(long pNanos) {
136     return new TimeSpan(pNanos, NANOSECONDS);
137   }
138
139   public static TimeSpan empty() {
140     return new TimeSpan(0, DAYS);
141   }
142
143   /**
144    * Converts the given {@link String} into a {@link TimeSpan} object. Supported units are day,
145    * hour, minute and second.
146    *
147    * @param input the {@link String} to convert
148    * @return a {@link TimeSpan} represented by the given {@link String}
149    * @throws IllegalArgumentException if the input is not a valid string representation of a {@link
150    *     TimeSpan}.
151    */
152   public static TimeSpan valueOf(String input) {
153
154     // only seconds: use simple regex
155     Matcher secondMatcher = ONLY_DIGITS.matcher(input);
156     if (secondMatcher.matches()) {
157       return ofSeconds(Long.parseLong(secondMatcher.group(1)));
158     }
159
160     // values with units: more elaborate parsing necessary
161     List<String> tokens = splitIntoTokens(input);
162
163     long days = 0;
164     long hours = 0;
165     long minutes = 0;
166     long seconds = 0;
167
168     Iterator<String> it = tokens.iterator();
169
170     while (it.hasNext()) {
171       // first: value
172       String nextString = it.next();
173       long value = Long.parseLong(nextString);
174
175       // second: unit
176       if (!it.hasNext()) {
177         throw new IllegalArgumentException("Value " + nextString + " has no unit.");
178       }
179
180       String unit = it.next();
181       switch (unit) {
182         case "day":
183         case "days":
184         case "d":
185           if (days != 0) {
186             throw new IllegalArgumentException("Days set twice: " + unit);
187           }
188           days = value;
189           break;
190
191         case "h":
192         case "hour":
193         case "hours":
194           if (hours != 0) {
195             throw new IllegalArgumentException("Hours set twice: " + unit);
196           }
197           hours = value;
198           break;
199
200         case "min":
201         case "m":
202           if (minutes != 0) {
203             throw new IllegalArgumentException("Minutes set twice: " + unit);
204           }
205           minutes = value;
206           break;
207
208         case "s":
209           if (seconds != 0) {
210             throw new IllegalArgumentException("Seconds set twice: " + unit);
211           }
212           seconds = value;
213           break;
214
215         default:
216           throw new IllegalArgumentException("Unknown unit: " + unit);
217       }
218     }
219
220     return sum(of(seconds, SECONDS), of(minutes, MINUTES), of(hours, HOURS), of(days, DAYS));
221   }
222
223   private static List<String> splitIntoTokens(String input) {
224     List<String> tokens = Lists.newArrayList();
225     CharType previous = CharType.BEGIN;
226     int pos = 0;
227
228     for (int i = 0; i <= input.length(); ++i) {
229
230       CharType current;
231       if (i == input.length()) {
232         current = CharType.END;
233       } else {
234         char currentChar = input.charAt(i);
235         if (Character.isLetter(currentChar)) {
236           current = CharType.LETTER;
237         } else if (Character.isDigit(currentChar)) {
238           current = CharType.DIGIT;
239         } else if (Character.isWhitespace(currentChar)) {
240           current = CharType.WHITESPACE;
241         } else {
242           throw new IllegalArgumentException(
243               "Unreconized character '" + currentChar + "' when parsing " + input);
244         }
245       }
246
247       if (current != previous) {
248         // we want to use the previous token
249         if (previous == CharType.LETTER || previous == CharType.DIGIT) {
250           tokens.add(input.substring(pos, i));
251         }
252
253         if (current == CharType.LETTER || current == CharType.DIGIT) {
254           pos = i;
255         }
256
257         previous = current;
258       }
259     }
260
261     return tokens;
262   }
263
264   /**
265    * Get the value of this TimeSpan represented in the given unit. If the given unit is larger than
266    * the current unit, precision may be lost.
267    *
268    * @throws ArithmeticException If the value cannot be represented in the given unit due to
269    *     overflow.
270    */
271   public long getChecked(TimeUnit dest) {
272     if (dest.compareTo(unit) < 0) {
273       // Example case: we have seconds, but we want milliseconds (can overflow)
274       long factor = dest.convert(1, unit);
275       assert factor > 1;
276       return LongMath.checkedMultiply(span, factor);
277     }
278
279     // Example case: we have nanoseconds, but we want seconds (cannot overflow)
280     return dest.convert(span, unit);
281   }
282
283   /**
284    * Get the value of this TimeSpan represented in the given unit. If the given unit is larger than
285    * the current unit, precision may be lost. If the value cannot be represented in the given unit
286    * due to overflow, Long.MAX_VALUE/Long.MIN_VALUE is returned.
287    */
288   public long getSaturated(TimeUnit dest) {
289     return dest.convert(span, unit);
290   }
291
292   /**
293    * Return a TimeSpan that represents (approximately) the same time span, but whose unit is the
294    * given unit. If the given unit is larger than the current unit, precision may be lost.
295    *
296    * @throws ArithmeticException If the value cannot be represented in the given unit
297    */
298   public TimeSpan toChecked(TimeUnit dest) {
299     if (dest.equals(unit)) {
300       return this;
301     }
302     return new TimeSpan(getChecked(dest), dest);
303   }
304
305   /**
306    * Return a TimeSpan that represents (approximately) the same time span, but whose unit is the
307    * given unit. If the given unit is larger than the current unit, precision may be lost. If the
308    * value cannot be represented in the given unit due to overflow, Long.MAX_VALUE/Long.MIN_VALUE is
309    * returned.
310    */
311   public TimeSpan toSaturated(TimeUnit dest) {
312     if (dest.equals(unit)) {
313       return this;
314     }
315     return new TimeSpan(getSaturated(dest), dest);
316   }
317
318   /**
319    * Return a TimeSpan that represents (approximately) the same time span, but whose unit is the
320    * given unit, if possible. If the given unit is larger than the current unit, precision may be
321    * lost. If the value cannot be represented in the given unit due to overflow, the resulting
322    * TimeSpan does not use the given unit, but the closest unit one that still allows to hold the
323    * exact value.
324    */
325   @VisibleForTesting
326   TimeSpan toIfPossible(TimeUnit dest) {
327     if (dest.equals(unit)) {
328       return this;
329     }
330     if (dest.compareTo(unit) < 0) {
331       // Example case: we have seconds, but we want milliseconds (can overflow).
332       // Overflow is expected to be very rare.
333       // Loop will terminate because at one time "dest" becomes equal to "this.unit"
334       // and then toChecked succeeds for sure.
335       while (true) {
336         try {
337           return toChecked(dest);
338         } catch (ArithmeticException e) {
339           dest = checkNotNull(ALL_UNITS.higher(dest));
340         }
341       }
342
343     } else {
344       // Example case: we have nanoseconds, but we want seconds (cannot overflow).
345       return new TimeSpan(getSaturated(dest), dest);
346     }
347   }
348
349   /**
350    * Get the value of this TimeSpan as seconds. If the current unit is smaller than seconds,
351    * precision may be lost.
352    *
353    * @throws ArithmeticException If the value cannot be represented as seconds due to overflow.
354    */
355   public long asSeconds() {
356     return getChecked(SECONDS);
357   }
358
359   /**
360    * Get the value of this TimeSpan as milliseconds. If the current unit is smaller than
361    * milliseconds, precision may be lost.
362    *
363    * @throws ArithmeticException If the value cannot be represented as milliseconds due to overflow.
364    */
365   public long asMillis() {
366     return getChecked(MILLISECONDS);
367   }
368
369   /**
370    * Get the value of this TimeSpan as nanoseconds.
371    *
372    * @throws ArithmeticException If the value cannot be represented as milliseconds due to overflow.
373    */
374   public long asNanos() {
375     return getChecked(NANOSECONDS);
376   }
377
378   public TimeUnit getUnit() {
379     return unit;
380   }
381
382   /**
383    * Return a strings that represents (approximately) this time span, in the given unit if possible.
384    * If the given unit is larger than the current unit, precision may be lost. If the value cannot
385    * be represented in the given unit due to overflow, the result does not use the given unit, but
386    * the closest unit one that still allows to hold the exact value.
387    */
388   public String formatAs(TimeUnit dest) {
389     if (dest.compareTo(unit) <= 0) {
390       // Example case: we have seconds, but we want milliseconds
391       return toIfPossible(dest).toString();
392     }
393
394     // Example case: we have nanoseconds, but we want seconds
395     long scaleFactor = unit.convert(1L, dest);
396     assert scaleFactor > 0;
397     return String.format(Locale.US, "%9.3f%s", (double) span / scaleFactor, TIME_UNITS.get(dest));
398   }
399
400   /** Check whether this time span is empty, i.e., represents 0ns (or 0ms or 0s or ...). */
401   public boolean isEmpty() {
402     return span == 0;
403   }
404
405   @Override
406   public boolean equals(@Nullable Object obj) {
407     if (obj == this) {
408       return true;
409     }
410     if (!(obj instanceof TimeSpan)) {
411       return false;
412     }
413     TimeSpan other = (TimeSpan) obj;
414     if (this.unit == other.unit) {
415       return this.span == other.span;
416     }
417     TimeUnit leastCommonUnit = leastCommonUnit(this, other);
418     try {
419       return this.getChecked(leastCommonUnit) == other.getChecked(leastCommonUnit);
420     } catch (ArithmeticException e) {
421       // In case of overflow, both values cannot be the same.
422       return false;
423     }
424   }
425
426   @Override
427   public int hashCode() {
428     // Need to use a fixed unit here to be consistent with equals:
429     // 60s and 1min need to have the same hashCode.
430     // Saturation is ok, all really large values just have the same hash code.
431     return Longs.hashCode(getSaturated(NANOSECONDS));
432   }
433
434   @Override
435   public int compareTo(TimeSpan other) {
436     if (this.unit == other.unit) {
437       return Long.compare(this.span, other.span);
438     }
439     TimeUnit leastCommonUnit = leastCommonUnit(this, other);
440     try {
441       return Long.compare(this.getChecked(leastCommonUnit), other.getChecked(leastCommonUnit));
442     } catch (ArithmeticException e) {
443       // Only one of the two calls can overflow,
444       // and it has to be the one with the larger unit.
445       // Thus in case of overflow the TimeSpan with the larger unit also has the larger value.
446       return this.unit.compareTo(other.unit);
447     }
448   }
449
450   private static TimeUnit leastCommonUnit(TimeSpan a, TimeSpan b) {
451     return Ordering.natural().min(a.unit, b.unit);
452   }
453
454   @Override
455   public String toString() {
456     return DEFAULT_FORMAT.apply(this);
457   }
458
459   /**
460    * Create a new time span that is the sum of two time spans. The unit of the returned time span is
461    * the more precise one if possible, otherwise the closest unit that still allows to hold both
462    * input values and the result. Note that this can loose precision when adding a very large and a
463    * very small value.
464    *
465    * @throws ArithmeticException If no unit is large enough to represent the result value.
466    */
467   public static TimeSpan sum(TimeSpan a, TimeSpan b) {
468     TimeUnit leastCommonUnit = leastCommonUnit(a, b);
469     while (true) {
470       try {
471         return new TimeSpan(
472             LongMath.checkedAdd(a.getChecked(leastCommonUnit), b.getChecked(leastCommonUnit)),
473             leastCommonUnit);
474       } catch (ArithmeticException e) {
475         // Overflow is expected to be very rare, thus handle exception case instead of checking.
476         // Try again with next unit.
477         leastCommonUnit = ALL_UNITS.higher(leastCommonUnit);
478         if (leastCommonUnit == null) {
479           // overflow from addition
480           throw e;
481         }
482       }
483     }
484   }
485
486   /**
487    * Create a new time span that is the sum of several time spans. The unit of the returned time
488    * span is the most precise one if possible, otherwise the closest unit that still allows to hold
489    * input values and the result. Note that this can loose precision when adding very large and very
490    * small values.
491    *
492    * @throws ArithmeticException If no unit is large enough to represent the result value.
493    */
494   public static TimeSpan sum(Iterable<TimeSpan> timeSpans) {
495     Iterator<TimeSpan> it = timeSpans.iterator();
496     checkArgument(it.hasNext());
497
498     TimeSpan result = it.next();
499     // TODO Summing in loop looses more precision than necessary.
500     while (it.hasNext()) {
501       result = sum(result, it.next());
502     }
503     return result;
504   }
505
506   /**
507    * Create a new time span that is the sum of several time spans. The unit of the returned time
508    * span is the most precise one.
509    */
510   public static TimeSpan sum(TimeSpan... t) {
511     return sum(Arrays.asList(t));
512   }
513
514   /**
515    * Create a new time span that is the difference of two time spans. The unit of the returned time
516    * span is the more precise one if possible, otherwise the closest unit that still allows to hold
517    * both input values and the result. Note that this can loose precision when subtracting a very
518    * large and a very small value.
519    */
520   public static TimeSpan difference(TimeSpan a, TimeSpan b) {
521     TimeUnit leastCommonUnit = leastCommonUnit(a, b);
522     while (true) {
523       try {
524         return new TimeSpan(
525             LongMath.checkedSubtract(a.getChecked(leastCommonUnit), b.getChecked(leastCommonUnit)),
526             leastCommonUnit);
527       } catch (ArithmeticException e) {
528         // Overflow is expected to be very rare, thus handle exception case instead of checking.
529         // Try again with next unit.
530         leastCommonUnit = ALL_UNITS.higher(leastCommonUnit);
531         if (leastCommonUnit == null) {
532           // overflow from subtraction
533           throw e;
534         }
535       }
536     }
537   }
538
539   /**
540    * Create a new time span that is the current one multiplied by a non-negative integral factor.
541    * The unit of the returned time span is the same as the current one if possible, otherwise the
542    * closest unit that still allows to the result. Note that this can loose precision.
543    */
544   @CheckReturnValue
545   public TimeSpan multiply(int factor) {
546     checkArgument(factor >= 0, "Cannot multiply TimeSpan with negative value %s", factor);
547     TimeUnit dest = unit;
548     while (true) {
549       try {
550         return new TimeSpan(LongMath.checkedMultiply(getChecked(dest), factor), dest);
551       } catch (ArithmeticException e) {
552         // Overflow is expected to be very rare, thus handle exception case instead of checking.
553         // Try again with next unit.
554         dest = ALL_UNITS.higher(dest);
555         if (dest == null) {
556           // overflow from multiplication
557           throw e;
558         }
559       }
560     }
561   }
562
563   /**
564    * Create a new time span that is the current one divided by a non-negative integral value. The
565    * result of the division is rounded down (integer division). The unit of the returned time span
566    * is the same as the current one.
567    */
568   @CheckReturnValue
569   public TimeSpan divide(int divisor) {
570     checkArgument(divisor >= 0, "Cannot divide TimeSpan by negative value %s", divisor);
571     return new TimeSpan(span / divisor, unit);
572   }
573
574   // Code for formatting as string
575
576   private static final Function<TimeSpan, String> FORMAT_SIMPLE =
577       pInput -> pInput.span + TIME_UNITS.get(pInput.unit);
578
579   @VisibleForTesting
580   static final Function<TimeSpan, String> FORMAT_HUMAN_READABLE_LARGE =
581       pInput -> {
582         TimeUnit unit = pInput.getUnit();
583         StringBuilder result = new StringBuilder();
584         boolean started = false;
585
586         long years = pInput.getChecked(DAYS) / 365;
587         if (years > 0) {
588           started = true;
589           result.append(years).append("a ");
590         }
591
592         long days = pInput.getChecked(DAYS) - years * 365;
593         if (started || days > 0) {
594           started = true;
595           result.append(days).append("d ");
596         }
597         if (unit.equals(DAYS)) {
598           return result.toString().trim();
599         }
600
601         long hours = pInput.getChecked(HOURS) - years * 365 * 24 - days * 24;
602         if (started || hours > 0) {
603           started = true;
604           result.append(String.format("%02dh ", hours));
605         }
606         if (unit.equals(HOURS)) {
607           return result.toString().trim();
608         }
609
610         long minutes =
611             pInput.getChecked(MINUTES) - years * 365 * 24 * 60 - days * 24 * 60 - hours * 60;
612         if (started || minutes > 0) {
613           result.append(String.format("%02dmin ", minutes));
614         }
615         if (unit.equals(MINUTES)) {
616           started = true;
617           return result.toString().trim();
618         }
619
620         long seconds =
621             pInput.getChecked(SECONDS)
622                 - years * 365 * 24 * 60 * 60
623                 - days * 24 * 60 * 60
624                 - hours * 60 * 60
625                 - minutes * 60;
626         result.append(String.format("%02ds", seconds));
627
628         return result.toString();
629       };
630
631   private static final String DEFAULT_FORMAT_PROPERTY_NAME =
632       TimeSpan.class.getCanonicalName() + ".defaultFormat";
633
634   private static final Function<TimeSpan, String> DEFAULT_FORMAT;
635
636   static {
637     String format =
638         Ascii.toUpperCase(System.getProperty(DEFAULT_FORMAT_PROPERTY_NAME, "SIMPLE").trim());
639     switch (format) {
640       case "HUMAN_READABLE_LARGE":
641         DEFAULT_FORMAT = FORMAT_HUMAN_READABLE_LARGE;
642         break;
643       case "SIMPLE":
644         DEFAULT_FORMAT = FORMAT_SIMPLE;
645         break;
646       default:
647         DEFAULT_FORMAT = FORMAT_SIMPLE;
648     }
649   }
650 }