Search code examples
javadictionaryguavamultimap

Filter Multimap By Key Based on Date Range


I've got a data set containing payment transactions consisting of dates and amounts. I'm storing these in a map data structure with the date as the key and amount as value.

Since there may be multiple payments per date, I'm using Multimap from the Google Guava library.

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy");

Multimap<LocalDate,BigDecimal> payments = ArrayListMultimap.create();

payments.put(LocalDate.parse("12/25/2016",formatter), new BigDecimal("1000"));
payments.put(LocalDate.parse("01/15/2017",formatter), new BigDecimal("250"));
payments.put(LocalDate.parse("01/25/2017",formatter), new BigDecimal("500"));
payments.put(LocalDate.parse("03/20/2017",formatter), new BigDecimal("500"));
payments.put(LocalDate.parse("04/15/2017",formatter), new BigDecimal("1000"));
payments.put(LocalDate.parse("06/15/2017",formatter), new BigDecimal("1000"));

What's the recommended approach to filtering this map based on a date range?

For example, show the entries between 3/2/2017-3/31/2017.


Solution

  • The Multimaps#filterKeys method allows you to filter an existing Multimap by keys matching any arbitrary Predicate that you code. Its return value is a Multimap that contains only the entries that satisfy the filtering predicate.

    First, let's define a helper method that can create a Predicate for checking that dates are between a specified range.

    private static Predicate<LocalDate> between(final LocalDate begin, final LocalDate end) {
        return new Predicate<LocalDate>() {
            @Override
            public boolean apply(LocalDate date) {
                return (date.compareTo(begin) >= 0 && date.compareTo(end) <= 0);
            }
        };
    }
    

    After that, you can use the predicate to filter on your desired range.

    void testFilterPayments() {
        Multimap<LocalDate, BigDecimal> payments = ArrayListMultimap.create();
    
        payments.put(LocalDate.parse("2016-12-25"), new BigDecimal("1000"));
        payments.put(LocalDate.parse("2017-01-15"), new BigDecimal("250"));
        payments.put(LocalDate.parse("2017-01-15"), new BigDecimal("1250"));
        payments.put(LocalDate.parse("2017-01-15"), new BigDecimal("2250"));
        payments.put(LocalDate.parse("2017-01-25"), new BigDecimal("500"));
        payments.put(LocalDate.parse("2017-03-20"), new BigDecimal("500"));
        payments.put(LocalDate.parse("2017-04-15"), new BigDecimal("1000"));
        payments.put(LocalDate.parse("2017-06-15"), new BigDecimal("1000"));
    
        System.out.println(Multimaps.filterKeys(payments,
                between(LocalDate.parse("2017-01-01"), LocalDate.parse("2017-04-01"))));
        // Output:
        // {2017-01-25=[500], 2017-03-20=[500], 2017-01-15=[250, 1250, 2250]}
    
        System.out.println(Multimaps.filterKeys(payments,
                between(LocalDate.parse("2017-01-01"), LocalDate.parse("2017-01-15"))));
        // Output:
        // {2017-01-15=[250, 1250, 2250]}
    
        System.out.println(Multimaps.filterKeys(payments,
                between(LocalDate.parse("2016-01-01"), LocalDate.parse("2017-12-31"))));
        // Output:
        // {2017-06-15=[1000], 2017-01-25=[500], 2017-03-20=[500], 2016-12-25=[1000], 2017-01-15=[250, 1250, 2250], 2017-04-15=[1000]}
    
        System.out.println(Multimaps.filterKeys(payments,
                between(LocalDate.parse("2001-01-01"), LocalDate.parse("2015-12-31"))));
        // Output:
        // {}
    }
    

    I have simplified this example to use the default date format for parsing. It appears your original sample uses a custom formatter, but all of the same techniques apply as far as filtering by a predicate.

    My predicate implementation matches on an inclusive range for both begin and end. If you had slightly different requirements (such as an exclusive range, such that end isn't included in the results), then you could adjust the implementation of apply accordingly.

    Please be aware of some of the details noted in the JavaDocs for the Multimap instance returned by the filterKeys method, such as:

    The returned multimap is a live view of unfiltered; changes to one affect the other.

    ...

    The resulting multimap's views have iterators that don't support remove()...

    ...

    The returned multimap isn't threadsafe or serializable, even if unfiltered is.

    ...

    Many of the filtered multimap's methods, such as size(), iterate across every key/value mapping in the underlying multimap and determine which satisfy the filter. When a live view is not needed, it may be faster to copy the filtered multimap and use the copy.

    As an added note, the between predicate can be made more flexible by changing the method signature to use generics that accept a variety of Comparable types, not just LocalDate.

    private static <T extends Comparable<? super T>> Predicate<T> between(final T begin, final T end) {
        return new Predicate<T>() {
            @Override
            public boolean apply(T value) {
                return (value.compareTo(begin) >= 0 && value.compareTo(end) <= 0);
            }
        };
    }