Skip to content

Commit 96ade7d

Browse files
committed
Merge remote-tracking branch 'kopilov/excel'
# Conflicts: # src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/xlsx.kt
2 parents 2741859 + 67f47b7 commit 96ade7d

File tree

6 files changed

+206
-10
lines changed

6 files changed

+206
-10
lines changed

docs/StardustDocs/topics/write.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,60 @@ val jsonStr = df.toJson(prettyPrint = true)
5353

5454
You can write your dataframe in XLS, XLSX format to a file or `OutputStream`
5555

56+
<!---FUN writeXls-->
57+
58+
```kotlin
59+
df.writeExcel(file)
60+
```
61+
62+
<!---END-->
63+
5664
Values of ColumnGroup, FrameColumn, i.e. AnyRow, AnyFrame will be serialized as JSON objects.
5765

66+
If you work directly with Apache POI, you can use created Workbook and Sheets in your code:
67+
68+
<!---FUN writeXlsAppendAndPostProcessing-->
69+
70+
```kotlin
71+
/**
72+
* Do something with generated sheets. Here we set bold style for headers and italic style for first data column
73+
*/
74+
fun setStyles(sheet: Sheet) {
75+
val headerFont = sheet.workbook.createFont()
76+
headerFont.bold = true
77+
val headerStyle = sheet.workbook.createCellStyle()
78+
headerStyle.setFont(headerFont)
79+
80+
val indexFont = sheet.workbook.createFont()
81+
indexFont.italic = true
82+
val indexStyle = sheet.workbook.createCellStyle()
83+
indexStyle.setFont(indexFont)
84+
85+
sheet.forEachIndexed { index, row ->
86+
if (index == 0) {
87+
for (cell in row) {
88+
cell.cellStyle = headerStyle
89+
}
90+
} else {
91+
row.first().cellStyle = indexStyle
92+
}
93+
}
94+
}
95+
96+
// Create a workbook (or use existing)
97+
val wb = WorkbookFactory.create(true)
98+
99+
// Create different sheets from different data frames in the workbook
100+
val allPersonsSheet = df.writeExcel(wb, sheetName = "allPersons")
101+
val happyPersonsSheet = df.filter { person -> person.isHappy }.remove("isHappy").writeExcel(wb, sheetName = "happyPersons")
102+
val unhappyPersonsSheet = df.filter { person -> !person.isHappy }.remove("isHappy").writeExcel(wb, sheetName = "unhappyPersons")
103+
104+
// Do anything you want by POI
105+
listOf(happyPersonsSheet, unhappyPersonsSheet).forEach { setStyles(it) }
106+
107+
// Save the result
108+
wb.write(file.outputStream())
109+
wb.close()
110+
```
111+
112+
<!---END-->

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ commonsCsv = "1.8"
1212
commonsCompress = "1.21"
1313
klaxon = "5.5"
1414
fuel = "2.3.1"
15-
poi = "5.2.0"
15+
poi = "5.2.2"
1616
kotlinDatetime = "0.3.1"
1717

1818
junit = "4.13.2"

src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/xlsx.kt

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import org.apache.poi.ss.usermodel.Sheet
1212
import org.apache.poi.ss.usermodel.Workbook
1313
import org.apache.poi.ss.usermodel.WorkbookFactory
1414
import org.apache.poi.ss.util.CellReference
15+
import org.apache.poi.util.LocaleUtil
16+
import org.apache.poi.util.LocaleUtil.getUserTimeZone
1517
import org.apache.poi.xssf.usermodel.XSSFWorkbook
1618
import org.jetbrains.kotlinx.dataframe.AnyFrame
1719
import org.jetbrains.kotlinx.dataframe.AnyRow
@@ -29,7 +31,8 @@ import java.io.OutputStream
2931
import java.net.URL
3032
import java.time.LocalDate
3133
import java.time.LocalDateTime
32-
import java.util.*
34+
import java.util.Calendar
35+
import java.util.Date
3336

3437
public class Excel : SupportedFormat {
3538
override fun readDataFrame(stream: InputStream, header: List<String>): AnyFrame = DataFrame.readExcel(stream)
@@ -82,7 +85,7 @@ public fun DataFrame.Companion.readExcel(
8285
return wb.use { readExcel(it, sheetName, columns, rowsCount) }
8386
}
8487

85-
internal fun DataFrame.Companion.readExcel(
88+
public fun DataFrame.Companion.readExcel(
8689
wb: Workbook,
8790
sheetName: String? = null,
8891
columns: String? = null,
@@ -91,7 +94,14 @@ internal fun DataFrame.Companion.readExcel(
9194
val sheet: Sheet = sheetName
9295
?.let { wb.getSheet(it) ?: error("Sheet with name $sheetName not found") }
9396
?: wb.getSheetAt(0)
97+
return readExcel(sheet, columns, rowsCount)
98+
}
9499

100+
public fun DataFrame.Companion.readExcel(
101+
sheet: Sheet,
102+
columns: String? = null,
103+
rowsCount: Int? = null
104+
): AnyFrame {
95105
val columnIndexes = if (columns != null) {
96106
columns.split(",").flatMap {
97107
if (it.contains(":")) {
@@ -108,7 +118,12 @@ internal fun DataFrame.Companion.readExcel(
108118
val headerRow = sheet.getRow(0)
109119
val valueRows = sheet.drop(1).let { if (rowsCount != null) it.take(rowsCount) else it }
110120
val columns = columnIndexes.map { index ->
111-
val name = headerRow.getCell(index)?.stringCellValue ?: CellReference.convertNumToColString(index)
121+
val headerCell = headerRow.getCell(index)
122+
val name = if (headerCell?.cellType == CellType.NUMERIC) {
123+
headerCell.numericCellValue.toString() // Support numeric-named columns
124+
} else {
125+
headerCell?.stringCellValue ?: CellReference.convertNumToColString(index) // Use Excel column names if no data
126+
}
112127
val values = valueRows.map {
113128
val cell: Cell? = it.getCell(index)
114129
when (cell?.cellType) {
@@ -171,6 +186,17 @@ public fun <T> DataFrame<T>.writeExcel(
171186
factory: () -> Workbook
172187
) {
173188
val wb: Workbook = factory()
189+
writeExcel(wb, columnsSelector, sheetName, writeHeader)
190+
wb.write(outputStream)
191+
wb.close()
192+
}
193+
194+
public fun <T> DataFrame<T>.writeExcel(
195+
wb: Workbook,
196+
columnsSelector: ColumnsSelector<T, *> = { all() },
197+
sheetName: String? = null,
198+
writeHeader: Boolean = true
199+
): Sheet {
174200
val sheet = if (sheetName != null) {
175201
wb.createSheet(sheetName)
176202
} else {
@@ -189,6 +215,14 @@ public fun <T> DataFrame<T>.writeExcel(
189215
i++
190216
}
191217

218+
val createHelper = wb.creationHelper
219+
val cellStyleDate = wb.createCellStyle()
220+
val cellStyleDateTime = wb.createCellStyle()
221+
val cellStyleTime = wb.createCellStyle()
222+
cellStyleDate.dataFormat = createHelper.createDataFormat().getFormat("dd.mm.yyyy")
223+
cellStyleDateTime.dataFormat = createHelper.createDataFormat().getFormat("dd.mm.yyyy hh:mm:ss")
224+
cellStyleTime.dataFormat = createHelper.createDataFormat().getFormat("hh:mm:ss")
225+
192226
columns.forEach {
193227
val row = sheet.createRow(i)
194228
it.values().forEachIndexed { index, any ->
@@ -198,12 +232,35 @@ public fun <T> DataFrame<T>.writeExcel(
198232
if (any != null) {
199233
val cell = row.createCell(index)
200234
cell.setCellValueByGuessedType(any)
235+
236+
when (any) {
237+
is LocalDate, is kotlinx.datetime.LocalDate -> {
238+
cell.cellStyle = cellStyleDate
239+
}
240+
is Calendar, is Date -> {
241+
cell.cellStyle = cellStyleDateTime
242+
}
243+
is LocalDateTime -> {
244+
if (any.year < 1900) {
245+
cell.cellStyle = cellStyleTime
246+
} else {
247+
cell.cellStyle = cellStyleDateTime
248+
}
249+
}
250+
is kotlinx.datetime.LocalDateTime -> {
251+
if (any.year < 1900) {
252+
cell.cellStyle = cellStyleTime
253+
} else {
254+
cell.cellStyle = cellStyleDateTime
255+
}
256+
}
257+
else -> {}
258+
}
201259
}
202260
}
203261
i++
204262
}
205-
wb.write(outputStream)
206-
wb.close()
263+
return sheet
207264
}
208265

209266
private fun Cell.setCellValueByGuessedType(any: Any) {
@@ -221,16 +278,16 @@ private fun Cell.setCellValueByGuessedType(any: Any) {
221278
this.setCellValue(any)
222279
}
223280
is LocalDateTime -> {
224-
this.setCellValue(any)
281+
this.setTime(any)
225282
}
226283
is Boolean -> {
227284
this.setCellValue(any)
228285
}
229286
is Calendar -> {
230-
this.setCellValue(any.time)
287+
this.setDate(any.time)
231288
}
232289
is Date -> {
233-
this.setCellValue(any)
290+
this.setDate(any)
234291
}
235292
is RichTextString -> {
236293
this.setCellValue(any)
@@ -242,7 +299,7 @@ private fun Cell.setCellValueByGuessedType(any: Any) {
242299
this.setCellValue(any.toJavaLocalDate())
243300
}
244301
is kotlinx.datetime.LocalDateTime -> {
245-
this.setCellValue(any.toJavaLocalDateTime())
302+
this.setTime(any.toJavaLocalDateTime())
246303
}
247304
// Another option would be to serialize everything else to string,
248305
// but people can convert columns to string with any serialization framework they want
@@ -252,3 +309,25 @@ private fun Cell.setCellValueByGuessedType(any: Any) {
252309
}
253310
}
254311
}
312+
313+
/**
314+
* Set LocalDateTime value correctly also if date have zero value in Excel.
315+
* Zero date is usually used fore storing time component only,
316+
* is displayed as 00.01.1900 in Excel and as 30.12.1899 in LibreOffice Calc and also in POI.
317+
* POI can not set 1899 year directly.
318+
*/
319+
private fun Cell.setTime(localDateTime: LocalDateTime) {
320+
this.setCellValue(DateUtil.getExcelDate(localDateTime.plusDays(1)) - 1.0)
321+
}
322+
323+
/**
324+
* Set Date value correctly also if date have zero value in Excel.
325+
* Zero date is usually used fore storing time component only,
326+
* is displayed as 00.01.1900 in Excel and as 30.12.1899 in LibreOffice Calc and also in POI.
327+
* POI can not set 1899 year directly.
328+
*/
329+
private fun Cell.setDate(date: Date) {
330+
val calStart = LocaleUtil.getLocaleCalendar()
331+
calStart.time = date
332+
this.setTime(calStart.toInstant().atZone(getUserTimeZone().toZoneId()).toLocalDateTime())
333+
}

src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/XlsxTest.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,12 @@ class XlsxTest {
6868
val df = DataFrame.read(testResource("datetime.xlsx"))
6969
df["time"].type() shouldBe typeOf<LocalDateTime>()
7070
}
71+
72+
@Test
73+
fun `write date time`() {
74+
val df = DataFrame.read(testResource("datetime.xlsx"))
75+
val temp = Files.createTempFile("excel", ".xlsx").toFile()
76+
df.writeExcel(temp)
77+
DataFrame.readExcel(temp) shouldBe df
78+
}
7179
}

tests/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dependencies {
2020
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
2121
}
2222
testImplementation(libs.kotlin.datetimeJvm)
23+
testImplementation(libs.poi)
2324
}
2425

2526
kotlin.sourceSets {

tests/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Write.kt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ package org.jetbrains.kotlinx.dataframe.samples.api
22

33
import io.kotest.matchers.string.shouldStartWith
44
import org.apache.commons.csv.CSVFormat
5+
import org.apache.poi.ss.usermodel.Sheet
6+
import org.apache.poi.ss.usermodel.WorkbookFactory
7+
import org.jetbrains.kotlinx.dataframe.api.filter
8+
import org.jetbrains.kotlinx.dataframe.api.remove
59
import org.jetbrains.kotlinx.dataframe.io.toCsv
610
import org.jetbrains.kotlinx.dataframe.io.toJson
711
import org.jetbrains.kotlinx.dataframe.io.writeCSV
@@ -64,7 +68,56 @@ class Write : TestBase() {
6468
@Test
6569
fun writeXls() {
6670
useTempFile { file ->
71+
// SampleStart
6772
df.writeExcel(file)
73+
// SampleEnd
74+
}
75+
}
76+
77+
@Test
78+
fun writeXlsAppendAndPostProcessing() {
79+
useTempFile { file ->
80+
// SampleStart
81+
/**
82+
* Do something with generated sheets. Here we set bold style for headers and italic style for first data column
83+
*/
84+
fun setStyles(sheet: Sheet) {
85+
val headerFont = sheet.workbook.createFont()
86+
headerFont.bold = true
87+
val headerStyle = sheet.workbook.createCellStyle()
88+
headerStyle.setFont(headerFont)
89+
90+
val indexFont = sheet.workbook.createFont()
91+
indexFont.italic = true
92+
val indexStyle = sheet.workbook.createCellStyle()
93+
indexStyle.setFont(indexFont)
94+
95+
sheet.forEachIndexed { index, row ->
96+
if (index == 0) {
97+
for (cell in row) {
98+
cell.cellStyle = headerStyle
99+
}
100+
} else {
101+
row.first().cellStyle = indexStyle
102+
}
103+
}
104+
}
105+
106+
// Create a workbook (or use existing)
107+
val wb = WorkbookFactory.create(true)
108+
109+
// Create different sheets from different data frames in the workbook
110+
val allPersonsSheet = df.writeExcel(wb, sheetName = "allPersons")
111+
val happyPersonsSheet = df.filter { person -> person.isHappy }.remove("isHappy").writeExcel(wb, sheetName = "happyPersons")
112+
val unhappyPersonsSheet = df.filter { person -> !person.isHappy }.remove("isHappy").writeExcel(wb, sheetName = "unhappyPersons")
113+
114+
// Do anything you want by POI
115+
listOf(happyPersonsSheet, unhappyPersonsSheet).forEach { setStyles(it) }
116+
117+
// Save the result
118+
wb.write(file.outputStream())
119+
wb.close()
120+
// SampleEnd
68121
}
69122
}
70123

0 commit comments

Comments
 (0)