Skip to content

Commit d2fd177

Browse files
Improve header deduplication logic and add edge case tests
Co-authored-by: Ivan Kyrylov <ikyrylov@griddynamics.com>
1 parent 797f32f commit d2fd177

4 files changed

Lines changed: 41 additions & 26 deletions

File tree

scautable/src/CSVUtils.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.github.quafadas.scautable
2+
import scala.collection.immutable.ListMap
3+
import scala.annotation.tailrec
4+
5+
object CSVUtils:
6+
def uniquifyHeaders(headers: List[String]): List[String] =
7+
if headers.toSet.sizeCompare(headers) == 0 then headers
8+
else
9+
val (newHeaders, _) = headers.foldLeft((List.empty[String], Map.empty[String, Int])):
10+
case ((acc, counter), elem) =>
11+
val (newElem, newCounter) = generateUniqueName(acc, counter, elem)
12+
(newElem :: acc) -> newCounter
13+
newHeaders.reverse
14+
15+
@tailrec
16+
private def generateUniqueName(acc: List[String], counter: Map[String, Int], elem: String): (String, Map[String, Int]) =
17+
if acc.contains(elem) then
18+
val newCounter = counter.updatedWith(elem)(_.map(_ + 1).orElse(Some(1)))
19+
val newElem = s"${elem}_${newCounter(elem)}"
20+
generateUniqueName(acc, newCounter, newElem)
21+
else elem -> counter
22+
end CSVUtils

scautable/src/csv.scala

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ import scala.collection.View.FlatMap
1616
import io.github.quafadas.scautable.ConsoleFormat.*
1717
import ColumnTyped.*
1818

19-
import scala.annotation.tailrec
2019
import scala.math.Fractional.Implicits.*
2120
import scala.collection.View.Single
22-
import scala.collection.immutable.ListMap
21+
import io.github.quafadas.scautable.CSVUtils.*
2322

2423
object CSV:
2524

@@ -167,16 +166,4 @@ object CSV:
167166
end match
168167
end deduplicateHeadersCode
169168

170-
val startHeaderIndex = 1
171-
def uniquifyHeaders(headers: List[String]): List[String] =
172-
if headers.toSet.sizeCompare(headers) == 0 then headers
173-
else
174-
val updatedHeaders = headers.foldLeft(ListMap.empty[String, Int]):
175-
case (acc, elem) =>
176-
acc
177-
.get(elem)
178-
.map(value => (acc + (s"${elem}_${value}" -> startHeaderIndex)).updatedWith(elem)(_.map(_ + 1)))
179-
.getOrElse(acc + (elem -> startHeaderIndex))
180-
updatedHeaders.keys.toList
181-
182169
end CSV

scautable/test/src-jvm/testCsv.scala

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -391,21 +391,12 @@ import CsvSchema.*"""
391391

392392
}
393393

394-
// test("csv has duplicate headers") {
395-
// def csv: CsvIterator[("colA", "colA", "colA", "colB", "colC", "colA")] = CSV.resource("dups.csv")
396-
397-
// // If the next two lines compile, this is a pretty good indicator that we've deduplicated the headers
398-
// def dedupCsv: CsvIterator[("colA", "colA_1", "colA_2", "colB", "colC", "colA_5")] = CSV.deduplicateHeaders(csv)
399-
// val testVal = dedupCsv.drop(1).next().colA_5
400-
// assert(testVal == "5")
401-
// }
402-
403-
test("csv has duplicate headers") {
394+
test("csv has duplicate headers") {
404395
def csv: CsvIterator[("colA", "colA", "colA", "colB", "colC", "colA")] = CSV.resource("dups.csv")
405396

406397
// If the next two lines compile, this is a pretty good indicator that we've deduplicated the headers
407-
def dedupCsv: CsvIterator[("colA", "colA_2", "colA_3", "colB", "colC", "colA_4")] = CSV.deduplicateHeaders(csv)
408-
val testVal = dedupCsv.drop(1).next().colA_4
398+
def dedupCsv: CsvIterator[("colA", "colA_1", "colA_2", "colB", "colC", "colA_3")] = CSV.deduplicateHeaders(csv)
399+
val testVal = dedupCsv.drop(1).next().colA_3
409400
assert(testVal == "5")
410401
}
411402

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.github.quafadas.scautable
2+
3+
class CSVUtilsSuite extends munit.FunSuite:
4+
test("uniquifyHeaders should suffix duplicates with index") {
5+
val headers = List("id", "name", "name", "email", "id", "name")
6+
val result = CSVUtils.uniquifyHeaders(headers)
7+
assertEquals(result, List("id", "name", "name_1", "email", "id_1", "name_2"))
8+
}
9+
10+
test("uniquifyHeaders should correctly suffix duplicates even when original headers contain numbered suffixes") {
11+
val headers = List("test", "test_1", "test", "test_1", "test_2")
12+
val result = CSVUtils.uniquifyHeaders(headers)
13+
assertEquals(result, List("test", "test_1", "test_1_1", "test_1_2", "test_2"))
14+
}
15+
end CSVUtilsSuite

0 commit comments

Comments
 (0)