I have a list of BigDecimal
that could be as elements:
1 = "76.2372" 2 = "0E-4" 3 = "80.2318" 4 = "82.1111" 5 = "88.0937"
I would like to calculate the absolute difference between elements not next to Zero value, in this example difference between 3 and 4
, 4 and 5
.
Expected result should be also of type List<BigDecimal>
.
Of course, the value of Zero could be in any position and I should keep calculating couples of values not adjacent to Zero.
Advertisement
Answer
TL;DR
This problem can be solved by implementing a custom Linked List and representing sequences of elements from the initial list terminated by zero as separate Linked Lists.
The Core idea
To generate differences between elements with are not followed by an element having a value of zero (i.e. new BigDecimal("0E-4")
, new BigDecimal("0.00")
which can be translated to double
as 0.0
) we can represent each part of the list terminated by a zero-valued element as a Linked List. And then we can fairly easy calculated difference between the values of Nodes in each Linked List.
But there’s caveat: everything that you’re doing with streams even if you’re planing to run it sequentially should also work correctly in parallel, because one day one of your colleagues (or even you) can switch it to parallel, and the code appear to be broken.
While executing the stream in parallel, each thread would get its portion of data to process, and then results produced by each thread should be merged. At this point, we should be able to distinguish between a Linked List terminated by a zero-valued element and a non-terminated Linked List (its thread simply run out of elements).
To visualize this concept, I’ve named the implementation of the Linked List, a Chain
. An attempt to add a zero-valued element into a Chain causes the Chain to be qualified as broken. If the Chain is not broken, we can append another Chain to it, even if another Chain is broken (in this case the resulting chain also becomes broken). But note that we can’t append another chain to a broken chain.
Implementing a Custom Comparator
Since we need to perform a lot of stateful mutating operations, the proper way to implement it with Stream API is to create a custom Collector. For that, we can make use of the static method Collector.of()
, expecting:
- a
Supplier
of accumulation type, providing a mutable container; - a consumer which defines a logic for populating the container;
- a combiner which tells how to merge containers while executing in parallel;
- and a finisher function performing the final transformation.
Note that it could be also implemented as generic and initialized with a Predicate
for discarding elements and a Function
for performing the final transformation, but to avoid complicating the code I would focus only on dialing with BigDecimal
.
public static Collector<BigDecimal, ?, List<BigDecimal>> getDiffCollector() { return Collector.of( ArrayDeque::new, (Deque<Chain> chains, BigDecimal next) -> { if (chains.isEmpty() || chains.getLast().isBroken()) chains.add(new Chain()); chains.getLast().accept(next); }, (left, right) -> { Chain merged = left.pollLast().merge(right.pollFirst()); // merging the last Chain of the left Deque and the first Chain of the right Deque left.add(merged); left.addAll(right); return left; }, deque -> deque.stream() .flatMap(chain -> chain.toDifferences().stream()) .toList() ); }
Implementing the Chain
In a nutshell, Chain
is a Singly linked list that has inner class Node
. For convenience, I’ve implemented Consumer
interface, and its method accept()
appends a new Node to the tail if the offered value is not zero.
public class Chain implements Consumer<BigDecimal> { private Node head; private Node tail; private boolean isBroken; private boolean isInitialized; @Override public void accept(BigDecimal value) { if (value.doubleValue() == 0) { // ignore the value, doubleValue() is used because new BigDecimal("0E-4") and new BigDecimal("0.00") are not equal to BigDecimal.ZERO if (isInitialized) isBroken = true; // break the Chain return; } Node newNode = new Node(value); if (!isInitialized) { // edge-case - adding the first Node, we need initialize the head isInitialized = true; head = newNode; } else { tail.setNext(newNode); // general case - add a new Node to the tail } tail = newNode; // new Node always becomes a new tail } public Chain merge(Chain other) { if (isBroken) throw new IllegalStateException(); // we can't append another Chain to a Chain that is broken if (!isInitialized) return other; if (!other.isInitialized) return this; tail.setNext(other.head); // joining the Linked Lists tail = other.tail; // reassigning the tail Node isBroken = other.isBroken; // is the next Chain was broken a merged Chain also becomes broken return this; } public List<BigDecimal> toDifferences() { if (head == null) return Collections.emptyList(); List<BigDecimal> result = new ArrayList<>(); Node current = head; while (current.next != null) { // iterate over the List until we hit the tail Node result.add(current.getDiff()); current = current.next; } return result; } public boolean isBroken() { return isBroken; } public boolean isInitialized() { return isInitialized; } private class Node { private BigDecimal value; private Node next; public Node(BigDecimal value) { this.value = value; } public BigDecimal getDiff() { return value.subtract(next.value); } public void setNext(Node next) { this.next = next; } } }
Usage example
public static void main(String[] args) { List<BigDecimal> values = List.of( new BigDecimal("76.2372"), new BigDecimal("0E-4"), new BigDecimal("80.2318"), new BigDecimal("82.1111"), new BigDecimal("88.0937") ); List<BigDecimal> differences = values.stream() .collect(getDiffCollector()); System.out.println(differences); }
Output:
[-1.8793, -5.9826] // 80.2318 - 82.1111, 82.1111 - 88.0937