Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 1.0 - 2025.06.07

- Update to Kotlin 2.2.0
- Change: Final API refinement (incompatible, see readme and tests for usage examples)

## 0.17 - 2025-05-04
### Fixed
- Issue #24 (Improve headers and rows detection)
Expand Down
58 changes: 37 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,52 +9,68 @@ Small, fast and convenient multiplatform CSV parser and builder written for one

# Architecture

<img src="http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/sergejsha/csv/master/documentation/architecture.v2.iuml">
<img src="http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/sergejsha/csv/master/documentation/architecture.v3.iuml">

# Usage

Here is what you can do with the library:
```kotlin

// (1) Build a CSV file using simple DSL
// (1) build csv
val csv = buildCsv {
row {
value("Code")
value("Name")
header {
column("Code")
column("Name")
}
row {
data {
value("DE")
value("Germany")
value("Deutschland")
}
row {
data {
value("BY")
value("Belarus")
}
}
} as CsvWithHeader

val code = csv.header.columnByName("Code") as CsvColumn
val name = csv.header.columnByName("Name") as CsvColumn

assertEquals(code, CsvColumn(0, "Code"))
assertEquals(name, CsvColumn(1, "Name"))
assertEquals(csv.data[0][code], "DE")
assertEquals(csv.data[0][name], "Deutschland")
assertEquals(csv.data[1][code], "BY")
assertEquals(csv.data[1][name], "Belarus")

// (2) Export a CSV object to a CSV string
// (2) csv to text
val csvText = csv.toCsvText()
assertEquals("Code,Name\nDE,Deutschland\nBY,Belarus\n", csvText)

// (3) Parse a CSV string to get a CSV object
val csv2 = Csv.parseCsvText(csvText)
// (3) parse csv text
val csv2 = CsvWithHeader.parseCsvText(csvText) as CsvWithHeader

// There are the data structures supported by the library
val allRows: List<Row> = csv2.rows
val header: HeaderRow = csv2.header
val data: List<DataRow> = csv2.data
assertEquals(csv.header, csv2.header)
assertEquals(csv.data, csv2.data)
assertEquals(csv.allRows, csv2.allRows)

// (4) Transform CSV data
val codes = data.map { it.value("Code") } // ["DE", "BY"]
val names = data.map { it.value("Name") } // ["Germany", "Belarus"]
// (4) transform csv
val csv3 = csv.copy(
data = csv.data.map { row ->
row.mapValueOf(name) { value ->
if (value == "Belarus") "Weißrussland" else value
}
}
)
assertEquals("Code,Name\nDE,Deutschland\nBY,Weißrussland\n", csv3.toCsvText())
```

# Dependencies

In `gradle/libs.versions.toml`
```toml
[versions]
kotlin = "2.1.20"
csv = "0.17"
kotlin = "2.2.0"
csv = "1.0"

[libraries]
csv = { module = "de.halfbit:csv", version.ref = "csv" }
Expand Down
12 changes: 2 additions & 10 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
}

group = "de.halfbit"
version = "0.17"
version = "1.0"

repositories {
mavenCentral()
Expand Down Expand Up @@ -67,15 +67,7 @@ if (canPublishToMaven) {
}
maven {
name = "central"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials {
username = project.getPropertyOrEmptyString("publishing.nexus.user")
password = project.getPropertyOrEmptyString("publishing.nexus.password")
}
}
maven {
name = "snapshot"
url = uri("https://oss.sonatype.org/content/repositories/snapshots")
url = uri("https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/")
credentials {
username = project.getPropertyOrEmptyString("publishing.nexus.user")
password = project.getPropertyOrEmptyString("publishing.nexus.password")
Expand Down
31 changes: 31 additions & 0 deletions documentation/architecture.v3.iuml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@startuml
skinparam packageStyle rectangle
hide empty members
hide stereotype
hide circle

class Csv {
allRows: List<CsvRow>
---
toCsvText(): String
}

class CsvNoHeader {
data: List<CsvDataRow>
--
fromCsvText(): CsvNoHeader
fromLists(): CsvNoHeader
}

class CsvWithHeader {
header: CsvHeaderRow
data: List<CsvDataRow>
--
fromCsvText(): CsvWithHeader?
fromLists(): CsvWithHeader?
}

CsvWithHeader -up-|> Csv
CsvNoHeader -up-|> Csv

@enduml
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
kotlin = "2.1.20"
kotlin = "2.2.0"

[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
5 changes: 0 additions & 5 deletions kotlin-js-store/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1871,11 +1871,6 @@ type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"

typescript@5.5.4:
version "5.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==

ua-parser-js@^0.7.30:
version "0.7.40"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.40.tgz#c87d83b7bb25822ecfa6397a0da5903934ea1562"
Expand Down
78 changes: 25 additions & 53 deletions src/commonMain/kotlin/Csv.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
/** Copyright 2023 Halfbit GmbH, Sergej Shafarenka */
/** Copyright 2023-2025 Halfbit GmbH, Sergej Shafarenka */
package de.halfbit.csv

import de.halfbit.csv.BaseCsv.Row

/**
* Object with comma-separated values stored as list of [Row]'s. Each row is a list
* of strings. Empty values are empty strings.
* Base class for CSV data, containing all rows of the CSV file.
*
* If your CSV-data has header row, use [BaseCsv] instead of this base type.
* @property allRows all rows in the CSV file, including header if present
*/
public interface BaseCsv {
public val allRows: List<Row>
public abstract class Csv(
public val allRows: List<CsvRow>,
) {
public abstract val data: List<CsvDataRow>

// Multiline issue: https://stackoverflow.com/questions/2668678/importing-csv-with-line-breaks-in-excel-2007
/**
* Converts the CSV data to a CSV-formatted string.
*
* @param newLine the line separator to use
* @param escapeWhitespaces whether to escape whitespaces in values
* @return the CSV-formatted string representation
*/
public fun toCsvText(
newLine: NewLine = NewLine.LF,
escapeWhitespaces: Boolean = false,
Expand All @@ -28,61 +33,28 @@ public interface BaseCsv {
append(newLine.value)
}
}

public interface Row : List<String> {
public fun replaceValue(valueIndex: Int, newValue: String): Row
}
}

/**
* CSV-object by with a mandatory header row. It has more convenient methods
* for working with columns by their names.
*/
public interface Csv : BaseCsv {
public val header: HeaderRow
public val data: List<DataRow>

public interface HeaderRow : Row {
public fun indexOfColumn(name: String): Int
}

public interface DataRow : Row {
public fun value(columnName: String): String
public fun replaceValue(columnName: String, newValue: String): DataRow
}

public companion object {
/** Use it for parsing a cvs-formatted text. */
public fun parserCsvText(csvText: String): Csv = parseCsv(csvText)
/** Represents a single row in a CSV file as a list of string values. */
public typealias CsvRow = List<String>

@Deprecated(
message = "Replaced with the more consistently named 'parserCsvText()' method." +
" This method will be removed in 0.18, please migrate.",
replaceWith = ReplaceWith("parserCsvText(csvText)"),
)
public fun parserText(csvText: String): Csv = parseCsv(csvText)

public fun fromLists(allRows: List<List<String>>): BaseCsv {
return BaseCsv(allRows.map { DefaultRow(it) })
}

public fun fromLists(header: List<String>, data: List<List<String>>): Csv {
val headerRow = DefaultHeaderRow(header)
return Csv(
header = headerRow,
data = data.map { DefaultDataRow(it, headerRow) },
)
}
}
}
/** Represents a data row in a CSV file, excluding the header row. */
public typealias CsvDataRow = List<String>

/**
* Specifies the line separator used in CSV output.
*
* @property value the string value of the line separator
*/
public enum class NewLine(
public val value: String,
) {
/** Line feed as the line terminator */
LF("\n"),

/** Carriage Return + Line feed as the line terminator */
CRLF("\r\n"),

/** Carriage Return as the line terminator */
CR("\r")
}
Expand Down
44 changes: 44 additions & 0 deletions src/commonMain/kotlin/CsvNoHeader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/** Copyright 2023-2025 Halfbit GmbH, Sergej Shafarenka */
package de.halfbit.csv

/**
* Represents CSV data without a header row.
*
* @property data the data rows
*/
public data class CsvNoHeader(
public override val data: List<CsvDataRow>,
) : Csv(data) {

override fun toString(): String =
buildString {
data.forEach { data ->
append(data)
append("\n")
}
}

public companion object {

/**
* Parses CSV text into a [CsvNoHeader] instance.
*
* @param csvText the CSV-formatted string
* @return a [CsvNoHeader] instance
*/
public fun fromCsvText(csvText: String): CsvNoHeader {
val (_, data) = parseCsv(csvText, false)
return CsvNoHeader(data)
}

/**
* Creates a [CsvNoHeader] from a list of rows.
*
* @param allRows all data rows
* @return a [CsvNoHeader] instance
*/
public fun fromLists(allRows: List<List<String>>): CsvNoHeader {
return CsvNoHeader(allRows)
}
}
}
Loading