├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── improvement-request.md └── workflows │ └── build_and_test.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── codecov.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── commonMain └── kotlin │ └── com │ └── github │ └── doyaaaaaken │ └── kotlincsv │ ├── client │ ├── BufferedLineReader.kt │ ├── CsvFileReader.kt │ ├── CsvReader.kt │ ├── CsvWriter.kt │ ├── ICsvFileWriter.kt │ └── Reader.kt │ ├── dsl │ ├── CsvReaderDsl.kt │ ├── CsvWriterDsl.kt │ └── context │ │ ├── CsvReaderContext.kt │ │ ├── CsvWriteQuoteContext.kt │ │ └── CsvWriterContext.kt │ ├── parser │ ├── CsvParser.kt │ └── ParseStateMachine.kt │ └── util │ ├── CSVException.kt │ ├── Const.kt │ ├── CsvDslMarker.kt │ └── logger │ ├── Logger.kt │ └── LoggerNop.kt ├── jsMain └── kotlin │ └── com │ └── github │ └── doyaaaaaken │ └── kotlincsv │ └── client │ ├── CsvReader.kt │ └── CsvWriter.kt ├── jvmMain └── kotlin │ └── com │ └── github │ └── doyaaaaaken │ └── kotlincsv │ └── client │ ├── Annotation.kt │ ├── CsvFileWriter.kt │ ├── CsvReader.kt │ ├── CsvWriter.kt │ └── Reader.kt └── jvmTest ├── kotlin └── com │ └── github │ └── doyaaaaaken │ └── kotlincsv │ ├── client │ ├── BufferedLineReaderTest.kt │ ├── CsvFileWriterTest.kt │ ├── CsvReadWriteCompatibilityTest.kt │ ├── CsvReaderTest.kt │ ├── CsvWriterTest.kt │ └── StringReaderTest.kt │ ├── dsl │ ├── CsvReaderDslTest.kt │ └── CsvWriterDslTest.kt │ └── parser │ └── CsvParserTest.kt └── resources └── testdata └── csv ├── backslash-escape.csv ├── bom.csv ├── different-fields-num.csv ├── different-fields-num2.csv ├── empty-bom.csv ├── empty-fields.csv ├── empty-line.csv ├── empty.csv ├── escape.csv ├── hash-separated-dollar-quote.csv ├── line-breaks.csv ├── malformed.csv ├── quoted-empty-line.csv ├── simple.csv ├── simple.tsv ├── unicode2028.csv ├── varying-column-lengths.csv ├── with-duplicate-header-auto-rename-failed.csv ├── with-duplicate-header.csv ├── with-header-different-size-row.csv └── with-header.csv /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: jsoizo 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | Attach code snippet which reproduce the bug. 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Environment** 21 | - kotlin-csv version [e.g. 0.10.0] 22 | - java version [e.g. java8] 23 | - OS: [e.g. MacOS] 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/improvement-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Improvement request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Suggest an idea for this project. 11 | For example, feature requests, documentation improvement, some refactorings, and so on. 12 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up JDK 8 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: 'temurin' 21 | java-version: '8' 22 | 23 | - name: Setup Gradle 24 | uses: gradle/actions/setup-gradle@v4 25 | 26 | - name: Cache Gradle dependencies 27 | uses: actions/cache@v4 28 | with: 29 | path: | 30 | ~/.gradle/caches 31 | ~/.gradle/wrapper 32 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 33 | restore-keys: | 34 | ${{ runner.os }}-gradle- 35 | 36 | - name: Verify dependencies 37 | run: ./gradlew dependencies 38 | 39 | - name: Run checks and generate report 40 | run: | 41 | ./gradlew clean check 42 | ./gradlew jacocoTestReport 43 | 44 | - name: Upload coverage report to Codecov 45 | uses: codecov/codecov-action@v4 46 | with: 47 | fail_ci_if_error: true 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | verbose: true 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | out 7 | 8 | # Ignore kotlintest tmp file 9 | .kotlintest 10 | test.csv 11 | 12 | # Project definition file 13 | *.iml 14 | .idea/* 15 | 16 | # Ignore yarn.lcok 17 | kotlin-js-store/yarn.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

kotlin-csv

2 | 3 |

4 | Version 5 | 6 | License: Apache License 2.0 7 | 8 | 9 | codecov 10 | 11 | 12 | CodeFactor 13 | 14 |

15 | 16 | Pure Kotlin CSV Reader/Writer. 17 | 18 | # Design goals 19 | 20 | ### 1. Simple interface 21 | 22 | * easy to setup 23 | * use DSL so easy to read 24 | 25 | ### 2. Automatic handling of I/O 26 | 27 | * in Java, we always need to close file. but it's boilerplate code and not friendly for non-JVM user. 28 | * provide interfaces which automatically close file without being aware. 29 | 30 | ### 3. Multiplatform 31 | 32 | * Kotlin Multiplatform projects support. 33 | 34 | # Usage 35 | 36 | ## Download 37 | 38 | ### Gradle 39 | 40 | for Kotlin DSL 41 | 42 | ```kotlin 43 | implementation("com.jsoizo:kotlin-csv-jvm:1.10.0") // for JVM platform 44 | implementation("com.jsoizo:kotlin-csv-js:1.10.0") // for Kotlin JS platform 45 | ``` 46 | 47 | for Gradle DSL 48 | 49 | ```groovy 50 | implementation 'com.jsoizo:kotlin-csv-jvm:1.10.0' // for JVM platform 51 | implementation 'com.jsoizo:kotlin-csv-js:1.10.0' // for Kotlin JS platform 52 | ``` 53 | 54 | ### Maven 55 | 56 | ```maven 57 | 58 | com.jsoizo 59 | kotlin-csv-jvm 60 | 1.10.0 61 | 62 | 63 | com.jsoizo 64 | kotlin-csv-js 65 | 1.10.0 66 | 67 | ``` 68 | 69 | ### [kscript](https://github.com/holgerbrandl/kscript) 70 | 71 | ```kotlin 72 | @file:DependsOn("com.jsoizo:kotlin-csv-jvm:1.10.0") // for JVM platform 73 | @file:DependsOn("com.jsoizo:kotlin-csv-js:1.10.0") // for Kotlin JS platform 74 | ``` 75 | 76 | ## Examples 77 | 78 | ### CSV Read examples 79 | 80 | #### Simple case 81 | 82 | You can read csv file from `String`, `java.io.File` or `java.io.InputStream` object. 83 | No need to do any I/O handling. (No need to call `use`, `close` and `flush` method.) 84 | 85 | ```kotlin 86 | // read from `String` 87 | val csvData: String = "a,b,c\nd,e,f" 88 | val rows: List> = csvReader().readAll(csvData) 89 | 90 | // read from `java.io.File` 91 | val file: File = File("test.csv") 92 | val rows: List> = csvReader().readAll(file) 93 | ``` 94 | 95 | #### Read with header 96 | 97 | ```kotlin 98 | val csvData: String = "a,b,c\nd,e,f" 99 | val rows: List> = csvReader().readAllWithHeader(csvData) 100 | println(rows) //[{a=d, b=e, c=f}] 101 | ``` 102 | 103 | #### Read as `Sequence` 104 | 105 | `Sequence` type allows to execute lazily.
106 | It starts to process each rows before reading all row data. 107 | 108 | Learn more about the `Sequence` type on [Kotlin's official documentation](https://kotlinlang.org/docs/reference/sequences.html). 109 | 110 | ```kotlin 111 | csvReader().open("test1.csv") { 112 | readAllAsSequence().forEach { row: List -> 113 | //Do something 114 | println(row) //[a, b, c] 115 | } 116 | } 117 | 118 | csvReader().open("test2.csv") { 119 | readAllWithHeaderAsSequence().forEach { row: Map -> 120 | //Do something 121 | println(row) //{id=1, name=jsoizo} 122 | } 123 | } 124 | ``` 125 | 126 | NOTE: `readAllAsSequence` and `readAllWithHeaderAsSequence` methods can only be called within the `open` lambda block. 127 | The input stream is closed after the `open` lambda block. 128 | 129 | #### Read line by line 130 | 131 | If you want to handle line-by-line, you can do it by using `open` method. Use `open` method and then use `readNext` 132 | method inside nested block to read row. 133 | 134 | ```kotlin 135 | csvReader().open("test.csv") { 136 | readNext() 137 | } 138 | ``` 139 | 140 | #### Read in a `Suspending Function` 141 | 142 | ```kotlin 143 | csvReader().openAsync("test.csv") { 144 | val container = mutalbeListOf>() 145 | delay(100) //other suspending task 146 | readAllAsSequence().asFlow().collect { row -> 147 | delay(100) // other suspending task 148 | container.add(row) 149 | } 150 | } 151 | ``` 152 | 153 | Note: `openAsync` can be and only be accessed through a `coroutine` or another `suspending` function 154 | 155 | #### Customize 156 | 157 | When you create CsvReader, you can choose read options: 158 | 159 | ```kotlin 160 | // this is tsv reader's option 161 | val tsvReader = csvReader { 162 | charset = "ISO_8859_1" 163 | quoteChar = '"' 164 | delimiter = '\t' 165 | escapeChar = '\\' 166 | } 167 | ``` 168 | 169 | | Option | default value | description | 170 | |--------------------------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 171 | | logger | _no-op_ | Logger instance for logging debug information at runtime. | 172 | | charset | `UTF-8` | Charset encoding. The value must be supported by [java.nio.charset.Charset](https://docs.oracle.com/javase/8/docs/api/java/nio/charset/Charset.html). | 173 | | quoteChar | `"` | Character used to quote fields. | 174 | | delimiter | `,` | Character used as delimiter between each field.
Use `"\t"` if reading TSV file. | 175 | | escapeChar | `"` | Character to escape quote inside field string.
Normally, you don't have to change this option.
See detail comment on [ICsvReaderContext](src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/context/CsvReaderContext.kt). | 176 | | skipEmptyLine | `false` | Whether to skip or error out on empty lines. | 177 | | autoRenameDuplicateHeaders | `false` | Whether to auto rename duplicate headers or throw an exception. | 178 | | ~~skipMissMatchedRow~~ | `false` | Deprecated. Replace with appropriate values in `excessFieldsRowBehaviour` and `insufficientFieldsRowBehaviour`, e.g. both set to `IGNORE`. ~~Whether to skip an invalid row. If `ignoreExcessCols` is true, only rows with less than the expected number of columns will be skipped.~~ | 179 | | excessFieldsRowBehaviour | `ERROR` | Behaviour to use when a row has more fields (columns) than expected. `ERROR` (default), `IGNORE` (skip the row) or `TRIM` (remove the excess fields at the end of the row to match the expected number of fields). | 180 | | insufficientFieldsRowBehaviour | `ERROR` | Behaviour to use when a row has fewer fields (columns) than expected. `ERROR` (default), `IGNORE` (skip the row) or `EMPTY_STRING` (replace missing fields with an empty string). | 181 | 182 | ### CSV Write examples 183 | 184 | #### Simple case 185 | 186 | You can start writing csv in one line, no need to do any I/O handling (No need to call `use`, `close` and `flush` 187 | method.): 188 | 189 | ```kotlin 190 | val rows = listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 191 | csvWriter().writeAll(rows, "test.csv") 192 | 193 | // if you'd append data on the tail of the file, assign `append = true`. 194 | csvWriter().writeAll(rows, "test.csv", append = true) 195 | 196 | // You can also write into OutpusStream. 197 | csvWriter().writeAll(rows, File("test.csv").outputStream()) 198 | ``` 199 | 200 | You can also write a csv file line by line by `open` method: 201 | 202 | ```kotlin 203 | val row1 = listOf("a", "b", "c") 204 | val row2 = listOf("d", "e", "f") 205 | 206 | csvWriter().open("test.csv") { 207 | writeRow(row1) 208 | writeRow(row2) 209 | writeRow("g", "h", "i") 210 | writeRows(listOf(row1, row2)) 211 | } 212 | ``` 213 | 214 | #### Write in a `Suspending Function` 215 | 216 | ```kotlin 217 | val rows = listOf(listOf("a", "b", "c"), listOf("d", "e", "f")).asSequence() 218 | csvWriter().openAsync(testFileName) { 219 | delay(100) //other suspending task 220 | rows.asFlow().collect { 221 | delay(100) // other suspending task 222 | writeRow(it) 223 | } 224 | } 225 | ``` 226 | 227 | #### Write as String 228 | 229 | ```kotlin 230 | val rows = listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 231 | val csvString: String = csvWriter().writeAllAsString(rows) //a,b,c\r\nd,e,f\r\n 232 | ``` 233 | 234 | #### long-running write (manual control for file close) 235 | 236 | If you want to close a file writer manually for performance reasons (e.g. streaming scenario), you can 237 | use `openAndGetRawWriter` and get a raw `CsvFileWriter`. 238 | **DO NOT forget to `close` the writer!** 239 | 240 | ```kotlin 241 | val row1 = listOf("a", "b", "c") 242 | 243 | @OptIn(KotlinCsvExperimental::class) 244 | val writer = csvWriter().openAndGetRawWriter("test.csv") 245 | writer.writeRow(row1) 246 | writer.close() 247 | ``` 248 | 249 | #### Customize 250 | 251 | When you create a CsvWriter, you can choose write options. 252 | 253 | ```kotlin 254 | val writer = csvWriter { 255 | charset = "ISO_8859_1" 256 | delimiter = '\t' 257 | nullCode = "NULL" 258 | lineTerminator = "\n" 259 | outputLastLineTerminator = true 260 | quote { 261 | mode = WriteQuoteMode.ALL 262 | char = '\'' 263 | } 264 | } 265 | ``` 266 | 267 | | Option | default value | description | 268 | |------------|---------------|-------------------------------------| 269 | | charset |`UTF-8`| Charset encoding. The value must be supported by [java.nio.charset.Charset](https://docs.oracle.com/javase/8/docs/api/java/nio/charset/Charset.html). | 270 | | delimiter | `,` | Character used as delimiter between each fields.
Use `"\t"` if reading TSV file. | 271 | | nullCode | `(empty string)` | Character used when a written field is null value. | 272 | | lineTerminator | `\r\n` | Character used as line terminator. | 273 | | outputLastLineTerminator | `true` | Output line break at the end of file or not. | 274 | | prependBOM | `false` | Output BOM (Byte Order Mark) at the beginning of file or not. | 275 | | quote.char | `"` | Character to quote each fields. | 276 | | 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) | 277 | 278 | # Links 279 | 280 | **Documents** 281 | 282 | * [Change Logs](https://github.com/jsoizo/kotlin-csv/releases) 283 | 284 | **Libraries which use kotlin-csv** 285 | 286 | * [kotlin-grass](https://github.com/blackmo18/kotlin-grass): Csv File to Kotlin Data Class Parser. 287 | 288 | # Miscellaneous 289 | 290 | ## 🤝 Contributing 291 | 292 | Contributions, [issues](https://github.com/jsoizo/kotlin-csv/issues) and feature requests are welcome! 293 | If you have questions, ask away in [Kotlin Slack's](https://kotlinlang.slack.com) `kotlin-csv` room. 294 | 295 | ## 💻 Development 296 | 297 | ```sh 298 | git clone git@github.com:jsoizo/kotlin-csv.git 299 | cd kotlin-csv 300 | ./gradlew check 301 | ``` 302 | 303 | ## Show your support 304 | 305 | Give a ⭐️ if this project helped you! 306 | 307 | ## 📝 License 308 | 309 | Copyright © 2024 [jsoizo](https://github.com/jsoizo). 310 | This project is licensed under [Apache 2.0](LICENSE). 311 | 312 | *** 313 | _This project is inspired ❤️ by [scala-csv](https://github.com/tototoshi/scala-csv)_ 314 | 315 | _This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ 316 | 317 | ## Acknowledgments 318 | 319 | This project was originally created by [@doyaaaaaken](https://github.com/doyaaaaaken). The initial work and contributions are greatly appreciated. 320 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | java 3 | kotlin("multiplatform") version "1.7.21" 4 | id("org.jetbrains.dokka").version("1.7.20") 5 | `maven-publish` 6 | signing 7 | jacoco 8 | } 9 | 10 | group = "com.jsoizo" 11 | version = "1.10.0" 12 | 13 | buildscript { 14 | repositories { 15 | mavenCentral() 16 | } 17 | dependencies { 18 | classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.7.20") 19 | } 20 | } 21 | 22 | repositories { 23 | mavenCentral() 24 | } 25 | 26 | val dokkaJar = task("dokkaJar") { 27 | group = JavaBasePlugin.DOCUMENTATION_GROUP 28 | archiveClassifier.set("javadoc") 29 | } 30 | 31 | kotlin { 32 | jvm { 33 | compilations.forEach { 34 | it.kotlinOptions.jvmTarget = "1.8" 35 | } 36 | //https://docs.gradle.org/current/userguide/publishing_maven.html 37 | mavenPublication { 38 | artifact(dokkaJar) 39 | } 40 | } 41 | js(BOTH) { 42 | browser { 43 | } 44 | nodejs { 45 | } 46 | } 47 | sourceSets { 48 | commonMain {} 49 | commonTest { 50 | dependencies { 51 | implementation(kotlin("test-common")) 52 | implementation(kotlin("test-annotations-common")) 53 | } 54 | } 55 | 56 | jvm().compilations["main"].defaultSourceSet { 57 | dependencies { 58 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") 59 | } 60 | } 61 | jvm().compilations["test"].defaultSourceSet { 62 | dependencies { 63 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") 64 | implementation("io.kotest:kotest-runner-junit5:4.6.3") 65 | implementation("io.kotest:kotest-assertions-core:4.6.3") 66 | } 67 | } 68 | js().compilations["main"].defaultSourceSet { 69 | dependencies { 70 | } 71 | } 72 | js().compilations["test"].defaultSourceSet { 73 | dependencies { 74 | implementation(kotlin("test-js")) 75 | } 76 | } 77 | } 78 | } 79 | 80 | tasks.withType() { 81 | useJUnitPlatform() 82 | } 83 | 84 | 85 | publishing { 86 | publications.all { 87 | (this as MavenPublication).pom { 88 | name.set("kotlin-csv") 89 | description.set("Kotlin CSV Reader/Writer") 90 | url.set("https://github.com/jsoizo/kotlin-csv") 91 | 92 | organization { 93 | name.set("com.jsoizo") 94 | url.set("https://github.com/jsoizo") 95 | } 96 | licenses { 97 | license { 98 | name.set("Apache License 2.0") 99 | url.set("https://github.com/jsoizo/kotlin-csv/blob/master/LICENSE") 100 | } 101 | } 102 | scm { 103 | url.set("https://github.com/jsoizo/kotlin-csv") 104 | connection.set("scm:git:git://github.com/jsoizo/kotlin-csv.git") 105 | developerConnection.set("https://github.com/jsoizo/kotlin-csv") 106 | } 107 | developers { 108 | developer { 109 | name.set("jsoizo") 110 | } 111 | } 112 | } 113 | } 114 | repositories { 115 | maven { 116 | credentials { 117 | val nexusUsername: String? by project 118 | val nexusPassword: String? by project 119 | username = nexusUsername 120 | password = nexusPassword 121 | } 122 | 123 | val releasesRepoUrl = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") 124 | val snapshotsRepoUrl = uri("https://oss.sonatype.org/content/repositories/snapshots/") 125 | url = if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl 126 | } 127 | } 128 | } 129 | 130 | signing { 131 | sign(publishing.publications) 132 | } 133 | 134 | ///////////////////////////////////////// 135 | // Jacoco setting // 136 | ///////////////////////////////////////// 137 | jacoco { 138 | toolVersion = "0.8.8" 139 | } 140 | tasks.jacocoTestReport { 141 | val coverageSourceDirs = arrayOf( 142 | "commonMain/src", 143 | "jvmMain/src" 144 | ) 145 | val classFiles = File("${buildDir}/classes/kotlin/jvm/") 146 | .walkBottomUp() 147 | .toSet() 148 | classDirectories.setFrom(classFiles) 149 | sourceDirectories.setFrom(files(coverageSourceDirs)) 150 | additionalSourceDirs.setFrom(files(coverageSourceDirs)) 151 | 152 | executionData 153 | .setFrom(files("${buildDir}/jacoco/jvmTest.exec")) 154 | 155 | reports { 156 | xml.required.set(true) 157 | html.required.set(false) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | threshold: 3% 6 | paths: 7 | - "src" 8 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsoizo/kotlin-csv/19c28835a7bde791c808fe8a7b05818122e0e28d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "kotlin-csv" -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/BufferedLineReader.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import com.github.doyaaaaaken.kotlincsv.util.Const 4 | 5 | /** 6 | * buffered reader which can read line with line terminator 7 | */ 8 | internal class BufferedLineReader( 9 | private val br: Reader 10 | ) { 11 | companion object { 12 | private const val BOM = Const.BOM 13 | } 14 | 15 | private fun StringBuilder.isEmptyLine(): Boolean = 16 | this.isEmpty() || this.length == 1 && this[0] == BOM 17 | 18 | fun readLineWithTerminator(): String? { 19 | val sb = StringBuilder() 20 | do { 21 | val c = br.read() 22 | 23 | if (c == -1) { 24 | if (sb.isEmptyLine()) { 25 | return null 26 | } else { 27 | break 28 | } 29 | } 30 | val ch = c.toChar() 31 | sb.append(ch) 32 | 33 | if (ch == '\n' || ch == '\u2028' || ch == '\u2029' || ch == '\u0085') { 34 | break 35 | } 36 | 37 | if (ch == '\r') { 38 | br.mark(1) 39 | val c2 = br.read() 40 | if (c2 == -1) { 41 | break 42 | } else if (c2.toChar() == '\n') { 43 | sb.append('\n') 44 | } else { 45 | br.reset() 46 | } 47 | 48 | break 49 | } 50 | } while (true) 51 | return sb.toString() 52 | } 53 | 54 | fun close() { 55 | br.close() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvFileReader.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import com.github.doyaaaaaken.kotlincsv.dsl.context.CsvReaderContext 4 | import com.github.doyaaaaaken.kotlincsv.dsl.context.ExcessFieldsRowBehaviour 5 | import com.github.doyaaaaaken.kotlincsv.dsl.context.InsufficientFieldsRowBehaviour 6 | import com.github.doyaaaaaken.kotlincsv.parser.CsvParser 7 | import com.github.doyaaaaaken.kotlincsv.util.CSVAutoRenameFailedException 8 | import com.github.doyaaaaaken.kotlincsv.util.CSVFieldNumDifferentException 9 | import com.github.doyaaaaaken.kotlincsv.util.logger.Logger 10 | import com.github.doyaaaaaken.kotlincsv.util.MalformedCSVException 11 | 12 | /** 13 | * CSV Reader class, which controls file I/O flow. 14 | * 15 | * @author doyaaaaaken 16 | */ 17 | class CsvFileReader internal constructor( 18 | private val ctx: CsvReaderContext, 19 | reader: Reader, 20 | private val logger: Logger, 21 | ) { 22 | 23 | private val reader = BufferedLineReader(reader) 24 | private var rowNum = 0L 25 | 26 | private val parser = CsvParser(ctx.quoteChar, ctx.delimiter, ctx.escapeChar) 27 | 28 | /** 29 | * read next csv row 30 | * (which may contain multiple lines, because csv fields may contain line feed) 31 | * 32 | * @return return fields in row as List. 33 | * or return null, if all line are already read. 34 | */ 35 | @Deprecated("We are considering making it a private method. If you have feedback, please comment on Issue #100.") 36 | fun readNext(): List? { 37 | return readUntilNextCsvRow("") 38 | } 39 | 40 | /** 41 | * read all csv rows as Sequence 42 | */ 43 | fun readAllAsSequence(fieldsNum: Int? = null): Sequence> { 44 | var expectedNumFieldsInRow: Int? = fieldsNum 45 | return generateSequence { 46 | @Suppress("DEPRECATION") readNext() 47 | }.mapIndexedNotNull { idx, row -> 48 | // If no expected number of fields was passed in, then set it based on the first row. 49 | if (expectedNumFieldsInRow == null) expectedNumFieldsInRow = row.size 50 | // Assign this number to a non-nullable type to avoid need for thread-safety null checks. 51 | val numFieldsInRow: Int = expectedNumFieldsInRow ?: row.size 52 | @Suppress("DEPRECATION") 53 | if (row.size > numFieldsInRow) { 54 | if (ctx.excessFieldsRowBehaviour == ExcessFieldsRowBehaviour.TRIM) { 55 | logger.info("trimming excess rows. [csv row num = ${idx + 1}, fields num = ${row.size}, fields num of row = $numFieldsInRow]") 56 | row.subList(0, numFieldsInRow) 57 | } else if (ctx.skipMissMatchedRow || ctx.excessFieldsRowBehaviour == ExcessFieldsRowBehaviour.IGNORE) { 58 | skipMismatchedRow(idx, row, numFieldsInRow) 59 | } else { 60 | throw CSVFieldNumDifferentException(numFieldsInRow, row.size, idx + 1) 61 | } 62 | } else if (numFieldsInRow != row.size) { 63 | if (ctx.skipMissMatchedRow || ctx.insufficientFieldsRowBehaviour == InsufficientFieldsRowBehaviour.IGNORE) { 64 | skipMismatchedRow(idx, row, numFieldsInRow) 65 | } else if (ctx.insufficientFieldsRowBehaviour == InsufficientFieldsRowBehaviour.EMPTY_STRING) { 66 | val numOfMissingFields = numFieldsInRow - row.size 67 | row.plus(List(numOfMissingFields) { "" }) 68 | } else { 69 | throw CSVFieldNumDifferentException(numFieldsInRow, row.size, idx + 1) 70 | } 71 | } else { 72 | row 73 | } 74 | } 75 | } 76 | 77 | private fun skipMismatchedRow( 78 | idx: Int, 79 | row: List, 80 | numFieldsInRow: Int 81 | ): Nothing? { 82 | logger.info("skip miss matched row. [csv row num = ${idx + 1}, fields num = ${row.size}, fields num of first row = $numFieldsInRow]") 83 | return null 84 | } 85 | 86 | /** 87 | * read all csv rows as Sequence with header information 88 | */ 89 | fun readAllWithHeaderAsSequence(): Sequence> { 90 | @Suppress("DEPRECATION") 91 | var headers = readNext() ?: return emptySequence() 92 | if (ctx.autoRenameDuplicateHeaders) { 93 | headers = deduplicateHeaders(headers) 94 | } else { 95 | val duplicated = findDuplicate(headers) 96 | if (duplicated != null) throw MalformedCSVException("header '$duplicated' is duplicated. please consider to use 'autoRenameDuplicateHeaders' option.") 97 | } 98 | return readAllAsSequence(headers.size).map { fields -> headers.zip(fields).toMap() } 99 | } 100 | 101 | fun close() { 102 | reader.close() 103 | } 104 | 105 | /** 106 | * read next csv row (which may contain multiple lines) 107 | * 108 | * @return return fields in row as List. 109 | * or return null, if all line are already read. 110 | */ 111 | private tailrec fun readUntilNextCsvRow(leftOver: String = ""): List? { 112 | val nextLine = reader.readLineWithTerminator() 113 | rowNum++ 114 | return if (nextLine == null) { 115 | if (leftOver.isNotEmpty()) { 116 | throw MalformedCSVException("\"$leftOver\" on the tail of file is left on the way of parsing row") 117 | } else { 118 | null 119 | } 120 | } else if (ctx.skipEmptyLine && nextLine.isBlank() && leftOver.isBlank()) { 121 | readUntilNextCsvRow(leftOver) 122 | } else { 123 | val value = if (leftOver.isEmpty()) { 124 | "$nextLine" 125 | } else { 126 | "$leftOver$nextLine" 127 | } 128 | parser.parseRow(value, rowNum) ?: readUntilNextCsvRow("$leftOver$nextLine") 129 | } 130 | } 131 | 132 | private fun findDuplicate(headers: List): String? { 133 | val set = mutableSetOf() 134 | headers.forEach { h -> 135 | if (set.contains(h)) { 136 | return h 137 | } else { 138 | set.add(h) 139 | } 140 | } 141 | return null 142 | } 143 | 144 | /** 145 | * deduplicate headers based on occurrence by appending "_" 146 | * Ex: [a,b,b,b,c,a] => [a,b,b_2,b_3,c,a_2] 147 | * 148 | * @return return headers as List. 149 | */ 150 | private fun deduplicateHeaders(headers: List): List { 151 | val occurrences = mutableMapOf() 152 | return headers.map { header -> 153 | val count = occurrences.getOrPut(header) { 0 } + 1 154 | occurrences[header] = count 155 | when { 156 | count > 1 -> "${header}_$count" 157 | else -> header 158 | } 159 | }.also { results -> 160 | if (results.size != results.distinct().size) throw CSVAutoRenameFailedException() 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvReader.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import com.github.doyaaaaaken.kotlincsv.dsl.context.CsvReaderContext 4 | 5 | /** 6 | * CSV Reader class 7 | * 8 | * @author doyaaaaaken 9 | */ 10 | expect class CsvReader( 11 | ctx: CsvReaderContext = CsvReaderContext() 12 | ) { 13 | /** 14 | * read csv data as String, and convert into List> 15 | */ 16 | fun readAll(data: String): List> 17 | 18 | /** 19 | * read csv data with header, and convert into List> 20 | */ 21 | fun readAllWithHeader(data: String): List> 22 | } 23 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvWriter.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import com.github.doyaaaaaken.kotlincsv.dsl.context.CsvWriterContext 4 | 5 | /** 6 | * CSV Writer class 7 | * 8 | * @author doyaaaaaken 9 | */ 10 | expect class CsvWriter(ctx: CsvWriterContext = CsvWriterContext()) { 11 | 12 | fun open(targetFileName: String, append: Boolean = false, write: ICsvFileWriter.() -> Unit) 13 | 14 | fun writeAll(rows: List>, targetFileName: String, append: Boolean = false) 15 | 16 | suspend fun writeAllAsync(rows: List>, targetFileName: String, append: Boolean = false) 17 | 18 | suspend fun openAsync(targetFileName: String, append: Boolean = false, write: suspend ICsvFileWriter.() -> Unit) 19 | } 20 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/ICsvFileWriter.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | interface ICsvFileWriter { 4 | fun writeRow(row: List) 5 | 6 | fun writeRow(vararg entry: Any?) 7 | 8 | fun writeRows(rows: List>) 9 | 10 | fun writeRows(rows: Sequence>) 11 | 12 | fun flush() 13 | 14 | fun close() 15 | } 16 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/Reader.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | internal interface Reader { 4 | /** 5 | * Reads a single character. 6 | * Returns: The character read, as an integer in the range 0 to 65535 (0x00-0xffff), or -1 if the end of the stream has been reached 7 | */ 8 | fun read(): Int 9 | 10 | /** 11 | * Marks the present position in the stream. Subsequent calls to reset() 12 | * will attempt to reposition the stream to this point. 13 | * 14 | * @param readAheadLimit Limit on the number of characters that may be 15 | * read while still preserving the mark. An attempt 16 | * to reset the stream after reading characters 17 | * up to this limit or beyond may fail. 18 | * A limit value larger than the size of the input 19 | * buffer will cause a new buffer to be allocated 20 | * whose size is no smaller than limit. 21 | * Therefore large values should be used with care. 22 | * 23 | * @exception IllegalArgumentException If {@code readAheadLimit < 0} 24 | */ 25 | fun mark(readAheadLimit: Int): Unit 26 | 27 | /** 28 | * Resets the stream to the most recent mark. 29 | */ 30 | fun reset(): Unit 31 | 32 | 33 | fun close() 34 | } 35 | 36 | /** 37 | * Multiplatform implementation of the Reader interface that uses a String as the backing 38 | * data source. 39 | */ 40 | internal class StringReaderImpl(private val data: String) : Reader { 41 | private var nextChar = 0 42 | private var mark = -1 43 | 44 | override fun read(): Int { 45 | return if (nextChar == data.length) { 46 | -1 47 | } else { 48 | data[nextChar++].code 49 | } 50 | } 51 | 52 | override fun mark(readAheadLimit: Int) { 53 | mark = nextChar 54 | } 55 | 56 | override fun reset() { 57 | nextChar = mark 58 | } 59 | 60 | override fun close() { 61 | } 62 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/CsvReaderDsl.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.dsl 2 | 3 | import com.github.doyaaaaaken.kotlincsv.client.CsvReader 4 | import com.github.doyaaaaaken.kotlincsv.dsl.context.CsvReaderContext 5 | 6 | /** 7 | * DSL Method which provides `CsvReader` 8 | * 9 | * @return CsvReader 10 | * 11 | * Usage example: 12 | * 13 | * 1. Use default setting 14 | *
15 |  *  val reader: CsvReader = csvReader()
16 |  *  reader.read("a,b,c\nd,e,f))
17 |  *  
18 | * 19 | * 2. Customize Setting 20 | *
21 |  *  val reader: CsvReader = csvReader {
22 |  *      delimiter = '\t'
23 |  *      //...
24 |  *  }
25 |  *  
26 | * 27 | * @see CsvReaderContext 28 | * @see CsvReader 29 | * 30 | * @author doyaaaaaken 31 | */ 32 | fun csvReader(init: CsvReaderContext.() -> Unit = {}): CsvReader { 33 | val context = CsvReaderContext().apply(init) 34 | return CsvReader(context) 35 | } 36 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/CsvWriterDsl.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.dsl 2 | 3 | import com.github.doyaaaaaken.kotlincsv.client.CsvWriter 4 | import com.github.doyaaaaaken.kotlincsv.dsl.context.CsvWriterContext 5 | 6 | /** 7 | * DSL Method which provides `CsvWriter` 8 | * 9 | * @return CsvWriter 10 | * 11 | * Usage example: 12 | * 13 | * 1. Use default setting 14 | *
15 |  *  val writer: CsvWriter = csvWriter()
16 |  *  
17 | * 18 | * 2. Customize Setting 19 | *
20 |  *  val writer: CsvWriter = csvWriter {
21 |  *      charset = Charsets.ISO_8859_1
22 |  *      //...
23 |  *  }
24 |  *  
25 | * 26 | * @see CsvWriterContext 27 | * @see CsvWriter 28 | * 29 | * @author doyaaaaaken 30 | */ 31 | fun csvWriter(init: CsvWriterContext.() -> Unit = {}): CsvWriter { 32 | val context = CsvWriterContext().apply(init) 33 | return CsvWriter(context) 34 | } 35 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/context/CsvReaderContext.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.dsl.context 2 | 3 | import com.github.doyaaaaaken.kotlincsv.util.Const 4 | import com.github.doyaaaaaken.kotlincsv.util.CsvDslMarker 5 | import com.github.doyaaaaaken.kotlincsv.util.logger.Logger 6 | import com.github.doyaaaaaken.kotlincsv.util.logger.LoggerNop 7 | 8 | /** 9 | * Interface for CSV Reader settings 10 | * 11 | * @author doyaaaaaken 12 | */ 13 | @CsvDslMarker 14 | interface ICsvReaderContext { 15 | 16 | /** 17 | * Logger instance for logging debug statements. 18 | * Default instance does not log anything. 19 | */ 20 | val logger: Logger 21 | 22 | /** 23 | * Charset encoding 24 | * 25 | * The name must be supported by [java.nio.charset.Charset]. 26 | * 27 | * ex.) 28 | * "UTF-8" 29 | * "SJIS" 30 | */ 31 | val charset: String 32 | 33 | /** 34 | * Character used as quote between each fields 35 | * 36 | * ex.) 37 | * '"' 38 | * '\'' 39 | */ 40 | val quoteChar: Char 41 | 42 | /** 43 | * Character used as delimiter between each fields 44 | * 45 | * ex.) 46 | * "," 47 | * "\t" (TSV file) 48 | */ 49 | val delimiter: Char 50 | 51 | /** 52 | * Character to escape quote inside field string. 53 | * Normally, you don't have to change this option. 54 | * 55 | * According to [CSV specification](https://tools.ietf.org/html/rfc4180#section-2), 56 | * > If double-quotes are used to enclose fields, then a double-quote appearing inside a field must be escaped by preceding it with another double quote. 57 | * > For example: 58 | * > "aaa","b""bb","ccc" 59 | */ 60 | val escapeChar: Char 61 | 62 | /** 63 | * If empty line is found, skip it or not (=throw an exception). 64 | */ 65 | val skipEmptyLine: Boolean 66 | 67 | /** 68 | * If a invalid row which has different number of fields from other rows is found, skip it or not (=throw an exception). 69 | */ 70 | @Deprecated("Use insufficientFieldsRowBehaviour and excessRowsBehaviour to specify 'ignore'") 71 | val skipMissMatchedRow: Boolean 72 | 73 | /** 74 | * If a header occurs multiple times whether auto renaming should be applied when `readAllWithHeaderAsSequence()` (=throw an exception). 75 | * 76 | * Renaming is done based on occurrence and only applied from the first detected duplicate onwards. 77 | * ex: 78 | * [a,b,b,b,c,a] => [a,b,b_2,b_3,c,a_2] 79 | */ 80 | val autoRenameDuplicateHeaders: Boolean 81 | 82 | /** 83 | * If a row does not have the expected number of fields (columns), how, and if, the reader should proceed 84 | */ 85 | val insufficientFieldsRowBehaviour: InsufficientFieldsRowBehaviour 86 | 87 | /** 88 | * If a row exceeds have the expected number of fields (columns), how, and if, the reader should proceed 89 | */ 90 | val excessFieldsRowBehaviour: ExcessFieldsRowBehaviour 91 | } 92 | 93 | enum class InsufficientFieldsRowBehaviour { 94 | 95 | /** 96 | * Throw an exception (default) 97 | */ 98 | ERROR, 99 | 100 | /** 101 | * Ignore the row and skip to the next row 102 | */ 103 | IGNORE, 104 | /** 105 | * Treat missing fields as an empty string 106 | */ 107 | EMPTY_STRING 108 | } 109 | 110 | enum class ExcessFieldsRowBehaviour { 111 | 112 | /** 113 | * Throw an exception (default) 114 | */ 115 | ERROR, 116 | 117 | /** 118 | * Ignore the row and skip to the next row 119 | */ 120 | IGNORE, 121 | 122 | /** 123 | * Trim the excess fields from the row (e.g. if 8 fields are present and 7 are expected, return the first 7 fields) 124 | */ 125 | TRIM 126 | } 127 | 128 | /** 129 | * CSV Reader settings used in `csvReader` DSL method. 130 | * 131 | * @author doyaaaaaken 132 | */ 133 | @CsvDslMarker 134 | class CsvReaderContext : ICsvReaderContext { 135 | override var logger: Logger = LoggerNop 136 | override var charset = Const.defaultCharset 137 | override var quoteChar: Char = '"' 138 | override var delimiter: Char = ',' 139 | override var escapeChar: Char = '"' 140 | override var skipEmptyLine: Boolean = false 141 | override var skipMissMatchedRow: Boolean = false 142 | override var autoRenameDuplicateHeaders: Boolean = false 143 | override var insufficientFieldsRowBehaviour: InsufficientFieldsRowBehaviour = InsufficientFieldsRowBehaviour.ERROR 144 | override var excessFieldsRowBehaviour: ExcessFieldsRowBehaviour = ExcessFieldsRowBehaviour.ERROR 145 | } 146 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/context/CsvWriteQuoteContext.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.dsl.context 2 | 3 | import com.github.doyaaaaaken.kotlincsv.util.CsvDslMarker 4 | 5 | /** 6 | * DSL method for Quote settings on writing csv. 7 | * 8 | * @author doyaaaaaken 9 | */ 10 | @CsvDslMarker 11 | class CsvWriteQuoteContext { 12 | /** 13 | * Character to quote each fields 14 | */ 15 | var char: Char = '"' 16 | 17 | /** 18 | * Quote mode 19 | * 20 | * CANONICAL: 21 | * Not quote normally, but quote special characters (quoteChar, delimiter, line feed). 22 | * This is specification of CSV. 23 | * See https://tools.ietf.org/html/rfc4180#section-2 24 | * ALL: 25 | * Quote all fields. 26 | */ 27 | var mode: WriteQuoteMode = WriteQuoteMode.CANONICAL 28 | } 29 | 30 | /** 31 | * Mode for writing quote 32 | * 33 | * Usage 34 | * 35 | * CANONICAL: 36 | * Not quote normally, but quote special characters (quoteChar, delimiter, line feed). 37 | * This is specification of CSV. 38 | * See https://tools.ietf.org/html/rfc4180#section-2 39 | * ALL: 40 | * Quote all fields. 41 | */ 42 | enum class WriteQuoteMode { 43 | CANONICAL, 44 | ALL, 45 | NON_NUMERIC 46 | } 47 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/context/CsvWriterContext.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.dsl.context 2 | 3 | import com.github.doyaaaaaken.kotlincsv.util.Const 4 | import com.github.doyaaaaaken.kotlincsv.util.CsvDslMarker 5 | 6 | /** 7 | * Interface for CSV Writer settings 8 | * 9 | * @author doyaaaaaken 10 | */ 11 | @CsvDslMarker 12 | interface ICsvWriterContext { 13 | /** 14 | * Charset encoding 15 | * 16 | * The name must be supported by [java.nio.charset.Charset]. 17 | * 18 | * ex.) 19 | * "UTF-8" 20 | * "SJIS" 21 | */ 22 | val charset: String 23 | 24 | /** 25 | * Character used as delimiter between each fields 26 | * 27 | * ex.) 28 | * "," 29 | * "\t" (TSV file) 30 | */ 31 | val delimiter: Char 32 | 33 | /** 34 | * Character used when a written field is null value 35 | * 36 | * ex.) 37 | * "" (empty field) 38 | * "NULL" 39 | * "-" 40 | */ 41 | val nullCode: String 42 | 43 | /** 44 | * Character used as line terminator 45 | * 46 | * ex.) 47 | * "\r\n" 48 | * "\n" 49 | */ 50 | val lineTerminator: String 51 | 52 | /** 53 | * Output line break at the end of file or not. 54 | * 55 | * According to [CSV specification](https://tools.ietf.org/html/rfc4180#section-2), 56 | * > The last record in the file may or may not have an ending line break. 57 | */ 58 | val outputLastLineTerminator: Boolean 59 | 60 | /** 61 | * Output BOM (Byte Order Mark) at the beginning of file or not. 62 | * See https://github.com/doyaaaaaken/kotlin-csv/issues/84 63 | */ 64 | val prependBOM: Boolean 65 | 66 | /** 67 | * Options about quotes of each fields 68 | */ 69 | val quote: CsvWriteQuoteContext 70 | } 71 | 72 | /** 73 | * CSV Writer settings used in `csvWriter` DSL method. 74 | * 75 | * @author doyaaaaaken 76 | */ 77 | @CsvDslMarker 78 | class CsvWriterContext : ICsvWriterContext { 79 | override var charset = Const.defaultCharset 80 | override var delimiter: Char = ',' 81 | override var nullCode: String = "" 82 | override var lineTerminator: String = "\r\n" 83 | override var outputLastLineTerminator = true 84 | override var prependBOM = false 85 | override val quote: CsvWriteQuoteContext = CsvWriteQuoteContext() 86 | 87 | fun quote(init: CsvWriteQuoteContext.() -> Unit) { 88 | quote.init() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/parser/CsvParser.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.parser 2 | 3 | /** 4 | * Csv Parse logic while reading csv 5 | * 6 | * @author doyaaaaaken 7 | */ 8 | internal class CsvParser( 9 | private val quoteChar: Char, 10 | private val delimiter: Char, 11 | private val escapeChar: Char 12 | ) { 13 | 14 | /** 15 | * parse line (or lines if there is any quoted field containing line terminator) 16 | * and return csv row's fields as List. 17 | * 18 | * @return return parsed row fields 19 | * return null, if passed line string is on the way of csv row. 20 | */ 21 | fun parseRow(line: String, rowNum: Long = 1): List? { 22 | val stateMachine = ParseStateMachine(quoteChar, delimiter, escapeChar) 23 | var lastCh: Char? = line.firstOrNull() 24 | var skipCount = 0L 25 | line.zipWithNext { ch, nextCh -> 26 | if (skipCount > 0) { 27 | skipCount-- 28 | } else { 29 | skipCount = stateMachine.read(ch, nextCh, rowNum) - 1 30 | } 31 | lastCh = nextCh 32 | } 33 | if (lastCh != null && skipCount == 0L) { 34 | stateMachine.read(requireNotNull(lastCh), null, rowNum) 35 | } 36 | return stateMachine.getResult() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/parser/ParseStateMachine.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.parser 2 | 3 | import com.github.doyaaaaaken.kotlincsv.util.CSVParseFormatException 4 | import com.github.doyaaaaaken.kotlincsv.util.Const 5 | 6 | /** 7 | * @author doyaaaaaaken 8 | */ 9 | internal class ParseStateMachine( 10 | private val quoteChar: Char, 11 | private val delimiter: Char, 12 | private val escapeChar: Char 13 | ) { 14 | 15 | private var state = ParseState.START 16 | 17 | private val fields = ArrayList() 18 | 19 | private var field = StringBuilder() 20 | 21 | private var pos = 0L 22 | 23 | /** 24 | * Read character and change state 25 | * 26 | * @return read character count (1 or 2) 27 | */ 28 | fun read(ch: Char, nextCh: Char?, rowNum: Long): Long { 29 | val prevPos = pos 30 | when (state) { 31 | ParseState.START -> { 32 | when (ch) { 33 | Const.BOM -> Unit 34 | quoteChar -> state = ParseState.QUOTE_START 35 | delimiter -> { 36 | flushField() 37 | state = ParseState.DELIMITER 38 | } 39 | '\n', '\u2028', '\u2029', '\u0085' -> { 40 | flushField() 41 | state = ParseState.END 42 | } 43 | '\r' -> { 44 | if (nextCh == '\n') pos += 1 45 | flushField() 46 | state = ParseState.END 47 | } 48 | else -> { 49 | field.append(ch) 50 | state = ParseState.FIELD 51 | } 52 | } 53 | pos += 1 54 | } 55 | ParseState.FIELD -> { 56 | when (ch) { 57 | escapeChar -> { 58 | if (nextCh != escapeChar) throw CSVParseFormatException( 59 | rowNum, 60 | pos, 61 | ch, 62 | "must appear escapeChar($escapeChar) after escapeChar($escapeChar)" 63 | ) 64 | field.append(nextCh) 65 | state = ParseState.FIELD 66 | pos += 1 67 | } 68 | delimiter -> { 69 | flushField() 70 | state = ParseState.DELIMITER 71 | } 72 | '\n', '\u2028', '\u2029', '\u0085' -> { 73 | flushField() 74 | state = ParseState.END 75 | } 76 | '\r' -> { 77 | if (nextCh == '\n') pos += 1 78 | flushField() 79 | state = ParseState.END 80 | } 81 | else -> { 82 | field.append(ch) 83 | state = ParseState.FIELD 84 | } 85 | } 86 | pos += 1 87 | } 88 | ParseState.DELIMITER -> { 89 | when (ch) { 90 | quoteChar -> state = ParseState.QUOTE_START 91 | delimiter -> { 92 | flushField() 93 | state = ParseState.DELIMITER 94 | } 95 | '\n', '\u2028', '\u2029', '\u0085' -> { 96 | flushField() 97 | state = ParseState.END 98 | } 99 | '\r' -> { 100 | if (nextCh == '\n') pos += 1 101 | flushField() 102 | state = ParseState.END 103 | } 104 | else -> { 105 | field.append(ch) 106 | state = ParseState.FIELD 107 | } 108 | } 109 | pos += 1 110 | } 111 | ParseState.QUOTE_START, ParseState.QUOTED_FIELD -> { 112 | if (ch == escapeChar && escapeChar != quoteChar) { 113 | if (nextCh == null) throw CSVParseFormatException(rowNum, pos, ch, "end of quote doesn't exist") 114 | if (nextCh != escapeChar && nextCh != quoteChar) throw CSVParseFormatException( 115 | rowNum, 116 | pos, 117 | ch, 118 | "escape character must appear consecutively twice" 119 | ) 120 | field.append(nextCh) 121 | state = ParseState.QUOTED_FIELD 122 | pos += 1 123 | } else if (ch == quoteChar) { 124 | if (nextCh == quoteChar) { 125 | field.append(quoteChar) 126 | state = ParseState.QUOTED_FIELD 127 | pos += 1 128 | } else { 129 | state = ParseState.QUOTE_END 130 | } 131 | } else { 132 | field.append(ch) 133 | state = ParseState.QUOTED_FIELD 134 | } 135 | pos += 1 136 | } 137 | ParseState.QUOTE_END -> { 138 | when (ch) { 139 | delimiter -> { 140 | flushField() 141 | state = ParseState.DELIMITER 142 | } 143 | '\n', '\u2028', '\u2029', '\u0085' -> { 144 | flushField() 145 | state = ParseState.END 146 | } 147 | '\r' -> { 148 | if (nextCh == '\n') pos += 1 149 | flushField() 150 | state = ParseState.END 151 | } 152 | else -> throw CSVParseFormatException( 153 | rowNum, 154 | pos, 155 | ch, 156 | "must appear delimiter or line terminator after quote end" 157 | ) 158 | } 159 | pos += 1 160 | } 161 | ParseState.END -> throw CSVParseFormatException(rowNum, pos, ch, "unexpected error") 162 | } 163 | return pos - prevPos 164 | } 165 | 166 | /** 167 | * @return return parsed CSV Fields. 168 | * return null, if current position is on the way of csv row. 169 | */ 170 | fun getResult(): List? { 171 | return when (state) { 172 | ParseState.DELIMITER -> { 173 | fields.add("") 174 | fields.toList() 175 | } 176 | ParseState.QUOTED_FIELD -> null 177 | ParseState.FIELD, ParseState.QUOTE_END -> { 178 | fields.add(field.toString()) 179 | fields.toList() 180 | } 181 | else -> fields.toList() 182 | } 183 | } 184 | 185 | private fun flushField() { 186 | fields.add(field.toString()) 187 | field.clear() 188 | } 189 | } 190 | 191 | private enum class ParseState { 192 | START, 193 | FIELD, 194 | DELIMITER, 195 | END, 196 | QUOTE_START, 197 | QUOTE_END, 198 | QUOTED_FIELD 199 | } 200 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/util/CSVException.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.util 2 | 3 | /** 4 | * General purpose Exception 5 | */ 6 | open class MalformedCSVException(message: String) : RuntimeException(message) 7 | 8 | /** 9 | * Exception when parsing each csv row 10 | */ 11 | class CSVParseFormatException( 12 | val rowNum: Long, 13 | val colIndex: Long, 14 | val char: Char, 15 | message: String = "Exception happened on parsing csv" 16 | ) : MalformedCSVException("$message [rowNum = $rowNum, colIndex = $colIndex, char = $char]") 17 | 18 | /** 19 | * Exception when field's num is different on each csv row. 20 | * 21 | * This is according to [CSV Specification](https://tools.ietf.org/html/rfc4180#section-2). 22 | * > Each line should contain the same number of fields throughout the file. 23 | * 24 | * For example, below csv data is invalid on 2nd csv row (`d, e`). 25 | *
26 |  * a,b,c
27 |  * d,e
28 |  * f,g,h
29 |  * 
30 | */ 31 | class CSVFieldNumDifferentException( 32 | val fieldNum: Int, 33 | val fieldNumOnFailedRow: Int, 34 | val csvRowNum: Int 35 | ) : MalformedCSVException("Fields num seems to be $fieldNum on each row, but on ${csvRowNum}th csv row, fields num is $fieldNumOnFailedRow.") 36 | 37 | class CSVAutoRenameFailedException : 38 | MalformedCSVException("auto renaming by 'autoRenameDuplicateHeaders' option is failed.") 39 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/util/Const.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.util 2 | 3 | /** 4 | * Constant variables used in this project 5 | * 6 | * @author doyaaaaaken 7 | */ 8 | internal object Const { 9 | const val defaultCharset = "UTF-8" 10 | 11 | const val BOM = '\uFEFF' 12 | } 13 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/util/CsvDslMarker.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.util 2 | 3 | /** 4 | * @author doyaaaaaken 5 | */ 6 | @DslMarker 7 | annotation class CsvDslMarker 8 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/util/logger/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.util.logger 2 | 3 | /** 4 | * Logger interface for logging debug statements at runtime. 5 | * Library consumers may provide implementations suiting their needs. 6 | * @see [com.github.doyaaaaaken.kotlincsv.dsl.context.ICsvReaderContext.logger] 7 | */ 8 | interface Logger { 9 | fun info(message: String) 10 | } 11 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/doyaaaaaken/kotlincsv/util/logger/LoggerNop.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.util.logger 2 | 3 | /** 4 | * Internal no-operation logger implementation, which does not log anything. 5 | */ 6 | internal object LoggerNop : Logger { 7 | override fun info(message: String) = Unit 8 | } 9 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvReader.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import com.github.doyaaaaaken.kotlincsv.dsl.context.CsvReaderContext 4 | import com.github.doyaaaaaken.kotlincsv.dsl.context.ICsvReaderContext 5 | 6 | /** 7 | * CSV Reader class 8 | * 9 | * @author doyaaaaaken 10 | */ 11 | actual class CsvReader actual constructor( 12 | private val ctx: CsvReaderContext 13 | ) : ICsvReaderContext by ctx { 14 | 15 | /** 16 | * read csv data as String, and convert into List> 17 | */ 18 | actual fun readAll(data: String): List> { 19 | return CsvFileReader(ctx, StringReaderImpl(data), logger).readAllAsSequence().toList() 20 | } 21 | 22 | /** 23 | * read csv data with header, and convert into List> 24 | */ 25 | actual fun readAllWithHeader(data: String): List> { 26 | return CsvFileReader(ctx, StringReaderImpl(data), logger).readAllWithHeaderAsSequence().toList() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvWriter.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import com.github.doyaaaaaken.kotlincsv.dsl.context.CsvWriterContext 4 | 5 | /** 6 | * CSV Writer class 7 | * 8 | * @author doyaaaaaken 9 | */ 10 | actual class CsvWriter actual constructor(ctx: CsvWriterContext) { 11 | 12 | actual fun open(targetFileName: String, append: Boolean, write: ICsvFileWriter.() -> Unit) { 13 | TODO("Not Implemented") 14 | } 15 | 16 | actual fun writeAll(rows: List>, targetFileName: String, append: Boolean) { 17 | TODO("Not Implemented") 18 | } 19 | 20 | actual suspend fun writeAllAsync(rows: List>, targetFileName: String, append: Boolean) { 21 | TODO("Not Implemented") 22 | } 23 | 24 | actual suspend fun openAsync(targetFileName: String, append: Boolean, write: suspend ICsvFileWriter.() -> Unit) { 25 | TODO("Not Implemented") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/Annotation.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | @RequiresOptIn( 4 | message = "This API is experimental. It may be changed in the future without notice.", 5 | level = RequiresOptIn.Level.WARNING 6 | ) 7 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) 8 | annotation class KotlinCsvExperimental 9 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvFileWriter.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import com.github.doyaaaaaken.kotlincsv.dsl.context.CsvWriterContext 4 | import com.github.doyaaaaaken.kotlincsv.dsl.context.WriteQuoteMode 5 | import com.github.doyaaaaaken.kotlincsv.util.Const 6 | import java.io.Closeable 7 | import java.io.Flushable 8 | import java.io.IOException 9 | import java.io.PrintWriter 10 | 11 | /** 12 | * CSV Writer class, which controls file I/O flow. 13 | * 14 | * @author doyaaaaaken 15 | */ 16 | class CsvFileWriter internal constructor( 17 | private val ctx: CsvWriterContext, 18 | private val writer: PrintWriter 19 | ) : ICsvFileWriter, Closeable, Flushable { 20 | 21 | private val quoteNeededChars = setOf('\r', '\n', ctx.quote.char, ctx.delimiter) 22 | 23 | private var hasWroteInitialChar: Boolean = false 24 | 25 | /** 26 | * write one row 27 | */ 28 | override fun writeRow(row: List) { 29 | willWritePreTerminator() 30 | writeNext(row) 31 | willWriteEndTerminator() 32 | 33 | if (writer.checkError()) { 34 | throw IOException("Failed to write") 35 | } 36 | } 37 | 38 | /** 39 | * write one row 40 | */ 41 | override fun writeRow(vararg entry: Any?) { 42 | writeRow(entry.toList()) 43 | } 44 | 45 | /** 46 | * write rows 47 | */ 48 | override fun writeRows(rows: List>) { 49 | willWritePreTerminator() 50 | rows.forEachIndexed { index, list -> 51 | writeNext(list) 52 | if (index < rows.size - 1 && list.isNotEmpty()) { 53 | writeTerminator() 54 | } 55 | } 56 | willWriteEndTerminator() 57 | if (writer.checkError()) { 58 | throw IOException("Failed to write") 59 | } 60 | } 61 | 62 | /** 63 | * write rows from Sequence 64 | */ 65 | override fun writeRows(rows: Sequence>) { 66 | willWritePreTerminator() 67 | 68 | val itr = rows.iterator() 69 | while (itr.hasNext()) { 70 | val row = itr.next() 71 | writeNext(row) 72 | if (itr.hasNext() && row.isNotEmpty()) writeTerminator() 73 | } 74 | 75 | willWriteEndTerminator() 76 | if (writer.checkError()) { 77 | throw IOException("Failed to write") 78 | } 79 | } 80 | 81 | override fun flush() { 82 | writer.flush() 83 | } 84 | 85 | override fun close() { 86 | writer.close() 87 | } 88 | 89 | private fun writeNext(row: List) { 90 | if (!hasWroteInitialChar && ctx.prependBOM) { 91 | writer.print(Const.BOM) 92 | } 93 | 94 | val rowStr = row.joinToString(ctx.delimiter.toString()) { field -> 95 | if (field == null) { 96 | ctx.nullCode 97 | } else { 98 | attachQuote(field.toString()) 99 | } 100 | } 101 | writer.print(rowStr) 102 | hasWroteInitialChar = true 103 | } 104 | 105 | /** 106 | * Will write terminator if writer has not wrote last line terminator on previous line. 107 | */ 108 | private fun willWritePreTerminator() { 109 | if (!ctx.outputLastLineTerminator && hasWroteInitialChar) { 110 | writeTerminator() 111 | } 112 | } 113 | 114 | /** 115 | * write terminator for next line 116 | */ 117 | private fun writeTerminator() { 118 | writer.print(ctx.lineTerminator) 119 | } 120 | 121 | private fun willWriteEndTerminator() { 122 | if (ctx.outputLastLineTerminator) { 123 | writeTerminator() 124 | } 125 | } 126 | 127 | private fun attachQuote(field: String): String { 128 | val shouldQuote = when (ctx.quote.mode) { 129 | WriteQuoteMode.ALL -> true 130 | WriteQuoteMode.CANONICAL -> field.any { ch -> quoteNeededChars.contains(ch) } 131 | WriteQuoteMode.NON_NUMERIC -> { 132 | var foundDot = false 133 | field.any { ch -> 134 | if (ch == '.') { 135 | if (foundDot) { 136 | true 137 | } else { 138 | foundDot = true 139 | false 140 | } 141 | } else { 142 | ch < '0' || ch > '9' 143 | } 144 | } 145 | } 146 | } 147 | 148 | return buildString { 149 | if (shouldQuote) append(ctx.quote.char) 150 | field.forEach { ch -> 151 | if (ch == ctx.quote.char) { 152 | append(ctx.quote.char) 153 | } 154 | append(ch) 155 | } 156 | if (shouldQuote) append(ctx.quote.char) 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvReader.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import com.github.doyaaaaaken.kotlincsv.dsl.context.CsvReaderContext 4 | import com.github.doyaaaaaken.kotlincsv.dsl.context.ICsvReaderContext 5 | import java.io.File 6 | import java.io.InputStream 7 | import java.nio.charset.Charset 8 | 9 | /** 10 | * CSV Reader class 11 | * 12 | * @author doyaaaaaken 13 | */ 14 | actual class CsvReader actual constructor( 15 | private val ctx: CsvReaderContext 16 | ) : ICsvReaderContext by ctx { 17 | 18 | private val charsetCode = Charset.forName(charset) 19 | 20 | /** 21 | * read csv data as String, and convert into List> 22 | * 23 | * No need to close InputStream when calling this method. 24 | */ 25 | actual fun readAll(data: String): List> { 26 | val br = data.byteInputStream(charsetCode).bufferedReader(charsetCode) 27 | return open(br) { readAllAsSequence().toList() } 28 | } 29 | 30 | /** 31 | * read csv data as File, and convert into List> 32 | * 33 | * No need to close InputStream when calling this method. 34 | */ 35 | fun readAll(file: File): List> { 36 | val br = file.inputStream().bufferedReader(charsetCode) 37 | return open(br) { readAllAsSequence().toList() } 38 | } 39 | 40 | /** 41 | * read csv data as InputStream, and convert into List> 42 | * 43 | * No need to close InputStream when calling this method. 44 | */ 45 | fun readAll(ips: InputStream): List> { 46 | val br = ips.bufferedReader(charsetCode) 47 | return open(br) { readAllAsSequence().toList() } 48 | } 49 | 50 | /** 51 | * read csv data with header, and convert into List> 52 | * 53 | * No need to close InputStream when calling this method. 54 | */ 55 | actual fun readAllWithHeader(data: String): List> { 56 | val br = data.byteInputStream(charsetCode).bufferedReader(charsetCode) 57 | return open(br) { readAllWithHeaderAsSequence().toList() } 58 | } 59 | 60 | /** 61 | * read csv data with header, and convert into List> 62 | * 63 | * No need to close InputStream when calling this method. 64 | */ 65 | fun readAllWithHeader(file: File): List> { 66 | val br = file.inputStream().bufferedReader(charsetCode) 67 | return open(br) { readAllWithHeaderAsSequence().toList() } 68 | } 69 | 70 | /** 71 | * read csv data with header, and convert into List> 72 | * 73 | * No need to close InputStream when calling this method. 74 | */ 75 | fun readAllWithHeader(ips: InputStream): List> { 76 | val br = ips.bufferedReader(charsetCode) 77 | return open(br) { readAllWithHeaderAsSequence().toList() } 78 | } 79 | 80 | /** 81 | * open inputStreamReader and execute reading process. 82 | * 83 | * If you want to control read flow precisely, use this method. 84 | * Otherwise, use utility method (e.g. CsvReader.readAll ). 85 | * 86 | * Usage example: 87 | *
 88 |      *   val data: Sequence> = csvReader().open("test.csv") {
 89 |      *       readAllAsSequence()
 90 |      *           .map { fields -> fields.map { it.trim() } }
 91 |      *           .map { fields -> fields.map { if(it.isBlank()) null else it } }
 92 |      *   }
 93 |      * 
94 | */ 95 | fun open(fileName: String, read: CsvFileReader.() -> T): T { 96 | return open(File(fileName), read) 97 | } 98 | 99 | /** 100 | * open inputStreamReader and execute reading process on a **suspending** function. 101 | * 102 | * If you want to control read flow precisely, use this method. 103 | * Otherwise, use utility method (e.g. CsvReader.readAll ). 104 | * 105 | * Usage example: 106 | *
107 |      *   val data: Sequence> = csvReader().openAsync("test.csv") {
108 |      *       readAllAsSequence()
109 |      *           .map { fields -> fields.map { it.trim() } }
110 |      *           .map { fields -> fields.map { if(it.isBlank()) null else it } }
111 |      *   }
112 |      * 
113 | */ 114 | suspend fun openAsync(fileName: String, read: suspend CsvFileReader.() -> T): T { 115 | return openAsync(File(fileName), read) 116 | } 117 | 118 | /** 119 | * open inputStreamReader and execute reading process. 120 | * 121 | * If you want to control read flow precisely, use this method. 122 | * Otherwise, use utility method (e.g. CsvReader.readAll ). 123 | * 124 | * Usage example: 125 | * @see open method 126 | */ 127 | fun open(file: File, read: CsvFileReader.() -> T): T { 128 | val br = file.inputStream().bufferedReader(charsetCode) 129 | return open(br, read) 130 | } 131 | 132 | /** 133 | * open inputStreamReader and execute reading process on a **suspending** function. 134 | * 135 | * If you want to control read flow precisely, use this method. 136 | * Otherwise, use utility method (e.g. CsvReader.readAll ). 137 | * 138 | * Usage example: 139 | * @see openAsync method 140 | */ 141 | suspend fun openAsync(file: File, read: suspend CsvFileReader.() -> T): T { 142 | val br = file.inputStream().bufferedReader(charsetCode) 143 | return openAsync(br, read) 144 | } 145 | 146 | /** 147 | * open inputStreamReader and execute reading process on a **suspending** function. 148 | * 149 | * If you want to control read flow precisely, use this method. 150 | * Otherwise, use utility method (e.g. CsvReader.readAll ). 151 | * 152 | * Usage example: 153 | * @see open method 154 | */ 155 | fun open(ips: InputStream, read: CsvFileReader.() -> T): T { 156 | val br = ips.bufferedReader(charsetCode) 157 | return open(br, read) 158 | } 159 | 160 | /** 161 | * open inputStreamReader and execute reading process on a **suspending** function. 162 | * 163 | * If you want to control read flow precisely, use this method. 164 | * Otherwise, use utility method (e.g. CsvReader.readAll ). 165 | * 166 | * Usage example: 167 | * @see openAsync method 168 | */ 169 | suspend fun openAsync(ips: InputStream, read: suspend CsvFileReader.() -> T): T { 170 | val br = ips.bufferedReader(charsetCode) 171 | return openAsync(br, read) 172 | } 173 | 174 | private fun open(br: Reader, doRead: CsvFileReader.() -> T): T { 175 | val reader = CsvFileReader(ctx, br, logger) 176 | return reader.use { 177 | reader.doRead() 178 | } 179 | } 180 | 181 | private suspend fun openAsync(br: Reader, doRead: suspend CsvFileReader.() -> T): T { 182 | val reader = CsvFileReader(ctx, br, logger) 183 | return reader.use { 184 | reader.doRead() 185 | } 186 | } 187 | } 188 | 189 | private inline fun CsvFileReader.use(block: (CsvFileReader) -> R): R { 190 | var exception: Throwable? = null 191 | try { 192 | return block(this) 193 | } catch (e: Throwable) { 194 | exception = e 195 | throw e 196 | } finally { 197 | when (exception) { 198 | null -> close() 199 | else -> 200 | try { 201 | close() 202 | } catch (t: Throwable) { 203 | exception.addSuppressed(t) 204 | } 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvWriter.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import com.github.doyaaaaaken.kotlincsv.dsl.context.CsvWriterContext 4 | import com.github.doyaaaaaken.kotlincsv.dsl.context.ICsvWriterContext 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | import java.io.* 8 | import java.nio.charset.Charset 9 | 10 | /** 11 | * CSV Writer class, which decides where to write and returns CsvFileWriter class (class for controlling File I/O). 12 | * 13 | * @see CsvFileWriter 14 | * 15 | * @author doyaaaaaken 16 | */ 17 | actual class CsvWriter actual constructor( 18 | private val ctx: CsvWriterContext 19 | ) : ICsvWriterContext by ctx { 20 | 21 | actual fun open(targetFileName: String, append: Boolean, write: ICsvFileWriter.() -> Unit) { 22 | val targetFile = File(targetFileName) 23 | open(targetFile, append, write) 24 | } 25 | 26 | actual suspend fun openAsync(targetFileName: String, append: Boolean, write: suspend ICsvFileWriter.() -> Unit) { 27 | val targetFile = File(targetFileName) 28 | openAsync(targetFile, append, write) 29 | } 30 | 31 | fun open(targetFile: File, append: Boolean = false, write: ICsvFileWriter.() -> Unit) { 32 | val fos = FileOutputStream(targetFile, append).buffered() 33 | open(fos, write) 34 | } 35 | 36 | suspend fun openAsync(targetFile: File, append: Boolean = false, write: suspend ICsvFileWriter.() -> Unit) = 37 | withContext(Dispatchers.IO) { 38 | val fos = FileOutputStream(targetFile, append).buffered() 39 | openAsync(fos, write) 40 | } 41 | 42 | fun open(ops: OutputStream, write: ICsvFileWriter.() -> Unit) { 43 | val osw = OutputStreamWriter(ops, ctx.charset) 44 | val writer = CsvFileWriter(ctx, PrintWriter(osw)) 45 | writer.use { it.write() } 46 | } 47 | 48 | suspend fun openAsync(ops: OutputStream, write: suspend ICsvFileWriter.() -> Unit) = withContext(Dispatchers.IO) { 49 | val osw = OutputStreamWriter(ops, ctx.charset) 50 | val writer = CsvFileWriter(ctx, PrintWriter(osw)) 51 | writer.use { it.write() } 52 | } 53 | 54 | internal fun writeAsString(write: ICsvFileWriter.() -> Unit): String { 55 | val baos = ByteArrayOutputStream() 56 | open(baos, write) 57 | return String(baos.toByteArray(), Charset.forName(ctx.charset)) 58 | } 59 | 60 | /** 61 | * *** ONLY for long-running write case *** 62 | * 63 | * Get and use [CsvFileWriter] directly. 64 | * MUST NOT forget to close [CsvFileWriter] after using it. 65 | * 66 | * Use this method If you want to close file writer manually (i.e. streaming scenario). 67 | */ 68 | @KotlinCsvExperimental 69 | fun openAndGetRawWriter(targetFileName: String, append: Boolean = false): CsvFileWriter { 70 | val targetFile = File(targetFileName) 71 | return openAndGetRawWriter(targetFile, append) 72 | } 73 | 74 | /** 75 | * *** ONLY for long-running write case *** 76 | * 77 | * Get and use [CsvFileWriter] directly. 78 | * MUST NOT forget to close [CsvFileWriter] after using it. 79 | * 80 | * Use this method If you want to close file writer manually (i.e. streaming scenario). 81 | */ 82 | @KotlinCsvExperimental 83 | fun openAndGetRawWriter(targetFile: File, append: Boolean = false): CsvFileWriter { 84 | val fos = FileOutputStream(targetFile, append) 85 | return openAndGetRawWriter(fos) 86 | } 87 | 88 | /** 89 | * *** ONLY for long-running write case *** 90 | * 91 | * Get and use [CsvFileWriter] directly. 92 | * MUST NOT forget to close [CsvFileWriter] after using it. 93 | * 94 | * Use this method If you want to close file writer manually (i.e. streaming scenario). 95 | */ 96 | @KotlinCsvExperimental 97 | fun openAndGetRawWriter(ops: OutputStream): CsvFileWriter { 98 | val osw = OutputStreamWriter(ops, ctx.charset) 99 | return CsvFileWriter(ctx, PrintWriter(osw)) 100 | } 101 | 102 | /** 103 | * write all rows on assigned target file 104 | */ 105 | actual fun writeAll(rows: List>, targetFileName: String, append: Boolean) { 106 | open(targetFileName, append) { writeRows(rows) } 107 | } 108 | 109 | /** 110 | * write all rows on assigned target file 111 | */ 112 | actual suspend fun writeAllAsync(rows: List>, targetFileName: String, append: Boolean) { 113 | openAsync(targetFileName, append) { writeRows(rows) } 114 | } 115 | 116 | /** 117 | * write all rows on assigned target file 118 | */ 119 | fun writeAll(rows: List>, targetFile: File, append: Boolean = false) { 120 | open(targetFile, append) { writeRows(rows) } 121 | } 122 | 123 | /** 124 | * write all rows on assigned target file 125 | */ 126 | suspend fun writeAllAsync(rows: List>, targetFile: File, append: Boolean = false) { 127 | openAsync(targetFile, append) { writeRows(rows) } 128 | } 129 | 130 | /** 131 | * write all rows on assigned output stream 132 | */ 133 | fun writeAll(rows: List>, ops: OutputStream) { 134 | open(ops) { writeRows(rows) } 135 | } 136 | 137 | /** 138 | * write all rows on assigned output stream 139 | */ 140 | suspend fun writeAllAsync(rows: List>, ops: OutputStream) { 141 | openAsync(ops) { writeRows(rows) } 142 | } 143 | 144 | /** 145 | * write all rows to string 146 | */ 147 | fun writeAllAsString(rows: List>): String { 148 | return writeAsString { writeRows(rows) } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/com/github/doyaaaaaken/kotlincsv/client/Reader.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import java.io.InputStream 4 | import java.nio.charset.Charset 5 | 6 | class ReaderJvmImpl(private val reader: java.io.BufferedReader) : Reader { 7 | override fun read(): Int { 8 | return reader.read() 9 | } 10 | 11 | override fun mark(readAheadLimit: Int) { 12 | reader.mark(readAheadLimit) 13 | } 14 | 15 | override fun reset() { 16 | reader.reset() 17 | } 18 | 19 | override fun close() { 20 | reader.close() 21 | } 22 | } 23 | 24 | internal fun InputStream.bufferedReader(charset: Charset = Charsets.UTF_8): Reader = 25 | ReaderJvmImpl(reader(charset).buffered()) 26 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/client/BufferedLineReaderTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import io.kotest.assertions.assertSoftly 4 | import io.kotest.core.spec.style.StringSpec 5 | import io.kotest.matchers.shouldBe 6 | 7 | class BufferedLineReaderTest : StringSpec({ 8 | "regard \\n as line terminator" { 9 | val str = "a,b,c\nd,e,f" 10 | val br = str.byteInputStream().bufferedReader() 11 | val blr = BufferedLineReader(br) 12 | assertSoftly { 13 | blr.readLineWithTerminator() shouldBe "a,b,c\n" 14 | blr.readLineWithTerminator() shouldBe "d,e,f" 15 | blr.readLineWithTerminator() shouldBe null 16 | } 17 | } 18 | 19 | "regard \\r\\n as line terminator" { 20 | val str = "a,b,c\r\nd,e,f" 21 | val br = str.byteInputStream().bufferedReader() 22 | val blr = BufferedLineReader(br) 23 | assertSoftly { 24 | blr.readLineWithTerminator() shouldBe "a,b,c\r\n" 25 | blr.readLineWithTerminator() shouldBe "d,e,f" 26 | blr.readLineWithTerminator() shouldBe null 27 | } 28 | } 29 | 30 | "regard \\r as line terminator" { 31 | val str = "a,b,c\rd,e,f" 32 | val br = str.byteInputStream().bufferedReader() 33 | val blr = BufferedLineReader(br) 34 | assertSoftly { 35 | blr.readLineWithTerminator() shouldBe "a,b,c\r" 36 | blr.readLineWithTerminator() shouldBe "d,e,f" 37 | blr.readLineWithTerminator() shouldBe null 38 | } 39 | } 40 | 41 | "regard \\u2028 as line terminator" { 42 | val str = "a,b,c\u2028d,e,f" 43 | val br = str.byteInputStream().bufferedReader() 44 | val blr = BufferedLineReader(br) 45 | assertSoftly { 46 | blr.readLineWithTerminator() shouldBe "a,b,c\u2028" 47 | blr.readLineWithTerminator() shouldBe "d,e,f" 48 | blr.readLineWithTerminator() shouldBe null 49 | } 50 | } 51 | 52 | "regard \\u2029 as line terminator" { 53 | val str = "a,b,c\u2029d,e,f" 54 | val br = str.byteInputStream().bufferedReader() 55 | val blr = BufferedLineReader(br) 56 | assertSoftly { 57 | blr.readLineWithTerminator() shouldBe "a,b,c\u2029" 58 | blr.readLineWithTerminator() shouldBe "d,e,f" 59 | blr.readLineWithTerminator() shouldBe null 60 | } 61 | } 62 | 63 | "regard \\u0085 as line terminator" { 64 | val str = "a,b,c\u0085d,e,f" 65 | val br = str.byteInputStream().bufferedReader() 66 | val blr = BufferedLineReader(br) 67 | assertSoftly { 68 | blr.readLineWithTerminator() shouldBe "a,b,c\u0085" 69 | blr.readLineWithTerminator() shouldBe "d,e,f" 70 | blr.readLineWithTerminator() shouldBe null 71 | } 72 | } 73 | 74 | "deal with \\r at the end of file" { 75 | val str = "a,b,c\r" 76 | val br = str.byteInputStream().bufferedReader() 77 | val blr = BufferedLineReader(br) 78 | assertSoftly { 79 | blr.readLineWithTerminator() shouldBe "a,b,c\r" 80 | blr.readLineWithTerminator() shouldBe null 81 | } 82 | } 83 | }) 84 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvFileWriterTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import com.github.doyaaaaaken.kotlincsv.dsl.csvWriter 4 | import io.kotest.assertions.throwables.shouldThrow 5 | import io.kotest.core.spec.style.WordSpec 6 | import io.kotest.matchers.shouldBe 7 | import kotlinx.coroutines.delay 8 | import java.io.File 9 | import java.io.IOException 10 | import java.nio.charset.Charset 11 | import java.time.LocalDate 12 | import java.time.LocalDateTime 13 | 14 | class CsvFileWriterTest : WordSpec({ 15 | val testFileName = "test.csv" 16 | 17 | afterTest { File(testFileName).delete() } 18 | 19 | fun readTestFile(charset: Charset = Charsets.UTF_8): String { 20 | return File(testFileName).readText(charset) 21 | } 22 | 23 | "writeRow method" should { 24 | "write any primitive types" { 25 | val row = listOf("String", 'C', 1, 2L, 3.45, true, null) 26 | val expected = "String,C,1,2,3.45,true,\r\n" 27 | csvWriter().open(testFileName) { 28 | writeRow(row) 29 | } 30 | val actual = readTestFile() 31 | actual shouldBe expected 32 | } 33 | "write java.time.LocalDate and java.time.LocalDateTime types" { 34 | val row = listOf( 35 | LocalDate.of(2019, 8, 19), 36 | LocalDateTime.of(2020, 9, 20, 14, 32, 21) 37 | ) 38 | val expected = "2019-08-19,2020-09-20T14:32:21\r\n" 39 | csvWriter().open(testFileName) { 40 | writeRow(row) 41 | } 42 | val actual = readTestFile() 43 | actual shouldBe expected 44 | } 45 | "write row from variable arguments" { 46 | 47 | val date1 = LocalDate.of(2019, 8, 19) 48 | val date2 = LocalDateTime.of(2020, 9, 20, 14, 32, 21) 49 | 50 | val expected = "a,b,c\r\n" + 51 | "d,e,f\r\n" + 52 | "1,2,3\r\n" + 53 | "2019-08-19,2020-09-20T14:32:21\r\n" 54 | csvWriter().open(testFileName) { 55 | writeRow("a", "b", "c") 56 | writeRow("d", "e", "f") 57 | writeRow(1, 2, 3) 58 | writeRow(date1, date2) 59 | } 60 | val actual = readTestFile() 61 | actual shouldBe expected 62 | } 63 | } 64 | "writeAll method" should { 65 | "write Sequence data" { 66 | val rows = listOf(listOf("a", "b", "c"), listOf("d", "e", "f")).asSequence() 67 | val expected = "a,b,c\r\nd,e,f\r\n" 68 | csvWriter().open(testFileName) { 69 | writeRows(rows) 70 | } 71 | val actual = readTestFile() 72 | actual shouldBe expected 73 | } 74 | "write escaped field when a field contains quoteChar in it" { 75 | val rows = listOf(listOf("a", "\"b", "c"), listOf("d", "e", "f\"")) 76 | val expected = "a,\"\"\"b\",c\r\nd,e,\"f\"\"\"\r\n" 77 | csvWriter().open(testFileName) { 78 | writeRows(rows) 79 | } 80 | val actual = readTestFile() 81 | actual shouldBe expected 82 | } 83 | "write escaped field when a field contains delimiter in it" { 84 | val rows = listOf(listOf("a", ",b", "c"), listOf("d", "e", "f,")) 85 | val expected = "a,\",b\",c\r\nd,e,\"f,\"\r\n" 86 | csvWriter().open(testFileName) { 87 | writeRows(rows) 88 | } 89 | val actual = readTestFile() 90 | actual shouldBe expected 91 | } 92 | "write quoted field when a field contains cr or lf in it" { 93 | val rows = listOf(listOf("a", "\nb", "c"), listOf("d", "e", "f\r\n")) 94 | val expected = "a,\"\nb\",c\r\nd,e,\"f\r\n\"\r\n" 95 | csvWriter().open(testFileName) { 96 | writeRows(rows) 97 | } 98 | val actual = readTestFile() 99 | actual shouldBe expected 100 | } 101 | "write no line terminator when row is empty for rows from list" { 102 | val rows = listOf(listOf("a", "b", "c"), listOf(), listOf("d", "e", "f")) 103 | val expected = "a,b,c\r\nd,e,f\r\n" 104 | csvWriter().open(testFileName) { 105 | writeRows(rows) 106 | } 107 | val actual = readTestFile() 108 | actual shouldBe expected 109 | } 110 | "write no line terminator when row is empty for rows from sequence" { 111 | val rows = listOf(listOf("a", "b", "c"), listOf(), listOf("d", "e", "f")).asSequence() 112 | val expected = "a,b,c\r\nd,e,f\r\n" 113 | csvWriter().open(testFileName) { 114 | writeRows(rows) 115 | } 116 | val actual = readTestFile() 117 | actual shouldBe expected 118 | } 119 | } 120 | "close method" should { 121 | "throw Exception when stream is already closed" { 122 | val row = listOf("a", "b") 123 | shouldThrow { 124 | csvWriter().open(testFileName) { 125 | close() 126 | writeRow(row) 127 | } 128 | } 129 | } 130 | } 131 | "flush method" should { 132 | "flush stream" { 133 | val row = listOf("a", "b") 134 | csvWriter().open(testFileName) { 135 | writeRow(row) 136 | flush() 137 | val actual = readTestFile() 138 | actual shouldBe "a,b\r\n" 139 | } 140 | } 141 | } 142 | "suspend writeRow method" should { 143 | "suspend write any primitive types" { 144 | val row = listOf("String", 'C', 1, 2L, 3.45, true, null) 145 | val expected = "String,C,1,2,3.45,true,\r\n" 146 | csvWriter().openAsync(testFileName) { 147 | writeRow(row) 148 | } 149 | val actual = readTestFile() 150 | actual shouldBe expected 151 | } 152 | "suspend write row from variable arguments" { 153 | 154 | val date1 = LocalDate.of(2019, 8, 19) 155 | val date2 = LocalDateTime.of(2020, 9, 20, 14, 32, 21) 156 | 157 | val expected = "a,b,c\r\n" + 158 | "d,e,f\r\n" + 159 | "1,2,3\r\n" + 160 | "2019-08-19,2020-09-20T14:32:21\r\n" 161 | csvWriter().openAsync(testFileName) { 162 | writeRow("a", "b", "c") 163 | writeRow("d", "e", "f") 164 | writeRow(1, 2, 3) 165 | writeRow(date1, date2) 166 | } 167 | val actual = readTestFile() 168 | actual shouldBe expected 169 | } 170 | "suspend write all Sequence data" { 171 | val rows = listOf(listOf("a", "b", "c"), listOf("d", "e", "f")).asSequence() 172 | val expected = "a,b,c\r\nd,e,f\r\n" 173 | csvWriter().openAsync(testFileName) { 174 | writeRows(rows) 175 | } 176 | val actual = readTestFile() 177 | actual shouldBe expected 178 | } 179 | } 180 | "suspend close method" should { 181 | "throw Exception when stream is already closed" { 182 | val row = listOf("a", "b") 183 | shouldThrow { 184 | csvWriter().openAsync(testFileName) { 185 | close() 186 | writeRow(row) 187 | } 188 | } 189 | } 190 | } 191 | "suspend flush method" should { 192 | "flush stream" { 193 | val row = listOf("a", "b") 194 | csvWriter().openAsync(testFileName) { 195 | writeRow(row) 196 | flush() 197 | val actual = readTestFile() 198 | actual shouldBe "a,b\r\n" 199 | } 200 | } 201 | } 202 | "validate suspend test as flow" should { 203 | "execute line" { 204 | val rows = listOf(listOf("a", "b", "c"), listOf("d", "e", "f")).asSequence() 205 | val expected = "a,b,c\r\nd,e,f\r\n" 206 | csvWriter().openAsync(testFileName) { 207 | delay(100) 208 | rows.forEach { 209 | delay(100) 210 | writeRow(it) 211 | } 212 | } 213 | val actual = readTestFile() 214 | actual shouldBe expected 215 | } 216 | } 217 | 218 | }) 219 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvReadWriteCompatibilityTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import com.github.doyaaaaaken.kotlincsv.dsl.csvReader 4 | import com.github.doyaaaaaken.kotlincsv.dsl.csvWriter 5 | import io.kotest.core.spec.style.StringSpec 6 | import io.kotest.matchers.shouldBe 7 | import java.io.File 8 | 9 | 10 | class CsvReadWriteCompatibilityTest : StringSpec({ 11 | 12 | val testFileName = "compatibility-test.csv" 13 | 14 | afterTest { File(testFileName).delete() } 15 | 16 | "CSVReader and CSVWriter are compatible" { 17 | val data = listOf( 18 | listOf("a", "bb", "ccc"), 19 | listOf("d", "ee", "fff") 20 | ) 21 | csvWriter().open(testFileName) { 22 | writeRows(data) 23 | } 24 | val actual = csvReader().readAll(File(testFileName)) 25 | actual shouldBe data 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvReaderTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import com.github.doyaaaaaken.kotlincsv.dsl.context.CsvReaderContext 4 | import com.github.doyaaaaaken.kotlincsv.dsl.context.ExcessFieldsRowBehaviour 5 | import com.github.doyaaaaaken.kotlincsv.dsl.context.InsufficientFieldsRowBehaviour 6 | import com.github.doyaaaaaken.kotlincsv.dsl.csvReader 7 | import com.github.doyaaaaaken.kotlincsv.util.CSVFieldNumDifferentException 8 | import com.github.doyaaaaaken.kotlincsv.util.CSVParseFormatException 9 | import com.github.doyaaaaaken.kotlincsv.util.Const 10 | import com.github.doyaaaaaken.kotlincsv.util.MalformedCSVException 11 | import io.kotest.assertions.assertSoftly 12 | import io.kotest.assertions.throwables.shouldThrow 13 | import io.kotest.core.spec.style.WordSpec 14 | import io.kotest.matchers.shouldBe 15 | import kotlinx.coroutines.flow.asFlow 16 | import kotlinx.coroutines.flow.collect 17 | import java.io.File 18 | 19 | class CsvReaderTest : WordSpec({ 20 | "CsvReader class constructor" should { 21 | "be created with no argument" { 22 | val reader = CsvReader() 23 | reader.charset shouldBe Const.defaultCharset 24 | } 25 | "be created with CsvReaderContext argument" { 26 | val context = CsvReaderContext().apply { 27 | charset = Charsets.ISO_8859_1.name() 28 | quoteChar = '\'' 29 | delimiter = '\t' 30 | escapeChar = '"' 31 | skipEmptyLine = true 32 | } 33 | val reader = CsvReader(context) 34 | assertSoftly { 35 | reader.charset shouldBe Charsets.ISO_8859_1.name() 36 | reader.quoteChar shouldBe '\'' 37 | reader.delimiter shouldBe '\t' 38 | reader.escapeChar shouldBe '"' 39 | reader.skipEmptyLine shouldBe true 40 | } 41 | } 42 | } 43 | 44 | "readAll method (with String argument)" should { 45 | "read simple csv" { 46 | val result = csvReader().readAll( 47 | """a,b,c 48 | |d,e,f 49 | """.trimMargin() 50 | ) 51 | result shouldBe listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 52 | } 53 | "read csv with line separator" { 54 | val result = csvReader().readAll( 55 | """a,b,c,"x","y 56 | | hoge" 57 | |d,e,f,g,h 58 | """.trimMargin() 59 | ) 60 | val firstRow = listOf( 61 | "a", "b", "c", "x", """y 62 | | hoge""".trimMargin() 63 | ) 64 | val secondRow = listOf("d", "e", "f", "g", "h") 65 | result shouldBe listOf(firstRow, secondRow) 66 | } 67 | "get failed rowNum and colIndex when exception happened on parsing CSV" { 68 | val reader = csvReader() 69 | val ex1 = shouldThrow { 70 | reader.readAll("a,\"\"failed") 71 | } 72 | val ex2 = shouldThrow { 73 | reader.readAll("a,b\nc,\"\"failed") 74 | } 75 | val ex3 = shouldThrow { 76 | reader.readAll("a,\"b\nb\"\nc,\"\"failed") 77 | } 78 | 79 | assertSoftly { 80 | ex1.rowNum shouldBe 1 81 | ex1.colIndex shouldBe 4 82 | ex1.char shouldBe 'f' 83 | 84 | ex2.rowNum shouldBe 2 85 | ex2.colIndex shouldBe 4 86 | ex2.char shouldBe 'f' 87 | 88 | ex3.rowNum shouldBe 3 89 | ex3.colIndex shouldBe 4 90 | ex3.char shouldBe 'f' 91 | } 92 | } 93 | } 94 | 95 | "readAll method (with InputStream argument)" should { 96 | "read simple csv" { 97 | val file = readTestDataFile("simple.csv") 98 | val result = csvReader().readAll(file.inputStream()) 99 | result shouldBe listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 100 | } 101 | } 102 | 103 | "readAll method (with File argument)" should { 104 | "read simple csv" { 105 | val result = csvReader().readAll(readTestDataFile("simple.csv")) 106 | result shouldBe listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 107 | } 108 | "read tsv file" { 109 | val result = csvReader { 110 | delimiter = '\t' 111 | }.readAll(readTestDataFile("simple.tsv")) 112 | result shouldBe listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 113 | } 114 | "read csv with empty field" { 115 | val result = csvReader().readAll(readTestDataFile("empty-fields.csv")) 116 | result shouldBe listOf(listOf("a", "", "b", "", "c", ""), listOf("d", "", "e", "", "f", "")) 117 | } 118 | "read csv with escaped field" { 119 | val result = csvReader().readAll(readTestDataFile("escape.csv")) 120 | result shouldBe listOf(listOf("a", "b", "c"), listOf("d", "\"e", "f")) 121 | } 122 | "read csv with line breaks enclosed in double quotes" { 123 | val result = csvReader().readAll(readTestDataFile("line-breaks.csv")) 124 | result shouldBe listOf(listOf("a", "b\nb", "c"), listOf("\nd", "e", "f")) 125 | } 126 | "read csv with custom quoteChar and delimiter" { 127 | val result = csvReader { 128 | delimiter = '#' 129 | quoteChar = '$' 130 | }.readAll(readTestDataFile("hash-separated-dollar-quote.csv")) 131 | result shouldBe listOf(listOf("Foo ", "Bar ", "Baz "), listOf("a", "b", "c")) 132 | } 133 | "read csv with custom escape character" { 134 | val result = csvReader { 135 | escapeChar = '\\' 136 | }.readAll(readTestDataFile("backslash-escape.csv")) 137 | result shouldBe listOf(listOf("\"a\"", "\"This is a test\""), listOf("\"b\"", "This is a \"second\" test")) 138 | } 139 | "read csv with BOM" { 140 | val result = csvReader { 141 | escapeChar = '\\' 142 | }.readAll(readTestDataFile("bom.csv")) 143 | result shouldBe listOf(listOf("a", "b", "c")) 144 | } 145 | "read empty csv with BOM" { 146 | val result = csvReader { 147 | escapeChar = '\\' 148 | }.readAll(readTestDataFile("empty-bom.csv")) 149 | result shouldBe listOf() 150 | } 151 | //refs https://github.com/tototoshi/scala-csv/issues/22 152 | "read csv with \u2028 field" { 153 | val result = csvReader().readAll(readTestDataFile("unicode2028.csv")) 154 | result shouldBe listOf(listOf("\u2028")) 155 | } 156 | "read csv with empty lines" { 157 | val result = csvReader { 158 | skipEmptyLine = true 159 | }.readAll(readTestDataFile("empty-line.csv")) 160 | result shouldBe listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 161 | } 162 | "read csv with quoted empty line field" { 163 | val result = csvReader { 164 | skipEmptyLine = true 165 | }.readAll(readTestDataFile("quoted-empty-line.csv")) 166 | result shouldBe listOf(listOf("a", "b", "c\n\nc"), listOf("d", "e", "f")) 167 | } 168 | "throw exception when reading malformed csv" { 169 | shouldThrow { 170 | csvReader().readAll(readTestDataFile("malformed.csv")) 171 | } 172 | } 173 | "throw exception when reading csv with different fields num on each row" { 174 | val ex = shouldThrow { 175 | csvReader().readAll(readTestDataFile("different-fields-num.csv")) 176 | } 177 | 178 | assertSoftly { 179 | ex.fieldNum shouldBe 3 180 | ex.fieldNumOnFailedRow shouldBe 2 181 | ex.csvRowNum shouldBe 2 182 | } 183 | } 184 | "Trim row when reading csv with greater num of fields on a subsequent row" { 185 | val expected = listOf(listOf("a", "b"), listOf("c", "d")) 186 | val actual = 187 | csvReader { 188 | excessFieldsRowBehaviour = ExcessFieldsRowBehaviour.TRIM 189 | }.readAll(readTestDataFile("different-fields-num2.csv")) 190 | 191 | assertSoftly { 192 | actual shouldBe expected 193 | actual.size shouldBe 2 194 | } 195 | } 196 | "it should be be possible to skip rows with both excess and insufficient fields" { 197 | val expected = listOf(listOf("a", "b")) 198 | val actual = 199 | csvReader { 200 | excessFieldsRowBehaviour = ExcessFieldsRowBehaviour.IGNORE 201 | insufficientFieldsRowBehaviour = InsufficientFieldsRowBehaviour.IGNORE 202 | }.readAll(readTestDataFile("varying-column-lengths.csv")) 203 | 204 | assertSoftly { 205 | actual shouldBe expected 206 | actual.size shouldBe 1 207 | } 208 | } 209 | "it should be be possible to replace insufficient fields with strings and skip rows with excess fields" { 210 | val expected = listOf(listOf("a", "b"), listOf("c", "")) 211 | val actual = 212 | csvReader { 213 | excessFieldsRowBehaviour = ExcessFieldsRowBehaviour.IGNORE 214 | insufficientFieldsRowBehaviour = InsufficientFieldsRowBehaviour.EMPTY_STRING 215 | }.readAll(readTestDataFile("varying-column-lengths.csv")) 216 | 217 | assertSoftly { 218 | actual shouldBe expected 219 | actual.size shouldBe 2 220 | } 221 | } 222 | "it should be be possible to replace insufficient fields with strings and trim rows with excess fields" { 223 | val expected = listOf(listOf("a", "b"), listOf("c", ""), listOf("d", "e")) 224 | val actual = 225 | csvReader { 226 | excessFieldsRowBehaviour = ExcessFieldsRowBehaviour.TRIM 227 | insufficientFieldsRowBehaviour = InsufficientFieldsRowBehaviour.EMPTY_STRING 228 | }.readAll(readTestDataFile("varying-column-lengths.csv")) 229 | 230 | assertSoftly { 231 | actual shouldBe expected 232 | actual.size shouldBe 3 233 | } 234 | } 235 | "it should be be possible to trim excess columns and skip insufficient row columns" { 236 | val expected = listOf(listOf("a", "b"), listOf("d", "e")) 237 | val actual = 238 | csvReader { 239 | excessFieldsRowBehaviour = ExcessFieldsRowBehaviour.TRIM 240 | insufficientFieldsRowBehaviour = InsufficientFieldsRowBehaviour.IGNORE 241 | }.readAll(readTestDataFile("varying-column-lengths.csv")) 242 | 243 | assertSoftly { 244 | actual shouldBe expected 245 | actual.size shouldBe 2 246 | } 247 | } 248 | "If the excess fields behaviour is ERROR and the insufficient behaviour is IGNORE then an error should be thrown if there are excess columns" { 249 | val ex = shouldThrow { 250 | csvReader { 251 | insufficientFieldsRowBehaviour = InsufficientFieldsRowBehaviour.IGNORE 252 | }.readAll(readTestDataFile("varying-column-lengths.csv")) 253 | } 254 | 255 | assertSoftly { 256 | ex.fieldNum shouldBe 2 257 | ex.fieldNumOnFailedRow shouldBe 3 258 | ex.csvRowNum shouldBe 3 259 | } 260 | } 261 | "If the excess fields behaviour is IGNORE or TRIM and the insufficient behaviour is ERROR then an error should be thrown if there are columns with insufficient rows" { 262 | val ex1 = shouldThrow { 263 | csvReader { 264 | excessFieldsRowBehaviour = ExcessFieldsRowBehaviour.IGNORE 265 | }.readAll(readTestDataFile("varying-column-lengths.csv")) 266 | } 267 | val ex2 = shouldThrow { 268 | csvReader { 269 | excessFieldsRowBehaviour = ExcessFieldsRowBehaviour.TRIM 270 | }.readAll(readTestDataFile("varying-column-lengths.csv")) 271 | } 272 | assertSoftly { 273 | ex1.fieldNum shouldBe 2 274 | ex1.fieldNumOnFailedRow shouldBe 1 275 | ex1.csvRowNum shouldBe 2 276 | 277 | ex2.fieldNum shouldBe 2 278 | ex2.fieldNumOnFailedRow shouldBe 1 279 | ex2.csvRowNum shouldBe 2 280 | } 281 | } 282 | "should not throw exception when reading csv with different fields num on each row with expected number of columns" { 283 | val expected = listOf(listOf("a", "b", "c")) 284 | val actual = csvReader { 285 | skipMissMatchedRow = true 286 | }.readAll(readTestDataFile("different-fields-num.csv")) 287 | 288 | val expected2 = listOf(listOf("a", "b")) 289 | val actual2 = csvReader { 290 | skipMissMatchedRow = true 291 | }.readAll(readTestDataFile("different-fields-num2.csv")) 292 | 293 | assertSoftly { 294 | actual shouldBe expected 295 | actual.size shouldBe 1 296 | 297 | actual2 shouldBe expected2 298 | actual2.size shouldBe 1 299 | } 300 | } 301 | "should not throw exception when reading csv with header and different fields num on each row" { 302 | val expected = listOf( 303 | mapOf("h1" to "a", "h2" to "b", "h3" to "c"), 304 | mapOf("h1" to "g", "h2" to "h", "h3" to "i") 305 | ) 306 | val actual = csvReader { 307 | skipMissMatchedRow = true 308 | }.readAllWithHeader(readTestDataFile("with-header-different-size-row.csv")) 309 | 310 | assertSoftly { 311 | actual.size shouldBe 2 312 | expected shouldBe actual 313 | } 314 | } 315 | } 316 | 317 | "readAllWithHeader method" should { 318 | val expected = listOf( 319 | mapOf("h1" to "a", "h2" to "b", "h3" to "c"), 320 | mapOf("h1" to "d", "h2" to "e", "h3" to "f") 321 | ) 322 | 323 | "read simple csv file" { 324 | val file = readTestDataFile("with-header.csv") 325 | val result = csvReader().readAllWithHeader(file) 326 | result shouldBe expected 327 | } 328 | 329 | "throw on duplicated headers" { 330 | val file = readTestDataFile("with-duplicate-header.csv") 331 | shouldThrow { csvReader().readAllWithHeader(file) } 332 | } 333 | 334 | "auto rename duplicated headers" { 335 | val deduplicateExpected = listOf( 336 | mapOf("a" to "1", "b" to "2", "b_2" to "3", "b_3" to "4", "c" to "5", "c_2" to "6"), 337 | ) 338 | val file = readTestDataFile("with-duplicate-header.csv") 339 | val result = csvReader { 340 | autoRenameDuplicateHeaders = true 341 | }.readAllWithHeader(file) 342 | result shouldBe deduplicateExpected 343 | } 344 | 345 | "auto rename failed" { 346 | val file = readTestDataFile("with-duplicate-header-auto-rename-failed.csv") 347 | shouldThrow { csvReader().readAllWithHeader(file) } 348 | } 349 | 350 | "read from String" { 351 | val data = """h1,h2,h3 352 | |a,b,c 353 | |d,e,f 354 | """.trimMargin() 355 | val result = csvReader().readAllWithHeader(data) 356 | result shouldBe expected 357 | } 358 | 359 | "read from InputStream" { 360 | val file = readTestDataFile("with-header.csv") 361 | val result = csvReader().readAllWithHeader(file.inputStream()) 362 | result shouldBe expected 363 | } 364 | 365 | "read from String containing line break" { 366 | val data = """h1,"h 367 | |2",h3 368 | |a,b,c 369 | """.trimMargin() 370 | val result = csvReader().readAllWithHeader(data) 371 | val h2 = """h 372 | |2""".trimMargin() 373 | result shouldBe listOf(mapOf("h1" to "a", h2 to "b", "h3" to "c")) 374 | } 375 | "number of fields in a row has to be based on the header #82" { 376 | val data = "1,2,3\na,b\nx,y,z" 377 | 378 | val exception = shouldThrow { 379 | csvReader().readAllWithHeader(data) 380 | } 381 | exception.fieldNum shouldBe 3 382 | } 383 | } 384 | 385 | "open method (with fileName argument)" should { 386 | val rows = csvReader().open("src/jvmTest/resources/testdata/csv/simple.csv") { 387 | val row1 = readNext() 388 | val row2 = readNext() 389 | listOf(row1, row2) 390 | } 391 | rows shouldBe listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 392 | } 393 | 394 | "open method (with InputStream argument)" should { 395 | val file = readTestDataFile("simple.csv") 396 | val rows = csvReader().open(file.inputStream()) { 397 | val row1 = readNext() 398 | val row2 = readNext() 399 | listOf(row1, row2) 400 | } 401 | rows shouldBe listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 402 | } 403 | "execute as suspending function" should { 404 | "open suspending method (with fileName argument)" { 405 | val rows = csvReader().openAsync("src/jvmTest/resources/testdata/csv/simple.csv") { 406 | val row1 = readNext() 407 | val row2 = readNext() 408 | listOf(row1, row2) 409 | } 410 | rows shouldBe listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 411 | } 412 | "open suspending method (with file argument)" { 413 | val file = readTestDataFile("simple.csv") 414 | val rows = csvReader().openAsync(file) { 415 | val row1 = readNext() 416 | val row2 = readNext() 417 | listOf(row1, row2) 418 | } 419 | rows shouldBe listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 420 | } 421 | "open suspending method (with InputStream argument)" { 422 | val fileStream = readTestDataFile("simple.csv").inputStream() 423 | val rows = csvReader().openAsync(fileStream) { 424 | val row1 = readNext() 425 | val row2 = readNext() 426 | listOf(row1, row2) 427 | } 428 | rows shouldBe listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 429 | } 430 | "validate test as flow" { 431 | val fileStream = readTestDataFile("simple.csv").inputStream() 432 | val rows = mutableListOf>() 433 | csvReader().openAsync(fileStream) { 434 | readAllAsSequence().asFlow().collect { 435 | rows.add(it) 436 | } 437 | } 438 | rows shouldBe listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 439 | } 440 | } 441 | }) 442 | 443 | private fun readTestDataFile(fileName: String): File { 444 | return File("src/jvmTest/resources/testdata/csv/$fileName") 445 | } 446 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/client/CsvWriterTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import com.github.doyaaaaaken.kotlincsv.dsl.context.CsvWriterContext 4 | import com.github.doyaaaaaken.kotlincsv.dsl.context.WriteQuoteMode 5 | import com.github.doyaaaaaken.kotlincsv.dsl.csvWriter 6 | import com.github.doyaaaaaken.kotlincsv.util.Const 7 | import io.kotest.assertions.assertSoftly 8 | import io.kotest.core.spec.style.WordSpec 9 | import io.kotest.matchers.shouldBe 10 | import java.io.File 11 | import java.nio.charset.Charset 12 | 13 | 14 | class CsvWriterTest : WordSpec({ 15 | 16 | val testFileName = "test.csv" 17 | 18 | afterTest { File(testFileName).delete() } 19 | 20 | fun readTestFile(charset: Charset = Charsets.UTF_8): String { 21 | return File(testFileName).readText(charset) 22 | } 23 | 24 | "CsvWriter class constructor" should { 25 | "be created with no argument" { 26 | val writer = CsvWriter() 27 | writer.charset shouldBe Const.defaultCharset 28 | } 29 | "be created with CsvWriterContext argument" { 30 | val context = CsvWriterContext().apply { 31 | charset = Charsets.ISO_8859_1.name() 32 | delimiter = '\t' 33 | nullCode = "NULL" 34 | lineTerminator = "\n" 35 | outputLastLineTerminator = false 36 | prependBOM = true 37 | quote { 38 | char = '\'' 39 | mode = WriteQuoteMode.ALL 40 | } 41 | } 42 | val writer = CsvWriter(context) 43 | assertSoftly { 44 | writer.charset shouldBe Charsets.ISO_8859_1.name() 45 | writer.delimiter shouldBe '\t' 46 | writer.nullCode shouldBe "NULL" 47 | writer.lineTerminator shouldBe "\n" 48 | writer.outputLastLineTerminator shouldBe false 49 | writer.prependBOM shouldBe true 50 | writer.quote.char = '\'' 51 | writer.quote.mode = WriteQuoteMode.ALL 52 | } 53 | } 54 | } 55 | 56 | "open method" should { 57 | val row1 = listOf("a", "b", null) 58 | val row2 = listOf("d", "2", "1.0") 59 | val expected = "a,b,\r\nd,2,1.0\r\n" 60 | 61 | "write simple csv data into file with writing each rows" { 62 | csvWriter().open(testFileName) { 63 | writeRow(row1) 64 | writeRow(row2) 65 | } 66 | val actual = readTestFile() 67 | actual shouldBe expected 68 | } 69 | 70 | "write simple csv data into file with writing all at one time" { 71 | csvWriter().open(testFileName) { writeRows(listOf(row1, row2)) } 72 | val actual = readTestFile() 73 | actual shouldBe expected 74 | } 75 | 76 | "write simple csv data to the tail of existing file with append = true" { 77 | val writer = csvWriter() 78 | writer.open(File(testFileName), true) { 79 | writeRows(listOf(row1, row2)) 80 | } 81 | writer.open(File(testFileName), true) { 82 | writeRows(listOf(row1, row2)) 83 | } 84 | val actual = readTestFile() 85 | actual shouldBe expected + expected 86 | } 87 | 88 | "overwrite simple csv data with append = false" { 89 | val writer = csvWriter() 90 | writer.open(File(testFileName), false) { 91 | writeRows(listOf(row2, row2, row2)) 92 | } 93 | writer.open(File(testFileName), false) { 94 | writeRows(listOf(row1, row2)) 95 | } 96 | val actual = readTestFile() 97 | actual shouldBe expected 98 | } 99 | } 100 | 101 | "writeAsString method" should { 102 | val row1 = listOf("a", "b", null) 103 | val row2 = listOf("d", "2", "1.0") 104 | val expected = "a,b,\r\nd,2,1.0\r\n" 105 | 106 | "write simple csv data to String" { 107 | val actual = csvWriter().writeAsString { 108 | writeRow(row1) 109 | writeRow(row2) 110 | } 111 | actual shouldBe expected 112 | } 113 | } 114 | 115 | "writeAll method without calling `open` method" should { 116 | val rows = listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 117 | val expected = "a,b,c\r\nd,e,f\r\n" 118 | 119 | "write data with target file name" { 120 | csvWriter().writeAll(rows, testFileName) 121 | val actual = readTestFile() 122 | actual shouldBe expected 123 | } 124 | 125 | "write data with target file (java.io.File)" { 126 | csvWriter().writeAll(rows, File(testFileName)) 127 | val actual = readTestFile() 128 | actual shouldBe expected 129 | } 130 | 131 | "write data with target output stream (java.io.OutputStream)" { 132 | csvWriter().writeAll(rows, File(testFileName).outputStream()) 133 | val actual = readTestFile() 134 | actual shouldBe expected 135 | } 136 | 137 | "write data to String" { 138 | val actual = csvWriter().writeAllAsString(rows) 139 | actual shouldBe expected 140 | } 141 | } 142 | 143 | "Customized CsvWriter" should { 144 | "write csv with SJIS charset" { 145 | csvWriter { 146 | charset = "SJIS" 147 | }.open(File(testFileName)) { 148 | writeRows(listOf(listOf("あ", "い"))) 149 | } 150 | val actual = readTestFile(Charset.forName("SJIS")) 151 | actual shouldBe "あ,い\r\n" 152 | } 153 | "write csv with '|' delimiter" { 154 | val row1 = listOf("a", "b") 155 | val row2 = listOf("c", "d") 156 | val expected = "a|b\r\nc|d\r\n" 157 | csvWriter { 158 | delimiter = '|' 159 | }.open(File(testFileName)) { 160 | writeRows(listOf(row1, row2)) 161 | } 162 | val actual = readTestFile() 163 | actual shouldBe expected 164 | } 165 | "write null with customized null code" { 166 | val row = listOf(null, null) 167 | csvWriter { 168 | nullCode = "NULL" 169 | }.open(testFileName) { 170 | writeRow(row) 171 | } 172 | val actual = readTestFile() 173 | actual shouldBe "NULL,NULL\r\n" 174 | } 175 | "write csv with \n line terminator" { 176 | val row1 = listOf("a", "b") 177 | val row2 = listOf("c", "d") 178 | val expected = "a,b\nc,d\n" 179 | csvWriter { 180 | lineTerminator = "\n" 181 | }.open(File(testFileName)) { 182 | writeRows(listOf(row1, row2)) 183 | } 184 | val actual = readTestFile() 185 | actual shouldBe expected 186 | } 187 | "write csv with WriteQuoteMode.ALL mode" { 188 | val row1 = listOf("a", "b") 189 | val row2 = listOf("c", "d") 190 | val expected = "\"a\",\"b\"\r\n\"c\",\"d\"\r\n" 191 | csvWriter { 192 | quote { 193 | mode = WriteQuoteMode.ALL 194 | } 195 | }.open(File(testFileName)) { 196 | writeRows(listOf(row1, row2)) 197 | } 198 | val actual = readTestFile() 199 | actual shouldBe expected 200 | } 201 | "write csv with WriteQuoteMode.NON_NUMERIC mode" { 202 | val row1 = listOf("a", "b", 1) 203 | val row2 = listOf(2.0, "03.0", "4.0.0") 204 | val expected = "\"a\",\"b\",1\r\n2.0,03.0,\"4.0.0\"\r\n" 205 | csvWriter { 206 | quote { 207 | mode = WriteQuoteMode.NON_NUMERIC 208 | } 209 | }.open(File(testFileName)) { 210 | writeRows(listOf(row1, row2)) 211 | } 212 | val actual = readTestFile() 213 | actual shouldBe expected 214 | } 215 | "write csv with custom quote character" { 216 | val row1 = listOf("a'", "b") 217 | val row2 = listOf("'c", "d") 218 | val expected = "'a''',b\r\n'''c',d\r\n" 219 | csvWriter { 220 | quote { 221 | char = '\'' 222 | } 223 | }.open(File(testFileName)) { 224 | writeRows(listOf(row1, row2)) 225 | } 226 | val actual = readTestFile() 227 | actual shouldBe expected 228 | } 229 | "write csv with custom quote character on WriteQuoteMode.ALL mode" { 230 | val rows = listOf(listOf("a1", "b1"), listOf("a2", "b2")) 231 | val expected = "_a1_,_b1_\r\n_a2_,_b2_\r\n" 232 | csvWriter { 233 | quote { 234 | mode = WriteQuoteMode.ALL 235 | char = '_' 236 | } 237 | }.writeAll(rows, testFileName) 238 | val actual = readTestFile() 239 | actual shouldBe expected 240 | } 241 | "write simple csv with disabled last line terminator with custom terminator" { 242 | val row1 = listOf("a", "b") 243 | val row2 = listOf("c", "d") 244 | val expected = "a,b\nc,d" 245 | csvWriter { 246 | lineTerminator = "\n" 247 | outputLastLineTerminator = false 248 | }.open(File(testFileName)) { 249 | writeRows(listOf(row1, row2)) 250 | } 251 | val actual = readTestFile() 252 | actual shouldBe expected 253 | } 254 | "write simple csv with enabled last line and custom terminator" { 255 | val row1 = listOf("a", "b") 256 | val row2 = listOf("c", "d") 257 | val expected = "a,b\nc,d\n" 258 | csvWriter { 259 | lineTerminator = "\n" 260 | outputLastLineTerminator = true 261 | }.open(File(testFileName)) { 262 | writeRows(listOf(row1, row2)) 263 | } 264 | val actual = readTestFile() 265 | actual shouldBe expected 266 | } 267 | "write simple csv with disabled last line terminator" { 268 | val row1 = listOf("a", "b") 269 | val row2 = listOf("c", "d") 270 | val expected = "a,b\r\nc,d" 271 | csvWriter { 272 | outputLastLineTerminator = false 273 | }.open(File(testFileName)) { 274 | writeRows(listOf(row1, row2)) 275 | } 276 | val actual = readTestFile() 277 | actual shouldBe expected 278 | } 279 | "write simple csv with prepending BOM" { 280 | val row1 = listOf("a", "b") 281 | val row2 = listOf("c", "d") 282 | val expected = "\uFEFFa,b\r\nc,d\r\n" 283 | csvWriter { 284 | prependBOM = true 285 | }.open(File(testFileName)) { 286 | writeRows(listOf(row1, row2)) 287 | } 288 | val actual = readTestFile() 289 | actual shouldBe expected 290 | } 291 | "write simple csv with disabled last line terminator multiple writes" { 292 | val row1 = listOf("a", "b") 293 | val row2 = listOf("c", "d") 294 | val row3 = listOf("e", "f") 295 | val row4 = listOf("g", "h") 296 | val row5 = listOf("1", "2") 297 | val row6 = listOf("3", "4") 298 | val expected = "a,b\r\nc,d\r\ne,f\r\ng,h\r\n1,2\r\n3,4" 299 | csvWriter { 300 | outputLastLineTerminator = false 301 | }.open(File(testFileName)) { 302 | writeRow(row1) 303 | writeRows(listOf(row2, row3)) 304 | writeRow(row4) 305 | writeRows(listOf(row5, row6)) 306 | } 307 | val actual = readTestFile() 308 | actual shouldBe expected 309 | } 310 | } 311 | 312 | "openAndGetRawWriter method" should { 313 | val row1 = listOf("a", "b", null) 314 | val row2 = listOf("d", "2", "1.0") 315 | val expected = "a,b,\r\nd,2,1.0\r\n" 316 | 317 | "get raw writer from fileName string and can use it" { 318 | @OptIn(KotlinCsvExperimental::class) 319 | val writer = csvWriter().openAndGetRawWriter(testFileName) 320 | writer.writeRow(row1) 321 | writer.writeRow(row2) 322 | writer.close() 323 | 324 | val actual = readTestFile() 325 | actual shouldBe expected 326 | } 327 | 328 | "get raw writer from java.io.File and can use it" { 329 | @OptIn(KotlinCsvExperimental::class) 330 | val writer = csvWriter().openAndGetRawWriter(File(testFileName)) 331 | writer.writeRow(row1) 332 | writer.writeRow(row2) 333 | writer.close() 334 | 335 | val actual = readTestFile() 336 | actual shouldBe expected 337 | } 338 | 339 | "get raw writer from OutputStream and can use it" { 340 | val ops = File(testFileName).outputStream() 341 | 342 | @OptIn(KotlinCsvExperimental::class) 343 | val writer = csvWriter().openAndGetRawWriter(ops) 344 | writer.writeRow(row1) 345 | writer.writeRow(row2) 346 | writer.close() 347 | 348 | val actual = readTestFile() 349 | actual shouldBe expected 350 | } 351 | } 352 | }) 353 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/client/StringReaderTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.client 2 | 3 | import com.github.doyaaaaaken.kotlincsv.dsl.context.CsvReaderContext 4 | import com.github.doyaaaaaken.kotlincsv.util.CSVFieldNumDifferentException 5 | import com.github.doyaaaaaken.kotlincsv.util.CSVParseFormatException 6 | import com.github.doyaaaaaken.kotlincsv.util.MalformedCSVException 7 | import com.github.doyaaaaaken.kotlincsv.util.logger.LoggerNop 8 | import io.kotest.assertions.assertSoftly 9 | import io.kotest.assertions.throwables.shouldThrow 10 | import io.kotest.core.spec.style.WordSpec 11 | import io.kotest.matchers.shouldBe 12 | 13 | class StringReaderTest : WordSpec({ 14 | "readAll method (with String argument)" should { 15 | "read simple csv" { 16 | val result = readAll( 17 | """a,b,c 18 | |d,e,f 19 | """.trimMargin() 20 | ) 21 | result shouldBe listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 22 | } 23 | "read csv with line separator" { 24 | val result = readAll( 25 | """a,b,c,"x","y 26 | | hoge" 27 | |d,e,f,g,h 28 | """.trimMargin() 29 | ) 30 | result shouldBe listOf( 31 | listOf( 32 | "a", "b", "c", "x", """y 33 | | hoge""".trimMargin() 34 | ), listOf("d", "e", "f", "g", "h") 35 | ) 36 | } 37 | "get failed rowNum and colIndex when exception happened on parsing CSV" { 38 | val ex1 = shouldThrow { 39 | readAll("a,\"\"failed") 40 | } 41 | val ex2 = shouldThrow { 42 | readAll("a,b\nc,\"\"failed") 43 | } 44 | val ex3 = shouldThrow { 45 | readAll("a,\"b\nb\"\nc,\"\"failed") 46 | } 47 | 48 | assertSoftly { 49 | ex1.rowNum shouldBe 1 50 | ex1.colIndex shouldBe 4 51 | ex1.char shouldBe 'f' 52 | 53 | ex2.rowNum shouldBe 2 54 | ex2.colIndex shouldBe 4 55 | ex2.char shouldBe 'f' 56 | 57 | ex3.rowNum shouldBe 3 58 | ex3.colIndex shouldBe 4 59 | ex3.char shouldBe 'f' 60 | } 61 | } 62 | } 63 | 64 | "readAll method" should { 65 | "read simple csv" { 66 | val result = readAll(readTestDataFile("simple.csv")) 67 | result shouldBe listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) 68 | } 69 | "read csv with empty field" { 70 | val result = readAll(readTestDataFile("empty-fields.csv")) 71 | result shouldBe listOf(listOf("a", "", "b", "", "c", ""), listOf("d", "", "e", "", "f", "")) 72 | } 73 | "read csv with escaped field" { 74 | val result = readAll(readTestDataFile("escape.csv")) 75 | result shouldBe listOf(listOf("a", "b", "c"), listOf("d", "\"e", "f")) 76 | } 77 | "read csv with line breaks enclosed in double quotes" { 78 | val result = readAll(readTestDataFile("line-breaks.csv")) 79 | result shouldBe listOf(listOf("a", "b\nb", "c"), listOf("\nd", "e", "f")) 80 | } 81 | //refs https://github.com/tototoshi/scala-csv/issues/22 82 | "read csv with \u2028 field" { 83 | val result = readAll(readTestDataFile("unicode2028.csv")) 84 | result shouldBe listOf(listOf("\u2028")) 85 | } 86 | "throw exception when reading malformed csv" { 87 | shouldThrow { 88 | readAll(readTestDataFile("malformed.csv")) 89 | } 90 | } 91 | "throw exception when reading csv with different fields num on each row" { 92 | val ex = shouldThrow { 93 | readAll(readTestDataFile("different-fields-num.csv")) 94 | } 95 | assertSoftly { 96 | ex.fieldNum shouldBe 3 97 | ex.fieldNumOnFailedRow shouldBe 2 98 | ex.csvRowNum shouldBe 2 99 | } 100 | } 101 | } 102 | 103 | "readAllWithHeader method" should { 104 | val expected = listOf( 105 | mapOf("h1" to "a", "h2" to "b", "h3" to "c"), 106 | mapOf("h1" to "d", "h2" to "e", "h3" to "f") 107 | ) 108 | 109 | "read simple csv file" { 110 | val file = readTestDataFile("with-header.csv") 111 | val result = readAllWithHeader(file) 112 | result shouldBe expected 113 | } 114 | 115 | "read from String" { 116 | val data = """h1,h2,h3 117 | |a,b,c 118 | |d,e,f 119 | """.trimMargin() 120 | val result = readAllWithHeader(data) 121 | result shouldBe expected 122 | } 123 | 124 | "read from String containing line break" { 125 | val data = """h1,"h 126 | |2",h3 127 | |a,b,c 128 | """.trimMargin() 129 | val result = readAllWithHeader(data) 130 | val h2 = """h 131 | |2""".trimMargin() 132 | result shouldBe listOf(mapOf("h1" to "a", h2 to "b", "h3" to "c")) 133 | } 134 | "number of fields in a row has to be based on the header #82" { 135 | val data = "1,2,3\na,b\nx,y,z" 136 | 137 | val exception = shouldThrow { 138 | readAllWithHeader(data) 139 | } 140 | exception.fieldNum shouldBe 3 141 | } 142 | } 143 | }) 144 | 145 | private fun readTestDataFile(fileName: String): String { 146 | return java.io.File("src/jvmTest/resources/testdata/csv/$fileName").readText() 147 | } 148 | 149 | /** 150 | * read csv data as String, and convert into List> 151 | */ 152 | private fun readAll(data: String): List> { 153 | return CsvFileReader(CsvReaderContext(), StringReaderImpl(data), LoggerNop).readAllAsSequence().toList() 154 | } 155 | 156 | /** 157 | * read csv data with header, and convert into List> 158 | */ 159 | private fun readAllWithHeader(data: String): List> { 160 | return CsvFileReader(CsvReaderContext(), StringReaderImpl(data), LoggerNop).readAllWithHeaderAsSequence().toList() 161 | } 162 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/CsvReaderDslTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.dsl 2 | 3 | import com.github.doyaaaaaken.kotlincsv.client.CsvReader 4 | import com.github.doyaaaaaken.kotlincsv.dsl.context.ExcessFieldsRowBehaviour 5 | import com.github.doyaaaaaken.kotlincsv.dsl.context.InsufficientFieldsRowBehaviour 6 | import io.kotest.assertions.assertSoftly 7 | import io.kotest.core.spec.style.StringSpec 8 | import io.kotest.matchers.shouldBe 9 | import io.kotest.matchers.types.shouldBeTypeOf 10 | 11 | /** 12 | * @author doyaaaaaken 13 | */ 14 | class CsvReaderDslTest : StringSpec({ 15 | "csvReader method should work as global method with no argument" { 16 | val reader = csvReader() 17 | reader.shouldBeTypeOf() 18 | } 19 | "csvReader method should work as dsl" { 20 | val reader = csvReader { 21 | charset = Charsets.ISO_8859_1.name() 22 | quoteChar = '\'' 23 | delimiter = '\t' 24 | escapeChar = '"' 25 | skipEmptyLine = true 26 | skipMissMatchedRow = true 27 | insufficientFieldsRowBehaviour = InsufficientFieldsRowBehaviour.IGNORE 28 | excessFieldsRowBehaviour = ExcessFieldsRowBehaviour.IGNORE 29 | } 30 | assertSoftly { 31 | reader.charset shouldBe Charsets.ISO_8859_1.name() 32 | reader.quoteChar shouldBe '\'' 33 | reader.delimiter shouldBe '\t' 34 | reader.skipEmptyLine shouldBe true 35 | reader.skipMissMatchedRow shouldBe true 36 | reader.insufficientFieldsRowBehaviour shouldBe InsufficientFieldsRowBehaviour.IGNORE 37 | reader.excessFieldsRowBehaviour shouldBe ExcessFieldsRowBehaviour.IGNORE 38 | } 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/dsl/CsvWriterDslTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.dsl 2 | 3 | import com.github.doyaaaaaken.kotlincsv.client.CsvWriter 4 | import com.github.doyaaaaaken.kotlincsv.dsl.context.WriteQuoteMode 5 | import io.kotest.assertions.assertSoftly 6 | import io.kotest.core.spec.style.StringSpec 7 | import io.kotest.matchers.shouldBe 8 | import io.kotest.matchers.types.shouldBeTypeOf 9 | 10 | /** 11 | * @author doyaaaaaken 12 | */ 13 | class CsvWriterDslTest : StringSpec({ 14 | "csvWriter method should work as global method with no argument" { 15 | val writer = csvWriter() 16 | writer.shouldBeTypeOf() 17 | } 18 | "csvWriter method should work as dsl" { 19 | val writer = csvWriter { 20 | charset = Charsets.ISO_8859_1.name() 21 | delimiter = '\t' 22 | nullCode = "NULL" 23 | lineTerminator = "\n" 24 | outputLastLineTerminator = false 25 | prependBOM = true 26 | quote { 27 | char = '\'' 28 | mode = WriteQuoteMode.ALL 29 | } 30 | } 31 | assertSoftly { 32 | writer.charset shouldBe Charsets.ISO_8859_1.name() 33 | writer.delimiter shouldBe '\t' 34 | writer.nullCode shouldBe "NULL" 35 | writer.lineTerminator shouldBe "\n" 36 | writer.outputLastLineTerminator shouldBe false 37 | writer.prependBOM shouldBe true 38 | writer.quote.char shouldBe '\'' 39 | writer.quote.mode = WriteQuoteMode.ALL 40 | } 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/com/github/doyaaaaaken/kotlincsv/parser/CsvParserTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.doyaaaaaken.kotlincsv.parser 2 | 3 | import com.github.doyaaaaaken.kotlincsv.util.CSVParseFormatException 4 | import io.kotest.assertions.assertSoftly 5 | import io.kotest.assertions.throwables.shouldThrow 6 | import io.kotest.core.spec.style.WordSpec 7 | import io.kotest.matchers.shouldBe 8 | 9 | class CsvParserTest : WordSpec({ 10 | val parser = CsvParser('"', ',', '"') 11 | val lineTerminators = listOf("\n", "\u2028", "\u2029", "\u0085", "\r", "\r\n") 12 | 13 | "CsvParser.parseRow" should { 14 | "parseEmptyRow" { 15 | parser.parseRow("") shouldBe emptyList() 16 | } 17 | "return null if line is on the way of csv row" { 18 | parser.parseRow("a,\"b") shouldBe null 19 | } 20 | } 21 | 22 | "ParseStateMachine logic" should { 23 | "parse delimiter at the start of row" { 24 | parser.parseRow(",a") shouldBe listOf("", "a") 25 | } 26 | "parse line terminator at the start of row" { 27 | lineTerminators.forEach { lt -> 28 | parser.parseRow(lt) shouldBe listOf("") 29 | } 30 | } 31 | "parse row with delimiter at the end" { 32 | parser.parseRow("a,") shouldBe listOf("a", "") 33 | } 34 | "parse line terminator after quote end" { 35 | lineTerminators.forEach { lt -> 36 | parser.parseRow("""a,"b"$lt""") shouldBe listOf("a", "b") 37 | } 38 | } 39 | "parse line terminator after delimiter" { 40 | lineTerminators.forEach { lt -> 41 | parser.parseRow("a,$lt") shouldBe listOf("a", "") 42 | } 43 | } 44 | "parse line terminator after field" { 45 | lineTerminators.forEach { lt -> 46 | parser.parseRow("a$lt") shouldBe listOf("a") 47 | } 48 | } 49 | "parse escape character after field" { 50 | parser.parseRow("a\"\"") shouldBe listOf("a\"") 51 | } 52 | "throw exception when parsing 2 rows" { 53 | lineTerminators.forEach { lt -> 54 | shouldThrow { 55 | parser.parseRow("a${lt}b") 56 | } 57 | } 58 | } 59 | "thrown exception message contains correct rowNum and colIndex" { 60 | val ex1 = shouldThrow { 61 | parser.parseRow("a,\"\"failed") 62 | } 63 | val ex2 = shouldThrow { 64 | parser.parseRow("a,\"\"failed", 2) 65 | } 66 | 67 | assertSoftly { 68 | ex1.rowNum shouldBe 1 69 | ex1.colIndex shouldBe 4 70 | ex1.char shouldBe 'f' 71 | 72 | ex2.rowNum shouldBe 2 73 | ex2.colIndex shouldBe 4 74 | ex2.char shouldBe 'f' 75 | } 76 | } 77 | } 78 | }) 79 | -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/backslash-escape.csv: -------------------------------------------------------------------------------- 1 | "\"a\"","\"This is a test\"" 2 | "\"b\"","This is a \"second\" test" 3 | -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/bom.csv: -------------------------------------------------------------------------------- 1 | "a","b","c" 2 | -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/different-fields-num.csv: -------------------------------------------------------------------------------- 1 | a,b,c 2 | d,e -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/different-fields-num2.csv: -------------------------------------------------------------------------------- 1 | a,b 2 | c,d,e -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/empty-bom.csv: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/empty-fields.csv: -------------------------------------------------------------------------------- 1 | a,,b,"",c,"" 2 | d,,e,"",f,"" -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/empty-line.csv: -------------------------------------------------------------------------------- 1 | a,b,c 2 | 3 | 4 | d,e,f 5 | 6 | -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/empty.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsoizo/kotlin-csv/19c28835a7bde791c808fe8a7b05818122e0e28d/src/jvmTest/resources/testdata/csv/empty.csv -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/escape.csv: -------------------------------------------------------------------------------- 1 | a,b,c 2 | d,"""e",f -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/hash-separated-dollar-quote.csv: -------------------------------------------------------------------------------- 1 | $Foo $#$Bar $#$Baz $ 2 | $a$#$b$#$c$ -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/line-breaks.csv: -------------------------------------------------------------------------------- 1 | a,"b 2 | b",c 3 | " 4 | d",e,f -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/malformed.csv: -------------------------------------------------------------------------------- 1 | this,is,malformed,"csv,data 2 | -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/quoted-empty-line.csv: -------------------------------------------------------------------------------- 1 | a,b,"c 2 | 3 | c" 4 | 5 | d,e,f 6 | 7 | -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/simple.csv: -------------------------------------------------------------------------------- 1 | a,b,c 2 | d,e,f -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/simple.tsv: -------------------------------------------------------------------------------- 1 | a b c 2 | d e f 3 | -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/unicode2028.csv: -------------------------------------------------------------------------------- 1 | "
" 2 | -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/varying-column-lengths.csv: -------------------------------------------------------------------------------- 1 | a,b 2 | c 3 | d,e,f -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/with-duplicate-header-auto-rename-failed.csv: -------------------------------------------------------------------------------- 1 | a,a,a_2 2 | 1,2,3 -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/with-duplicate-header.csv: -------------------------------------------------------------------------------- 1 | a,b,b,b,c,c 2 | 1,2,3,4,5,6 -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/with-header-different-size-row.csv: -------------------------------------------------------------------------------- 1 | h1,h2,h3 2 | a,b,c 3 | d,e 4 | g,h,i -------------------------------------------------------------------------------- /src/jvmTest/resources/testdata/csv/with-header.csv: -------------------------------------------------------------------------------- 1 | h1,h2,h3 2 | a,b,c 3 | d,e,f --------------------------------------------------------------------------------