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
10 * http://www.apache.org/licenses/LICENSE-2.0
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
16 * ============LICENSE_END==========================================================================
17 ******************************************************************************/
19 * SoSy-Lab Common is a library of useful utilities.
20 * This file is part of SoSy-Lab Common.
22 * Copyright (C) 2007-2015 Dirk Beyer
23 * All rights reserved.
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
29 * http://www.apache.org/licenses/LICENSE-2.0
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.
37 package org.onap.ccsdk.features.sdnr.wt.devicemanager.base.internalTypes;
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;
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;
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;
72 * This is an immutable representation of some time span, using a {@link TimeUnit} and a value.
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.
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.
81 public final class TimeSpan implements Comparable<TimeSpan>, Serializable {
83 private static final long serialVersionUID = -4013592312989551009L;
85 private static final ImmutableSortedSet<TimeUnit> ALL_UNITS =
86 ImmutableSortedSet.copyOf(EnumSet.allOf(TimeUnit.class));
89 assert ALL_UNITS.higher(SECONDS).equals(MINUTES); // assert expected order of set
92 private static final EnumHashBiMap<TimeUnit, String> TIME_UNITS =
93 EnumHashBiMap.create(TimeUnit.class);
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");
105 private static final Pattern ONLY_DIGITS = Pattern.compile(" *([0-9]+) *");
107 private enum CharType {
115 private final long span;
116 private final TimeUnit unit;
118 private TimeSpan(long pSpan, TimeUnit pUnit) {
120 unit = checkNotNull(pUnit);
123 public static TimeSpan of(long pSpan, TimeUnit pUnit) {
124 return new TimeSpan(pSpan, pUnit);
127 public static TimeSpan ofSeconds(long pSeconds) {
128 return new TimeSpan(pSeconds, SECONDS);
131 public static TimeSpan ofMillis(long pMillis) {
132 return new TimeSpan(pMillis, MILLISECONDS);
135 public static TimeSpan ofNanos(long pNanos) {
136 return new TimeSpan(pNanos, NANOSECONDS);
139 public static TimeSpan empty() {
140 return new TimeSpan(0, DAYS);
144 * Converts the given {@link String} into a {@link TimeSpan} object. Supported units are day,
145 * hour, minute and second.
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
152 public static TimeSpan valueOf(String input) {
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)));
160 // values with units: more elaborate parsing necessary
161 List<String> tokens = splitIntoTokens(input);
168 Iterator<String> it = tokens.iterator();
170 while (it.hasNext()) {
172 String nextString = it.next();
173 long value = Long.parseLong(nextString);
177 throw new IllegalArgumentException("Value " + nextString + " has no unit.");
180 String unit = it.next();
186 throw new IllegalArgumentException("Days set twice: " + unit);
195 throw new IllegalArgumentException("Hours set twice: " + unit);
203 throw new IllegalArgumentException("Minutes set twice: " + unit);
210 throw new IllegalArgumentException("Seconds set twice: " + unit);
216 throw new IllegalArgumentException("Unknown unit: " + unit);
220 return sum(of(seconds, SECONDS), of(minutes, MINUTES), of(hours, HOURS), of(days, DAYS));
223 private static List<String> splitIntoTokens(String input) {
224 List<String> tokens = Lists.newArrayList();
225 CharType previous = CharType.BEGIN;
228 for (int i = 0; i <= input.length(); ++i) {
231 if (i == input.length()) {
232 current = CharType.END;
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;
242 throw new IllegalArgumentException(
243 "Unreconized character '" + currentChar + "' when parsing " + input);
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));
253 if (current == CharType.LETTER || current == CharType.DIGIT) {
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.
268 * @throws ArithmeticException If the value cannot be represented in the given unit due to
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);
276 return LongMath.checkedMultiply(span, factor);
279 // Example case: we have nanoseconds, but we want seconds (cannot overflow)
280 return dest.convert(span, unit);
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.
288 public long getSaturated(TimeUnit dest) {
289 return dest.convert(span, unit);
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.
296 * @throws ArithmeticException If the value cannot be represented in the given unit
298 public TimeSpan toChecked(TimeUnit dest) {
299 if (dest.equals(unit)) {
302 return new TimeSpan(getChecked(dest), dest);
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
311 public TimeSpan toSaturated(TimeUnit dest) {
312 if (dest.equals(unit)) {
315 return new TimeSpan(getSaturated(dest), dest);
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
326 TimeSpan toIfPossible(TimeUnit dest) {
327 if (dest.equals(unit)) {
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.
337 return toChecked(dest);
338 } catch (ArithmeticException e) {
339 dest = checkNotNull(ALL_UNITS.higher(dest));
344 // Example case: we have nanoseconds, but we want seconds (cannot overflow).
345 return new TimeSpan(getSaturated(dest), dest);
350 * Get the value of this TimeSpan as seconds. If the current unit is smaller than seconds,
351 * precision may be lost.
353 * @throws ArithmeticException If the value cannot be represented as seconds due to overflow.
355 public long asSeconds() {
356 return getChecked(SECONDS);
360 * Get the value of this TimeSpan as milliseconds. If the current unit is smaller than
361 * milliseconds, precision may be lost.
363 * @throws ArithmeticException If the value cannot be represented as milliseconds due to overflow.
365 public long asMillis() {
366 return getChecked(MILLISECONDS);
370 * Get the value of this TimeSpan as nanoseconds.
372 * @throws ArithmeticException If the value cannot be represented as milliseconds due to overflow.
374 public long asNanos() {
375 return getChecked(NANOSECONDS);
378 public TimeUnit getUnit() {
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.
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();
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));
400 /** Check whether this time span is empty, i.e., represents 0ns (or 0ms or 0s or ...). */
401 public boolean isEmpty() {
406 public boolean equals(@Nullable Object obj) {
410 if (!(obj instanceof TimeSpan)) {
413 TimeSpan other = (TimeSpan) obj;
414 if (this.unit == other.unit) {
415 return this.span == other.span;
417 TimeUnit leastCommonUnit = leastCommonUnit(this, other);
419 return this.getChecked(leastCommonUnit) == other.getChecked(leastCommonUnit);
420 } catch (ArithmeticException e) {
421 // In case of overflow, both values cannot be the same.
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));
435 public int compareTo(TimeSpan other) {
436 if (this.unit == other.unit) {
437 return Long.compare(this.span, other.span);
439 TimeUnit leastCommonUnit = leastCommonUnit(this, other);
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);
450 private static TimeUnit leastCommonUnit(TimeSpan a, TimeSpan b) {
451 return Ordering.natural().min(a.unit, b.unit);
455 public String toString() {
456 return DEFAULT_FORMAT.apply(this);
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
465 * @throws ArithmeticException If no unit is large enough to represent the result value.
467 public static TimeSpan sum(TimeSpan a, TimeSpan b) {
468 TimeUnit leastCommonUnit = leastCommonUnit(a, b);
472 LongMath.checkedAdd(a.getChecked(leastCommonUnit), b.getChecked(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
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
492 * @throws ArithmeticException If no unit is large enough to represent the result value.
494 public static TimeSpan sum(Iterable<TimeSpan> timeSpans) {
495 Iterator<TimeSpan> it = timeSpans.iterator();
496 checkArgument(it.hasNext());
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());
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.
510 public static TimeSpan sum(TimeSpan... t) {
511 return sum(Arrays.asList(t));
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.
520 public static TimeSpan difference(TimeSpan a, TimeSpan b) {
521 TimeUnit leastCommonUnit = leastCommonUnit(a, b);
525 LongMath.checkedSubtract(a.getChecked(leastCommonUnit), b.getChecked(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
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.
545 public TimeSpan multiply(int factor) {
546 checkArgument(factor >= 0, "Cannot multiply TimeSpan with negative value %s", factor);
547 TimeUnit dest = unit;
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);
556 // overflow from multiplication
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.
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);
574 // Code for formatting as string
576 private static final Function<TimeSpan, String> FORMAT_SIMPLE =
577 pInput -> pInput.span + TIME_UNITS.get(pInput.unit);
580 static final Function<TimeSpan, String> FORMAT_HUMAN_READABLE_LARGE =
582 TimeUnit unit = pInput.getUnit();
583 StringBuilder result = new StringBuilder();
584 boolean started = false;
586 long years = pInput.getChecked(DAYS) / 365;
589 result.append(years).append("a ");
592 long days = pInput.getChecked(DAYS) - years * 365;
593 if (started || days > 0) {
595 result.append(days).append("d ");
597 if (unit.equals(DAYS)) {
598 return result.toString().trim();
601 long hours = pInput.getChecked(HOURS) - years * 365 * 24 - days * 24;
602 if (started || hours > 0) {
604 result.append(String.format("%02dh ", hours));
606 if (unit.equals(HOURS)) {
607 return result.toString().trim();
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));
615 if (unit.equals(MINUTES)) {
617 return result.toString().trim();
621 pInput.getChecked(SECONDS)
622 - years * 365 * 24 * 60 * 60
623 - days * 24 * 60 * 60
626 result.append(String.format("%02ds", seconds));
628 return result.toString();
631 private static final String DEFAULT_FORMAT_PROPERTY_NAME =
632 TimeSpan.class.getCanonicalName() + ".defaultFormat";
634 private static final Function<TimeSpan, String> DEFAULT_FORMAT;
638 Ascii.toUpperCase(System.getProperty(DEFAULT_FORMAT_PROPERTY_NAME, "SIMPLE").trim());
640 case "HUMAN_READABLE_LARGE":
641 DEFAULT_FORMAT = FORMAT_HUMAN_READABLE_LARGE;
644 DEFAULT_FORMAT = FORMAT_SIMPLE;
647 DEFAULT_FORMAT = FORMAT_SIMPLE;