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
}