Search

Dark theme | Light theme

March 30, 2023

Spocklight: Creating Temporary Files And Directories With FileSystemFixture

If we write specification where we need to use files and directories we can use the @TempDir annotation on a File or Path instance variable. By using this annotation we make sure the file is created in the directory defined by the Java system property java.io.tmpdir. We could overwrite the temporary root directory using Spock configuration if we want, but the default should be okay for most situations. The @TempDir annotation can actually be used on any class that has a constructor with a File or Path argument. Since Spock 2.2 we can use the FileSystemFixture class provided by Spock. With this class we have a nice DSL to create directory structures and files in a simple matter. We can use the Groovy extensions to File and Path to also immediattely create contents for the files. If we want to use the extensions to Path we must make sure we include org.apache.groovy:groovy-nio as dependency to our test runtime classpath. The FileSystemFixture class also has the method copyFromClasspath that we can use to copy files and their content directory into our newly created directory structure.

In the following example specification we use FileSystemFixture to define a new directory structure wi

package mrhaki

import spock.lang.Specification
import spock.lang.Subject
import spock.lang.TempDir
import spock.util.io.FileSystemFixture

import java.nio.file.Path
import java.nio.file.Paths

class FileFixturesSpec extends Specification {

    /**
     * Class we want to test. The class has a method
     * File renderDoc(File input, File outputDir) that takes
     * an input file and stores a rendered file in the given
     * output directory.
     */
    @Subject
    private DocumentBuilder documentBuilder = new DocumentBuilder()

    /**
     * With the TempDir annotation we make sure our directories and
     * files created with FileSystemFixture are deleted after
     * each feature method run.
     */
    @TempDir
    private FileSystemFixture fileSystemFixture

    void "convert document"() {
        given:
        // Create a new directory structure in the temporary directory
        // <root>
        //  +-- src
        //  |    +-- docs
        //  |         +-- input.adoc
        //  |         +-- convert.adoc
        //  +-- output
        fileSystemFixture.create {
            dir("src") {
                dir("docs") {
                    // file(String) returns a Path and with
                    // groovy-nio module on the classpath we can use
                    // extensions to add text to file. E.g. via the text property.
                    file("input.adoc").text = '''\
                    = Sample

                    Welcome to *AsciidoctorJ*.
                    '''.stripIndent()

                    // Copy file from classpath (src/test/resources)
                    // and rename it at the same time.
                    // Without rename it would be
                    // copyFromClasspath("/samples/sample.adoc")
                    copyFromClasspath("/samples/sample.adoc", "convert.adoc")
                }
            }
            dir("output")
        }

        and:
        // Using resolve we get the actual Path to the file
        Path inputDoc = fileSystemFixture.resolve("src/docs/input.adoc")
        Path convertDoc = fileSystemFixture.resolve("src/docs/convert.adoc")

        // We can also use Paths to resolve the actual Path.
        Path outputDir = fileSystemFixture.resolve(Paths.get("output"))

        when:
        File resultDoc = documentBuilder.renderDoc(inputDoc.toFile(), outputDir.toFile())

        then:
        resultDoc.text =~ "<p>Welcome to <strong>AsciidoctorJ</strong>.</p>"

        when:
        File resultConvert = documentBuilder.renderDoc(convertDoc.toFile(), outputDir.toFile())

        then:
        resultConvert.text =~ "<p>Testing <strong>AsciidoctorJ</strong> with Spock 🚀</p>"
    }

    void "convert document from non-temporary dir"() {
        given:
        // Create FileSystemFixture in our project build directory.
        FileSystemFixture fileSystemFixture = new FileSystemFixture(Paths.get("build"))
        fileSystemFixture.create {
            dir("test-docs") {
                dir("src") {
                    dir("docs") {
                        copyFromClasspath("/samples/sample.adoc")
                    }
                }
                dir("output")
            }
        }

        and:
        Path convertDoc = fileSystemFixture.resolve("test-docs/src/docs/sample.adoc")
        Path outputDir = fileSystemFixture.resolve(Paths.get("test-docs/output"))

        when:
        File resultDoc = documentBuilder.renderDoc(convertDoc.toFile(), outputDir.toFile())

        then:
        resultDoc.text =~ "<p>Testing <strong>AsciidoctorJ</strong> with Spock 🚀</p>"

        cleanup:
        // We can delete the test-docs directory ourselves.
        fileSystemFixture.resolve("test-docs").deleteDir()
    }
}

In order to use the Groovy extensions for java.nio.Path we must add the groovy-nio module to the test classpath. For example we can do this if we use Gradle by using the JVM TestSuite plugin extension:

plugins {
    java
    groovy
}

...

repositories {
    mavenCentral()
}

testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            // Define we want to use Spock.
            useSpock("2.3-groovy-4.0")
            dependencies {
                // Add groovy-nio module.
                implementation("org.apache.groovy:groovy-nio")
            }
        }
    }
}

Written with Spock 2.3-groovy-4.0 and Gradle 8.0.2.

March 26, 2023

Mastering Maven: Setting Default JVM Options Using jvm.config

In order to add default JVM options to our Maven mvn command we can define an environment variable MAVEN_OPTS. But we can also create a file jvm.config in the directory .mvn in our project root directory. On each line we define a Java option we want to apply. We can specify JVM options, but also Java system properties we want to apply each time we run the mvn command. This directory and file can be added to our source control so that all users that have access to the repository will use the same JVM options.

The following example defines some Java system properties to change the Maven logging. We also add the JVM option --show-version, so that each time we run mvn we also see the version of Java that is used to run our build. We create the file .mvn/jvm.config and the following content:

-Dorg.slf4j.simpleLogger.showDateTime=true
-Dorg.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss
-Dorg.slf4j.simpleLogger.showThreadName=true
--show-version

Now when we run mvn we see in the output the Java version displayed and each log line has a time and thread name:

$ mvn verify
openjdk 19.0.2 2023-01-17
OpenJDK Runtime Environment Temurin-19.0.2+7 (build 19.0.2+7)
OpenJDK 64-Bit Server VM Temurin-19.0.2+7 (build 19.0.2+7, mixed mode, sharing)
22:12:35 [main] [INFO] Scanning for projects...
...
22:12:39 [main] [INFO] ------------------------------------------------------------------------
22:12:39 [main] [INFO] BUILD SUCCESS
22:12:39 [main] [INFO] ------------------------------------------------------------------------
22:12:39 [main] [INFO] Total time:  3.844 s
22:12:39 [main] [INFO] Finished at: 2023-03-26T22:12:39+02:00
22:12:39 [main] [INFO] ------------------------------------------------------------------------
$

Written with Maven 3.9.1

Gradle Goodness: Publish Version Catalog For Sharing Between Projects

A version catalog in Gradle is a central place in our project where we can define dependency references with their version or version rules. We can define a version catalog using an API in our build file, but we can also create an external file where we define our dependencies and version. In our dependencies section we can refer to the names in the version catalog using a type-safe accessor (if we use Kotlin for writing our build script) with code completion in a supported IDE (IntelliJ IDEA). If we want to share a version catalog between projects we can publish a version catalog to a Maven repository with a groupId, artifactId and version.

In order to do this we need to create a new Gradle project that will only contain the definitions for our version catalog. We must add two gradle plugins: version-catalog and maven-publish. The version-catalog plugin adds a new extension versionCatalog to our build file. Here we define the content of the version catalog we want to share. We can refer to an external version catalog file written in the TOML format that is dictated by Gradle. But we can also use an API provided by VersionCatalogBuilder to define our versions, plugins, libraries and bundles. The publication outcome of this project is a Maven POM file and generated version catalog file in TOML format. Using the maven-publish plugin we can publish our version catalog to a Maven repository. Other projects can then refer to this published version catalog in their Gradle settings file. In the build script we can use the dependencies using the type-safe accessor we alread know for a project version catalog.

In the following example we first look at the Gradle project that has all the data to publish a version catalog. We first create an external version catalog file:

# File: gradle/libs.versions.toml
[versions]
junit5 = "5.9.1"

[libraries]
junit-api = {
    module = "org.junit.jupiter:junit-jupiter-api",
    version.ref = "junit5"
}
junit-engine = {
    module = "org.junit.jupiter:junit-jupiter-engine",
    version.ref = "junit5"
}

helidon-deps = "io.helidon:helidon-dependencies:3.2.0"

We define the rootProject name as this will be the artifact identifier of the version catalog we will publish:

// File: settings.gradle.kts
rootProject.name = "version-catalog"

Finally we have a build file where we define the version catalog and Maven publish plugins. Furthermore we configure our version catalog with data from the external file and we use the API. Lastly we define the publishing configuration:

// File: build.gradle.kts
plugins {
    // Version catalog plugin will add the catalog extension
    // to our build file where we can define the version
    // catalog contents.
    `version-catalog`

    // Maven publish plugin so we can publish our version catalog
    // to a Maven repository.
    `maven-publish`
}

group = "mrhaki.shared"
version = "1.1.0"

// catalog extension added by version-catalog plugin.
catalog {
    versionCatalog {
        // We can refer to an external version catalog file.
        // This could even be a published version catalog as well.
        from(files("gradle/libs.versions.toml"))

        // But also use the methods of the VersionCatalogBuilder.
        library("helidon-deps", "io.helidon:helidon-dependencies:3.2.0")
    }
}

publishing {
    publications {
        create<MavenPublication>("maven") {
            // The version-catalog plugin adds a new component
            // "versionCatalog" that we can use a publication.
            // We will get a POM file and a generated version catalog
            // TOML file that are part of the publication.
            from(components["versionCatalog"])
        }
    }

    repositories {
        // Configuration for Maven repo to publish our version catalog to.
        maven {
            url = uri("https://intranet.repo/repository/maven-releases")

            credentials {
                val mavenRepoUsername: String by project
                val mavenRepoPassword: String by project
                username = mavenRepoUsername
                password = mavenRepoPassword
            }
        }
    }
}

Now we create the Gradle project build files that will use our published version catalog. First we must refer to the published version catalog in our settings file. We define the Maven repository where we published the catalog to and then refer to the artifact to assign it to the sharedLibs version catalog accessor:

// File: settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        // Configuration for Maven repo to get our
        // published version catalog from.
        maven {
            url = uri("https://intranet.repo/repository/maven-public")

            credentials {
                val mavenRepoUsername: String by settings
                val mavenRepoPassword: String by settings
                username = mavenRepoUsername
                password = mavenRepoPassword
            }
        }
    }

    versionCatalogs {
        // We create a new version catalog with the
        // given name sharedLibs.
        // We are free to use any name, Gradle will
        // create Kotlin accessors we can use in our build file.
        create("sharedLibs") {
            from("mrhaki.shared:version-catalog:1.1.0")
        }
    }
}

Now we are all setup and in our build file we can use the type-safe accessors to get the dependencies in our project dependencies section:

// File: build.gradle.kts
plugins {
    java // We want to craete a Java Helidon app.
}

repositories {
    // Repository for downloading the dependencies.
    mavenCentral()
}

dependencies {
    // We can reference sharedLibs.helidon.deps
    // from our shared version catalog.
    implementation(platform(sharedLibs.helidon.deps))

    // List of dependencies for Helidon where the version
    // can be left out as we use platform(libs.helidon.deps)
    // to include our Bill of Materials (BOM).
    implementation("io.helidon.webserver:helidon-webserver")
    implementation("io.helidon.config:helidon-config-yaml")
    implementation("io.helidon.media:helidon-media-jsonp")

    // We can reference sharedLibs.junit.api and
    // sharedLibs.junit.engine from our shared version catalog.
    testImplementation(sharedLibs.junit.api)
    testImplementation(sharedLibs.junit.engine)

    testImplementation("io.helidon.webclient:helidon-webclient")
}

Written with Gradle 8.0.2

March 24, 2023

Spocklight: Assert Elements In Collections In Any Order

Since Spock 2.1 we have 2 new operators we can use for assertions to check collections: =~ and ==~. We can use these operators with implementations of the Iterable interface when we want to check that a given collection has the same elements as an expected collection and we don’t care about the order of the elements. Without the new operators we would have to cast our collections to a Set first and than use the == operator.

The difference between the operators =~ and ==~ is that =~ is lenient and ==~ is strict. With the lenient match operator we expect that each element in our expected collection appears at least once in the collection we want to assert. The strict match operator expects that each element in our expected collection appears exactly once in the collection we want to assert.

In the following example we see different uses of the new operators and some other idiomatic Groovy ways to check elements in a collection in any order:

package mrhaki

import spock.lang.Specification;

class CollectionConditions extends Specification {

    void "check items in list are present in the same order"() {
        when:
        List<Integer> result = [1, 10, 2, 20]

        then:
        // List result is ordered so the items
        // are not the same as the expected List.
        result != [20, 10, 2, 1]
        result == [1, 10, 2, 20]

        // We can cast the result and expected list to Set
        // and now the contents and order is the same.
        result as Set == [20, 10, 2, 1] as Set
    }

    void "check all items in list are present in any order"() {
        when:
        List<Integer> result = [1, 10, 2, 20]

        then:
        result ==~ [20, 10, 2, 1]

        /* The following assert would fail:
           result ==~ [20, 10]

           Condition not satisfied:

           result ==~ [20, 10]
           |      |
           |      false
           [1, 10, 2, 20]

           Expected: iterable with items [<20>, <10>] in any order
                but: not matched: <1>*/

        // Negating also works
        result !==~ [20, 10]
    }

    void "lenient check all items in list are present in any order"() {
        when:
        List<Integer> result = [1, 1, 10, 2, 2, 20]

        then:
        // result has extra values 1, 2 but with lenient
        // check the assert is still true.
        result =~ [20, 10, 2, 1]

        /* The following assert would fail:

        result =~ [20, 10]

        Condition not satisfied:

        result =~ [20, 10]
        |      |
        |      false
        |      2 differences (50% similarity, 0 missing, 2 extra)
        |      missing: []
        |      extra: [1, 2]
        [1, 10, 2, 20] */

        // Negating works
        result !=~ [20, 10]
    }

    void "check at least one item in list is part of expected list in any order"() {
        when:
        List<Integer> result = [1, 10, 2, 20]

        then:
        result.any { i -> i in [20, 10]}
    }

    void "check every item in list is part of expected list in any order"() {
        when:
        List<Integer> result = [1, 1, 10, 2, 2, 20]

        then:
        result.every { i -> i in [20, 10, 2, 1]}
    }
}

Written with Spock 2.3-groovy-4.0.

December 16, 2022

Kotlin Kandy: Create Fix Sized Sublists From Iterables With chunked

The chunked extension method is added to the Iterable Java class and makes it possible to split an iterable into fixed sized lists. We define the size of the lists as argument to the chunked method. The return result is a list of lists. Each of the lists will have the number of elements we have specified as argument. The last list can have less elements if the total number of elements cannot be divided exactly by the size we specified as argument. We can specify a lambda transformation function as second argument. The lambda function has the new sublist as argument and we can write code to transform that sublist.

In the following example we use the chunked method with different arguments and see what the results look like:

// Sample list of letters.
val letters = listOf('a', 'B', 'c', 'D', 'e', 'F', 'g')

// With chunked we specify the number of items
// that should be grouped together into a new list.
// The return type is a List of List instances.
assert(letters.chunked(3) == listOf(
    listOf('a', 'B', 'c'),
    listOf('D', 'e', 'F'),
    listOf('g')))

assert(letters.chunked(4) == listOf(
    listOf('a', 'B', 'c', 'D'),
    listOf('e', 'F', 'g')))


// If the number of items left is less than the given
// size it is a "remainder".
// In the following helper function we can include or
// exclude the remainder List.
fun chunckedRemainder(chars: List<Char> , size: Int, remainder: Boolean) =
    if (remainder) chars.chunked(size)
    else chars.chunked(size).filter { it.size == size }

// We can skip the remainder in the end result.
assert(chunckedRemainder(letters, 4, false) == listOf(
    listOf('a', 'B', 'c', 'D')))

assert(chunckedRemainder(letters, 4, true) == listOf(
    listOf('a', 'B', 'c', 'D'),
    listOf('e', 'F', 'g')))


// We can use a second argument and provide a transformation
// lambda function to transform the lists that are returned
// from chunking the original collection.
val chunkedJoined = letters.chunked(3) { list -> list.joinToString(separator = "") }

assert(chunkedJoined == listOf("aBc", "DeF", "g"))

Written with Kotlin 1.7.21.

December 15, 2022

Kotlin Kandy: Split Collection Or String With Partition

The method partition is available in Kotlin for arrays and iterable objects to split it into two lists. We pass a predicate lambda function to the partition method. The predicate should return either true or false based on a condition for each element from the array or iterable. The return result is a Pair instance where the first element is a List object with all elements that returned true from the predicate. The second element in the Pair object contains all elements for which the predicate returned false. As a String can be seen as an iterable of characters we can also use partition on a String instance.

In the following example code we use partition on different objects:

// Create an infinite sequence of increasing numbers.
val numbers = generateSequence(0) { i -> i + 1 }

// We take the first 20 numbers from our sequence and
// partition it to two pairs.
// First element of the pair is a list of all even numbers,
// second element is the list of all odd numbers.
// We use destructurizing to assign the pair values to
// variables even and odd.
val (even, odd) = numbers.take(20)
    .partition { n -> n % 2 == 0}

assert(even == listOf(0, 2, 4, 6, 8, 10, 12, 14, 16, 18))
assert(odd == listOf(1, 3, 5, 7, 9, 11, 13, 15, 17, 19))


// Sample map with data.
val data = mapOf("language" to "Java", "username" to "mrhaki", "age" to 49)

// We can also use partition on the entries of the map.
val (stringValues, nonStringValues) = data.entries.partition { entry -> entry.value is String }

assert(stringValues.associate { it.toPair() } == mapOf("language" to "Java", "username" to "mrhaki"))
assert(nonStringValues.associate { it.toPair() } == mapOf("age" to 49))


// Sample string to use with partition.
val s = "Kotlin kandy!"

// We can also use partition on a string where
// the predicate is applied for each character.
val (letters, others) = s.partition(Char::isLetter)

assert(letters == "Kotlinkandy")
assert(others == " !")

Written with Kotlin 1.7.20.

December 14, 2022

Kotlin Kandy: Taking Or Dropping Characters From A String

Kotlin adds a lot of extension methods to the String class. For example we can use the take method to get a certain number of characters from the start of a string value. With the drop method where we remove a given number of characters from the start of the string to get a new string. We can also take and drop a certain number of characters from the end of a string using the methods takeLast and dropLast.

Instead of using the number of characters we want to take or drop we can also use a condition defined with a predicate lambda function. We take or drop characters as long as the lambda returns true. The names of the methods for taking characters are takeWhile and takeLastWhile and for dropping characters dropWhile and dropLastWhile.

In the following example we use different methods to take and drop characters from a string value to get a new string value:

val s = "Kotlin kandy!"

// Take the first 6 characters.
assert(s.take(6) == "Kotlin")

// Take the last 6 characters.
assert(s.takeLast(6) == "kandy!")

// Take characters until lowercase k is encountered.
assert(s.takeWhile { it != 'k'} == "Kotlin ")

// Take characters from the end of the string
// to the beginning until a space is encountered.
assert(s.takeLastWhile { it != ' '} == "kandy!")


// Drop the first 7 characters.
assert(s.drop(7) == "kandy!")

// Drop the last 7 characters.
assert(s.dropLast(7) == "Kotlin")

// Drop characters until a lowercase k is encountered.
assert(s.dropWhile { it != 'k'} == "kandy!")

// Drop characters starting at the end of the string
// until a space is encountered.
assert(s.dropLastWhile { it != ' '} == "Kotlin ")

Written with Kotlin 1.7.20.

December 13, 2022

Kotlin Kandy: Find Common Prefix Or Suffix In Strings

If we want to find the longest shared prefix or suffix for two string values we can use the String extension methods commonPrefixWith and commonSuffixWith. The result is the prefix or suffix value that is common for both values. We can pass a second argument to the method to indicate if we want to ignore the casing of the letters. The default value for this argument is false, so if we don’t set it explicitly the casing of the letters should also match.

In the following example we use the commonPrefixWith and commonSuffixWith methods:

// Find the common prefix of 2 strings.
assert("Sample text".commonPrefixWith("Sampler") == "Sample")

// The second argument can be used to ignore the case
// of letters. By default this is false.
assert("sample text".commonPrefixWith("Sampler", true) == "sample")
assert("sample text".commonSuffixWith("Sampler") == "")


// Find the common suffix of 2 strings.
assert("Sample string".commonSuffixWith("Example thing") == "ing")


// Sample list of string values with a common prefix.
// We want to find the common prefix for these string values.
val values = listOf("Sample value", "Salt", "Sample string", "Sampler")

val commonPrefix = values
    // Combine each value with the next in a pair
    .zipWithNext()
    // Transform each pair into the common prefix of the
    // first and second element from the pair.
    .map { pair -> pair.first.commonPrefixWith(pair.second) }
    // The shortest common prefix is the winner.
    .minBy { common -> common.length }

assert(commonPrefix == "Sa")

Written with Kotlin 1.7.2.0.

December 11, 2022

Kotlin Kandy: Transform Items In A Collection To A Map With associate

Kotlin gives us the associate method for collection objects, like lists, iterables and arrays. With this method we can convert the items in the collection to a new Map instance. The associate method accepts a lambda function as argument and we must return a Pair from the lambda. The first item of the pair will be the key and the second element is the value of the key/value pair in the resulting map.

If we want to use the elements in our collection as key, but want to transform the value we must use associateWith. The lambda for this method must return the value part of our key/value pair. Alternatively if we only want to transform the key value we can use associateBy with one lambda function. The lambda function must return the result for the key in the key/value pair of the map. The method associateBy is overloaded where we can pass two lambda functions. The first lambda function is for transforming the key and the second lambda function is for transforming the value.

In the following example we use all variants of the associate methods:

// Sample list of string vlaues.
val languages = listOf("Kotlin", "Groovy", "Java", "Clojure")

// With associate we use a lambda to return a Pair.
// The Pair is the key/value for the resulting map.
// In this example we use the item in lowercase as key and
// the length of the item as value.
assert(languages.associate { s -> s.lowercase() to s.length } ==
         mapOf("kotlin" to 6, "groovy" to 6,
               "java" to 4, "clojure" to 7))

// associateBy accepts a lambda to return the key value
// of a pair and the value is the element from the collection.
// Here we want to use the lowercase value of the item as key.
assert(languages.associateBy(String::lowercase) ==
         mapOf("kotlin" to "Kotlin", "groovy" to "Groovy",
               "java" to "Java", "clojure" to "Clojure"))

// associateBy accepts a second lambda to also transform
// the value part of the key/value pair.
// With a second lambda we transform the item to it's length as value.
assert(languages.associateBy(String::lowercase, String::length) ==
         mapOf("kotlin" to 6, "groovy" to 6,
                "java" to 4, "clojure" to 7))

// associateWith accepts a lambda to transform the value part
// of the key/value pair. The key is then the element
// from the collection.
// We use the item of the collectio as key, but use the
// length of the string value as value for our key.
assert(languages.associateWith(String::length) ==
         mapOf("Kotlin" to 6, "Groovy" to 6,
                "Java" to 4, "Clojure" to 7))

Each of the assiocate methods return a Map with key/value pairs. If we want to add the result to an existing, mutable Map instance we must use the methods associateTo, associateByTo and associateWithTo. The first argument is a mutable Map and the rest of the arguments is the same as for the associate methods without To.

In the following example we want to add new key/value pairs to an existing Map using associate method variants:

// Example list of some numbers.
val numbers = listOf(2, 3, 4)

// Helper function to return the square value of a given number.
fun square(n: Int) = n * n

// For each of the associate/associateBy/associateWith methods there is
// an equivalent associateTo/associateByTo/associateWithTo to add the result
// to an existing mutable map instance.
assert(numbers.associateTo(mutableMapOf(0 to 0, 1 to 1)) { n -> n to square(n) } ==
         mapOf(0 to 0, 1 to 1, 2 to 4, 3 to 9, 4 to 16))
         
assert(numbers.associateByTo(mutableMapOf(0 to 0, 1 to 1)) { n -> n * 10 } ==
        mapOf(0 to 0, 1 to 1, 20 to 2, 30 to 3, 40 to 4))

assert(numbers.associateByTo(mutableMapOf(0 to 0, 1 to 1), { n -> n }, ::square) ==
         mapOf(0 to 0, 1 to 1, 2 to 4, 3 to 9, 4 to 16))

assert(numbers.associateWithTo(mutableMapOf(0 to 0, 1 to 1), ::square) ==
         mapOf(0 to 0, 1 to 1, 2 to 4, 3 to 9, 4 to 16))

Written with Kotlin 1.7.20.

December 10, 2022

Kotlin Kandy: Padding Strings

Kotlin extends the String class with a couple of padding methods. These methods allows us to define a fixed width a string value must occupy. If the string itself is less than the fixed width then the space is padded with spaces or any other character we define. We can pad to the left or the right of the string using the padStart and padEnd methods. When we don’t define an argument a space character is used for padding, but we can also add our own custom character as argument that will be used as padding character.

In the following example code we use the padEnd and padStart methods with and without arguments:

assert("Kotlin".padEnd(12) == "Kotlin      ")
assert("Kotlin".padStart(12) == "      Kotlin")

assert("Kotlin".padEnd(12, '-') == "Kotlin------")
assert("Kotlin".padStart(12, '.') == "......Kotlin")

val table = listOf(
    Triple("page1.html", 200, 1201),
    Triple("page2.html", 42, 8853),
    Triple("page3.html", 98, 3432),
    Triple("page4.html", 432, 900)
)

val output = table.map { data: Triple<String, Int, Int> ->
    data.first.padEnd(14, '.') +
            data.second.toString().padStart(5, '.') +
            data.third.toString().padStart(8)
}.joinToString(System.lineSeparator())

print(output)

assert(output == """
page1.html......200    1201
page2.html.......42    8853
page3.html.......98    3432
page4.html......432     900
""".trimIndent())

Written with Kotlin 1.7.20.