Filtering objects from collection based on user choice/input

Viewed 897

I have a rather simple exercise but I'm limited by my knowledge and the requirements to use design patterns (+ unit tests). The goal of a project is to create a console app that will allow you to save(add), print(show all), remove(remove by cryteria) and filter(show by criteria) messages from the collection.

private String title;
private String author;
private String content;
private String creationDate;

I was able to create "add" function and "show all". My problem is with the filtering. I must create an option to filter saved objects based on the criteria given by the user (all possible combinations like: filter by title and creationDate, filter by title etc.). I thought about giving the user an option to choose it from the menu by using switch and methods like this:

private final List<Message> storage = new ArrayList<Message>();

public List<Message> getAll() {
    final ArrayList<Message> messages = new ArrayList<>();
    messages.addAll(storage);
    return messages;
}
List<Message> find(String author) {
    return simpleStorage.getAll().stream()
        .filter(item -> item.getAuthor() == author)
        .collect(toList());
}

but I think it's not a good practice to copy lots of similar code. Also, I might find myself in the future that such solution would be tiresome and even impossible (each new parameter would add new combinations). Is there a better way to do it? Like choosing a criteria "one by one" so the users could create a combinations by themselves? I've had a tip that predicates could help me with this but I don't know where to go with this.

2 Answers

My problem is with the filtering. I must create an option to filter saved objects based on the criteria given by the user (all possible combinations like: filter by title and creationDate, filter by title etc.).

This is something you can try, use or improvise on it. The code is a working example (uses Java SE 8). The example has the MessageFilter class:

  • Creates a List of test messages.
  • Accepts user input from console - the message field and its value. E.g., "title" and "message 1".
  • Based on the message field and its value gets a Predicate from the getPredicate method.
  • The predicate is applied to the messages and prints the filtered result.

The example shows filter by "title" and "author" - individually. I think the concept in the example can be applied for other filter criteria.

Example Code:

class Message { // represents a message

    private String title;
    private String author;

    Message(String s1, String s2) {
        title = s1;
        author = s2;
    }

    String getTitle() {
        return title;
    }
    String getAuthor() {
        return author;
    }

    public String toString() {
        return String.join(", ", "("+title, author+ ")");
    }
}

public class MessageFilter {

    public static void main(String [] args) {

        // Create some messages

        Message [] array = {new Message("msg1", "auth1"),
                            new Message("msg2", "auth2"),
                            new Message("msg3", "auth1"),
                            new Message("msg9", "auth3")
                            };
        List<Message> messages = Arrays.asList(array);
        System.out.println(messages);

        // Accept user input: the field name and its value

        Scanner scanner = new Scanner(System.in);

        System.out.print("Enter the property to filter (title, author, etc): ");
        String filterCriteria = scanner.nextLine();
        System.out.print("Enter the property value: ");
        String filterValue = scanner.nextLine();

        // Get the predicate based on user input

        Predicate<Message> predicate = getPredicate(filterCriteria, filterValue);

        // Filter the data using the predicate got from user input, and print...

        List<Message> result = messages.stream()
                                        .filter(predicate)
                                        .collect(Collectors.toList());

        System.out.println("Result: " + result);
    }

    private static Predicate<Message> getPredicate(String criteria, String value) {

        Predicate<Message> p = msg -> true; // by default returns all messages

        switch(criteria) {
            case "title":
                p = msg -> msg.getTitle().equals(value);
                break;
            case "author":
                p = msg -> msg.getAuthor().equals(value);
                break;  
        }

        return p;
    }
}

For each criterion, you could have a BiPredicate<String, Message>1 which takes a raw input from the user and a message, and tells if the message matches the filtering option2.

Map<String, BiPredicate<String, Message>> criteria = Map.of(
        "title", (userTitle, message) -> input.equals(message.getTitle())
        ...
);

I'll give you a simplified example of how that map can be used:

Scanner scanner = new Scanner(System.in);

String filteringOption = scanner.nextLine();
String userInput = scanner.nextLine();

BiPredicate<String, Message> predicate = criteria.get(filteringOption);

// get all messages from the storage
getAll()  
        // make a stream out of them
        .stream()
        // apply the filtering rule from the map
        .filter(m -> predicate.test(userInput, m))  
         // collect them into a list to display
        .collect(Collectors.toList());

Later, those predicates may be combined by the logical operations like or(), and() to form a custom filter option. A user's option can be added to the map for consequent calls or calculated on the fly each time the user requests it, like

BiPredicate<String, Message> titleAndDateFilter = 
    criteria.get("title").and(criteria.get("date"));

1 You could use a Predicate<Message>, but it would make these functions less isolated since you need to compare a message's context to the given input.
2 I used Java 9's Map.of.

Related