Skip to content

Commit

Permalink
Merge pull request #20 from 4sh/faster-xlsx-autosizecolumn-method
Browse files Browse the repository at this point in the history
Faster xlsx autosizecolumn method
  • Loading branch information
mathiasmuller4sh authored Feb 6, 2025
2 parents f43b0bf + 75a0c45 commit ec5e0a4
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 30 deletions.
96 changes: 66 additions & 30 deletions src/main/kotlin/io/retable/RetableExcelSupport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ package io.retable

import org.apache.poi.common.usermodel.HyperlinkType
import org.apache.poi.ss.usermodel.*
import org.apache.poi.xssf.usermodel.XSSFCell
import org.apache.poi.xssf.usermodel.XSSFCellStyle
import org.apache.poi.xssf.usermodel.XSSFFont
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.apache.poi.xssf.usermodel.*
import java.io.InputStream
import java.io.OutputStream
import java.text.SimpleDateFormat
Expand All @@ -21,7 +18,8 @@ class ExcelReadOptions(
trimValues: Boolean = true,
ignoreEmptyLines: Boolean = true,
firstRecordAsHeader: Boolean = true,
val defaultDateTimeFormatter: DateTimeFormatter? = null
val defaultDateTimeFormatter: DateTimeFormatter? = null,
val useExperimentalFasterAutoSizeColumn: Boolean = false
) : ReadOptions(trimValues, ignoreEmptyLines, firstRecordAsHeader)

class RetableExcelSupport<T : RetableColumns>(
Expand Down Expand Up @@ -82,41 +80,66 @@ class RetableExcelSupport<T : RetableColumns>(
)
}

records.forEach { record ->
val maxColumnLength = records.map { record ->
val row = sheet.createRow(record.lineNumber.toInt() - 1)
columns.list().forEach { col ->
record[col]?.let { value ->
val cell = row.createCell(col.index - 1)
when (value) {
is Number -> {
cell.cellType = CellType.NUMERIC
cell.setCellValue(value.toDouble())
}
is LocalDate -> writeLocalDateCell(cell, styleDate, value)
is Instant -> cell.setCellValue(Date(value.toEpochMilli()))
else -> {
val stringValue = value.toString()
cell.setCellValue(stringValue)
if (col.writeUrlAsHyperlink && stringValue.isUrl()) {
cell.hyperlink = workbook.creationHelper.createHyperlink(HyperlinkType.URL)
.also { it.address = stringValue }
cell.cellStyle = hyperlinkStyle
} else {
cell.cellStyle = styleText
}
}
columns.list().map { col ->
createCell(record, col, row, workbook, styleDate, hyperlinkStyle, styleText)
}
}

if (options.useExperimentalFasterAutoSizeColumn) {
sheet.autoSizeColumn(
maxColumnLength.reduce { acc, value ->
acc.mapIndexed { index, length ->
maxOf(length, value[index], Comparator.comparing(ColLength::length))
}
}
.toList()
)
} else {
maxColumnLength.toList()
for (index in 0 until columns.maxIndex) {
sheet.autoSizeColumn(index)
}
}

for (index in 0..columns.maxIndex - 1) {
sheet.autoSizeColumn(index)
}
workbook.write(outputStream)
workbook.close()
}

private fun createCell(
record: RetableRecord,
col: RetableColumn<*>,
row: XSSFRow,
workbook: XSSFWorkbook,
styleDate: XSSFCellStyle,
hyperlinkStyle: XSSFCellStyle,
styleText: XSSFCellStyle
): ColLength =
record[col]?.let { value ->
val cell = row.createCell(col.index - 1)
when (value) {
is Number -> {
cell.cellType = CellType.NUMERIC
cell.setCellValue(value.toDouble())
}
is LocalDate -> writeLocalDateCell(cell, styleDate, value)
is Instant -> cell.setCellValue(Date(value.toEpochMilli()))
else -> {
val stringValue = value.toString()
cell.setCellValue(stringValue)
if (col.writeUrlAsHyperlink && stringValue.isUrl()) {
cell.hyperlink = workbook.creationHelper.createHyperlink(HyperlinkType.URL)
.also { it.address = stringValue }
cell.cellStyle = hyperlinkStyle
} else {
cell.cellStyle = styleText
}
}
}
ColLength(cell.columnIndex, cell.toString().length)
} ?: ColLength(col.index - 1, 0)

private fun writeLocalDateCell(cell: XSSFCell, style: XSSFCellStyle, value: LocalDate) {
cell.cellStyle = style
val calendar = Calendar.getInstance()
Expand Down Expand Up @@ -168,3 +191,16 @@ class RetableExcelSupport<T : RetableColumns>(
private fun String.isUrl(): Boolean =
take(7) == "http://" || take(8) == "https://"
}

data class ColLength(
val index: Int,
val length: Int
)
private fun Sheet.autoSizeColumn(maxColumnLength: List<ColLength>) {
maxColumnLength.forEach {
if (it.length > 0) {
val width = (it.length * 1.14388 * 256).toInt()
setColumnWidth(it.index, if (width > 255 * 256) 254 * 256 else width)
}
}
}
106 changes: 106 additions & 0 deletions src/test/kotlin/io/retable/RetablePerformanceTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package io.retable

import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import strikt.api.expectThat
import strikt.assertions.isTrue
import java.io.File
import java.time.Instant
import java.time.LocalDate
import java.util.*

class RetablePerformanceTest {

fun List<Char>.random() = this[Random().nextInt(this.size)]

fun buildRandomString(minLength: Int, variation: Int): String {
val chars = ('A'..'Z') + ('a'..'z')
val length = Random().nextInt(variation) + minLength
return (1..length).map { chars.random() }
.joinToString("")
}

@ParameterizedTest
// @ValueSource(ints = [1_000, 10_000, 20_000, 30_000, 40_000])
@ValueSource(ints = [40_000])
fun `should list columns`(recordNumber: Int) {
val columns = object : RetableColumns() {
val FIRST_NAME = string("first_name", index = 2)
val LAST_NAME2 = string("last_name1", index = 4)
val LAST_NAME3 = string("last_name2", index = 5)
val LAST_NAME4 = string("last_name3", index = 6)
val LAST_NAME5 = string("last_name4", index = 7)
val AGE = int("age", index = 3)
val DATE_2 = localDate("localDate", index = 9)
}
val values = (0..recordNumber).map {
Person2(
buildRandomString(3, 6),
buildRandomString(3, 30),
buildRandomString(3, 30),
buildRandomString(3, 30),
buildRandomString(3, 30),
buildRandomString(3, 30),
Random().nextInt(30)
)
}

var resultFilePath = pathTo("export_${recordNumber}_EXPERIMENTAL_records_indexed_cols.xlsx")

val retable = Retable(columns)
.data(
values
) {
mapOf(
FIRST_NAME to it.firstName,
LAST_NAME2 to it.lastName2,
LAST_NAME3 to it.lastName3,
LAST_NAME4 to it.lastName4,
LAST_NAME5 to it.lastName5,
AGE to it.age,
DATE_2 to it.date2
)
}

var start = Instant.now().toEpochMilli()
retable.write(
Retable.excel(
columns,
ExcelReadOptions(useExperimentalFasterAutoSizeColumn = true)
) to File(resultFilePath).outputStream()
)
var end = Instant.now().toEpochMilli()
val customDuration = end - start
println("Write Duration (Experimental) : $customDuration")
expectThat(File(resultFilePath)) {
get { exists() }.isTrue()
}

resultFilePath = pathTo("export_${recordNumber}_POI_records_indexed_cols.xlsx")
start = Instant.now().toEpochMilli()
retable
.write(Retable.excel(columns) to File(resultFilePath).outputStream())
end = Instant.now().toEpochMilli()
val classicDuration = end - start
println("Write Duration (ApachePoi) : $classicDuration")
expectThat(File(resultFilePath)) {
get { exists() }.isTrue()
}

println("Write Duration (Experimental / ApachePoi) : $customDuration / $classicDuration")
}

fun pathTo(s: String) = "src/test/resources/examples/$s"

data class Person2(
val firstName: String,
val lastName: String,
val lastName2: String,
val lastName3: String,
val lastName4: String,
val lastName5: String,
val age: Int,
val date1: Instant = Instant.now(),
val date2: LocalDate = LocalDate.now()
)
}

0 comments on commit ec5e0a4

Please sign in to comment.