Wiki

Clone wiki

ptt-kotlin / Home

| Home | The concept >

Plain-text Table Formatter Kotlin DSL Guide

Introduction

Have you ever been lost finding data in a log file, when it was logged as plain text? Have you ever been trouble to look over a list of objects in a log file? Have you ever spent long hours to format tabular information in text files for better readability?

The Plan-text table formatter offers a fluent way to format and print your data as a table in text. The main goal of the library to offer a declarative way to describe the structure of the table, while give the freedom of printing any business object.

Where to go now?

To learn about the DSL elements in details, follow the following links:

What PTT is?

A lightweight and highly flexible and extensible library to configure plain text table.

Main functional features:

  • Direct user object to table conversion: no wrapping or intermediate classes are required. Any valid Java or Kotlin class is accepted.

  • Separated configuration and processing: easy reuse of configuration elements.

  • Separated conversion steps: the formatting is done in separated and independent steps. Each step has a well-defined scope of responsibility, keeping them simple and interoperable.

  • Sensible default values for easy prototyping: use defaults for quick output then fine tune it later.

  • Stateful columns and aggregation: columns may maintain state information and provide aggregated values (such as count, sum, avg, or moving window calculations).

  • Different layout presets: you can pick from one of the out-of-box layouts and use it as is, or tweak it for your needs.

  • CSV export support: out-of-box support for csv export.

Main architectural features:

  • Declarative DSL: declarative definition based on Kotlin internal DSL architecture

  • Modular configuration: splits the conversion process into steps, which makes it easy to add your implementation to the point you wish without much boiler code.

  • Most of the configuration classes are imutables for wide and safe reusability.

  • Ultra lightweight: the main jar is less than 100k, no external library requirements.

The concept behind

The concept behind the design of the library is to allow a fluent way to present any table-like data in plain text. To achieve flexibility while keeping simplicity, the formatting process is divided into several steps:

  1. Data extract: the cell value is extracted from the input record (both the source object and the extracted data may be any Java or Kotlin class). This is the only step depends on your business data structure.

  2. Data conversion: Then convert the extracted value to a string while applying type specific formatting on it.

  3. Cell content formatting: Decorate the converted value to match the column specification.

  4. Table build: build up the table from the cell contents and the border rules.

These four steps are independent and may be configured independently. For example, you may use the default formatting of your numeric data, but apply special cell content formatting.

For quick prototyping most of the steps has their sensible defaults.

Getting started

To get familiar with the DSL, let's see some very simple examples. The library offers much more, these examples will only scratch the surface.

The whole example is available on bitbucket.

Note: most of the examples in this tutorial uses the input class TutorialData:

data class TutorialData(
        val id: Long = 0,
        val name: String = "",
        val quantity: Double = 1.0,
        val date: LocalDateTime = LocalDateTime.now(),
        val duration: Duration = Duration.ZERO,
        val isValid: Boolean? = false,
        val length: Int = 0,
        val flags: List<String> = listOf())

1.1 The classic Hello world

Although this example has little practical advantage, it is a good start to create our first, simple table. Let's define the table:

    val formatter = tableFormatter<TutorialData> {
        simple("Column") { d -> d.name }
    }

Now we define the data set (containing only a single record), and the table formatter is applied on it:

    val data = listOf(
            TutorialData(name = "Hello world")
    )

    println(formatter.apply(data))

The result will be this:

+-------------+
| Column      |
+-------------+
| Hello world |
+-------------+

It was easy.

1.2 A more complex example

Let's see a little more complex one. Now we have two simple columns:

    val formatter = tableFormatter<TutorialData> {
        // For one-to-one data fetching, the column reference
        // format may also be used
        simple("Name", TutorialData::name)
        // Here we align the cell to the right side
        simple("Quantity", right, TutorialData::quantity)
    }

If the execution part is the following:

    val data = listOf(
            TutorialData(name = "apple", quantity = 10.0),
            TutorialData(name = "banana", quantity = 5.5)
    )

    println(formatter.apply(data))

The output would be the following:

+--------+----------+
| Name   | Quantity |
+--------+----------+
| apple  |     10.0 |
| banana |      5.5 |
+--------+----------+

Let's have a look under the hood. Let's see what happened when the quantity column of the first row is calculated:

Step Description Input Output
1. Data extractor The data extractor extracts the value from the field quantity of the object and returns it as a double. TutorialData.quantity Double(10.0)
2. Data converter The data converter takes the extracted value and formats it into a String. The default formatter we use simply converts the value into a string. Double(10.0) String("10.0")
3. Cell content formatter The algorithm previously calculated the width of the column (it is the width of the title, which is 8). The cell content formatter takes the String "10.0" and right align it, padding to the length of 8. (We indicating the spaces as ° for presentation only.) String("10.0") String("°°°°10.0")
4. Table formatter The border and padding is added to the cell value according the default border settings. String("°°°°10.0") String("|°°°°°10.0°|")

1.3 Let's sum it

The table column values may be aggregated (summed, counted, calculating average, etc.). This needs a little more work, because the column has to be aggregated, so it has to be stateful.

Let's see the table definition:

    // We need a state class to maintain the aggregation state
    class TutorialAggregator(var sum: Double = 0.0)

    val formatter = tableFormatter<TutorialData> {
        // Enables aggregation
        showAggregation=true

        simple("Name", TutorialData::name)

        // A stateful column is a little more complex
        stateful<Double, TutorialAggregator>("Quantity", right) {
            // Initialization of the aggregation state
            initState { TutorialAggregator() }

            // The extractor of a stateful column has an additional 
            // responsibility above returning the value: it has to 
            // maintain the aggregation state
            extractor { d, a ->
                a.sum += d.quantity
                d.quantity
            }

            // The aggregator produces the aggregated value
            aggregator { _, a -> a.sum }
        }
    }

Running it on the same input as in the previous example:

    val data = listOf(
            TutorialData(name = "apple", quantity = 10.0),
            TutorialData(name = "banana", quantity = 5.5)
    )

    println(formatter.apply(data))

would produce the following output:

+--------+----------+
| Name   | Quantity |
+--------+----------+
| apple  |     10.0 |
| banana |      5.5 |
+--------+----------+
|        |     15.5 |
+--------+----------+

| Home | The concept >

Updated