JLDD challenge arrives in the DEN

Donald Raab
4 min readOct 8, 2024

--

Jet Lag Driven Development resumes at the inaugural dev2next conference

JLDD challenge witnessed by opposing rock formations in the Garden of the Gods, Colorado Springs

My friend José Paumard arrived at the dev2next conference in Denver (DEN), Colorado with a gift. He offered an opportunity to participate in a “new” Jet Lag Driven Development (JLDD) challenge. The challenge was apparently one that we had discussed previously, but hadn’t implemented at a conference years ago. Better late than never. Let’s go!

The Challenge

The problem as described was to split letters in a String into lists. Every change in letters should result in a new list. Duplicates will be added to the same list, but only when they are in order together. Here’s an example of several String instances being converted to a List of List of char values.

"" -> []
"aaa" -> [['a', 'a', 'a']]
"abc" -> [['a'], ['b'], ['c']]
"aabbaa" -> [['a', 'a'], ['b', 'b'], ['a', 'a']]
"abbccdc" -> [['a'], ['b','b'], ['c', 'c'], ['d'], ['c']]

Solution 1: Java Stream w/ a custom Collector

Here’s a solution I came up with using Java Streams.

Here is the test code for various examples.

@Test
public void letterSplitterStreamTests()
{
Assertions.assertEquals(
List.of(),
this.letterSplitterStream(""));
Assertions.assertEquals(
this.expectedListStream("a", "b", "cc", "d", "ee", "f"),
this.letterSplitterStream("abccdeef"));
Assertions.assertEquals(
this.expectedListStream("aaaa"),
this.letterSplitterStream("aaaa"));
Assertions.assertEquals(
this.expectedListStream("aa", "bb", "aa"),
this.letterSplitterStream("aabbaa"));
Assertions.assertEquals(
this.expectedListStream("a", "b", "c"),
this.letterSplitterStream("abc"));
}

private List<List<Character>> expectedListStream(String... strings)
{
return Stream.of(strings)
.map(s -> s.chars()
.mapToObj(i -> (char) i)
.toList())
.toList();
}

Here’s the solution code in the letterSplitterStream method.

public List<List<Character>> letterSplitterStream(String value)
{
var collector = Collector.of(
ArrayList::new,
(List<List<Character>> list, Character c) -> {
List<Character> charList =
list.isEmpty() ? null : list.getLast();
if (charList == null || !charList.contains(c))
{
list.add(charList = new ArrayList<>());
}
charList.add(c);
},
(l, r) -> {throw new UnsupportedOperationException();},
Collector.Characteristics.IDENTITY_FINISH);

return value.chars()
.mapToObj(i -> (char) i)
.collect(collector);
}

I used var in this solution to simplify the code. If this code looks confusing using var, here’s the type for the collector variable.

Collector<Character, List<List<Character>>, List<List<Character>>>

The Collector.of() call takes four parameters — A Supplier, a BiConsumer, a BinaryOperator, and a Characteristics array. I made a conscious decision to not support parallelism using the Stream solution, so the BinaryOperator that would normally be implemented merging results throws an UnsupportedOperationException.

Each int value in the IntStream returned by calling chars is cast to a char and converted to a Character in the call to mapToObj. The call to collect passes in the Collector that was created above.

The Supplier simply creates a new ArrayList. The BiConsumer lambda does all the work here. I will leave it as an exercise to the reader to understand how the code in the BiConsumer works.

Solution 2: Eclipse Collections CharAdapter

The first solution I wrote was actually the Eclipse Collections solution, but I thought it would be useful to show the Stream solution first, as most Java developers should be familiar with Java Stream code by now.

Here is the test code for various examples using Eclipse Collections.

@Test
public void letterSplitterTests()
{
Assertions.assertEquals(
Lists.mutable.empty(),
this.letterSplitter(""));
Assertions.assertEquals(
this.expectedList("a", "b", "cc", "d", "ee", "f"),
this.letterSplitter("abccdeef"));
Assertions.assertEquals(
this.expectedList("aaaa"),
this.letterSplitter("aaaa"));
Assertions.assertEquals(
this.expectedList("aa", "bb", "aa"),
this.letterSplitter("aabbaa"));
Assertions.assertEquals(
this.expectedList("a", "b", "c"),
this.letterSplitter("abc"));
}

private MutableList<CharList> expectedList(String... strings)
{
return Lists.mutable.with(strings).collect(Strings::asChars);
}

Here’s the solution code in the letterSplitter method.

public MutableList<MutableCharList> letterSplitter(String value)
{
return Strings.asChars(value).injectInto(
Lists.mutable.empty(),
(list, c) -> {
MutableCharList charList = list.getLast();
if (charList == null || !charList.contains(c))
{
return list.with(CharLists.mutable.with(c));
}
charList.add(c);
return list;
});
}

The method injectInto is available on both Object and primitive collections in Eclipse Collections. The call to Strings.asChars(value) creates a CharAdapter object. The method injectInto takes an initial parameter that will be used as a mutable accumulator and will be the return result of the Function2 lambda that is the second parameter. The implementation of the Function2 should look similar to the BiConsumer in the Collector example. The major difference is that the char values do not need to be boxed as Character objects when using Eclipse Collections because we can use a MutableCharList. There are some other minor differences which are the result of some convenient methods available in Eclipse Collections.

Update: Solution 3: String.chars().forEach()

I wasn’t quite satisfied with the complexity of using a custom Collector, so thought I would just write a version using String.chars().forEach(). The code is similar to the code in the BiConsumer, without all the excess boilerplate code required.

public List<List<Character>> letterSplitterForEach(String value)
{
List<List<Character>> list = new ArrayList<>();
value.chars().forEach(i -> {
Character c = (char) i;
List<Character> charList =
list.isEmpty() ? null : list.getLast();
if (charList == null || !charList.contains(c))
{
list.add(charList = new ArrayList<>());
}
charList.add(c);
});
return list;
}

@Test
public void letterSplitterForEachTests()
{
Assertions.assertEquals(
List.of(),
this.letterSplitterForEach(""));
Assertions.assertEquals(
this.expectedListStream("a", "b", "cc", "d", "ee", "f"),
this.letterSplitterForEach("abccdeef"));
Assertions.assertEquals(
this.expectedListStream("aaaa"),
this.letterSplitterForEach("aaaa"));
Assertions.assertEquals(
this.expectedListStream("aa", "bb", "aa"),
this.letterSplitterForEach("aabbaa"));
Assertions.assertEquals(
this.expectedListStream("a", "b", "c"),
this.letterSplitterForEach("abc"));
}

Update: Solution 4: CharAdapter.forEach()

Since I wrote a Java Stream version using IntStream.forEach(), I figured I might as well write a CharAdapter.forEach(). Enjoy!

public MutableList<MutableCharList> letterSplitterForEachEC(String value)
{
MutableList<MutableCharList> list = Lists.mutable.empty();
Strings.asChars(value).forEach(c -> {
MutableCharList charList = list.getLast();
if (list.isEmpty() || !charList.contains(c))
{
list.add(charList = CharLists.mutable.empty());
}
charList.add(c);
});
return list;
}

@Test
public void letterSplitterForEachECTests()
{
Assertions.assertEquals(
Lists.mutable.empty(),
this.letterSplitterForEachEC(""));
Assertions.assertEquals(
this.expectedList("a", "b", "cc", "d", "ee", "f"),
this.letterSplitterForEachEC("abccdeef"));
Assertions.assertEquals(
this.expectedList("aaaa"),
this.letterSplitterForEachEC("aaaa"));
Assertions.assertEquals(
this.expectedList("aa", "bb", "aa"),
this.letterSplitterForEachEC("aabbaa"));
Assertions.assertEquals(
this.expectedList("a", "b", "c"),
this.letterSplitterForEachEC("abc"));
}

Summary

This was a fun and relatively straightforward challenge. The tricky part was trying to write it in as little code as possible. I’m excited to see the solution(s) that José Paumard comes up with. I’d also love to see other alternative solutions folks can come up with. The challenge is open!

Thanks for reading!

I am the creator of and committer for the Eclipse Collections OSS project, which is managed at the Eclipse Foundation. Eclipse Collections is open for contributions. I am writing a book this year about Eclipse Collections. Stay tuned!

--

--

Donald Raab
Donald Raab

Written by Donald Raab

Java Champion. Creator of the Eclipse Collections OSS Java library (https://github.com/eclipse/eclipse-collections). Inspired by Smalltalk. Opinions are my own.

No responses yet