Ipkiss
Ipkiss

Reputation: 801

How can I calculate a total value based on whether adjacent elements have the same post code in Java 8 using streams?

What I wanted to do was to add 10min if consecutive orders have the same postcode, otherwise, add 30 min to the journey. For example, if I have a class like the following this is how i achieve that imperatively.

@Data
@AllArgsConstructor
public static class Order{
    private String orderNumber;
    private final String postCode;
}

Order order1 = new Order("1","N1");
Order order2 = new Order("2","N1");
Order order3 = new Order("3","N3");
Order order4 = new Order("4","N4");

List<Order> orders = List.of(order1, order2, order3, order4);

int totalMin = 0;
for (int i=0;i < orders.size()-1 ; i++ ){
  if (orders.get(i+1).getPostCode().equals(orders.get(i).getPostCode())){
      totalMin+=10;
    }
  else{
      totalMin+=30;
  }
}

System.out.println(totalMin) // 70
// if order 2 is new Order("2","N2"); // 90

How can I achieve the same thing, or convert the above using Java 8 streams? I tried the reduce function but couldn't get my head around it.

Upvotes: 4

Views: 86

Answers (3)

M. Justin
M. Justin

Reputation: 21084

The windowSliding gatherer in the upcoming Java 24 (added as part of the Stream Gatherers feature) can be used to achieve the stated goal, comparing each adjacent pair of elements:

int totalMin = orders.stream()
        .gather(Gatherers.windowSliding(2))
        .mapToInt(w -> w.getFirst().getPostCode().equals(w.getLast().getPostCode()) ? 10 : 30)
        .sum();

This converts the Stream<Order> to a Stream<List<Order>>. Each list element in the new stream is a 2-element list of an adjacent pair of orders.

Upvotes: 0

M. Justin
M. Justin

Reputation: 21084

Here is a Collector-based approach that achieves the desired goal:

class State {
    String firstPostCode;
    String lastPostCode;
    int total;
}
int totalMin = orders.stream().collect(Collector.of(
        State::new,
        (State s, Order o) -> {
            if (s.firstPostCode == null) {
                s.firstPostCode = o.getPostCode();
            } else {
                s.total += o.getPostCode().equals(s.lastPostCode) ? 10 : 30;
            }
            s.lastPostCode = o.getPostCode();
        },
        (State s1, State s2) -> {
            s1.total += s2.total +
                    (s1.lastPostCode.equals(s2.firstPostCode) ? 10 : 30);
            s1.lastPostCode = s2.lastPostCode;
            return s1;
        },
        (State s) -> s.total)
);

This (parallelizable) collector sums the elements according to the desired rules.

Since collectors are required to support parallel execution (e.g. orders.parallelStream().collect(collector), some complexity is added to the implementation. Notably, the State keeps track of the fist and last post code of each run, so that when two adjacent states are combined, it is known whether the adjacent orders were in the same post codes or not.

The accumulator compares the post codes of the current and previous elements, and adds 10 or 30 to the total depending on whether they're the same. If it was the first element encountered, it sets the first post code. It also sets the last post code to the current post code.

The combiner adds the totals of the two states together, plus 10 or 30 depending on whether the last post code of the first state was equal to the first post code of the second state (i.e. whether the adjacent boundary elements were in the same post code). It uses the first post code of the first state and the last post code of the second state.

The supplier function creates an empty state with total 0, and the finisher function returns the total of the state (throwing away the rest of the state data).

Upvotes: 0

davidxxx
davidxxx

Reputation: 131316

You could use IntStream for that :

int totalMin =
IntStream.range(0, orders.size()-1)
         .map(i-> orders.get(i+1).getPostCode().equals(orders.get(i).getPostCode()) ? 10 : 30)
         .sum();

Upvotes: 3

Related Questions