package com.supaham.commons.relatives;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.supaham.commons.utils.DurationUtils;
import com.supaham.commons.utils.TimeUtils;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Duration;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import pluginbase.config.annotation.SerializeWith;
/**
* Represents a class for relativity with {@link Duration}s. This feature and class should not be used in a
* predictable environment. To clarify, this class is only meant to be used when some maths is to be done by the work
* of some external data, such as a configuration file. If your program functionality is strictly kept inside the
* program please resort to {@link Duration}'s arithmetic methods for your operations as it may be more convenient for
* both the programmer and the JVM.
*
* <p />
* The {@link #isRelative()} feature is provided for ease of use in general cases. If the user wishes to have true
* relativity they will have to check the boolean themselves and handle it from there. For more information see {@link
* #isRelative()}.
*
* @see ArithmeticOperator
* @see RelativeNumberSerializer
*/
@SerializeWith(RelativeDurationSerializer.class)
public final class RelativeDuration implements Function<Duration, Duration> {
private static final Pattern NO_SPECIFIED_UNIT = Pattern.compile(TimeUtils.PATTERN.pattern() + "?");
public static final RelativeDuration ZERO = from(ArithmeticOperator.ADDITION, Duration.ZERO);
private final ArithmeticOperator operator;
private final Duration duration;
private final boolean relative;
/**
* Deserialization in the form of {@link #toString()}. It is important to note that the duration must be compatible
* with SupaCommon's format in {@link TimeUtils#parseDurationMs(CharSequence)}. The following table shows the valid
* and invalid serialized forms:
* <table>
* <thead>
* <tr>
* <th>Valid</th>
* <th>Invalid</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td>1d</td>
* <td>1dasd</td>
* </tr>
*
* <tr>
* <td>-1d</td>
* <td>-1dasd</td>
* </tr>
*
* <tr>
* <td>~1d</td>
* <td>~1dasd</td>
* </tr>
*
* <tr>
* <td>~+1d</td>
* <td>~1d+</td>
* </tr>
*
* <tr>
* <td>~-1d</td>
* <td>~1d-</td>
* </tr>
*
* <tr>
* <td>~*1d</td>
* <td>~1d*</td>
* </tr>
*
* <tr>
* <td>~/1d</td>
* <td>~1d/</td>
* </tr>
*
* <tr>
* <td>~%1d</td>
* <td>~1d%</td>
* </tr>
*
* <tr>
* <td>~^1d</td>
* <td>~1d^</td>
* </tr>
* </tbody>
* </table>
*
* @param string string to deserialize
*
* @return deserialized string in the form of {@link RelativeDuration}
*/
public static RelativeDuration fromString(@Nonnull String string) {
Preconditions.checkNotNull(string, "string cannot be null.");
boolean relative = false;
Duration duration = null;
ArithmeticOperator operator;
string = string.trim();
// Make sure the string isn't just "" or "~".
if (string.isEmpty() || string.equals("~")) {
return ZERO;
}
if (string.startsWith("~")) {
relative = true;
string = string.substring(1).trim();
}
if (Character.isDigit(string.charAt(0))) {
operator = ArithmeticOperator.ADDITION;
} else {
operator = ArithmeticOperator.fromChar(string.charAt(0));
// If the string is something like -123, make sure we pass the number as -123 and not 123 with subtraction
// operator. This makes Relativity with deserialized objects work as expected.
if (operator == ArithmeticOperator.SUBTRACTION && string.charAt(1) != '-') {
operator = ArithmeticOperator.ADDITION;
} else {
string = string.substring(1).trim();
}
}
Matcher matcher = NO_SPECIFIED_UNIT.matcher(string);
if (matcher.find()) {
if (matcher.group(2) == null) {
duration = Duration.ofSeconds((long) Double.parseDouble(matcher.group(1)));
}
}
if (duration == null) {
duration = DurationUtils.parseDuration(string);
}
Preconditions.checkNotNull(operator, "operator cannot be null.");
return new RelativeDuration(operator, duration, relative);
}
public static RelativeDuration from(@Nonnull ArithmeticOperator operator, @Nonnull Duration duration) {
Preconditions.checkNotNull(duration, "duration cannot be null.");
return new RelativeDuration(operator, duration);
}
/**
* Applies a {@link Duration} to this {@link RelativeDuration} for mathematical operation based on {@link
* #getOperator()}. If the given {@code duration} is null, {@link #getDuration()} is returned.
*
* @param duration duration to apply to the operator, nullable
*
* @return result of the operation
*/
@Override public Duration apply(Duration duration) throws ArithmeticException {
if (duration == null) {
return this.duration;
}
double d1 = BigDecimal.valueOf(this.duration.getSeconds())
.add(BigDecimal.valueOf(this.duration.getNano(), 9)).doubleValue();
double d2 = BigDecimal.valueOf(duration.getSeconds())
.add(BigDecimal.valueOf(duration.getNano(), 9)).doubleValue();
double result = this.operator.applyToDouble(d1, d2);
if (result > Long.MAX_VALUE) {
throw new ArithmeticException("duration of " + result + " is too large.");
}
BigDecimal bigDecimal = BigDecimal.valueOf(result);
BigInteger nanos = bigDecimal.movePointRight(9).toBigInteger();
BigInteger[] divRem = nanos.divideAndRemainder(TimeUtils.BI_NANOS_PER_SECOND);
return Duration.ofSeconds(divRem[0].longValue(), divRem[1].intValue());
}
private RelativeDuration(ArithmeticOperator operator, Duration duration) {
this(operator, duration, true);
}
private RelativeDuration(ArithmeticOperator operator, Duration duration, boolean relative) {
this.operator = operator;
this.duration = duration;
this.relative = relative;
}
@Override public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
RelativeDuration that = (RelativeDuration) object;
return relative == that.relative &&
operator == that.operator &&
Objects.equal(duration, that.duration);
}
@Override public int hashCode() {
return Objects.hashCode(operator, duration, relative);
}
/**
* Serializes this object as "~[op]duration". Where ~ is provided if {@link #isRelative()}, [op] is only provided if
* the {@link #getOperator()} is not {@link ArithmeticOperator#ADDITION}, duration is provided at all times.
*
* @return serialized relative duration
*/
@Override public String toString() {
StringBuilder sb = new StringBuilder();
if (this.relative) {
sb.append("~");
}
// We automatically interpret number only as ADDITION in deserialization, so omit it from serialization.
if (this.operator != ArithmeticOperator.ADDITION) {
sb.append(this.operator.getChar());
}
sb.append(DurationUtils.toString(this.duration, true));
return sb.toString();
}
public ArithmeticOperator getOperator() {
return operator;
}
public Duration getDuration() {
return duration;
}
/**
* Returns whether this {@link RelativeDuration} is truly of relative nature. The only case where this returns false
* is if this instance was deserialized as a whole number via {@link #fromString(String)}, e.g. "123".
*/
public boolean isRelative() {
return relative;
}
}