Wiki
Clone wikiptt-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:
- Basic usage
- The concept - Gives some headstart to understand the concepts behind the library
- Table formatter - Configuring the table-scoped values: headings, borders.
- Column definition - Overview the column definition basics: stateless and stateful columns.
- Data extractor - Setting up the data extractor.
- Data converter - Setting up the data converter.
- Cell formatter - Setting up the cell formatter.
- Advanced features
- Special features - Introduction to templates, separator lines and special columns.
- Table Input Builder - A wrapper solution to get even more out of the formatter.
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:
-
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.
-
Data conversion: Then convert the extracted value to a string while applying type specific formatting on it.
-
Cell content formatting: Decorate the converted value to match the column specification.
-
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