/tabulate

Kotlin library for exporting collections of object into tabular formats.

Primary LanguageKotlinApache License 2.0Apache-2.0

Tabulate logo

CodeFactor Build Status Maven Central

Tabulate helps you with exporting collections into tabular file formats.

Why ?

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 may want to use tabulate if:

  • 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.

Current version

Current version of library: Maven Central

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.
}

Basic usage

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)
    }
}
  1. xlsx file sheet name will be the same as table name,

  2. sheet will consist of columns each containing property values extracted by property getter references.

  3. 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")
    }
}
  1. 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
                }
            }
        }
    }
}
  1. 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.

Docs

HTML | PDF

Building

Import project into IDE as new gradle project.

You can also build and execute tests from console:

gradlew clean build

Contributing

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.

Roadmap

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.

v0.2.x [current]

  • Introduce 'proposals' which documents ideas that need to incubate, and may be included in future.

  • Introduce first proposal: documents API

v0.3.x

  • PDF table export operations.

v0.4.x

  • Multi-part output files. (chunking large files)

v0.5.x

  • Codegen for user defined attributes.

TBD …​

License

The project license file is available here.