Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 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
@@ -1,15 +1,20 @@
package org.togetherjava.tjbot.commands.tags;

import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.AutoCompleteQuery;
import net.dv8tion.jda.api.interactions.commands.Command;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction;
import org.togetherjava.tjbot.commands.CommandVisibility;
import org.togetherjava.tjbot.commands.SlashCommandAdapter;
import org.togetherjava.tjbot.commands.utils.StringDistances;

import java.time.Instant;
import java.util.Objects;
import java.util.Collection;

/**
* Implements the {@code /tag} command which lets the bot respond content of a tag that has been
Expand All @@ -34,16 +39,16 @@ public TagCommand(TagSystem tagSystem) {

this.tagSystem = tagSystem;

// TODO Think about adding an ephemeral selection menu with pagination support
// if the user calls this without id or similar
getData().addOption(OptionType.STRING, ID_OPTION, "The id of the tag to display", true)
.addOption(OptionType.USER, REPLY_TO_USER_OPTION,
"Optionally, the user who you want to reply to", false);
getData().addOptions(
new OptionData(OptionType.STRING, ID_OPTION, "The id of the tag to display", true,
true),
new OptionData(OptionType.USER, REPLY_TO_USER_OPTION,
"Optionally, the user who you want to reply to", false));
}

@Override
public void onSlashCommand(SlashCommandInteractionEvent event) {
String id = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString();
String id = event.getOption(ID_OPTION).getAsString();
OptionMapping replyToUserOption = event.getOption(REPLY_TO_USER_OPTION);

if (tagSystem.handleIsUnknownTag(id, event)) {
Expand All @@ -62,4 +67,22 @@ public void onSlashCommand(SlashCommandInteractionEvent event) {
}
message.queue();
}

@Override
public void onAutoComplete(CommandAutoCompleteInteractionEvent event) {
AutoCompleteQuery focusedOption = event.getFocusedOption();

if (focusedOption.getName().equals(ID_OPTION)) {
return;
}

Collection<Command.Choice> choices = StringDistances
.autocompleteSuggestions(focusedOption.getValue(), tagSystem.getAllIds(), 0.5)
.stream()
.map(id -> new Command.Choice(id, id))
.limit(OptionData.MAX_CHOICES)
.toList();

event.replyChoices(choices).queue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,29 @@ public static <S extends CharSequence> Optional<S> autocomplete(CharSequence pre
.min(Comparator.comparingInt(candidate -> prefixEditDistance(prefix, candidate)));
}

/**
* Gives sorted suggestion to autocomplete a prefix string from the given options
*
* @param prefix the prefix to give suggestion for
* @param candidates all the possible suggestions
* @param errorMargin error margin of suggestions, error percentage / 100
* @return collection of autocomplete suggestions
*/
public static Collection<String> autocompleteSuggestions(CharSequence prefix,
Collection<String> candidates, double errorMargin) {
int firstCandidateScore = candidates.stream()
.mapToInt(candidate -> prefixEditDistance(prefix, candidate))
.min()
.orElse(0);
return candidates.stream()
.map(candidate -> new MatchScore(candidate, prefixEditDistance(prefix, candidate)))
.sorted()
.takeWhile(matchScore -> matchScore.score() <= firstCandidateScore
+ firstCandidateScore * errorMargin)
.map(MatchScore::candidate)
.toList();
}

/**
* Distance to receive {@code destination} from {@code source} by editing.
* <p>
Expand Down Expand Up @@ -141,4 +164,17 @@ private static int[][] computeLevenshteinDistanceTable(CharSequence source,

return table;
}

private record MatchScore(String candidate, int score) implements Comparable<MatchScore> {
@Override
public int compareTo(MatchScore otherMatchScore) {
int compareResult = Integer.compare(this.score, otherMatchScore.score);

if (compareResult == 0) {
return this.candidate.compareTo(otherMatchScore.candidate);
}

return compareResult;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,37 @@

import org.junit.jupiter.api.Test;

import java.util.Collection;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;

final class StringDistancesTest {

@Test
void autoCompleteSuggestions() {
record TestCase(String name, Collection<String> expectedSuggestions, String prefix,
Collection<String> candidates, double errorMargin) {
}

List<TestCase> tests =
List.of(new TestCase("empty_candidates", List.of(), "prefix", List.of(), 0),
new TestCase("empty_prefix", List.of("one", "three", "two"), "",
List.of("one", "two", "three"), 0),
new TestCase("all_empty", List.of(), "", List.of(), 1),
new TestCase("max_error", List.of("aa"), "a",
List.of("json", "one", "aa", "two", "++"), 1),
new TestCase("real_test", List.of("j", "java", "js", "one"), "jo",
List.of("java", "xj", "bs", "one", "yes", "js", "a", "j"), 0.5));

for (TestCase test : tests) {
assertEquals(
test.expectedSuggestions, StringDistances.autocompleteSuggestions(test.prefix,
test.candidates, test.errorMargin),
"Test '%s' failed".formatted(test.name));
}
}

@Test
void editDistance() {
record TestCase(String name, int expectedDistance, String source, String destination) {
Expand Down