diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 13a713965c02..d51745c29505 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -81,6 +81,23 @@ Format: `help` Displays a list of all available decks. If inside a deck displays all cards in that deck. + Format: `list` +==== Filter by performance +This feature allows you to filter the displayed cards by their difficulty. + +Format: `list [difficulty] [more difficulties]...` + +When inside a deck, the `list` command can be used to filter the displayed cards by their difficulty, by passing + in the desired difficulty as a keyword after the command. + +For example: + +`list hard` will display all cards with the `hard` difficulty + +Multiple difficulties can be specified as well, in which case all cards with a difficulty matching one of those +specified will be displayed: + +`list easy hard` will display all cards with either an `easy` or `hard` difficulty. + === Navigating into a deck : `cd` To enter a deck identified by the INDEX_OF_DECK in the visible deck list. + diff --git a/docs/team/tomforge.adoc b/docs/team/tomforge.adoc index 2fec0507492d..25fc511cdeae 100644 --- a/docs/team/tomforge.adoc +++ b/docs/team/tomforge.adoc @@ -116,6 +116,23 @@ image::classifyExample3.png[width="500"] + image::classifyExample4.png[width="500"] +==== Filter by performance +This feature allows you to filter the displayed cards by their difficulty. + +Format: `list [difficulty] [more difficulties]...` + +When inside a deck, the `list` command can be used to filter the displayed cards by their difficulty, by passing + in the desired difficulty as a keyword after the command. + +For example: + +`list hard` will display all cards with the `hard` difficulty + +Multiple difficulties can be specified as well, in which case all cards with a difficulty matching one of those +specified will be displayed: + +`list easy hard` will display all cards with either an `easy` or `hard` difficulty. + == Contributions to the Developer Guide diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 4e69742dacd3..b9e345c40ee3 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -5,9 +5,14 @@ import static seedu.address.model.Model.PREDICATE_SHOW_ALL_CARDS; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_DECKS; +import java.util.function.Predicate; +import java.util.stream.Collectors; + import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.deck.Card; +import seedu.address.model.deck.CardPerformanceMatchesPerformancesPredicate; /** @@ -18,9 +23,33 @@ public class ListCommand extends Command { public static final String COMMAND_WORD = "list"; public static final String MESSAGE_SUCCESS_DECK = "Listed all decks"; - public static final String MESSAGE_SUCCESS_CARD = "Listed all cards"; + public static final String MESSAGE_SUCCESS_ALL_CARDS = "Listed all cards"; + public static final String MESSAGE_SUCCESS_PERFORMANCE_CARDS = "Listed %1$s cards"; public static final String AUTOCOMPLETE_TEXT = COMMAND_WORD; + private Predicate cardPredicate; + private String messageSuccessCard; + + /** + * Creates the default ListCommand, which lists all cards + */ + public ListCommand() { + this.cardPredicate = PREDICATE_SHOW_ALL_CARDS; + this.messageSuccessCard = MESSAGE_SUCCESS_ALL_CARDS; + } + + /** + * Creates a ListCommand with the given performance matching predicate, which will filter by the performances + * defined in said predicate + * + * @param cardPerformancePredicate the performance matching predicate + */ + public ListCommand(CardPerformanceMatchesPerformancesPredicate cardPerformancePredicate) { + this.cardPredicate = cardPerformancePredicate; + String performanceString = cardPerformancePredicate.performancesAsStrings().stream().collect(Collectors + .joining(", ")).toLowerCase(); + this.messageSuccessCard = String.format(MESSAGE_SUCCESS_PERFORMANCE_CARDS, performanceString); + } @Override @@ -31,8 +60,8 @@ public CommandResult execute(Model model, CommandHistory history) throws Command } if (model.isInsideDeck()) { - model.updateFilteredCardList(PREDICATE_SHOW_ALL_CARDS); - return new CommandResult(MESSAGE_SUCCESS_CARD); + model.updateFilteredCardList(cardPredicate); + return new CommandResult(messageSuccessCard); } else { model.updateFilteredDeckList(PREDICATE_SHOW_ALL_DECKS); return new CommandResult(MESSAGE_SUCCESS_DECK); diff --git a/src/main/java/seedu/address/logic/parser/ListCommandParser.java b/src/main/java/seedu/address/logic/parser/ListCommandParser.java new file mode 100644 index 000000000000..c7fe057bf8ec --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ListCommandParser.java @@ -0,0 +1,32 @@ +package seedu.address.logic.parser; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import seedu.address.logic.commands.ListCommand; +import seedu.address.model.deck.CardPerformanceMatchesPerformancesPredicate; +import seedu.address.model.deck.Performance; + +/** + * Parses input elements and creates a new ListCommand object + */ +public class ListCommandParser implements ParserInterface { + /** + * Parses the given {@code String} of arguments in the context of the ListCommand + * and returns an ListCommand object for execution. + */ + public ListCommand parse(String args) { + String trimmedArgs = args.trim(); + if (!trimmedArgs.isEmpty()) { + String[] keywords = trimmedArgs.split("\\s+"); + Set performanceSet = + Arrays.stream(keywords).filter(Performance::isValidPerformance) + .map(Performance::type).collect(Collectors.toSet()); + if (!performanceSet.isEmpty()) { + return new ListCommand(new CardPerformanceMatchesPerformancesPredicate(performanceSet)); + } + } + return new ListCommand(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/seedu/address/logic/parser/Parser.java index d0905d86a321..ebc65626bd31 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/seedu/address/logic/parser/Parser.java @@ -108,7 +108,7 @@ public Command parseCommand(String userInput) throws ParseException { return new HelpCommand(); case ListCommand.COMMAND_WORD: - return new ListCommand(); + return new ListCommandParser().parse(arguments); case ExportDeckCommand.COMMAND_WORD: return new ExportDeckCommandParser().parse(arguments); diff --git a/src/main/java/seedu/address/model/deck/CardPerformanceMatchesPerformancesPredicate.java b/src/main/java/seedu/address/model/deck/CardPerformanceMatchesPerformancesPredicate.java new file mode 100644 index 000000000000..eb75bf758932 --- /dev/null +++ b/src/main/java/seedu/address/model/deck/CardPerformanceMatchesPerformancesPredicate.java @@ -0,0 +1,43 @@ +package seedu.address.model.deck; + +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Tests that a {@code Card}'s {@code Performance} matches any of the Performances given. + */ +public class CardPerformanceMatchesPerformancesPredicate implements Predicate { + private final Set performances; + + public CardPerformanceMatchesPerformancesPredicate(Set performances) { + this.performances = performances; + } + + /** + * Returns the constituent performances as a list of strings + * + * @return + */ + public List performancesAsStrings() { + return performances.stream().map(Performance::toString).collect(Collectors.toList()); + } + + @Override + public boolean test(Card card) { + return performances.contains(card.getPerformance()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (!(other instanceof CardPerformanceMatchesPerformancesPredicate)) { + return false; + } + CardPerformanceMatchesPerformancesPredicate predicate = (CardPerformanceMatchesPerformancesPredicate) other; + return performances.size() == predicate.performances.size() + && performances.containsAll(predicate.performances); + } +} diff --git a/src/main/java/seedu/address/model/deck/Performance.java b/src/main/java/seedu/address/model/deck/Performance.java index c1bf18e8dc92..128d6c629d13 100644 --- a/src/main/java/seedu/address/model/deck/Performance.java +++ b/src/main/java/seedu/address/model/deck/Performance.java @@ -30,7 +30,7 @@ public static boolean isValidPerformance(String type) { return false; } try { - Performance dummy = Performance.type(type); + Performance.type(type); return true; } catch (IllegalArgumentException e) { return false; diff --git a/src/test/java/seedu/address/logic/commands/ListCommandTest.java b/src/test/java/seedu/address/logic/commands/ListCommandTest.java index fcac31e52310..d9b18454902b 100644 --- a/src/test/java/seedu/address/logic/commands/ListCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ListCommandTest.java @@ -1,11 +1,23 @@ package seedu.address.logic.commands; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; import static seedu.address.logic.commands.CommandTestUtil.showDeckAtIndex; +import static seedu.address.logic.commands.ListCommand.MESSAGE_SUCCESS_PERFORMANCE_CARDS; +import static seedu.address.testutil.TypicalCards.CARD_EASY; +import static seedu.address.testutil.TypicalCards.CARD_HARD; import static seedu.address.testutil.TypicalDecks.DECK_WITH_CARDS; +import static seedu.address.testutil.TypicalDecks.THERE; import static seedu.address.testutil.TypicalDecks.getTypicalAnakin; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_DECK; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + import org.junit.Before; import org.junit.Test; @@ -13,6 +25,9 @@ import seedu.address.model.Model; import seedu.address.model.ModelManager; import seedu.address.model.UserPrefs; +import seedu.address.model.deck.Card; +import seedu.address.model.deck.CardPerformanceMatchesPerformancesPredicate; +import seedu.address.model.deck.Performance; /** * Contains integration tests (interaction with the Model) and unit tests for ListCommand. @@ -37,7 +52,7 @@ public void execute_listIsNotFiltered_showsSameList() { model.getIntoDeck(DECK_WITH_CARDS); expectedModel.getIntoDeck(DECK_WITH_CARDS); assertCommandSuccess(new ListCommand(), model, commandHistory, - ListCommand.MESSAGE_SUCCESS_CARD, expectedModel); + ListCommand.MESSAGE_SUCCESS_ALL_CARDS, expectedModel); } @Test @@ -50,6 +65,45 @@ public void execute_listIsFiltered_showsEverything() { expectedModel.getIntoDeck(DECK_WITH_CARDS); //showCardAtIndexOfCurrentDeck(model, INDEX_FIRST_CARD); assertCommandSuccess(new ListCommand(), model, commandHistory, - ListCommand.MESSAGE_SUCCESS_CARD, expectedModel); + ListCommand.MESSAGE_SUCCESS_ALL_CARDS, expectedModel); + } + + @Test + public void execute_hard_showsHard() { + model.getIntoDeck(THERE); + expectedModel.getIntoDeck(THERE); + String expectedMessage = String.format(MESSAGE_SUCCESS_PERFORMANCE_CARDS, "hard"); + CardPerformanceMatchesPerformancesPredicate hardPredicate = preparePerformancePredicate(Set.of(Performance + .HARD)); + ListCommand command = new ListCommand(hardPredicate); + expectedModel.updateFilteredCardList(hardPredicate); + assertCommandSuccess(command, model, commandHistory, expectedMessage, expectedModel); + assertEquals(Collections.singletonList(CARD_HARD), model.getFilteredCardList()); + } + + @Test + public void execute_easyHard_showsEasyHard() { + model.getIntoDeck(THERE); + expectedModel.getIntoDeck(THERE); + CardPerformanceMatchesPerformancesPredicate easyHardPredicate = preparePerformancePredicate(Set.of(Performance + .HARD, Performance.EASY)); + String displayPerformanceStrings = convertPerformanceStringsForDisplay(easyHardPredicate + .performancesAsStrings()); + String expectedMessage = String.format(MESSAGE_SUCCESS_PERFORMANCE_CARDS, displayPerformanceStrings); + ListCommand command = new ListCommand(easyHardPredicate); + expectedModel.updateFilteredCardList(easyHardPredicate); + assertCommandSuccess(command, model, commandHistory, expectedMessage, expectedModel); + Set expectedCards = Set.of(CARD_EASY, CARD_HARD); + Set actualCards = new HashSet<>(model.getFilteredCardList()); + assertEquals(expectedCards.size(), actualCards.size()); + assertTrue(expectedCards.containsAll(actualCards)); + } + + private CardPerformanceMatchesPerformancesPredicate preparePerformancePredicate(Set performances) { + return new CardPerformanceMatchesPerformancesPredicate(performances); + } + + public String convertPerformanceStringsForDisplay(List performanceStrings) { + return performanceStrings.stream().collect(Collectors.joining(", ")).toLowerCase(); } } diff --git a/src/test/java/seedu/address/model/deck/CardPerformanceMatchesPerformancesTest.java b/src/test/java/seedu/address/model/deck/CardPerformanceMatchesPerformancesTest.java new file mode 100644 index 000000000000..e3c6e71c6469 --- /dev/null +++ b/src/test/java/seedu/address/model/deck/CardPerformanceMatchesPerformancesTest.java @@ -0,0 +1,54 @@ +package seedu.address.model.deck; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Set; + +import org.junit.Test; + +import seedu.address.testutil.CardBuilder; + +public class CardPerformanceMatchesPerformancesTest { + private Set hardPerformanceSet = Set.of(Performance.HARD); + private Set easyHardPerformancesSet = Set.of(Performance.HARD, Performance.EASY); + private Set hardEasyPerformancesSet = Set.of(Performance.EASY, Performance.HARD); + private Set allPerformancesSet = Set.of(Performance.values()); + private CardPerformanceMatchesPerformancesPredicate hardPerformancesPredicate = new + CardPerformanceMatchesPerformancesPredicate(hardPerformanceSet); + private CardPerformanceMatchesPerformancesPredicate easyHardPerformancesPredicate = new + CardPerformanceMatchesPerformancesPredicate(easyHardPerformancesSet); + private CardPerformanceMatchesPerformancesPredicate hardEasyPerformancesPredicate = new + CardPerformanceMatchesPerformancesPredicate(hardEasyPerformancesSet); + private CardPerformanceMatchesPerformancesPredicate allPerformanecsPredicate = new + CardPerformanceMatchesPerformancesPredicate(allPerformancesSet); + + private Card easyCard = new CardBuilder().withPerformance(Performance.EASY).build(); + private Card normalCard = new CardBuilder().withPerformance(Performance.NORMAL).build(); + private Card hardCard = new CardBuilder().withPerformance(Performance.HARD).build(); + + @Test + public void equals() { + assertEquals(easyHardPerformancesPredicate, hardEasyPerformancesPredicate); + assertEquals(allPerformanecsPredicate, allPerformanecsPredicate); + assertNotEquals(easyHardPerformancesPredicate, hardPerformancesPredicate); + } + + @Test + public void matchingPerformance_returnsTrue() { + assertTrue(hardPerformancesPredicate.test(hardCard)); + assertTrue(easyHardPerformancesPredicate.test(easyCard)); + assertTrue(easyHardPerformancesPredicate.test(hardCard)); + assertTrue(allPerformanecsPredicate.test(easyCard)); + assertTrue(allPerformanecsPredicate.test(normalCard)); + assertTrue(allPerformanecsPredicate.test(hardCard)); + } + + @Test + public void notMatchingPerformance_returnsFalse() { + assertFalse(hardPerformancesPredicate.test(normalCard)); + assertFalse(easyHardPerformancesPredicate.test(normalCard)); + } +} diff --git a/src/test/java/seedu/address/model/deck/PerformanceTest.java b/src/test/java/seedu/address/model/deck/PerformanceTest.java new file mode 100644 index 000000000000..2371cd95c04d --- /dev/null +++ b/src/test/java/seedu/address/model/deck/PerformanceTest.java @@ -0,0 +1,41 @@ +package seedu.address.model.deck; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import seedu.address.testutil.Assert; + +public class PerformanceTest { + private static final String INVALID_PERFORMANCE_STRING = "tough"; + private static final Performance VALID_PERFORMANCE = Performance.HARD; + private static final String VALID_PERFORMANCE_STRING = "hard"; + private static final String VALID_PERFORMANCE_STRING_MIXED_CASE = "hArD"; + + @Test + public void type_validPerformanceToString_returnsPerformance() { + Performance expectedPerformance = Performance.HARD; + Performance actualPerformance = Performance.type(expectedPerformance.toString()); + assertEquals(expectedPerformance, actualPerformance); + } + + @Test + public void type_validPerformanceString_returnsPerformance() { + assertEquals(VALID_PERFORMANCE, Performance.type(VALID_PERFORMANCE_STRING)); + } + + @Test + public void type_validPerformanceStringMixedCase_returnsPerformance() { + assertEquals(VALID_PERFORMANCE, Performance.type(VALID_PERFORMANCE_STRING_MIXED_CASE)); + } + + @Test + public void type_invalidPerformanceString_throwsIllegalArgumentException() { + Assert.assertThrows(IllegalArgumentException.class, () -> Performance.type(INVALID_PERFORMANCE_STRING)); + } + + @Test + public void type_nullString_throwsNullPointerException() { + Assert.assertThrows(NullPointerException.class, () -> Performance.type(null)); + } +} diff --git a/src/test/java/seedu/address/testutil/TypicalCards.java b/src/test/java/seedu/address/testutil/TypicalCards.java index 982087ce96fd..999d61d3231a 100644 --- a/src/test/java/seedu/address/testutil/TypicalCards.java +++ b/src/test/java/seedu/address/testutil/TypicalCards.java @@ -29,6 +29,11 @@ public class TypicalCards { public static final Card IS = new CardBuilder().withQuestion("dreams is").build(); public static final Card ALL = new CardBuilder().withQuestion("all it's gonna take").build(); + public static final Card CARD_EASY = new CardBuilder().withQuestion("An easy card") + .withPerformance(Performance.EASY).build(); + public static final Card CARD_HARD = new CardBuilder().withQuestion("A hard card") + .withPerformance(Performance.HARD).build(); + public static final Card CARD_A_WITH_META = new CardBuilder().withQuestion("An").withAnswer("Empty") .withPerformance(Performance.EASY).withTimesReviewed(5).build(); @@ -45,6 +50,7 @@ public static Deck getTypicalDeck() { public static List getTypicalCards() { return new ArrayList<>(Arrays.asList(CARD_A, CARD_B, CARD_C, CARD_D, CARD_E, CARD_F, CARD_G, + CARD_EASY, CARD_HARD, MILLION, IS, ALL)); } }