ElPiter
ElPiter

Reputation: 4324

Java Streams - Compose a value depending on values of the list

Using Java 8 and working with lists of certain object, using Streams, I need to assign a property to each value of the list depending on properties of other values.

Imagine a class in Java MyClass with these properties:

String value1;
String value2;
LocalDate theDate;
String uniqueId;

And imagine a list of that MyClass objects, just like:

List<MyClass> myList = new ArrayList<>();

The property uniqueId is empty at the beggining of this exercise.

After mapping the list, the uniqueId has to be set following this strategy:

<value1>-<value2>-N

where N is the number of appearances of the <value1>-<value2> combination sorted by theDate.

Given this example given in JSON:

[
{"value1":"1111111", "value2":"A", "theDate": "2020-06-01", "uniqueId": null},
{"value1":"1111111", "value2":"A", "theDate": "2020-06-02", "uniqueId": null},
{"value1":"1111111", "value2":"A", "theDate": "2020-06-03", "uniqueId": null},
{"value1":"1111111", "value2":"A", "theDate": "2020-06-04", "uniqueId": null},
{"value1":"1111111", "value2":"B", "theDate": "2020-06-01", "uniqueId": null},
{"value1":"1111111", "value2":"B", "theDate": "2020-06-02", "uniqueId": null},
{"value1":"1111111", "value2":"B", "theDate": "2020-06-03", "uniqueId": null}
]

should result after the execution in:

[
{"value1":"1111111", "value2":"A", "theDate": "2020-06-01", "uniqueId": "1111111-A-1"},
{"value1":"1111111", "value2":"A", "theDate": "2020-06-02", "uniqueId": "1111111-A-2"},
{"value1":"1111111", "value2":"A", "theDate": "2020-06-03", "uniqueId": "1111111-A-3"},
{"value1":"1111111", "value2":"A", "theDate": "2020-06-04", "uniqueId": "1111111-A-4"},
{"value1":"1111111", "value2":"B", "theDate": "2020-06-01", "uniqueId": "1111111-B-1"},
{"value1":"1111111", "value2":"B", "theDate": "2020-06-02", "uniqueId": "1111111-B-2"},
{"value1":"1111111", "value2":"B", "theDate": "2020-06-03", "uniqueId": "1111111-B-3"}
]

I clearly could do this using a traditional loop strategy. But the question is, using Streams, how do I manage to keep the track of the rest of the values of the list so I can compose my combined value? Or is it not possible using Streams?

myList.stream()
    .map(o -> {
    // How do I make here to keep the track of the rest of the values of the list?
    })
    .collect(Collectors.toList());

Thank you.

Upvotes: 0

Views: 824

Answers (2)

Polygnome
Polygnome

Reputation: 7820

This isn't really something were streams shine, and this has two reasons. The first is you are manipulating the MyClass objects via side-effects (granted, that could be avoided) and because Java lacks out-of-the-box support for something like mapWithIndex or zipWithIndex, which would be very helpful here. You can find some more information about how to solve that particlar problem here.

Anyways, it is possible to do via streams.

First things first, I have defined this helper method:

    public static MyClass setUniqueId(MyClass myClass, int i) {
        myClass.uniqueId = String.format("%s-%s-%s", myClass.value1, myClass.value2, i);
        return myClass;
    }

It should be fairly self-explanatory - it sets the unique id using the provided int and returns the object itself, now with the id set.

Then, doing what you want is possible by grouping the elements in your stream by equal value1 and value2 values. After grouping the elements, we can set their unique Ids by zipping with their index. As Java lacks support for this, we need to do this in a bit of a backwards way by using an IntStream with a range that we use to grab indices from. Putting it all together, we get this:

        list.stream()
            .collect(Collectors.groupingBy(obj -> obj.value1 + "-" + obj.value2))
            .forEach((klass, values) -> {
                IntStream.range(0, values.size())
                    .forEach(i -> setUniqueId(values.get(i), i));
        });

Another, perhaps more clear approach is to flatMap the result of the grouping:

        var results = list.stream()
            .collect(Collectors.groupingBy(obj -> obj.value1 + "-" + obj.value2))
            .values().stream()
            .flatMap(values -> 
                IntStream.range(0, values.size())
                    .mapToObj(i -> setUniqueId(values.get(i), i)))
            .sorted((a, b) -> a.date.compareTo(b.date))
            .collect(Collectors.toList());

Upvotes: 4

Hadi
Hadi

Reputation: 17299

You can do like this:

for simplify I just created a constructor with last parameter.

List<MyClass> result = myClasses.stream()
            .collect(() -> new HashMap<String, List<MyClass>>(), (m, myClass) ->
                    m.merge(myClass.getValue2(),

                           //create new value with the current object 
                            new ArrayList<>(singletonList(new MyClass(myClass.getValue1()
                                    + "-" + myClass.getValue2() + "-" +
                                    (m.getOrDefault(myClass.getValue2(),
                                            new ArrayList<>()).size() + 1)))),
                             //merge for duplicate key
                            (l1, l2) -> {l1.addAll(l2);return l1;}),
                    HashMap::putAll)
            .values().stream().flatMap(List::stream).collect(Collectors.toList());

Here I use HashMap to store all myclass object with the same value2 property. when I want to create new object from current myclass object, consider the map value size for the key(value2 property).for the absent value I've used m.getOrDefault(myClass.getValue2(),new ArrayList<>())

Upvotes: 0

Related Questions