Scala: рисовать таблицу на консоли

Мне нужно отобразить таблицу в консоли.

Мое простое решение, если бы вы назвали его «решением», выглядит следующим образом:

  override def toString() = {
    var res = "\n"
      var counter = 1;
      res += stateDb._1 + "\n"
      res += "  +----------------------------+\n"
      res += "  +     State Table            +\n"
      res += "  +----------------------------+\n"
      for (entry <- stateDb._2) {
        res += "  | " + counter + "\t | " + entry._1 + " | " + entry._2 + " |\n"
        counter += 1;
      }
      res += "  +----------------------------+\n"
      res += "\n"
    res

  }

Нам не нужно спорить об этом

  • a выглядит плохо при отображении
  • код b выглядит немного запутанным

На самом деле такой вопрос был задан для C #, но я хотел бы знать хорошее решение и для Scala.

Так как же (хороший/хороший/простой/какой угодно) способ нарисовать такую ​​таблицу в Scala на консоли?

-------------------------------------------------------------------------
|    Column 1     |    Column 2     |    Column 3     |    Column 4     |
-------------------------------------------------------------------------
|                 |                 |                 |                 |
|                 |                 |                 |                 |
|                 |                 |                 |                 |
-------------------------------------------------------------------------

person evildead    schedule 24.09.2011    source источник


Ответы (4)


Я вытащил следующее из моего текущего проекта:

object Tabulator {
  def format(table: Seq[Seq[Any]]) = table match {
    case Seq() => ""
    case _ => 
      val sizes = for (row <- table) yield (for (cell <- row) yield if (cell == null) 0 else cell.toString.length)
      val colSizes = for (col <- sizes.transpose) yield col.max
      val rows = for (row <- table) yield formatRow(row, colSizes)
      formatRows(rowSeparator(colSizes), rows)
  }

  def formatRows(rowSeparator: String, rows: Seq[String]): String = (
    rowSeparator :: 
    rows.head :: 
    rowSeparator :: 
    rows.tail.toList ::: 
    rowSeparator :: 
    List()).mkString("\n")

  def formatRow(row: Seq[Any], colSizes: Seq[Int]) = {
    val cells = (for ((item, size) <- row.zip(colSizes)) yield if (size == 0) "" else ("%" + size + "s").format(item))
    cells.mkString("|", "|", "|")
  }

  def rowSeparator(colSizes: Seq[Int]) = colSizes map { "-" * _ } mkString("+", "+", "+")
}

scala> Tabulator.format(List(List("head1", "head2", "head3"), List("one", "two", "three"), List("four", "five", "six")))
res1: java.lang.String = 
+-----+-----+-----+
|head1|head2|head3|
+-----+-----+-----+
|  one|  two|three|
| four| five|  six|
+-----+-----+-----+
person Duncan McGregor    schedule 24.09.2011
comment
это прекрасно. Попробовал и это именно то, что я искал. - person evildead; 25.09.2011
comment
в качестве дополнения это выравнивание по левому краю (% + размер + s).format(item) это правое (%- + size + s).format(item) - person evildead; 25.09.2011
comment
Было бы еще лучше, как implicit class, который добавляет, например. .asTable напр. Seq[Seq[Any]] :) - person Erik Kaplun; 05.02.2014
comment
Это аккуратно, спасибо. Однако использование цветов консоли, похоже, искажает интервалы, есть идеи, почему? - person Evan Sebastian; 23.07.2015
comment
О, неважно, я думаю, это из-за специальных символов, используемых для отображения цвета, засчитывается в максимальный размер столбца. - person Evan Sebastian; 23.07.2015
comment
@evildead: Разве не наоборот? (%- + размер + s).format(item) для выравнивания по левому краю? В любом случае, спасибо! Этот комментарий очень помог адаптировать приведенный выше код к моей проблеме. - person Toaditoad; 25.08.2016

Огромное спасибо за код Табулятора!

Существует модификация для табличной печати набора данных Spark.

Я имею в виду, что вы можете распечатать содержимое DataFrame или извлеченный набор результатов, например

Tabulator(hiveContext.sql("SELECT * FROM stat"))
Tabulator(hiveContext.sql("SELECT * FROM stat").take(20))

Второй, конечно, будет без заголовка, для реализации DF вы можете указать, сколько строк извлекать из фрейма данных Spark для печати и нужен ли вам заголовок или нет.

 /**
 * Tabular representation of Spark dataset.
 * Usage:
 * 1. Import source to spark-shell:
 *   spark-shell.cmd --master local[2] --packages com.databricks:spark-csv_2.10:1.3.0 -i /path/to/Tabulator.scala
 * 2. Tabulator usage:
 *   import org.apache.spark.sql.hive.HiveContext
 *   val hiveContext = new HiveContext(sc)
 *   val stat = hiveContext.read.format("com.databricks.spark.csv").option("header", "true").option("inferSchema", "true").option("delimiter", "\t").load("D:\\data\\stats-belablotski.tsv")
 *   stat.registerTempTable("stat")
 *   Tabulator(hiveContext.sql("SELECT * FROM stat").take(20))
 *   Tabulator(hiveContext.sql("SELECT * FROM stat"))
 */
object Tabulator {

  def format(table: Seq[Seq[Any]], isHeaderNeeded: Boolean) : String = table match {
    case Seq() => ""
    case _ => 
      val sizes = for (row <- table) yield (for (cell <- row) yield if (cell == null) 0 else cell.toString.length)
      val colSizes = for (col <- sizes.transpose) yield col.max
      val rows = for (row <- table) yield formatRow(row, colSizes)
      formatRows(rowSeparator(colSizes), rows, isHeaderNeeded)
  }

  def formatRes(table: Array[org.apache.spark.sql.Row]): String = {
    val res: Seq[Seq[Any]] = (for { r <- table } yield r.toSeq).toSeq
    format(res, false)
  }

  def formatDf(df: org.apache.spark.sql.DataFrame, n: Int = 20, isHeaderNeeded: Boolean = true): String = {
    val res: Seq[Seq[Any]] = (for { r <- df.take(n) } yield r.toSeq).toSeq
    format(List(df.schema.map(_.name).toSeq) ++ res, isHeaderNeeded)
  }

  def apply(table: Array[org.apache.spark.sql.Row]): Unit = 
    println(formatRes(table))

  /**
   * Print DataFrame in a formatted manner.
   * @param df Data frame
   * @param n How many row to take for tabular printing
   */
  def apply(df: org.apache.spark.sql.DataFrame, n: Int = 20, isHeaderNeeded: Boolean = true): Unit =
    println(formatDf(df, n, isHeaderNeeded))

  def formatRows(rowSeparator: String, rows: Seq[String], isHeaderNeeded: Boolean): String = (
    rowSeparator :: 
    (rows.head + { if (isHeaderNeeded) "\n" + rowSeparator else "" }) :: 
    rows.tail.toList ::: 
    rowSeparator :: 
    List()).mkString("\n")

  def formatRow(row: Seq[Any], colSizes: Seq[Int]) = {
    val cells = (for ((item, size) <- row.zip(colSizes)) yield if (size == 0) "" else ("%" + size + "s").format(item))
    cells.mkString("|", "|", "|")
  }

  def rowSeparator(colSizes: Seq[Int]) = colSizes map { "-" * _ } mkString("+", "+", "+")

}
person beloblotskiy    schedule 12.12.2015

Если вы хотите, чтобы он был несколько более компактным. Бонус: выровнено по левому краю и дополнено 1 символом с обеих сторон. На основе ответа Дункана МакГрегора (https://stackoverflow.com/a/7542476/8547501):

def formatTable(table: Seq[Seq[Any]]): String = {
  if (table.isEmpty) ""
  else {
    // Get column widths based on the maximum cell width in each column (+2 for a one character padding on each side)
    val colWidths = table.transpose.map(_.map(cell => if (cell == null) 0 else cell.toString.length).max + 2)
    // Format each row
    val rows = table.map(_.zip(colWidths).map { case (item, size) => (" %-" + (size - 1) + "s").format(item) }
      .mkString("|", "|", "|"))
    // Formatted separator row, used to separate the header and draw table borders
    val separator = colWidths.map("-" * _).mkString("+", "+", "+")
    // Put the table together and return
    (separator +: rows.head +: separator +: rows.tail :+ separator).mkString("\n")
  }
}

scala> formatTable(Seq(Seq("head1", "head2", "head3"), Seq("one", "two", "three"), Seq("four", "five", "six")))
res0: String =
+-------+-------+-------+
| head1 | head2 | head3 |
+-------+-------+-------+
| one   | two   | three |
| four  | five  | six   |
+-------+-------+-------+
person Svyat    schedule 13.03.2019

Токенизируйте это. Я бы начал с создания нескольких объектов case и классов, чтобы вы могли создать токенизированный список, с которым можно работать для целей отображения:

sealed trait TableTokens{
  val width: Int
}
case class Entry(value: String) extends TableTokens{
  val width = value.length
}
case object LineBreak extends TableTokens{
  val width = 0
}
case object Div extends TableTokens{
  val width = 1
}

Итак, вы можете сформировать определенные ограничения с помощью какого-то объекта строки:

case class Row(contents: List[TableTokens]) extends TableTokens{
  val width = contents.foldLeft(0)((x,y) => x = y.width)
}

Затем вы можете проверить ограничения и подобные вещи неизменяемым образом. Возможно, создание методов добавления таблиц и выравнивания...

case class Table(contents: List[TableTokens])

Это означает, что у вас может быть несколько разных вариантов таблиц, где ваш стиль отличается от вашей структуры, а-ля HTML и CSS.

person wheaties    schedule 24.09.2011
comment
возможно, вы можете добавить небольшой пример. Я не понимаю этого в полной мере. - person evildead; 24.09.2011
comment
Во-вторых, @evildead — зачем токенизировать что-либо, если речь идет о рендеринге существующей последовательности? - person Erik Kaplun; 05.02.2014