From 37bc9da217c79e2cef2d121f8ce2b36bcf0c4bba Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 4 Dec 2016 01:38:59 +0000 Subject: [PATCH 1/2] add tests, tweak functionality --- build.sbt | 4 +- project/plugins.sbt | 2 +- .../org/hammerlab/csv/ProductToCSVRow.scala | 3 +- .../org/hammerlab/csv/ProductsToCSV.scala | 37 ++++++++---- .../hammerlab/strings/TruncatedToString.scala | 58 +++++++++++-------- .../org/hammerlab/csv/ProductsToCSVTest.scala | 35 +++++++++++ .../strings/TruncatedToStringTest.scala | 34 +++++++++++ 7 files changed, 133 insertions(+), 40 deletions(-) create mode 100644 src/test/scala/org/hammerlab/csv/ProductsToCSVTest.scala create mode 100644 src/test/scala/org/hammerlab/strings/TruncatedToStringTest.scala diff --git a/build.sbt b/build.sbt index 1dffd34..64f0ab6 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,2 @@ name := "strings" -version := "1.0.0" -libraryDependencies ++= Seq( -) +version := "1.0.0-SNAPSHOT" diff --git a/project/plugins.sbt b/project/plugins.sbt index b0e479a..da1587e 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1 +1 @@ -addSbtPlugin("org.hammerlab" % "sbt-parent" % "1.2.4") +addSbtPlugin("org.hammerlab" % "sbt-parent" % "1.2.5") diff --git a/src/main/scala/org/hammerlab/csv/ProductToCSVRow.scala b/src/main/scala/org/hammerlab/csv/ProductToCSVRow.scala index 26175f8..685706b 100644 --- a/src/main/scala/org/hammerlab/csv/ProductToCSVRow.scala +++ b/src/main/scala/org/hammerlab/csv/ProductToCSVRow.scala @@ -1,6 +1,7 @@ -package org.hammerlab.pageant.utils +package org.hammerlab.csv class ProductToCSVRow(val prod: Product) extends AnyVal { + // cf. http://stackoverflow.com/questions/30271823/converting-a-case-class-to-csv-in-scala def toCSV = prod.productIterator.map { case Some(value) => value case None => "" diff --git a/src/main/scala/org/hammerlab/csv/ProductsToCSV.scala b/src/main/scala/org/hammerlab/csv/ProductsToCSV.scala index 710447d..c5e719b 100644 --- a/src/main/scala/org/hammerlab/csv/ProductsToCSV.scala +++ b/src/main/scala/org/hammerlab/csv/ProductsToCSV.scala @@ -1,24 +1,37 @@ -package org.hammerlab.pageant.utils +package org.hammerlab.csv -import ProductToCSVRow._ +import org.hammerlab.csv.ProductToCSVRow._ -class ProductsToCSV[T <: Product](products: BufferedIterator[T]) { +import scala.reflect.ClassTag + +class ProductsToCSV[T <: Product: ClassTag](products: Iterator[T]) { def toCSV(includeHeaderLine: Boolean = true): Iterator[String] = if (includeHeaderLine) { - val clazz = - if (products.hasNext) - products.head.getClass - else - classOf[T] + val (clazz, firstOpt) = + if (products.hasNext) { + val first = products.next() + (first.getClass, Some(first)) + } else { + val ctag = implicitly[reflect.ClassTag[T]] + (ctag.runtimeClass.asInstanceOf[Class[T]], None) + } - val headerLine = clazz.getDeclaredFields.map(_.getName).mkString(",") + val headerLine = + clazz + .getDeclaredFields + .map(_.getName) + .filterNot(_ == "$outer") // Field added on case classes nested inside another class. + .mkString(",") - Iterator(headerLine) ++ toCSV(includeHeaderLine = false) + Iterator(headerLine) ++ + firstOpt.map(_.toCSV).iterator ++ + toCSV(includeHeaderLine = false) } else products.map(_.toCSV) } object ProductsToCSV { - implicit def apply[T <: Product](products: Iterable[T]): ProductsToCSV[T] = new ProductsToCSV(products.iterator.buffered) - implicit def apply[T <: Product](products: Array[T]): ProductsToCSV[T] = new ProductsToCSV(products.iterator.buffered) + implicit def apply[T <: Product: ClassTag](products: Iterator[T]): ProductsToCSV[T] = new ProductsToCSV(products) + implicit def apply[T <: Product: ClassTag](products: Iterable[T]): ProductsToCSV[T] = new ProductsToCSV(products.iterator) + implicit def apply[T <: Product: ClassTag](products: Array[T]): ProductsToCSV[T] = new ProductsToCSV(products.iterator) } diff --git a/src/main/scala/org/hammerlab/strings/TruncatedToString.scala b/src/main/scala/org/hammerlab/strings/TruncatedToString.scala index 54e9530..f0f050d 100644 --- a/src/main/scala/org/hammerlab/strings/TruncatedToString.scala +++ b/src/main/scala/org/hammerlab/strings/TruncatedToString.scala @@ -1,38 +1,50 @@ package org.hammerlab.strings trait TruncatedToString { - override def toString: String = truncatedString(Int.MaxValue) - - /** String representation, truncated to maxLength characters. */ - def truncatedString(maxLength: Int = 500): String = - TruncatedToString(stringPieces, maxLength) + override def toString: String = toString(Int.MaxValue) - /** - * Iterator over string representations of data comprising this object. - */ - def stringPieces: Iterator[String] -} - -object TruncatedToString { /** * Like Scala's List.mkString method, but supports truncation. * * Return the concatenation of an iterator over strings, separated by separator, truncated to at most maxLength * characters. If truncation occurs, the string is terminated with ellipses. */ - def apply(pieces: Iterator[String], - maxLength: Int, - separator: String = ",", - ellipses: String = " [...]"): String = { + def toString(maxLength: Int, + separator: String = ",", + ellipses: String = "…"): String = { + val builder = StringBuilder.newBuilder var remaining: Int = maxLength - while (pieces.hasNext && remaining > ellipses.length) { - val string = pieces.next() - builder.append(string) - if (pieces.hasNext) builder.append(separator) - remaining -= string.length + separator.length + + val separatedPieces = + stringPieces + .flatMap(piece ⇒ Iterator(separator, piece)) + .drop(1) + .buffered + + while (separatedPieces.hasNext && remaining >= 0) { + val piece = separatedPieces.head.toString + val len = piece.length + + if (len <= remaining) { + builder.append(piece) + separatedPieces.next() + remaining -= len + } else { + remaining = -1 + } } - if (pieces.hasNext) builder.append(ellipses) - builder.result + + val result = builder.result + + if (separatedPieces.hasNext) + result.substring(0, maxLength - ellipses.length) + ellipses + else + result } + + /** + * Iterator over string representations of data comprising this object. + */ + def stringPieces: Iterator[String] } diff --git a/src/test/scala/org/hammerlab/csv/ProductsToCSVTest.scala b/src/test/scala/org/hammerlab/csv/ProductsToCSVTest.scala new file mode 100644 index 0000000..263a6a7 --- /dev/null +++ b/src/test/scala/org/hammerlab/csv/ProductsToCSVTest.scala @@ -0,0 +1,35 @@ +package org.hammerlab.csv + +import org.scalatest.{ FunSuite, Matchers } + +import ProductsToCSV._ + +class ProductsToCSVTest + extends FunSuite + with Matchers { + + // cf. http://stackoverflow.com/questions/30271823/converting-a-case-class-to-csv-in-scala + case class Person(name: String, age: Int, address: Option[String]) + + test("people") { + val ppl = + Seq( + Person("alice", 123, None), + Person("bob", 456, Some("foo")) + ) + + ppl.toCSV().mkString("\n") should be( + """name,age,address + |alice,123, + |bob,456,foo""".stripMargin + ) + } + + test("no people") { + Seq[Person]().toCSV().mkString("\n") should be("name,age,address") + } + + test("no people, no header") { + Seq[Person]().toCSV(includeHeaderLine = false).mkString("\n") should be("") + } +} diff --git a/src/test/scala/org/hammerlab/strings/TruncatedToStringTest.scala b/src/test/scala/org/hammerlab/strings/TruncatedToStringTest.scala new file mode 100644 index 0000000..6dcfab8 --- /dev/null +++ b/src/test/scala/org/hammerlab/strings/TruncatedToStringTest.scala @@ -0,0 +1,34 @@ +package org.hammerlab.strings + +import org.scalatest.{ FunSuite, Matchers } + +class TruncatedToStringTest + extends FunSuite + with Matchers { + + test("numbers") { + val o = new TruncatedToString { + override def stringPieces = (1 to 10).iterator.map(_.toString) + } + + o.toString(10) should be("1,2,3,4,5…") + o.toString(17) should be("1,2,3,4,5,6,7,8,…") + o.toString(18) should be("1,2,3,4,5,6,7,8,9…") + o.toString(19) should be("1,2,3,4,5,6,7,8,9,…") + o.toString(20) should be("1,2,3,4,5,6,7,8,9,10") + o.toString(100) should be("1,2,3,4,5,6,7,8,9,10") + o.toString() should be("1,2,3,4,5,6,7,8,9,10") + } + + test("empties") { + val o = new TruncatedToString { + override def stringPieces = Array.fill(5)("").iterator + } + + o.toString(5) should be(",,,,") + o.toString(4) should be(",,,,") + o.toString(3) should be(",,…") + o.toString(2) should be(",…") + o.toString(1) should be("…") + } +} From 84e0642301bda994c6468d8f49cf70705ecbc607 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 4 Dec 2016 01:52:01 +0000 Subject: [PATCH 2/2] bump plugin version down --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index da1587e..b0e479a 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1 +1 @@ -addSbtPlugin("org.hammerlab" % "sbt-parent" % "1.2.5") +addSbtPlugin("org.hammerlab" % "sbt-parent" % "1.2.4")