JLDD challenge arrives in the DEN
Jet Lag Driven Development resumes at the inaugural dev2next conference
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!