summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSom Snytt <som.snytt@gmail.com>2013-11-15 11:04:09 -0800
committerSom Snytt <som.snytt@gmail.com>2013-11-18 13:39:22 -0800
commit8f20fa23dbb5b000f0889132b8c6e2acfff096b3 (patch)
tree8babddcc8944c37d98c5f0d27948a5f1d5440432
parent02359a09ebb75deee2481d48835d5352b59e1c7e (diff)
downloadscala-8f20fa23dbb5b000f0889132b8c6e2acfff096b3.tar.gz
scala-8f20fa23dbb5b000f0889132b8c6e2acfff096b3.tar.bz2
scala-8f20fa23dbb5b000f0889132b8c6e2acfff096b3.zip
SI-7969 REPL variable columnar output
Extend column formatting to make columns only as wide as their widest element. This is similar to what `ls` does. Given the longest and shortest string, which bound the min and max column count, compute the layout for those possible column counts, and choose the minimal row count and minimal column count. The junit test is under pending because it uses expecty. (Edit: updated without expectytations.) Example that really benefits, witness the skinny columns: ``` scala> math. BigDecimal PartiallyOrdered cosh rint BigInt Pi exp round E ScalaNumber expm1 signum Equiv ScalaNumericAnyConversions floor sin Fractional ScalaNumericConversions hypot sinh IEEEremainder abs log sqrt Integral acos log10 tan LowPriorityEquiv asin log1p tanh LowPriorityOrderingImplicits atan max toDegrees Numeric atan2 min toRadians Ordered cbrt package ulp Ordering ceil pow PartialOrdering cos random ``` more
-rw-r--r--src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala72
-rw-r--r--src/repl/scala/tools/nsc/interpreter/JLineReader.scala2
-rw-r--r--test/junit/scala/tools/nsc/interpreter/TabulatorTest.scala44
3 files changed, 108 insertions, 10 deletions
diff --git a/src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala b/src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala
index 2206c8a772..d8efcda8b5 100644
--- a/src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala
+++ b/src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala
@@ -63,29 +63,29 @@ trait Tabulator {
def width: Int
def marginSize: Int
- private def fits(items: List[String], width: Int): Boolean = (
+ protected def fits(items: Seq[String], width: Int): Boolean = (
(items map (_.length)).sum + (items.length - 1) * marginSize < width
)
- def tabulate(items: List[String]): Seq[Seq[String]] = {
+ def tabulate(items: Seq[String]): Seq[Seq[String]] = (
if (fits(items, width)) Seq(Seq(items mkString " " * marginSize))
else printMultiLineColumns(items)
- }
- private def printMultiLineColumns(items: List[String]): Seq[Seq[String]] = {
+ )
+ protected def columnize(ss: Seq[String]): Seq[Seq[String]] = ss map (s => Seq(s))
+ protected def printMultiLineColumns(items: Seq[String]): Seq[Seq[String]] = {
import SimpleMath._
val longest = (items map (_.length)).max
- //val shortest = (items map (_.length)).min
val columnWidth = longest + marginSize
- val maxcols = {
+ val maxcols = (
if (columnWidth >= width) 1
else 1 max (width / columnWidth) // make sure it doesn't divide to 0
- }
+ )
val nrows = items.size /% maxcols
val ncols = items.size /% nrows
val groupSize = ncols
val padded = items map (s"%-${columnWidth}s" format _)
- val xwise = isAcross || ncols > items.length
+ val xwise = isAcross || ncols >= items.length
val grouped: Seq[Seq[String]] =
- if (groupSize == 1) Seq(items)
+ if (groupSize == 1) columnize(items)
else if (xwise) (padded grouped groupSize).toSeq
else {
val h = 1 max padded.size /% groupSize
@@ -98,6 +98,60 @@ trait Tabulator {
}
}
+/** Adjust the column width and number of columns to minimize the row count. */
+trait VariColumnTabulator extends Tabulator {
+ override protected def printMultiLineColumns(items: Seq[String]): Seq[Seq[String]] = {
+ import SimpleMath._
+ val longest = (items map (_.length)).max
+ val shortest = (items map (_.length)).min
+ val fattest = longest + marginSize
+ val skinny = shortest + marginSize
+
+ // given ncols, calculate nrows and a list of column widths, or none if not possible
+ // if ncols > items.size, then columnWidths.size == items.size
+ def layout(ncols: Int): Option[(Int, Seq[Int], Seq[Seq[String]])] = {
+ val nrows = items.size /% ncols
+ val xwise = isAcross || ncols >= items.length
+ def maxima(sss: Seq[Seq[String]]) =
+ (0 until (ncols min items.size)) map (i => (sss map (ss => ss(i).length)).max)
+ def resulting(rows: Seq[Seq[String]]) = {
+ val columnWidths = maxima(rows) map (_ + marginSize)
+ val linelen = columnWidths.sum
+ if (linelen <= width) Some((nrows, columnWidths, rows))
+ else None
+ }
+ if (ncols == 1) resulting(columnize(items))
+ else if (xwise) resulting((items grouped ncols).toSeq)
+ else {
+ val cols = (items grouped nrows).toList
+ val rows = for (i <- 0 until nrows) yield
+ for (j <- 0 until ncols) yield
+ if (j < cols.size && i < cols(j).size) cols(j)(i) else ""
+ resulting(rows)
+ }
+ }
+
+ if (fattest >= width) {
+ columnize(items)
+ } else {
+ // if every col is widest, we have at least this many cols
+ val mincols = 1 max (width / fattest)
+ // if every other col is skinniest, we have at most this many cols
+ val maxcols = 1 + ((width - fattest) / skinny)
+ val possibles = (mincols to maxcols).map(n => layout(n)).flatten
+ val minrows = (possibles map (_._1)).min
+
+ // select the min ncols that results in minrows
+ val (_, columnWidths, sss) = (possibles find (_._1 == minrows)).get
+
+ // format to column width
+ sss map (ss => ss.zipWithIndex map {
+ case (s, i) => s"%-${columnWidths(i)}s" format s
+ })
+ }
+ }
+}
+
private[interpreter] object SimpleMath {
implicit class DivRem(private val i: Int) extends AnyVal {
/** i/n + if (i % n != 0) 1 else 0 */
diff --git a/src/repl/scala/tools/nsc/interpreter/JLineReader.scala b/src/repl/scala/tools/nsc/interpreter/JLineReader.scala
index e50c7d3e0a..b6e834a1ed 100644
--- a/src/repl/scala/tools/nsc/interpreter/JLineReader.scala
+++ b/src/repl/scala/tools/nsc/interpreter/JLineReader.scala
@@ -33,7 +33,7 @@ class JLineReader(_completion: => Completion) extends InteractiveReader {
}
}
- class JLineConsoleReader extends ConsoleReader with ConsoleReaderHelper with Tabulator {
+ class JLineConsoleReader extends ConsoleReader with ConsoleReaderHelper with VariColumnTabulator {
val isAcross = interpreter.`package`.isAcross
this setPaginationEnabled interpreter.`package`.isPaged
diff --git a/test/junit/scala/tools/nsc/interpreter/TabulatorTest.scala b/test/junit/scala/tools/nsc/interpreter/TabulatorTest.scala
index e252942f89..21e338eac0 100644
--- a/test/junit/scala/tools/nsc/interpreter/TabulatorTest.scala
+++ b/test/junit/scala/tools/nsc/interpreter/TabulatorTest.scala
@@ -7,6 +7,7 @@ import org.junit.runner.RunWith
import org.junit.runners.JUnit4
case class Tabby(width: Int = 80, isAcross: Boolean = false, marginSize: Int = 3) extends Tabulator
+case class VTabby(width: Int = 80, isAcross: Boolean = false, marginSize: Int = 3) extends VariColumnTabulator
@RunWith(classOf[JUnit4])
class TabulatorTest {
@@ -38,4 +39,47 @@ class TabulatorTest {
assert(res(1).size == 1) // no trailing empty strings
assert(res(1)(0) startsWith "c")
}
+ // before, two 9-width cols don't fit in 20
+ // but now, 5-col and 9-col do fit.
+ @Test def twolinerVariable() = {
+ val sut = VTabby(width = 20)
+ val items = (1 to 9) map (i => i.toString * i)
+ val rows = sut tabulate items
+ assert(rows.size == 5)
+ assert(rows(0).size == 2)
+ assert(rows(0)(0).size == 8) // width is 55555 plus margin of 3
+ }
+ @Test def sys() = {
+ val sut = VTabby(width = 40)
+ val items = List("BooleanProp", "PropImpl", "addShutdownHook", "error",
+ "process", "CreatorImpl", "ShutdownHookThread", "allThreads",
+ "exit", "props", "Prop", "SystemProperties",
+ "env", "package", "runtime")
+ val rows = sut tabulate items
+ assert(rows.size == 8)
+ assert(rows(0).size == 2)
+ assert(rows(0)(0).size == "ShutdownHookThread".length + sut.marginSize) // 21
+ }
+ @Test def syswide() = {
+ val sut = VTabby(width = 120)
+ val items = List("BooleanProp", "PropImpl", "addShutdownHook", "error",
+ "process", "CreatorImpl", "ShutdownHookThread", "allThreads",
+ "exit", "props", "Prop", "SystemProperties",
+ "env", "package", "runtime")
+ val rows = sut tabulate items
+ assert(rows.size == 2)
+ assert(rows(0).size == 8)
+ assert(rows(0)(0).size == "BooleanProp".length + sut.marginSize) // 14
+ }
+ @Test def resultFits() = {
+ val sut = VTabby(width = 10)
+ // each of two lines would fit, but layout is two cols of width six > 10
+ // therefore, should choose ncols = 1
+ val items = List("a", "bcd",
+ "efg", "h")
+ val rows = sut tabulate items
+ assert(rows.size == 4)
+ assert(rows(0).size == 1)
+ assert(rows(0)(0).size == "efg".length + sut.marginSize) // 6
+ }
}