Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-520 Fix duration parser. Add an option to round values. #520

Merged
merged 2 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.Set;

public class DurationParser extends TemporalAmountParser<Duration> {

Expand All @@ -26,20 +27,24 @@ public class DurationParser extends TemporalAmountParser<Duration> {
.withUnit("y", ChronoUnit.YEARS);

public DurationParser() {
super(LocalDateTimeProvider.now());
super(ChronoUnit.SECONDS, LocalDateTimeProvider.now());
}

public DurationParser(LocalDateTimeProvider localDateTimeProvider) {
super(localDateTimeProvider);
public DurationParser(ChronoUnit defaultZero) {
super(defaultZero, LocalDateTimeProvider.now());
}

private DurationParser(Map<String, ChronoUnit> units, LocalDateTimeProvider baseForTimeEstimation) {
super(units, baseForTimeEstimation);
public DurationParser(ChronoUnit defaultZero, LocalDateTimeProvider localDateTimeProvider) {
super(defaultZero, localDateTimeProvider);
}

private DurationParser(ChronoUnit defaultZero, Map<String, ChronoUnit> units, Set<TimeModifier> modifiers, LocalDateTimeProvider baseForTimeEstimation) {
super(defaultZero, units, modifiers, baseForTimeEstimation);
}

@Override
protected TemporalAmountParser<Duration> clone(Map<String, ChronoUnit> units, LocalDateTimeProvider baseForTimeEstimation) {
return new DurationParser(units, baseForTimeEstimation);
protected TemporalAmountParser<Duration> clone(ChronoUnit defaultZero, Map<String, ChronoUnit> units, Set<TimeModifier> modifiers, LocalDateTimeProvider baseForTimeEstimation) {
return new DurationParser(defaultZero, units, modifiers, baseForTimeEstimation);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.Set;

public class PeriodParser extends TemporalAmountParser<Period> {

Expand All @@ -15,20 +16,29 @@ public class PeriodParser extends TemporalAmountParser<Period> {
.withUnit("y", ChronoUnit.YEARS);

public PeriodParser() {
super(LocalDateTimeProvider.now());
super(ChronoUnit.DAYS, LocalDateTimeProvider.now());
}

public PeriodParser(ChronoUnit defaultZero) {
super(defaultZero, LocalDateTimeProvider.now());
}

public PeriodParser(LocalDateTimeProvider baseForTimeEstimation) {
super(baseForTimeEstimation);
super(ChronoUnit.DAYS, baseForTimeEstimation);
}


public PeriodParser(ChronoUnit defaultZero, LocalDateTimeProvider baseForTimeEstimation) {
super(defaultZero, baseForTimeEstimation);
}

private PeriodParser(Map<String, ChronoUnit> units, LocalDateTimeProvider baseForTimeEstimation) {
super(units, baseForTimeEstimation);
private PeriodParser(ChronoUnit defaultZero, Map<String, ChronoUnit> units, Set<TimeModifier> modifiers, LocalDateTimeProvider baseForTimeEstimation) {
super(defaultZero, units, modifiers, baseForTimeEstimation);
}

@Override
protected TemporalAmountParser<Period> clone(Map<String, ChronoUnit> units, LocalDateTimeProvider baseForTimeEstimation) {
return new PeriodParser(units, baseForTimeEstimation);
protected TemporalAmountParser<Period> clone(ChronoUnit defaultZero, Map<String, ChronoUnit> units, Set<TimeModifier> modifiers, LocalDateTimeProvider baseForTimeEstimation) {
return new PeriodParser(defaultZero, units, modifiers, baseForTimeEstimation);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package dev.rollczi.litecommands.time;

import dev.rollczi.litecommands.shared.Lazy;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
Expand All @@ -9,9 +11,11 @@
import java.time.temporal.TemporalAmount;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;

/**
Expand All @@ -38,44 +42,44 @@
*/
public abstract class TemporalAmountParser<T extends TemporalAmount> {

private static final Map<ChronoUnit, Long> UNIT_TO_NANO = new LinkedHashMap<>();
private static final Map<ChronoUnit, Integer> PART_TIME_UNITS = new LinkedHashMap<>();
private static final Map<ChronoUnit, BigInteger> UNIT_TO_NANO = new LinkedHashMap<>();

static {
UNIT_TO_NANO.put(ChronoUnit.NANOS, 1L);
UNIT_TO_NANO.put(ChronoUnit.MICROS, 1_000L);
UNIT_TO_NANO.put(ChronoUnit.MILLIS, 1_000_000L);
UNIT_TO_NANO.put(ChronoUnit.SECONDS, 1_000_000_000L);
UNIT_TO_NANO.put(ChronoUnit.MINUTES, 60 * 1_000_000_000L);
UNIT_TO_NANO.put(ChronoUnit.HOURS, 60 * 60 * 1_000_000_000L);
UNIT_TO_NANO.put(ChronoUnit.DAYS, 24 * 60 * 60 * 1_000_000_000L);
UNIT_TO_NANO.put(ChronoUnit.WEEKS, 7 * 24 * 60 * 60 * 1_000_000_000L);
UNIT_TO_NANO.put(ChronoUnit.MONTHS, 30 * 24 * 60 * 60 * 1_000_000_000L);
UNIT_TO_NANO.put(ChronoUnit.YEARS, 365 * 24 * 60 * 60 * 1_000_000_000L);
UNIT_TO_NANO.put(ChronoUnit.DECADES, 10 * 365 * 24 * 60 * 60 * 1_000_000_000L);

PART_TIME_UNITS.put(ChronoUnit.NANOS, 1000);
PART_TIME_UNITS.put(ChronoUnit.MICROS, 1000);
PART_TIME_UNITS.put(ChronoUnit.MILLIS, 1000);
PART_TIME_UNITS.put(ChronoUnit.SECONDS, 60);
PART_TIME_UNITS.put(ChronoUnit.MINUTES, 60);
PART_TIME_UNITS.put(ChronoUnit.HOURS, 24);
PART_TIME_UNITS.put(ChronoUnit.DAYS, 7);
PART_TIME_UNITS.put(ChronoUnit.WEEKS, 4);
PART_TIME_UNITS.put(ChronoUnit.MONTHS, 12);
PART_TIME_UNITS.put(ChronoUnit.YEARS, Integer.MAX_VALUE);
UNIT_TO_NANO.put(ChronoUnit.NANOS, BigInteger.valueOf(1L));
UNIT_TO_NANO.put(ChronoUnit.MICROS, BigInteger.valueOf(1_000L));
UNIT_TO_NANO.put(ChronoUnit.MILLIS, BigInteger.valueOf(1_000_000L));
UNIT_TO_NANO.put(ChronoUnit.SECONDS, BigInteger.valueOf(1_000_000_000L));
UNIT_TO_NANO.put(ChronoUnit.MINUTES, BigInteger.valueOf(60 * 1_000_000_000L));
UNIT_TO_NANO.put(ChronoUnit.HOURS, BigInteger.valueOf(60 * 60 * 1_000_000_000L));
UNIT_TO_NANO.put(ChronoUnit.DAYS, BigInteger.valueOf(24 * 60 * 60 * 1_000_000_000L));
UNIT_TO_NANO.put(ChronoUnit.WEEKS, BigInteger.valueOf(7 * 24 * 60 * 60 * 1_000_000_000L));
UNIT_TO_NANO.put(ChronoUnit.MONTHS, BigInteger.valueOf(30 * 24 * 60 * 60 * 1_000_000_000L));
UNIT_TO_NANO.put(ChronoUnit.YEARS, BigInteger.valueOf(365 * 24 * 60 * 60 * 1_000_000_000L));
UNIT_TO_NANO.put(ChronoUnit.DECADES, BigInteger.valueOf(10 * 365 * 24 * 60 * 60 * 1_000_000_000L));
}

private final ChronoUnit defaultZero;
private final Lazy<String> defaultZeroSymbol;
private final Map<String, ChronoUnit> units = new LinkedHashMap<>();
private final Set<TimeModifier> modifiers = new HashSet<>();
private final LocalDateTimeProvider baseForTimeEstimation;

protected TemporalAmountParser(LocalDateTimeProvider baseForTimeEstimation) {
this.baseForTimeEstimation = baseForTimeEstimation;
protected TemporalAmountParser(ChronoUnit defaultZero, Map<String, ChronoUnit> units, Set<TimeModifier> modifiers, LocalDateTimeProvider baseForTimeEstimation) {
this(defaultZero, baseForTimeEstimation);
this.units.putAll(units);
this.modifiers.addAll(modifiers);
}

protected TemporalAmountParser(Map<String, ChronoUnit> units, LocalDateTimeProvider baseForTimeEstimation) {
protected TemporalAmountParser(ChronoUnit defaultZero, LocalDateTimeProvider baseForTimeEstimation) {
this.defaultZero = defaultZero;
this.baseForTimeEstimation = baseForTimeEstimation;
this.units.putAll(units);
this.defaultZeroSymbol = new Lazy<>(() -> this.units.entrySet()
.stream()
.filter(entry -> entry.getValue() == defaultZero)
.map(entry -> entry.getKey())
.findFirst()
.orElseThrow(() -> new IllegalStateException("Can not find default zero symbol for " + defaultZero))
);
}

public TemporalAmountParser<T> withUnit(String symbol, ChronoUnit chronoUnit) {
Expand All @@ -89,14 +93,61 @@ public TemporalAmountParser<T> withUnit(String symbol, ChronoUnit chronoUnit) {

Map<String, ChronoUnit> newUnits = new LinkedHashMap<>(this.units);
newUnits.put(symbol, chronoUnit);
return clone(newUnits, this.baseForTimeEstimation);
return clone(this.defaultZero, newUnits, this.modifiers, this.baseForTimeEstimation);
}

public TemporalAmountParser<T> withLocalDateTimeProvider(LocalDateTimeProvider baseForTimeEstimation) {
return clone(this.units, baseForTimeEstimation);
return clone(this.defaultZero, this.units, this.modifiers, baseForTimeEstimation);
}

public TemporalAmountParser<T> withDefaultZero(ChronoUnit defaultZero) {
return clone(defaultZero, this.units, this.modifiers, this.baseForTimeEstimation);
}

public TemporalAmountParser<T> withRounded(ChronoUnit unit, RoundingMode roundingMode) {
return withTimeModifier(duration -> {
BigInteger nanosInUnit = UNIT_TO_NANO.get(unit);
BigInteger nanos = durationToNano(duration);
BigInteger rounded = round(roundingMode, nanos, nanosInUnit);

return Duration.ofNanos(rounded.longValue());
});
}

private static BigInteger round(RoundingMode roundingMode, BigInteger nanos, BigInteger nanosInUnit) {
BigInteger remainder = nanos.remainder(nanosInUnit);
BigInteger subtract = nanos.subtract(remainder);
BigInteger add = subtract.add(nanosInUnit);

BigInteger roundedUp = remainder.equals(BigInteger.ZERO) ? nanos : (nanos.signum() > 0 ? add : subtract.subtract(nanosInUnit));
BigInteger roundedDown = remainder.equals(BigInteger.ZERO) ? nanos : (nanos.signum() > 0 ? subtract : add.subtract(nanosInUnit));

int compare = remainder.abs().multiply(BigInteger.valueOf(2)).compareTo(nanosInUnit);
switch (roundingMode) {
case UP:
return roundedUp;
case DOWN:
return roundedDown;
case CEILING:
return nanos.signum() >= 0 ? roundedUp : roundedDown;
case FLOOR:
return nanos.signum() >= 0 ? roundedDown : roundedUp;
case HALF_UP:
return compare >= 0 ? roundedUp : roundedDown;
case HALF_DOWN:
return (compare > 0) ? roundedUp : roundedDown;
default: throw new IllegalArgumentException("Unsupported rounding mode " + roundingMode);
}
}

protected abstract TemporalAmountParser<T> clone(Map<String, ChronoUnit> units, LocalDateTimeProvider baseForTimeEstimation);

private TemporalAmountParser<T> withTimeModifier(TimeModifier modifier) {
Set<TimeModifier> newRoundedUnits = new HashSet<>(this.modifiers);
newRoundedUnits.add(modifier);
return clone(this.defaultZero, this.units, newRoundedUnits, this.baseForTimeEstimation);
}

protected abstract TemporalAmountParser<T> clone(ChronoUnit defaultZeroUnit, Map<String, ChronoUnit> units, Set<TimeModifier> modifiers, LocalDateTimeProvider baseForTimeEstimation);

private boolean validCharacters(String content, Predicate<Character> predicate) {
for (int i = 0; i < content.length(); i++) {
Expand Down Expand Up @@ -265,6 +316,9 @@ public ChronoUnit getUnit() {
public String format(T temporalAmount) {
StringBuilder builder = new StringBuilder();
Duration duration = this.toDuration(this.baseForTimeEstimation, temporalAmount);
for (TimeModifier modifier : this.modifiers) {
duration = modifier.modify(duration);
}

if (duration.isNegative()) {
builder.append('-');
Expand All @@ -275,26 +329,34 @@ public String format(T temporalAmount) {
Collections.reverse(keys);

for (String key : keys) {
ChronoUnit chronoUnit = this.units.get(key);
Long part = UNIT_TO_NANO.get(chronoUnit);
ChronoUnit unit = this.units.get(key);
BigInteger nanosInOneUnit = UNIT_TO_NANO.get(unit);

if (part == null) {
throw new IllegalArgumentException("Unsupported unit " + chronoUnit);
if (nanosInOneUnit == null) {
throw new IllegalArgumentException("Unsupported unit " + unit);
}

BigInteger currentCount = this.durationToNano(duration).divide(BigInteger.valueOf(part));
BigInteger maxCount = BigInteger.valueOf(PART_TIME_UNITS.get(chronoUnit));
BigInteger count = currentCount.equals(maxCount) ? BigInteger.ONE : currentCount.mod(maxCount);
BigInteger nanosCount = this.durationToNano(duration);
BigInteger count = nanosCount.divide(nanosInOneUnit);

if (count.equals(BigInteger.ZERO)) {
continue;
}

BigInteger nanosCountCleared = count.multiply(nanosInOneUnit);

builder.append(count).append(key);
duration = duration.minusNanos(count.longValue() * part);
duration = duration.minusNanos(nanosCountCleared.longValue());
}

return builder.toString();
String result = builder.toString();

if (result.isEmpty()) {
String defaultSymbol = this.defaultZeroSymbol.get();
return "0" + defaultSymbol;
}

return result;
}

protected abstract Duration toDuration(LocalDateTimeProvider baseForTimeEstimation, T temporalAmount);
Expand Down Expand Up @@ -325,8 +387,12 @@ static LocalDateTimeProvider of(LocalDate localDate) {
}

}

protected interface TimeModifier {
Duration modify(Duration duration);
}

BigInteger durationToNano(Duration duration) {
private BigInteger durationToNano(Duration duration) {
return BigInteger.valueOf(duration.getSeconds())
.multiply(BigInteger.valueOf(1_000_000_000))
.add(BigInteger.valueOf(duration.getNano()));
Expand Down
Loading
Loading