Skip to content
Advertisement

List of BigDecimal: how to calculate absolute difference between elements not adjacent to Zero value using Java 8 Stream?

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
Advertisement