How to use Java 8 Functional Programming to Generate an Alphabetic Sequence
I’ve stumbled upon an interesting Stack Overflow question by user “mip”. The question was:
I’m looking for a way of generating an alphabetic sequence:
1A, B, C, ..., Z, AA, AB, AC, ..., ZZ.
This can be quickly recognised as the headings of an Excel spreadsheet, which does precisely that:
So far, none of the answers employed any Java 8 functional programming, which I accepted as a challenge. We’re going to use jOOλ, because the Java 8 Stream API does not offer enough functionality for this task.
But first, let’s decompose the algorithm in a functional way. What we need are these components:
- A (reproducible) representation of the alphabet
- An upper bound, i.e. how many letters we want to produce. The requested sequence goes to
ZZ
, which means the upper bound would be 2 - A way to combine each letter of the alphabet with the previously generated combined letters in a cartesian product
Let’s look into some code:
1. Generating the alphabet
We could be writing the alphabet like this:
1 | List<String> alphabet = Arrays.asList( "A" , "B" , ..., "Z" ); |
but that would be lame. Let’s generate it instead, using jOOλ:
1 2 3 4 | List<String> alphabet = Seq .rangeClosed( 'A' , 'Z' ) .map(Object::toString) .toList(); |
The above generates a “closed” range (Java-8-Stream-speak for a range with inclusive upper bound) of characters between A
and Z
, maps characters to strings and collects them into a list.
So far so good. Now:
2. Using an upper bound
The requested sequence of characters includes:
1 | A .. Z, AA, AB, .. ZZ |
But we could easily imagine to extend this requirement generally to produce the following, or even more.
1 | A .. Z, AA, AB, .. ZZ, AAA, AAB, .. ZZZ |
For this, we’ll use again rangeClosed()
:
1 2 3 4 | // 1 = A .. Z, 2 = AA .. ZZ, 3 = AAA .. ZZZ Seq.rangeClosed( 1 , 2 ) .flatMap(length -> ...) .forEach(System.out::println); |
The idea here is to produce a new stream for each individual length in the range [1 .. 2]
, and to flatten those streams into one single stream. flatMap()
is essentially the same as a nested loop in imperative programming.
3. Combine letters in a cartesian product
This is the trickiest part: We need to combine each letter with each letter length
times. For this, we’ll use the following stream:
1 2 3 4 5 | Seq.rangeClosed( 1 , length - 1 ) .foldLeft(Seq.seq(alphabet), (s, i) -> s.crossJoin(Seq.seq(alphabet)) .map(t -> t.v1 + t.v2)) ); |
We’re using again rangeClosed()
to produce values in the range [1 .. length-1]
. foldLeft()
is the same as reduce()
, except that foldLeft()
is guaranteed to go from “left to right” in a stream, without requiring the folding function to be associative. Whew.
In other, more understandable words: foldLeft()
is nothing else but an imperative loop. The “seed” of the loop, i.e. the loop’s initial value, is a complete alphabet (Seq.seq(alphabet)
). Now, for every value in the range [1 .. length-1]
, we produce a cartesian product (crossJoin()
) between the letters “folded” so far and a new alphabet, and we concatenate each combination into a single new string (t.v1
and t.v2
).
That’s it!
Combining everything
The following simple program prints all the values from A .. Z, AA .. ZZ, AAA .. ZZZ
to the console:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | import java.util.List; import org.jooq.lambda.Seq; public class Test { public static void main(String[] args) { int max = 3 ; List<String> alphabet = Seq .rangeClosed( 'A' , 'Z' ) .map(Object::toString) .toList(); Seq.rangeClosed( 1 , max) .flatMap(length -> Seq.rangeClosed( 1 , length - 1 ) .foldLeft(Seq.seq(alphabet), (s, i) -> s.crossJoin(Seq.seq(alphabet)) .map(t -> t.v1 + t.v2))) .forEach(System.out::println); } } |
Disclaimer
This is certainly not the most optimal algorithm for this particular case. One of the best implementations has been given by an unnamed user on Stack Overflow:
01 02 03 04 05 06 07 08 09 10 11 | import static java.lang.Math.*; private static String getString( int n) { char [] buf = new char [( int ) floor(log( 25 * (n + 1 )) / log( 26 ))]; for ( int i = buf.length - 1 ; i >= 0 ; i--) { n--; buf[i] = ( char ) ( 'A' + n % 26 ); n /= 26 ; } return new String(buf); } |
Unnecessary to say that the latter runs much much faster than the previous functional algorithm.
Reference: | How to use Java 8 Functional Programming to Generate an Alphabetic Sequence from our JCG partner Lukas Eder at the JAVA, SQL, AND JOOQ blog. |
I found this interesting, and since I have not hard of Jool, I decided to try this code in a new Maven project. I have a compile error. I looked through the code and did not find this IntStream method wrapped by Seq.
Error:(29, 36) java: cannot find symbol
symbol: method rangeClosed(char,char)
location: interface org.jooq.lambda.Seq
I am using:
org.jooq
jool
0.9.7
Yes, the timing of the article is a bit unfortunate as we haven’t released this code yet. You’ll find it on GitHub: https://github.com/jOOQ/jOOL