Skip to content

Commit

Permalink
feat: duration parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
qixils committed Dec 24, 2023
1 parent d0d8501 commit be58ff5
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package dev.qixils.quasicord.converter;

import dev.qixils.quasicord.Quasicord;
import dev.qixils.quasicord.converter.impl.DurationConverter;
import dev.qixils.quasicord.converter.impl.LocaleConverter;
import dev.qixils.quasicord.converter.impl.ZoneIdConverter;
import dev.qixils.quasicord.converter.impl.ZonedDateTimeConverter;
Expand Down Expand Up @@ -50,6 +51,7 @@ public ConverterRegistry(@NonNull Quasicord library) {
register(new ConverterImpl<>(Locale.class, DiscordLocale.class, (it, locale) -> DiscordLocale.from(locale)));
register(new ConverterImpl<>(DiscordLocale.class, Locale.class, (it, locale) -> locale.toLocale()));
register(new ZoneIdConverter());
register(new DurationConverter(library));
// channels
register(ConverterImpl.channel(TextChannel.class));
register(ConverterImpl.channel(PrivateChannel.class));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package dev.qixils.quasicord.converter.impl;

import dev.qixils.quasicord.Key;
import dev.qixils.quasicord.Quasicord;
import dev.qixils.quasicord.converter.Converter;
import dev.qixils.quasicord.error.UserError;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.dv8tion.jda.api.interactions.Interaction;
import org.checkerframework.checker.nullness.qual.NonNull;

import java.time.Duration;
import java.util.*;
import java.util.function.BiFunction;
import java.util.regex.Pattern;

@Getter
@RequiredArgsConstructor
public class DurationConverter implements Converter<String, Duration> {
private final @NonNull Class<? extends Interaction> interactionClass = Interaction.class;
private final @NonNull Class<String> inputClass = String.class;
private final @NonNull Class<Duration> outputClass = Duration.class;
private final @NonNull Quasicord library;

private static final @NonNull Set<String> RELATIVE_TIME_IGNORED_TOKENS = Set.of("in");
private static final @NonNull Pattern RELATIVE_TIME_PATTERN = Pattern.compile("^\\d+[A-Za-z]+");

@Override
public @NonNull Duration convert(@NonNull Interaction interaction, @NonNull String input) {
List<String> arguments = new ArrayList<>(Arrays.asList(input.split(" ")));
arguments.removeIf(s -> RELATIVE_TIME_IGNORED_TOKENS.contains(s.toLowerCase(Locale.ENGLISH)));
String strippedInput = String.join("", arguments);

if (!RELATIVE_TIME_PATTERN.matcher(strippedInput).matches()) {
throw new UserError(Key.library("exception.duration.regex"));
}

boolean negative = false;
if (arguments.get(0).startsWith("-")) {
arguments.set(0, arguments.get(0).substring(1));
negative = true;
}
if (arguments.get(arguments.size() - 1).equalsIgnoreCase("ago")) {
arguments.remove(arguments.size() - 1);
negative = !negative;
}

// vars for parsing the duration
Duration duration = Duration.ZERO;
StringBuilder nextToken = new StringBuilder();
boolean buildingAmount = true;
String amountToken = null;
String unitToken;
// vars for parsing the terms/tokens used fom the arg list
StringBuilder term = new StringBuilder();
int termIdx = 0;
int parsedTermIdx = 0;

for (char chr : strippedInput.toCharArray()) {
term.append(chr);
if (term.toString().equals(arguments.get(termIdx))) {
term = new StringBuilder();
++termIdx;
}
if (Character.isWhitespace(chr)) {
continue;
}

boolean isDigit = Character.isDigit(chr);
if (isDigit && !buildingAmount) {
// finished parsing unit of time (and duration of time)
buildingAmount = true;
unitToken = nextToken.toString();
nextToken = new StringBuilder();
// now attempt to find the corresponding RelativeTimeUnit
RelativeTimeUnit unit = RelativeTimeUnit.of(unitToken);
if (unit == null) {
throw new UserError(Key.library("exception.duration.unit"), unitToken);
}
// add relative time to current time object
duration = unit.addTimeTo(duration, Long.parseLong(amountToken)); // theoretically this shouldn't error
parsedTermIdx = termIdx;
// reset variables
amountToken = null;
} else if (!isDigit && buildingAmount) {
// finished parsing duration of time, save it & reset the builder
buildingAmount = false;
amountToken = nextToken.toString();
nextToken = new StringBuilder();
}
nextToken.append(chr);
}

if (negative)
duration = Duration.ZERO.minus(duration);

while (parsedTermIdx > 0) {
arguments.remove(0);
parsedTermIdx--;
}

return duration;
}

private enum RelativeTimeUnit {
SECOND(Duration::plusSeconds, "s", "sec", "secs", "second", "seconds"),
MINUTE(Duration::plusMinutes, "m", "min", "mins", "minute", "minutes"),
HOUR(Duration::plusHours, "h", "hr", "hrs", "hour", "hours"),
DAY(Duration::plusDays, "d", "day", "days"),
WEEK((duration, time) -> duration.plusDays(time * 7), "w", "week", "weeks");

private final @NonNull BiFunction<@NonNull Duration, @NonNull Long, @NonNull Duration> addTimeFunction;
private final String @NonNull [] tokens; // TODO: i18n plurals ?

RelativeTimeUnit(
@NonNull BiFunction<@NonNull Duration, @NonNull Long, @NonNull Duration> addTimeFunction,
String @NonNull ... tokens
) {
this.addTimeFunction = addTimeFunction;
this.tokens = tokens;
}

public boolean matches(@NonNull String input) {
for (String token : tokens) {
if (token.equalsIgnoreCase(input))
return true;
}
return false;
}

public @NonNull Duration addTimeTo(@NonNull Duration originalTime, long duration) {
return addTimeFunction.apply(originalTime, duration);
}

public static RelativeTimeUnit of(@NonNull String token) {
for (RelativeTimeUnit unit : values()) {
if (unit.matches(token))
return unit;
}
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
import dev.qixils.quasicord.db.collection.TimeZoneConfig;
import dev.qixils.quasicord.error.UserError;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.dv8tion.jda.api.interactions.Interaction;
import org.checkerframework.checker.nullness.qual.NonNull;

import java.time.Duration;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
Expand All @@ -24,25 +24,39 @@
import static dev.qixils.quasicord.Key.library;

@Getter
@RequiredArgsConstructor
public class ZonedDateTimeConverter implements Converter<String, ZonedDateTime> {
private final @NonNull Class<? extends Interaction> interactionClass = Interaction.class;
private final @NonNull Class<String> inputClass = String.class;
private final @NonNull Class<ZonedDateTime> outputClass = ZonedDateTime.class;
private final @NonNull Quasicord library;
private final @NonNull DurationConverter durationConverter;

private static final Pattern[] DATE_PATTERNS = {
Pattern.compile("(?<year>\\d{4})-(?<month>\\d{1,2})-(?<day>\\d{1,2})"),
Pattern.compile("(?<date1>\\d{1,2})/(?<date2>\\d{1,2})/(?<year>\\d{4})"),
Pattern.compile("(?<month>\\p{L}{3,}) (?<day>\\d{1,2}) (?<year>\\d{4})", Pattern.UNICODE_CHARACTER_CLASS),
Pattern.compile("(?<day>\\d{1,2}) (?<month>\\p{L}{3,}) (?<year>\\d{4})", Pattern.UNICODE_CHARACTER_CLASS),
Pattern.compile("(?<year>\\d{4})-(?<month>\\d{1,2})-(?<day>\\d{1,2})"),
Pattern.compile("(?<date1>\\d{1,2})/(?<date2>\\d{1,2})/(?<year>\\d{4})"),
// TODO month name parse
//Pattern.compile("(?<month>\\p{L}{3,}) (?<day>\\d{1,2}) (?<year>\\d{4})", Pattern.UNICODE_CHARACTER_CLASS),
//Pattern.compile("(?<day>\\d{1,2}) (?<month>\\p{L}{3,}) (?<year>\\d{4})", Pattern.UNICODE_CHARACTER_CLASS),
};
private static final Pattern TIME_PATTERN = Pattern.compile("(?<hour>\\d{1,2})(?::(?<minute>\\d{2})(?::(?<second>\\d{2})(?:\\.(?<nanos>\\d{1,9}))?)?)?(?: ?(?<meridiem>[Aa]|[Pp])\\.?[Mm]\\.?)?");

public ZonedDateTimeConverter(@NonNull Quasicord library) {
this.library = library;
durationConverter = new DurationConverter(library);
}

@Override
public @NonNull ZonedDateTime convert(@NonNull Interaction interaction, @NonNull String input) {
TimeZoneConfig config = library.getDatabaseManager().getById(interaction.getUser().getIdLong(), TimeZoneConfig.class).block();
ZoneId zone = config == null ? ZoneOffset.UTC : config.getTimeZone();
ZonedDateTime now = ZonedDateTime.now(zone);

try {
Duration add = durationConverter.convert(interaction, input);
return now.plus(add);
} catch (Exception ignored) {
}

int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0, nanos = 0;
char meridiem = 'x';

Expand All @@ -53,6 +67,7 @@ public class ZonedDateTimeConverter implements Converter<String, ZonedDateTime>
continue;
year = Integer.parseInt(dateMatcher.group("year"));
if (dateMatcher.group("month") != null) {
// TODO month name parse
month = Integer.parseInt(dateMatcher.group("month"));
day = Integer.parseInt(dateMatcher.group("day"));
} else {
Expand All @@ -63,14 +78,12 @@ public class ZonedDateTimeConverter implements Converter<String, ZonedDateTime>
}

// error if date is invalid
var now = ZonedDateTime.now(zone);
boolean autoDate = month == 0 && day == 0 && year == 0;
if (autoDate) {
year = now.getYear();
month = now.getMonthValue();
day = now.getDayOfMonth();
}
else if (month < 1 || day < 1 || year == 0 || month > 12 || day > 31)
} else if (month < 1 || day < 1 || year == 0 || month > 12 || day > 31)
throw new UserError(library("exception.invalid_date"));

// get time
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/langs/quasicord/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ en:
"exception.command_error": An internal error occurred while executing this command. The issue has been reported to the developers.
"exception.invalid_locale": Could not find a language by the name of `{0}`
"exception.invalid_timezone": Could not find a timezone by the ID of `{0}`
"exception.duration.regex": Duration should contain only numbers, letters, and spaces
"exception.duration.unit": Could not find a unit of time by the name `{0}`
"snowflake_confirm": "{0}: Did you mean {1}?"
"timezone_display": "{0} ({1})"

Expand Down

0 comments on commit be58ff5

Please sign in to comment.