Фильтр Multimap по ключу на основе диапазона дат

У меня есть набор данных, содержащий платежные транзакции, состоящие из дат и сумм. Я храню их в структуре данных карты с датой в качестве ключа и суммы в качестве значения.

Поскольку на дату может быть несколько платежей, я использую Multimap из библиотеки Google Guava.

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"));

Каков рекомендуемый подход к фильтрации этой карты на основе диапазона дат?

Например, показать записи между 3/2/2017-3/31/2017.

2 ответа

Решение

Multimaps#filterKeys метод позволяет фильтровать существующий Multimap по ключам, соответствующим любому Predicate что вы код Возвращаемое значение Multimap который содержит только записи, которые удовлетворяют предикату фильтрации.

Во-первых, давайте определим вспомогательный метод, который может создать Predicate для проверки того, что даты находятся между указанным диапазоном.

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);
        }
    };
}

После этого вы можете использовать предикат для фильтрации желаемого диапазона.

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:
    // {}
}

Я упростил этот пример, чтобы использовать формат даты по умолчанию для анализа. Похоже, ваш оригинальный образец использует пользовательский formatter, но все те же методы применяются к фильтрации по предикату.

Моя реализация предиката соответствует инклюзивному диапазону для обоих begin а также end, Если у вас были немного другие требования (например, эксклюзивный ассортимент, такой, что end не включены в результаты), то вы можете настроить реализацию apply соответственно.

Пожалуйста, обратите внимание на некоторые детали, указанные в JavaDocs для Multimap экземпляр возвращен filterKeys метод, такой как:

Возвращенная мультикарта представляет собой живое представление нефильтрованного; изменения одного влияют на другое.

...

Полученные мультикарты имеют итераторы, которые не поддерживают remove ()...

...

Возвращенная мультикарта не является поточно-ориентированной или сериализуемой, даже если она нефильтрована.

...

Многие из методов отфильтрованной мультикарты, такие как size(), выполняют итерацию для каждого сопоставления ключ / значение в базовой мультикарте и определяют, какие из них удовлетворяют фильтру. Когда просмотр в реальном времени не требуется, может быть быстрее скопировать отфильтрованную мультикарту и использовать копию.

Как добавленное примечание, between Предикат можно сделать более гибким, изменив сигнатуру метода для использования обобщений, которые принимают различные Comparable типы, а не только 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);
        }
    };
}

Вам нужно Multimap чтобы быть отсортированным, в противном случае вы должны повторить все это. К счастью, есть такая мультикарта:

ListMultimap<LocalDate, BigDecimal> multimap =
    MultimapBuilder.treeKeys().arrayListValues().build();

... fill

// This cast is safe.
SortedMap<LocalDate, Collection<BigDecimal>> asMap =
    (SortedMap<LocalDate, Collection<BigDecimal>>) multimap.asMap();
SortedMap<LocalDate, Collection<BigDecimal>> subMap =
    asMap.subMap(from, to);
for (Map.Entry<LocalDate, Collection<BigDecimal>> e : subMap.entrySet()) {
    ...
}
Другие вопросы по тегам