diff --git a/README.md b/README.md index 224d943..1f5fa70 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,7 @@ val writer = csvWriter { | nullCode | `(empty string)` | Character used when a written field is null value. | | lineTerminator | `\r\n` | Character used as line terminator. | | outputLastLineTerminator | `true` | Output line break at the end of file or not. | +| prependBOM | `false` | Output BOM (Byte Order Mark) at the beginning of file or not. | | quote.char | `"` | Character to quote each fields. | | quote.mode | `CANONICAL` | Quote mode.
- `CANONICAL`: Not quote normally, but quote special characters (quoteChar, delimiter, line feed). This is [the specification of CSV](https://tools.ietf.org/html/rfc4180#section-2).
- `ALL`: Quote all fields.
- `NON_NUMERIC`: Quote non-numeric fields. (ex. 1,"a",2.3) | diff --git a/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/BufferedLineReader.kt b/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/BufferedLineReader.kt index bb93c9c..5d63ed7 100644 --- a/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/BufferedLineReader.kt +++ b/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/BufferedLineReader.kt @@ -1,5 +1,7 @@ package com.github.doyaaaaaken.kotlincsv.client +import com.github.doyaaaaaken.kotlincsv.util.Const + /** * buffered reader which can read line with line terminator */ @@ -7,7 +9,7 @@ internal class BufferedLineReader( private val br: Reader ) { companion object { - private const val BOM = '\uFEFF' + private const val BOM = Const.BOM } private fun StringBuilder.isEmptyLine(): Boolean = diff --git a/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/context/CsvWriterContext.kt b/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/context/CsvWriterContext.kt index cb4114a..20f0c9f 100644 --- a/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/context/CsvWriterContext.kt +++ b/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/context/CsvWriterContext.kt @@ -57,6 +57,12 @@ interface ICsvWriterContext { */ val outputLastLineTerminator: Boolean + /** + * Output BOM (Byte Order Mark) at the beginning of file or not. + * See https://github.com/doyaaaaaken/kotlin-csv/issues/84 + */ + val prependBOM: Boolean + /** * Options about quotes of each fields */ @@ -75,6 +81,7 @@ class CsvWriterContext : ICsvWriterContext { override var nullCode: String = "" override var lineTerminator: String = "\r\n" override var outputLastLineTerminator = true + override var prependBOM = false override val quote: CsvWriteQuoteContext = CsvWriteQuoteContext() fun quote(init: CsvWriteQuoteContext.() -> Unit) { diff --git a/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/parser/ParseStateMachine.kt b/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/parser/ParseStateMachine.kt index edaa54b..e4d06c8 100644 --- a/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/parser/ParseStateMachine.kt +++ b/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/parser/ParseStateMachine.kt @@ -1,6 +1,7 @@ package com.github.doyaaaaaken.kotlincsv.parser import com.github.doyaaaaaken.kotlincsv.util.CSVParseFormatException +import com.github.doyaaaaaken.kotlincsv.util.Const /** * @author doyaaaaaaken @@ -11,7 +12,7 @@ internal class ParseStateMachine( private val escapeChar: Char ) { - private val BOM = '\uFEFF' + private val BOM = Const.BOM private var state = ParseState.START diff --git a/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/util/Const.kt b/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/util/Const.kt index 6e48a4e..a5cae91 100644 --- a/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/util/Const.kt +++ b/src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/util/Const.kt @@ -6,5 +6,7 @@ package com.github.doyaaaaaken.kotlincsv.util * @author doyaaaaaken */ internal object Const { - val defaultCharset = "UTF-8" + const val defaultCharset = "UTF-8" + + const val BOM = '\uFEFF' } diff --git a/src/jvmMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvFileWriter.kt b/src/jvmMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvFileWriter.kt index 2ddbe46..95dfd58 100644 --- a/src/jvmMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvFileWriter.kt +++ b/src/jvmMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvFileWriter.kt @@ -2,6 +2,7 @@ package com.github.doyaaaaaken.kotlincsv.client import com.github.doyaaaaaken.kotlincsv.dsl.context.CsvWriterContext import com.github.doyaaaaaken.kotlincsv.dsl.context.WriteQuoteMode +import com.github.doyaaaaaken.kotlincsv.util.Const import java.io.Closeable import java.io.Flushable import java.io.IOException @@ -85,6 +86,10 @@ class CsvFileWriter internal constructor( } private fun writeNext(row: List) { + if (!hasWroteInitialChar && ctx.prependBOM) { + writer.print(Const.BOM) + } + val rowStr = row.joinToString(ctx.delimiter.toString()) { field -> if (field == null) { ctx.nullCode diff --git a/src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvWriterTest.kt b/src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvWriterTest.kt index 1a50b49..511dfcf 100644 --- a/src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvWriterTest.kt +++ b/src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvWriterTest.kt @@ -32,6 +32,8 @@ class CsvWriterTest : WordSpec({ delimiter = '\t' nullCode = "NULL" lineTerminator = "\n" + outputLastLineTerminator = false + prependBOM = true quote { char = '\'' mode = WriteQuoteMode.ALL @@ -43,6 +45,8 @@ class CsvWriterTest : WordSpec({ writer.delimiter shouldBe '\t' writer.nullCode shouldBe "NULL" writer.lineTerminator shouldBe "\n" + writer.outputLastLineTerminator shouldBe false + writer.prependBOM shouldBe true writer.quote.char = '\'' writer.quote.mode = WriteQuoteMode.ALL } @@ -272,6 +276,18 @@ class CsvWriterTest : WordSpec({ val actual = readTestFile() actual shouldBe expected } + "write simple csv with prepending BOM" { + val row1 = listOf("a", "b") + val row2 = listOf("c", "d") + val expected = "\uFEFFa,b\r\nc,d\r\n" + csvWriter { + prependBOM = true + }.open(File(testFileName)) { + writeRows(listOf(row1, row2)) + } + val actual = readTestFile() + actual shouldBe expected + } "write simple csv with disabled last line terminator multiple writes" { val row1 = listOf("a", "b") val row2 = listOf("c", "d") diff --git a/src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/CsvWriterDslTest.kt b/src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/CsvWriterDslTest.kt index f998ba3..bd2d51a 100644 --- a/src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/CsvWriterDslTest.kt +++ b/src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/CsvWriterDslTest.kt @@ -22,6 +22,7 @@ class CsvWriterDslTest : StringSpec({ nullCode = "NULL" lineTerminator = "\n" outputLastLineTerminator = false + prependBOM = true quote { char = '\'' mode = WriteQuoteMode.ALL @@ -33,6 +34,7 @@ class CsvWriterDslTest : StringSpec({ writer.nullCode shouldBe "NULL" writer.lineTerminator shouldBe "\n" writer.outputLastLineTerminator shouldBe false + writer.prependBOM shouldBe true writer.quote.char shouldBe '\'' writer.quote.mode = WriteQuoteMode.ALL }