Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to shuffle a stream using the Stream API?

I decided to take the functional approach in generating a string or random characters, so far I came up with this, it should perform better than boxing and then using a StringJoiner as collector:

Random random = new Random();
String randomString = IntStream.concat(
        random.ints(8, 'a', 'z'),
        random.ints(8, 'A', 'Z')
)
        .collect(
                StringBuilder::new,
                (sb, i) -> sb.append((char)i),
                (sb1, sb2) -> sb1.append(sb2)
        ).toString();

I want to generate a stream of 16 characters, ranging from a-z or A-Z, the problem I have is that I do not know how to shuffle both streams.

I know that I am using IntStream.concat here, which will simply concatenate both streams, I'm looking for either of the following:

  • A static operator like IntStream.concat that does the shuffling when merging the streams.
  • A stream operator like sorted().

Both ways are viable in my opinion, however I am especially intrigued by how to make an operator alike sorted(). The key point here is that it is an operator that is stateful as it needs to see the whole stream before it can operate, is there a way to inject a stateful operator in a stream sequence?

So far the operations, excluding the needed work to shuffle them, seem to not be appropiate for a functional approach in Java 8.

like image 525
skiwi Avatar asked Jun 05 '14 09:06

skiwi


2 Answers

You are thinking too twisted

Random random = new Random();
String randomString=random.ints(16, 0, 26*2).map(i->(i>=26? 'a'-26: 'A')+i)
  .collect(StringBuilder::new,
           StringBuilder::appendCodePoint, StringBuilder::append)
  .toString();

Since you already have a source of random values there is no point in calling for a shuffle function (which would not work very well with streams).

Note that you also could define the allowed chars in a String explicitly and select them using: random.ints(16, 0, allowed.length()).map(allowed::charAt)

Similar pattern applies to selecting from a random access List.


Update: If you want to have code clearly showing the two ranges nature of the allowed characters you can combine your Stream.concat approach with the char selection solution described above:

StringBuilder allowed=
  IntStream.concat(IntStream.rangeClosed('a', 'z'), IntStream.rangeClosed('A', 'Z'))
    .collect(StringBuilder::new,
             StringBuilder::appendCodePoint, StringBuilder::append);
String randomString=random.ints(16, 0, allowed.length()).map(allowed::charAt)
  .collect(StringBuilder::new,
           StringBuilder::appendCodePoint, StringBuilder::append)
  .toString();

(Note: I replaced range with rangeClosed which I suspect to match your original intentions while it does not do what Random.ints(…, 'a', 'z') would do).

like image 177
Holger Avatar answered Oct 13 '22 01:10

Holger


This is probably not as elegant as you hoped for but it works:

final Random random = new Random();
String randomString = IntStream.concat(random.ints(8, 'a', 'z'+1), random.ints(8, 'A', 'Z'+1))
    .collect(StringBuilder::new, (sb, i) -> {
      int l = sb.length();
      if (l == 0) {
        sb.append((char) i);
      } else {
        int j = random.nextInt(l);
        char c = sb.charAt(j);
        sb.setCharAt(j, (char) i);
        sb.append(c);
      }
    }, (sb1, sb2) -> sb1.append(sb2)).toString();
System.out.println(randomString);

Alternatively you could do this:

final String randomString = random.ints(100, 'A', 'z' + 1)
        .filter(i -> i <= 'Z' || i >= 'a').limit(16)
        .collect(StringBuilder::new, (sb, i) -> sb.append((char) i), 
                StringBuilder::append).toString();
like image 28
Claude Martin Avatar answered Oct 13 '22 01:10

Claude Martin