4

Recently I stumbled on the need to split a list of values to lists of positive and negative numbers. However NOT one list positive and one for negative but basically beginning a new list once the sign changes (ignoring 0 values);

Example:

valuesInList = [-1, -3, -5, -120, 0, 15, 24, 42, 13, -15, -24, -42, 1, 2, 3]
splitList = [[-1, -3, -5, -120], [15, 24, 42, 13], [-15, -24, -42], [1, 2, 3]]

I wrote code that works, but it is something I am not really happy with:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Main {

    public static void main(String[] args) {
        Byte[] values = new Byte[]{-1, -3, -5, -120, 0, 15, 24, 42, 13, -15, -24, -42, 1, 2, 3}; //0 -> ignore
        List<Byte> valuesInList = Arrays.asList(values);
        System.out.println("valuesInList = " + valuesInList);
        int firstNotZero = valuesInList.stream().filter(b -> b != 0).collect(Collectors.toList()).get(0);
        boolean currentlyLookingForPositive = firstNotZero > 0;

        List<List<Byte>> splitList = new ArrayList<>();

        int index = 0;

        while (index < valuesInList.size()) {
            List<Byte> collection = new ArrayList<>();
            while (currentlyLookingForPositive && index < valuesInList.size()) {
                byte current = valuesInList.get(index);
                if (current > 0) {
                    collection.add(current);
                    index++;
                }
                else
                    currentlyLookingForPositive = false;
                if (current == 0)
                    index++;
            }
            if (!collection.isEmpty())
                splitList.add(collection);

            collection = new ArrayList<>();
            while (!currentlyLookingForPositive && index < valuesInList.size()) {
                byte current = valuesInList.get(index);
                if (current < 0) {
                    collection.add(current);
                    index++;
                }
                else
                    currentlyLookingForPositive = true;
            }
            if (!collection.isEmpty())
                splitList.add(collection);
        }

        System.out.println("splitList = " + splitList);
    }

}

I think the reason for that is rather obvious: repeating major parts of the code. However I have no clue how I could export this way to a method to make the code a lot clearer.
Further, now that I saw the massive potential of streams I wondered whether there was a convenient way of writing this bulk using Java 8 Streams or using other Java 8 features to at least run parts of the code in a method.

Edit: This question was flagged to be a possible duplicate of Group sequences of values . Eventhough the title might tell differently the questions (at least from my perspective) are not duplicates. The linked one asks for groups of ascending values whereas mine asks for grouping by sign (positive/negative).

geisterfurz007
  • 5,292
  • 5
  • 33
  • 54
  • 3
    Possible duplicate of [Group sequences of values](https://stackoverflow.com/questions/35234128/group-sequences-of-values) – Flown Aug 17 '17 at 06:58
  • I checked that one and it seemed promissing at first but the question essentially is an entirelly different one: I want to group by sign whereas the question you linked wants to group ascending number values. Sometimes I think that more general titles of questions are not always better ;) – geisterfurz007 Aug 17 '17 at 07:13
  • 2
    Then adapt the condition. Hint: `Math::signum` – Flown Aug 17 '17 at 07:24
  • That is indeed a nice hint! Eventhough @Eugene took a load of effort to post this I think I will actually vote as well to close this as duplicate. Thanks for pointing me to that question! – geisterfurz007 Aug 17 '17 at 07:44

2 Answers2

2

A quick and dirty solution:

List<List<Integer>> result = Arrays.asList(-1, -3, -5, -120, 0, 15, 24, 42, 13, -15, -24, -42, 1, 2, 3)
            .stream()
            .collect(Collector.of(
                    () -> {
                        List<List<Integer>> list = new ArrayList<>();
                        list.add(new ArrayList<>());
                        return list;
                    },
                    (list, x) -> {

                        if (x == 0) {
                            return;
                        }

                        if (list.size() == 0) {
                            list.get(0).add(x);
                        } else {
                            List<Integer> lastInner = list.get(list.size() - 1);
                            if (lastInner.size() > 0) {
                                int elem = lastInner.get(0);
                                if (elem >>> 31 == x >>> 31) {
                                    lastInner.add(x);
                                } else {
                                    List<Integer> oneMore = new ArrayList<>();
                                    oneMore.add(x);
                                    list.add(oneMore);
                                }
                            } else {
                                lastInner.add(x);
                            }

                        }

                    }, (left, right) -> {

                        throw new RuntimeException("Not for aprallel");
                    }));

    System.out.println(result);

I am still thinking if its possible to do for parallel processing (the combiner for the custom collector). Also it would probably be better to extract that to a method that returns this collector for further re-usage. Will update with that

EDIT

Or if there is no StreamEx:

List<Integer> input = Arrays.asList(-1, -3, -5, -120, 0, 15, 24, 42, 13, -15, -24, -42, 1, 2, 3);
    List<Integer> filtered = input.stream().filter(x -> x != 0).collect(Collectors.toList());

    int[] indexes = IntStream.range(0, filtered.size() - 2)
            .filter(x -> x == 0 || x == filtered.size() || filtered.get(x) >>> 31 != filtered.get(x + 1) >>> 31)
            .map(x -> x + 1)
            .toArray();

    List<List<Integer>> result = IntStream.range(0, indexes.length - 1)
            .mapToObj(x -> filtered.subList(indexes[x], indexes[x + 1]))
            .collect(Collectors.toList());

    System.out.println(result);
Eugene
  • 117,005
  • 15
  • 201
  • 306
  • Damn that must have taken a serious amount of time! Thanks for the effort you took. Sadly I actually have to agree with the dupe vote as it solves most of the problem. I will post the code in a second. – geisterfurz007 Aug 17 '17 at 07:49
  • 1
    @geisterfurz007 that actually did that not that much... I've also added a slight solution adapted from of the answers in the duplicate – Eugene Aug 17 '17 at 08:34
  • That is really impressive! I tried to add a filter to throw out the zeroes as well but `.filter(x -> input.get(x) != 0)` would not work (ie not filter). If you could add that bit that would be amazing! Else I will mix that part of me into yours ;) – geisterfurz007 Aug 17 '17 at 08:43
  • 2
    @geisterfurz007 I can only think of doing one more step and actually filtering the zeroes out... – Eugene Aug 17 '17 at 09:01
  • Alright I think I will do that on my own then :) Thanks for your time and responses! Edit: Damn you already did that! Thanks so much! – geisterfurz007 Aug 17 '17 at 09:08
2

After @Flown pointed me to a question I thought would not really help at first he further pointed me to changing the condition using Math#signum.

The result of that was the following code:

int[] values = new int[]{-1, -3, -5, -120, 0, 15, 24, 42, 13, -15, -24, -42, 1, 2, 3}; //0 -> ignore
IntStream seq = IntStream.of(values).filter(i -> i!=0);
List<List<Integer>> result = 
IntStreamEx.of(seq).boxed().groupRuns((i1, i2) -> Math.signum(i1) == Math.signum(i2)).toList();

Which indeed is massively shorter and after a second and with some comments is easier understandable than before.

IntStreamEx is taken from the StreamEx library which I got the reference to from the linked answer as well.

geisterfurz007
  • 5,292
  • 5
  • 33
  • 54