Eduard Grinchenko
Eduard Grinchenko

Reputation: 524

How to get the length of a path using Java 8 streams

I have List<Vector3D> , where Vector3D is a coordinate. Vector3D has method double distance(Vector3D) which returns the distance between two positions.

I want to find sum of all distances between Vector3D elements of the list. I want to find it using Java 8 streams. I tried to use reduce but it can't help me.

For example, I have a list with (1,0,0) (2,0,0) (3,0,0). The length of this path should be 3.

If we are using Java 7 or lower we have to do:

public static double calcPathLength(List<Vector3D> path){
    double length = 0d;
    for (int i=0; i< path.size()-1; i++){
        length += path.get(i).distance(path.get(i+1));
    }
    return length;
}

Upvotes: 3

Views: 1302

Answers (3)

M. Justin
M. Justin

Reputation: 21113

This can be accomplished using the Java 22 Stream Gatherers preview language feature to transform the stream to a sliding window of adjacent coordinate pairs. It is a preview feature, so it typically shouldn't be used in production code until it graduates to a finished feature.

return path.stream()
        .gather(Gatherers.windowSliding(2))
        .mapToDouble(pair -> pair.getFirst().distance(pair.getLast()))
        .sum();

This uses Gatherers.windowSliding to convert the Stream<Vector3D> to a Stream<List<Vector3D>> of 2-element lists of each consecutive pair of coordinates. Then the distance between each of these pairs is retrieved using Stream.mapToDouble and summed using DoubleStream.sum.

Example

Stream<Vector3D> vectors = path.stream();

[
  (1, 0, 0),
  (2, 0, 0),
  (3, 0, 0)
]

->

Stream<List<Vector3D>> pairs = vectors.gather(Gatherers.windowSliding(2))

[
  [(1, 0, 0), (2, 0, 0)],
  [(2, 0, 0), (3, 0, 0)]
]

->

DoubleStream distances =
        pairs.mapToDouble(pair -> pair.getFirst().distance(pair.getLast()));

[
  1.0,
  1.0
]

->

double length = distances.sum();

2.0

Javadocs

Gatherer:

An intermediate operation that transforms a stream of input elements into a stream of output elements, optionally applying a final action when the end of the upstream is reached. […]

[…]

There are many examples of gathering operations, including but not limited to: grouping elements into batches (windowing functions); de-duplicating consecutively similar elements; incremental accumulation functions (prefix scan); incremental reordering functions, etc. The class Gatherers provides implementations of common gathering operations.

Stream.gather:

Returns a stream consisting of the results of applying the given gatherer to the elements of this stream.

Gatherers.windowSliding

Returns a Gatherer that gathers elements into windows -- encounter-ordered groups of elements -- of a given size, where each subsequent window includes all elements of the previous window except for the least recent, and adds the next element in the stream. […]

Example:

// will contain: [[1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8]]
List<List<Integer>> windows2 =
    Stream.of(1,2,3,4,5,6,7,8).gather(Gatherers.windowSliding(2)).toList();

// will contain: [[1, 2, 3, 4, 5, 6], [2, 3, 4, 5, 6, 7], [3, 4, 5, 6, 7, 8]]
List<List<Integer>> windows6 =
    Stream.of(1,2,3,4,5,6,7,8).gather(Gatherers.windowSliding(6)).toList();

Upvotes: 0

Holger
Holger

Reputation: 298153

The operation you are performing is called Mutable reduction.

Pshemo’s answer shows how you can implement such operation ad-hoc by providing the three necessary functions. However, when all three functions are implemented by a dedicated class it might be useful to implement these functions inside a class implementing Collector for easier reuse:

public class Distance implements Collector<Vector3D, Distance.Helper, Double> {

    public static final Distance COLLECTOR = new Distance();

    static final class Helper {
        private double sum = 0;
        private Vector3D first = null, previous = null;
    }
    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
    public Supplier<Helper> supplier() {
        return Helper::new;
    }
    public BiConsumer<Helper, Vector3D> accumulator() {
        return (helper,vector3d)-> {
            if (helper.previous != null)
                helper.sum += vector3d.distance(helper.previous);
            else helper.first = vector3d;
            helper.previous = vector3d;
        };
    }
    public BinaryOperator<Helper> combiner() {
        return (h1,h2)-> {
            h2.sum += h1.sum;
            if(h1.previous!=null && h2.first!=null) {
                h2.sum += h1.previous.distance(h2.first);
                h2.first=h1.first;
            }
            return h2;
        };
    }
    public Function<Helper, Double> finisher() {
        return helper -> helper.sum;
    }
}

You will recognize the three function from the ad-hoc version. New is a fourth function, finisher which allows to specify how the final result can be extracted from the mutable container so we don’t need the getSum() call.

The use case simplifies to:

List<Vector3D> list;
//…
double distance=list.stream().collect(Distance.COLLECTOR);

Upvotes: 3

Pshemo
Pshemo

Reputation: 124225

One of the options would be creating some helper class which would remember previously used vector and based on it calculate difference between it and current vector. This class could look like

class DistanceHelper {
    private double sum = 0;
    private Vector3D first = null;
    private Vector3D last = null;

    public void add(Vector3D vector3d) {
        if (first == null)
            first = vector3d;
        if (last != null)
            sum += vector3d.distance(last);
        last = vector3d;
    }

    public void combine(DistanceHelper otherHelper) {
        //add distance of path from current thread with distance of path
        //from other thread
        sum += otherHelper.sum;
        //also add distance between paths handled by separate threads like
        // when path of Thread1 is A->B and Thread2 is C->D then we need to 
        // include path from `B` to `C`
        if (this.last!=null && otherHelper.first!=null)
            sum += this.last.distance(otherHelper.first);
        this.last = otherHelper.last;
    }

    public double getSum() {
        return sum;
    }
}

and you can use it for example with combine instead of reduce like

double sum = list
        .stream()//or parallelStream()
        .collect(DistanceHelper::new, DistanceHelper::add,
                DistanceHelper::combine).getSum();

Upvotes: 2

Related Questions