Skip to content

Commit

Permalink
Document time formats
Browse files Browse the repository at this point in the history
  • Loading branch information
Matyrobbrt committed Aug 24, 2024
1 parent 20610d6 commit 0293478
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 4 deletions.
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ dependencies {
annotationProcessor group: 'com.google.auto.service', name: 'auto-service', version: '1.0.1'

compileOnly 'org.jetbrains:annotations:24.0.1'

testImplementation("org.junit.jupiter:junit-jupiter-api:$junit_version")
testImplementation("org.junit.jupiter:junit-jupiter-engine:$junit_version")
testImplementation('org.assertj:assertj-core:3.25.1')
}

jar {
Expand Down Expand Up @@ -175,6 +179,10 @@ shadowJar {
from(tasks.gitLog.output)
}

test {
useJUnitPlatform()
}

publishing {
publications {
mavenJava(MavenPublication) {
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export default {
text: 'Setting up Camelot',
link: '/get-started'
},
{
text: 'Formats',
link: '/formats'
},
{
text: 'Modules',
link: '/modules/',
Expand Down
34 changes: 34 additions & 0 deletions docs/docs/formats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Formats used by Camelot
This page explains various formats used by Camelot when displaying or parsing inputs (i.e. durations).

## Durations

Durations are composed of sequences of numbers and unit specifiers without a space in between. The number will multiply the specified unit.
Camelot supports the following units:
- `n`: nanoseconds
- `l`: milliseconds (1000000 nanoseconds)
- `s`: seconds (1000 milliseconds)
- `m`: minutes (60 seconds)
- `h`: hours (60 minutes)
- `d`: days (24 hours)
- `w`: weeks (7 days)
- `M`: months (30 days)
- `y`: years (365 days)

Unit specifiers are **case-sensitive** and unrecognized specifiers will be considered _minutes_.
::: info
While nanoseconds and milliseconds are supported by the parser, most commands will not support that level of precision and will round them to the next second.
:::

If a component of a duration is prefixed by a minus (`-`), it will be subtracted from the duration instead of added.
::: info
The format is not a maths equation. Parenthesis are not supported and the minus only applies to the first component after it.
`-12m4s` subtracts 12 minutes and *adds* 4 seconds, while `-12m-4s` subtracts 12 minutes and 4 seconds.
:::

### Example durations
- `12m45s`: 12 minutes and 45 seconds
- `2y4M`: 2 years (730 days) and 4 months (120 days); 850 days in total
- `1w2d`: one week and 2 days
- `1d4h25m2s`: 1 day, 4 hours, 25 minutes and 2 seconds
- `2w-4d`: 2 weeks minus 4 days; 10 days in total
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ j2html_version=1.6.0
angus_mail_version=2.0.1
json_version=20231013
groovy_version=4.0.21

junit_version=5.10.0
57 changes: 53 additions & 4 deletions src/main/java/net/neoforged/camelot/util/DateUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import javax.annotation.Nonnull;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalUnit;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -11,6 +12,9 @@
* Some utility methods for working with time.
*/
public class DateUtils {
private static final TemporalUnit MONTHS = exactDays(30);
private static final TemporalUnit YEARS = exactDays(365);

/**
* Formats the given {@code duration} to a human-readable string. <br>
* e.g. for 176893282 seconds, this method returns {@code 5 years 7 months 8 days 8 hours 31 minutes 40 seconds}.
Expand Down Expand Up @@ -56,7 +60,7 @@ private static List<String> splitInput(String str) {
var builder = new StringBuilder();
for (final var ch : str.toCharArray()) {
builder.append(ch);
if (!Character.isDigit(ch)) {
if (ch != '-' && !Character.isDigit(ch)) {
list.add(builder.toString());
builder = new StringBuilder();
}
Expand All @@ -71,7 +75,11 @@ public static Duration getDurationFromInput(String input) {
final List<String> data = splitInput(input);
Duration duration = Duration.ofSeconds(0);
for (final String dt : data) {
duration = duration.plusSeconds(decode(dt).toSeconds());
if (dt.charAt(0) == '-') {
duration = duration.minusSeconds(decode(dt.substring(1)).toSeconds());
} else {
duration = duration.plusSeconds(decode(dt).toSeconds());
}
}
return duration;
}
Expand All @@ -90,8 +98,8 @@ public static Duration decode(@Nonnull final String time) {
case 'h' -> ChronoUnit.HOURS;
case 'd' -> ChronoUnit.DAYS;
case 'w' -> ChronoUnit.WEEKS;
case 'M' -> ChronoUnit.MONTHS;
case 'y' -> ChronoUnit.YEARS;
case 'M' -> MONTHS;
case 'y' -> YEARS;
default -> ChronoUnit.MINUTES;
};
final long tm = Long.parseLong(time.substring(0, time.length() - 1));
Expand All @@ -105,4 +113,45 @@ public static Duration decode(@Nonnull final String time) {
public static Duration of(long time, TemporalUnit unit) {
return unit.isDurationEstimated() ? Duration.ofSeconds(time * unit.getDuration().getSeconds()) : Duration.of(time, unit);
}

private static TemporalUnit exactDays(long amount) {
var dur = Duration.ofDays(amount);
return new TemporalUnit() {
@Override
public Duration getDuration() {
return dur;
}

@Override
public boolean isDurationEstimated() {
return false;
}

@Override
public boolean isDateBased() {
return false;
}

@Override
public boolean isTimeBased() {
return false;
}

@Override
public boolean isSupportedBy(Temporal temporal) {
return temporal.isSupported(this);
}

@SuppressWarnings("unchecked")
@Override
public <R extends Temporal> R addTo(R temporal, long amount) {
return (R) temporal.plus(amount, this);
}

@Override
public long between(Temporal temporal1Inclusive, Temporal temporal2Exclusive) {
return temporal1Inclusive.until(temporal2Exclusive, this);
}
};
}
}
41 changes: 41 additions & 0 deletions src/test/java/net/neoforged/camelot/test/DateUtilsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package net.neoforged.camelot.test;

import net.neoforged.camelot.util.DateUtils;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

public class DateUtilsTest {
@Test
void testSimpleParse() {
Assertions.assertThat(DateUtils.decode("45m"))
.hasMinutes(45);
Assertions.assertThat(DateUtils.decode("2h"))
.hasHours(2);
Assertions.assertThat(DateUtils.decode("2M"))
.hasDays(2 * 30);
Assertions.assertThat(DateUtils.decode("1y"))
.hasDays(365);
}

@Test
void testMinuteFallback() {
Assertions.assertThat(DateUtils.decode("2K"))
.hasMinutes(2);
}

@Test
void testMultipleParse() {
Assertions.assertThat(DateUtils.getDurationFromInput("12m45s"))
.hasSeconds(12 * 60 + 45);
Assertions.assertThat(DateUtils.getDurationFromInput("12y4M2w"))
.hasDays(12 * 365 + 4 * 30 + 2 * 7);
}

@Test
void testNegativeParse() {
Assertions.assertThat(DateUtils.getDurationFromInput("2w-4d"))
.hasDays(10);
Assertions.assertThat(DateUtils.getDurationFromInput("1h4s-4m-12s"))
.hasSeconds(60 * 60 + 4 - 4 * 60 - 12);
}
}

0 comments on commit 0293478

Please sign in to comment.