Tabulate helps you with exporting collections into tabular file formats.
Exporting data to tabular file formats can be tedious and cumbersome - especially when business wants to have reports covering vast majority of system functionalities. Writing every exporting method using imperative API directly will soon make code verbose, error prone, hard to read and maintain. In such cases You want to hide implementation details using abstractions, but this is additional effort which is not desirable.
Tabulate
tries to mitigate above problems with the help of Kotlin
, its type-safe DSL builders
and extension functions
.
-
You need to export data into following formats:
-
excel (xlsx),
-
csv,
-
pdf (work in progress),
-
cli (planned),
-
-
Exported data needs to be in tabular format,
-
You want to reuse table definition for exports targeting multiple file formats,
-
You need to preserve look & feel across different file formats,
-
You want to customise table look & feel by your own-crafted attributes.
Gradle script dependency setup:
dependencies { implementation(platform("io.github.voytech:tabulate-bom:0.1.1")) implementation("io.github.voytech","tabulate-core") // DSL, model, attributes, core logic. implementation("io.github.voytech","tabulate-excel") // streaming apache POI excel API. }
Let’s start simple. Given list of contracts as below:
val contracts = listOf(
Contract(
client = "Apollo",
contractCode = "2011/12/AP",
contractType = ContractType.FIXED_PRICE,
contractLength = 12,
dateSigned = LocalDate.parse("2011-12-23"),
expirationDate = LocalDate.parse("2012-12-23"),
dateOfFirstPayment = LocalDate.parse("2011-12-31"),
lastPaymentDate = LocalDate.parse("2012-12-23"),
monthlyGrossValue = BigDecimal.valueOf(200)
),
Contract(
client = "Columbia",
contractCode = "2021/12/CO",
contractType = ContractType.FIXED_PRICE,
contractLength = 12,
dateSigned = LocalDate.parse("2021-12-13"),
expirationDate = LocalDate.parse("2022-12-13"),
dateOfFirstPayment = LocalDate.parse("2021-12-31"),
lastPaymentDate = LocalDate.parse("2022-12-13"),
monthlyGrossValue = BigDecimal.valueOf(250)
)
)
Invoke tabulate
extension function with table builder (lambda with receiver) as argument:
contracts.tabulate("contracts_list.xlsx") {
name = "Active Contracts" // (1)
columns { // (2)
column(Contract::client)
column(Contract::contractCode)
column(Contract::contractLength)
column(Contract::dateSigned)
column(Contract::expirationDate)
column(Contract::dateOfFirstPayment)
column(Contract::lastPaymentDate)
column(Contract::monthlyGrossValue)
}
attributes {
columnWidth { auto = true } // (3)
}
}
-
xlsx file sheet name will be the same as table name,
-
sheet will consist of columns each containing property values extracted by property getter references.
-
column widths will be automatically adjusted to match the widest cell value.
Running above code produces xlsx file with data, but without any additional features like headers or cell styles.
Let’s add header now:
contracts.tabulate("contracts_list.xlsx") {
name = "Active Contracts"
attributes {
columnWidth { auto = true }
}
columns {
column(Contract::client)
column(Contract::contractCode)
column(Contract::contractLength)
column(Contract::dateSigned)
column(Contract::expirationDate)
column(Contract::dateOfFirstPayment)
column(Contract::lastPaymentDate)
column(Contract::monthlyGrossValue)
}
rows {
// (1)
header("Client", "Code", "Contract Length",
"Date Signed", "Expiration Date", "First Payment",
"Last Payment","Monthly Gross Value")
}
}
-
In fact there are many ways to add header. This one is the simplest. It is built-in
RowBuilderApi
extension function which takes column names and creates row with custom cells at index 0.
Now let’s add some style attributes. This should highlight header from remaining rows:
contracts.tabulate("contracts_list.xlsx") {
name = "Active Contracts"
attributes {
columnWidth { auto = true }
}
columns {
column(Contract::client)
column(Contract::contractCode)
column(Contract::contractLength)
column(Contract::dateSigned)
column(Contract::expirationDate)
column(Contract::dateOfFirstPayment)
column(Contract::lastPaymentDate)
column(Contract::monthlyGrossValue)
}
rows {
header {
columnTitles("Client", "Code", "Contract Length",
"Date Signed", "Expiration Date", "First Payment",
"Last Payment","Monthly Gross Value")
// (1)
attributes {
text { fontColor = Colors.WHITE }
background {
color = Colors.BLACK
}
}
}
}
}
-
To add attributes to header, we used overloaded
header
extension function.
So far, we were passing table definition directly to tabulate
method.
In real life scenario we may want to keep table definition as separate variable:
val contractsTable = typedTable<Contract> {
name = "Active contracts"
attributes {
columnWidth { auto = true }
}
columns {
column(Contract::client)
column(Contract::contractCode)
column(Contract::contractLength)
column(Contract::dateSigned)
column(Contract::expirationDate)
column(Contract::dateOfFirstPayment)
column(Contract::lastPaymentDate)
column(Contract::monthlyGrossValue)
}
rows {
header {
columnTitles("Client", "Code", "Contract Length", "Date Signed", "Expiration Date", "First Payment", "Last Payment","Monthly Gross Value")
attributes {
text { fontColor = Colors.WHITE }
background { color = Colors.BLACK }
}
}
}
}
Now, we can reuse table definition each time we want to export contracts collection:
contracts.tabulate("contracts.xlsx", contractsTable)
If You require different file format, just specify file name with adequate extension (e.g: .csv or .pdf):
contracts.tabulate("contracts.csv", contractsTable)
contracts.tabulate("contracts.pdf", contractsTable)
Note
|
Pdf format is not supported yet. |
Keeping table definition as a separate object is a first step into composing tables.
Suppose we have contractsTable
definition, and we want to reuse it to export past contracts. The only difference in here is the sheet name.
We can achieve this by overriding
existing definition:
contracts.tabulate("past_contracts.xlsx", contractsTable + { name = "Past Contracts" })
Above syntax is intuitive and shows some powers of Kotlin.
We have used ` operator in order to merge two table definitions.
Merging evaluates in the same order as normal method's arguments.
Logic behind this feature is very simple - `
operator takes two lambdas with receiver, then it returns another lambda with receiver which internally delegates invocations to original lambdas one by one.
Effectively it is nothing more than receiver configuration and re-configuration (invocation of subsequent builders on the same receiver one by one).
This is simple solution, yet imposes few restrictions on how to manage underlying builder state.
(Explanation is out of the scope of this README file.
I will try to cover this subject in more details in documentation)
Far more real-life templating example:
val headerStyles = table {
rows {
matching { header() } assign {
attributes {
background {
color = Colors.BLACK
}
text {
fontColor = Colors.WHITE
}
}
}
}
}
contracts.tabulate("contracts_list.xlsx", headerStyles + {
columns {
column(Contract::client)
column(Contract::contractCode)
column(Contract::contractLength)
column(Contract::dateSigned)
column(Contract::expirationDate)
column(Contract::dateOfFirstPayment)
column(Contract::lastPaymentDate)
column(Contract::monthlyGrossValue)
}
rows {
header("Client", "Code", "Contract Length",
"Date Signed", "Expiration Date", "First Payment",
"Last Payment","Monthly Gross Value")
}
})
In above example we can see that headerStyles
does not specify type of collection element. That means that you are no longer restricted to exporting contracts. This way you can achieve style consistency across applications.
Import project into IDE as new gradle project.
You can also build and execute tests from console:
gradlew clean build
Firstly, create an issue describing a bug, idea or enhancement You would like to deliver. After conversation thread concludes with approval, You can develop solution to problem and submit pull request.
Starting from version 0.1.0, minor version will advance relatively fast due to tiny milestones. This is because of one person (me) who is currently in charge, and due to my intention of working within non-blocking release cycles.
-
Introduce 'proposals' which documents ideas that need to incubate, and may be included in future.
-
Introduce first proposal: documents API
The project license file is available here.