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
4 changes: 1 addition & 3 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
name := "strings"
version := "1.0.0"
libraryDependencies ++= Seq(
)
version := "1.0.0-SNAPSHOT"
3 changes: 2 additions & 1 deletion src/main/scala/org/hammerlab/csv/ProductToCSVRow.scala
Original file line number Diff line number Diff line change
@@ -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 => ""
Expand Down
37 changes: 25 additions & 12 deletions src/main/scala/org/hammerlab/csv/ProductsToCSV.scala
Original file line number Diff line number Diff line change
@@ -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)
}
58 changes: 35 additions & 23 deletions src/main/scala/org/hammerlab/strings/TruncatedToString.scala
Original file line number Diff line number Diff line change
@@ -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]
}
35 changes: 35 additions & 0 deletions src/test/scala/org/hammerlab/csv/ProductsToCSVTest.scala
Original file line number Diff line number Diff line change
@@ -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("")
}
}
34 changes: 34 additions & 0 deletions src/test/scala/org/hammerlab/strings/TruncatedToStringTest.scala
Original file line number Diff line number Diff line change
@@ -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("…")
}
}