6

I'm trying to achieve this using only functional programming constructs (Streams, Collectors, lambda expressions).

Let's say list is a String[]:

{"Apple", "Samsung", "LG", "Oppo", "Apple", "Huawei", "Oppo"}

I want to print out a distinct list of brand names from this array, and number them, i.e.:

1. Apple
2. Huawei
3. LG
4. Oppo
5. Samsung

I can print out the unique elements (sorted):

Stream.of(list)
    .distinct()
    .sorted()
    .forEach(System.out::println);

But this does not show the preceding counter. I tried Collectors.counting() but that, of course, didn't work.

Any help from FP experts?


Edit: I understand some other questions have asked about iterating over a stream with indices. However, I can't simply do:

IntStream.range(0, list.length)
        .mapToObj(a -> (a+1) + ". " + list[a])
        .collect(Collectors.toList())
        .forEach(System.out::println);

because the original array contains duplicate elements. There might also be other map and filter operations I may need to perform on the stream before I print the result.

Nathan Hughes
  • 94,330
  • 19
  • 181
  • 276
light
  • 4,157
  • 3
  • 25
  • 38

4 Answers4

3

Edit after Holger’s comment: If you just want the distinct brands in the order encountered in the array, all you need to do is to omit the sort operation.

    String[] list = {"Apple", "Samsung", "LG", "Oppo", "Apple", "Huawei", "Oppo"};
    Stream.of(list)
            .distinct()
            .forEach(System.out::println);

Output is:

Apple
Samsung
LG
Oppo
Huawei

As Holger said, distinct maintains encounter order if the stream had encounter order before the distinct operation (which your stream has).

I actually need the preceding counter (1. 2. 3. 4. 5.).

My preferred way of doing it is using an enhanced for loop and no lambdas:

    int counter = 0;
    for (String brand : new LinkedHashSet<>(Arrays.asList(list))) {
        counter++;
        System.out.println("" + counter + ". " + brand);
    }

Output is:

1. Apple
2. Samsung
3. LG
4. Oppo
5. Huawei

Further comment from you:

I'm wondering what's the "ideal FP" solution.

I’m not necessarily convinced that any good purely functional programming solution exists. My attempt would be, inspired from the linked original question and its answers:

    List<String> distinctBrands
            = new ArrayList<>(new LinkedHashSet<>(Arrays.asList(list)));
    IntStream.range(0, distinctBrands.size())
            .mapToObj(index -> "" + (index + 1) + ". " + distinctBrands.get(index))
            .forEach(System.out::println);

A LinkedHashSet maintains insertion order and drops duplicates. Output is the same as before.

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
  • Out of curiosity: How were you able to post an answer if the question is marked as dupe? Any reputation-related thing? – lealceldeiro Jun 06 '19 at 13:31
  • Thanks for the answer. I actually need the preceding counter (1. 2. 3. 4. 5.). – light Jun 06 '19 at 13:33
  • I didn’t notice,@lealceldeiro. :-) I think that if one embarks on typing an answer before the question is marked as duplicate, one will be allowed to post the answer up to something like 1 or 5 minutes after the question is marked as duplicate. It’s treated somewhere on meta, don’t remember where, but you can search for it. – Ole V.V. Jun 06 '19 at 13:34
  • Yea, I guess this "misuse lambda" technique works, but it feels so wrong :D Upvoted. – light Jun 06 '19 at 13:43
  • Thanks, @light, sounds like we agree. See my new edit for a better solution. – Ole V.V. Jun 06 '19 at 13:45
  • I understand, and agree that a more imperative approach could be better for this problem. I'm actually doing some exercises in FP in Java, hence the artificial restrictions to use only FP. And this question propped up, so I'm wondering what's the "ideal FP" solution. – light Jun 06 '19 at 13:49
  • @light I have added one more possible solution. – Ole V.V. Jun 06 '19 at 14:05
  • 6
    `distinct()` on a Stream *does* maintain the order, if the Stream has an encounter order to begin with. But since the OP has chained `.sorted()` *after* the `.distinct()`, it didn’t matter if not. As a fun fact, when you chain `.distinct()` after `.sorted()`, itd does not only maintain the order, it will exploit the fact that the elements are already sorted for a cheap distinct operation. – Holger Jun 06 '19 at 15:01
3

You can do this in a functional style, without side-effects, by setting up one stream that sorts the items and a second stream for the line numbers, then combining the streams with zip.

This example uses Guava's zip function. Zip is a very common utility for functional programming.

import java.util.stream.Stream;
import com.google.common.collect.Streams;

public class ZipExample {
    public static void main(String[] args) {
        String[] a = {"Apple", "Samsung", "LG", "Oppo", "Apple", "Huawei", "Oppo"};
        Stream<String> items = Stream.of(a).sorted().distinct();
        Stream<Integer> indices = Stream.iterate(1, i -> i + 1);
        Streams.zip(items, indices, 
            (item, index) -> index + ". " + item)
            .forEach(System.out::println);
    }
}

It prints out

1. Apple
2. Huawei
3. LG
4. Oppo
5. Samsung

Rewritten as one expression it would look like:

Streams.zip(
    Stream.of(a).sorted().distinct(), 
    Stream.iterate(1, i -> i + 1), 
    (item, index) -> index + ". " + item)
    .forEach(System.out::println);
Nathan Hughes
  • 94,330
  • 19
  • 181
  • 276
3

A simpler solution could be to use a Set as a collection to ensure unique strings are accessed and incrementing the index along with for all such elements:

String[] list = {"Apple", "Samsung", "LG", "Oppo", "Apple", "Huawei", "Oppo"};
Set<String> setOfStrings = new HashSet<>(); // final strings
AtomicInteger index = new AtomicInteger(1); // index
Arrays.stream(list)
        .filter(setOfStrings::add) // use the return type of Set.add
        .forEach(str -> System.out.println(index.getAndIncrement() + ". " + str));

Edit: (Thanks to Holger) And instead of the index maintained as a separate variable, one can use the size of the collection as:

Arrays.stream(list)
        .filter(setOfStrings::add) // use the return type of Set.add
        .forEach(str -> System.out.println(setOfStrings.size() + ". " + str)); // use size for index
Naman
  • 27,789
  • 26
  • 218
  • 353
  • 3
    Since this isn’t going to win the “pure FP style” award, you can replace the `AtomicInteger` with the `Set`’s size: `Arrays.stream(list) .filter(setOfStrings::add) .forEach(str -> System.out.println(setOfStrings.size() + ". " + str));` – Holger Jun 06 '19 at 15:23
  • @Holger Thank you as always for pointing out in the right direction. – Naman Jun 06 '19 at 15:26
  • 2
    Well, it’s debatable whether this is the “right direction”, but at least, it’s simpler. I’d perhaps prefer the loop `for(String str: list) if(setOfStrings.add(str)) System.out.println(setOfStrings.size() + ". " + str);`. But a “pure FP” solution has significant disadvantages regarding complexity and performance… – Holger Jun 06 '19 at 15:30
2

With Guava:

Streams.mapWithIndex(Stream.of(list).distinct(), (s, i) -> (i + 1) + ". " + s)
       .forEach(System.out::println);

Output:

1. Apple
2. Samsung
3. LG
4. Oppo
5. Huawei
ZhekaKozlov
  • 36,558
  • 20
  • 126
  • 155