Lego Bricks with Groovy

Author: Paul King
Published: 2023-04-25 11:28PM (Last updated: 2023-04-27 10:42PM)


Donald Raab has continued has interesting series on learning Eclipse Collections. His latest blog post, part 4, looks at processing information in collections.

Basic Collection Processing

Donald has a useful comparison table of operations for basic collection processing. We’ll add a Groovy column:

Operation Eclipse Collections Java Streams Groovy

Do

forEach
(or each)

forEach

each

Filter

select (include)
reject (exclude)
partition (both)

filter
filter (negated predicate)
Collectors.partitioningBy

findAll
findAll (negated predicate)
split

Transform

collect

map

collect

Find

detect

filter().findFirst().orElse(null)

find

Test

anySatisfy
allSatisfy
nonSatisfy

anyMatch
allMatch
noneMatch

any
every
every (negated predicate)

Count

count

filter().count()

count

We highly recommend reading Donald’s blog post to give more context for these categories.

Also note that while Groovy has built-in collection processing capabilities (the third column), it also works well with Eclipse Collections and Java Streams. So, the first two columns are equally valid when using Groovy too.

Our example domain

We are going to follow one of the examples, that of Lego bricks, in Donald’s post.

We’ll simplify the example slightly for our purposes and for this post, ignore the different block types.

So, we’ll start with a color enum. We are just interested in representing simple blocks and use the colored dots to represent the top view of a block:

enum Color {
    RED("šŸ”“"),
    YELLOW("šŸŸ”"),
    BLUE("šŸ”µ"),
    GREEN("šŸŸ¢"),
    WHITE("āšŖļø"),
    BLACK("āš«ļø")

    final String circle

    Color(String circle) {
        this.circle = circle
    }
}

We’ll have a similar record to Donald’s post for dimensions (but include a toString():

record Dimensions(int width, int length) {
    String toString() { "$length X $width" }
}

Now, our lego brick record just combines the color and dimensions:

record LegoBrick(Color color, Dimensions dimensions) {
    LegoBrick(Color color, int width, int length) {
        this(color, new Dimensions(width, length))
    }

    static generateMultipleSizedBricks(int count, Set<Color> colors, Set<Dimensions> sizes) {
        [[colors, sizes].combinations() * count]*.collect{
            Color c, Dimensions d -> new LegoBrick(c, d)
        }.sum()
    }

    String toString() {
        ([color.circle * dimensions.length] * dimensions.width).join('\n')
    }

    int length() {
        dimensions.length()
    }

    int width() {
        dimensions.width()
    }
}

While we don’t use it in this post, we created an additional constructor for making it easier to create bricks of certain sizes. There’s also a factory method for putting together collections of bricks.

Some bricks to play with

The first thing we do in our test script is set up some bricks to use in the remaining examples:

Set sizes = [[1, 2], [2, 2], [1, 3], [2, 3], [2, 4]].collect {
    h, w -> new Dimensions(h, w)
}
Set colors = Color.values()
var bricks = LegoBrick.generateMultipleSizedBricks(5, colors, sizes)
assert bricks.size() == 150

The type of brick is determined by its color and size. There are FIVE different sizes, and SIX colors. There are FIVE of each type in the collection. Which makes a total of 150 bricks.

Using each ("Do")

Set seen = []
bricks.shuffled().each {
    if (seen.add(it.dimensions)) {
        println "$it ($it.dimensions)"
    }
}

We shuffle the bricks and them process them one by one. If we see a brick of a size we haven’t seen before we output it, and its size.

The output will be similar to this:

šŸ”“šŸ”“ (2 X 1)
šŸ”µšŸ”µšŸ”µ (3 X 1)
āš«ļøāš«ļøāš«ļø
āš«ļøāš«ļøāš«ļø (3 X 2)
šŸ”“šŸ”“
šŸ”“šŸ”“ (2 X 2)
āšŖļøāšŖļøāšŖļøāšŖļø
āšŖļøāšŖļøāšŖļøāšŖļø (4 X 2)

Due to the shuffling, you might see different colors or a different order for the sizes.

Using findAll ("Filter")

Let’s now find the unique sizes for red bricks that are of width two (and we’ll sort them by length):

var redWidthTwo = bricks.findAll(b -> b.width() == 2 && b.color == RED)
        .toSet()
        .sort(LegoBrick::length)
assert redWidthTwo.join(',\n') == '''\
šŸ”“šŸ”“
šŸ”“šŸ”“,
šŸ”“šŸ”“šŸ”“
šŸ”“šŸ”“šŸ”“,
šŸ”“šŸ”“šŸ”“šŸ”“
šŸ”“šŸ”“šŸ”“šŸ”“'''

Using split (also "Filter")

Let’s find the bricks of length 4 or more (and we’ll find just the unique variations and sort them by color):

def (selected, rejected) = bricks.findAll(b -> b.length() > 3)
        .toSet()
        .sort(LegoBrick::color)
        .split { b ->
            switch (b.color) {
                case GREEN, WHITE, YELLOW -> true
                case BLUE, RED, BLACK -> false
            }
        }

assert selected.join(',\n') == '''
    šŸŸ”šŸŸ”šŸŸ”šŸŸ”
    šŸŸ”šŸŸ”šŸŸ”šŸŸ”,
    šŸŸ¢šŸŸ¢šŸŸ¢šŸŸ¢
    šŸŸ¢šŸŸ¢šŸŸ¢šŸŸ¢,
    āšŖļøāšŖļøāšŖļøāšŖļø
    āšŖļøāšŖļøāšŖļøāšŖļø
'''.stripIndent().trim()
assert rejected.join(',\n') == '''
    šŸ”“šŸ”“šŸ”“šŸ”“
    šŸ”“šŸ”“šŸ”“šŸ”“,
    šŸ”µšŸ”µšŸ”µšŸ”µ
    šŸ”µšŸ”µšŸ”µšŸ”µ,
    āš«ļøāš«ļøāš«ļøāš«ļø
    āš«ļøāš«ļøāš«ļøāš«ļø
'''.stripIndent().trim()

Using collect ("Transform")

Let’s transform each brick into the toString for its dimensions and then find the unique values:

Set dims = bricks.collect(b -> b.dimensions.toString()).toUnique()
assert dims == ['2 X 1', '2 X 2', '3 X 1', '3 X 2', '4 X 2'] as Set

Using find ("Find")

Let’s shuffle the bricks again (no cheating here!) and then find the first green brick of width and length 2:

var greenTwoByTwo = bricks.shuffled().find {
    b -> b.width() == b.length() && b.color == GREEN
}
assert greenTwoByTwo.toString() == 'šŸŸ¢šŸŸ¢\nšŸŸ¢šŸŸ¢'

Using any and every ("Test")

Let’s check that there are no 1 x 1 (or some kind of 0 size bricks). Either the width or length must be strictly greater than 1. Also, let’s check there is some brick where the width is the same as the length (recall our earlier greenTwoByTwo as just one example).

assert bricks.every { b -> b.width() > 1 || b.length() > 1 }
assert bricks.any { b -> b.width() == b.length() }

Using count ("Count")

Let’s count how many green bricks there are, and how many have length of 4:

assert bricks.count { b -> b.color == GREEN } == 25
assert bricks.count { b -> b.length() == 4 } == 30

A mosaic of bricks

In our final example, we took a mosaic of bricks from (1 x 1) and larger sizes and put them together. We took the toString and to save space (and bring a moment of suspense) we compressed it and encoded it in chunked base64. Your challenge, should you choose to accept it, is to decode the brick mosaic from its compressed representation. Here is some code that might help:

var encodedCompressedLegoMosaic = '''
eJztmj1uwzAMhfdcvkunLN3duUDPkwskRwiCoKljm9Tjn0gZBmLCkmmB+h5NyUZOl/P39fdjOJse
gLs9VQhCYW9fnz/pQRw6HDpUsA8R/o5nT1Ywxs7B6M+5v/Mf1O6Af5HMFzVDsU/Eudhu0R4q61+/
IN5rupOFelHe6bApnHrYHOlZXamQw4ylLjkKwMONQl8g6RUSuGBHinc09ir1Zn3iAnqNmKrjdsRt
r/Qs5pspxNv09RRLMJdaNX88s912jcTIyJi853+/eQrTfJBGGVziZ12RHSG+a6TuxasWvoRwPYZh
t/vZUsnlO/3M8/R09ca+UshSkknSCPIL/7nWUMFBRPAtTPhoKSKgU0PXIIEWdCC6LbxlYxSnxguK
VVIdUqMea7J4EcKXCERLPgZ8wcF9pNp3EsMim10wL0+LGPjuLFkMHQ7kKYlCJkx2HyWC9ODp++qx
bSWvetaX67fIFg7Npvv6oOHdIoB/oPD5UkRGvHkL33T80KaDGnkYo8zC2VC5K8JdoETKW2+QlpJf
j6WWxpV6gRMO4j4nJH8DqYLcZOtGlJjGB70HVXmJ62fVsSlYO8NVoMyirA4+k3KF9OwpQzL0PWP2
nz91abD/we3DHmIUsqQYd3YE/rA=
'''.trim()

var os = new ByteArrayOutputStream()

try (var ios = new InflaterOutputStream(os)) {
    ios.write(encodedCompressedLegoMosaic.decodeBase64())
}
println os

The output is left as an exercise for the reader (but if you try on a terminal, you should have at least a 72 x 36 layout, and use a terminal that supports unicode).

Spoiler alert: if you don’t want to run the script yourself, try it here.

Conclusion

We have had a quick look at some of Groovy’s basic collection processing functionality. We’ve really only touched the surface. Take a look at an earlier blog post giving a Groovy list processing cheat sheet if you want to see a whole lot more methods. Also, we highly recommend you try out all of the Eclipse Collections examples in Donald’s original post using Groovy.