Search code examples
javajava-streamcollectors

Collect items into brackets (current and all preceding groups)


(Sorry if the term "bracket" is confusing, I took it from "tax brackets" as translated from dutch).

How can I collect items into groups and each preceding group based on a property threshold?

Example:

Loan A, duration of 6 months
Loan B, duration of 10 months
Loan C, duration of 12 months

Result as Map<Integer, List<Loan>>:

6 -> {A, B, C}
10 -> {B, C}
12 -> {C}

Currently I'm going the LinkedHashMap route and then after collecting the items from stream, run through all groups to update them:

Map<Integer, List<Loan>> loansByDuration = loans.stream()
        .collect(groupingBy(Loan::getDuration, LinkedHashMap::new, toList()));

List<Loan> previousGroup = null;
for (List<Loan> currentGroup : loansByDuration.values()) {
    if (previousGroup != null) {
        currentGroup.addAll(previousGroup);
    }
    previousGroup = currentGroup;
}

So what I'm doing is every subsequent list-value in the map will have all previous loans plus the loans only relevant for respective duration-key. Similar to cumulative subtotals.

Can this be done with standard API or else something like a custom collector?


Solution

  • For the use case is it necessary to work with a type like Map<Integer, List<Loan>>? Thus every list will have all previous loans. This means there would be redundant references to Loan.

    Or is the use case to provide views on loans grouped by duration?
    In such a case we can use an alternative approach: A view on the loans using Stream (based on Java 9+).

    public class Loans {
    
        private final List<Loan> loans;
    
        public Loans(List<Loan> loans) {
            this.loans = loans.stream()
                    .sorted(Comparator.comparingInt(Loan::getDuration))
                    .collect(Collectors.toList());
        }
    
        public Stream<Loan> getUpTo(int duration) {
            return loans.stream().takeWhile(l -> l.getDuration() <= duration);
        }
    
    }
    

    Since we have a List<Loan> sorted by duration we can use Stream.takeWhile to get a Stream of the desired Loans for a certain duration.

    This works e.g. like that:

    Loans loans = new Loans(List.of(new Loan("A", 6), new Loan("B", 10), new Loan("C", 12));
    loans.getUpTo(1); // <empty>
    loans.getUpTo(5); // <empty>
    loans.getUpTo(6); // Loan("A", 6)
    loans.getUpTo(10); // Loan("A", 6), new Loan("B", 10)
    loans.getUpTo(11); // Loan("A", 6), new Loan("B", 10)
    loans.getUpTo(12); // Loan("A", 6), new Loan("B", 10), Loan("C", 12)
    loans.getUpTo(100); // Loan("A", 6), new Loan("B", 10), Loan("C", 12)
    

    In case a List<Loan> should be needed, we can still collect the streamed elements toList().