Here's some modifications of @Duncan McGregor answer to accept unicode's box drawing or custom characters using Scala 3.
First we define a class to host the custom separators:
type ColumnSep = (Char, Char, Char)
case class TableSeparator(horizontal: Char, vertical: Char, upLeft: Char, upMiddle: Char, upRight: Char, middleLeft: Char, middleMiddle: Char, middleRight: Char, downLeft: Char, downMiddle: Char, downRight: Char):
def separate(sep: TableSeparator => ColumnSep)(seq: Seq[Any]): String =
val (a, b, c) = sep(this)
seq.mkString(a.toString, b.toString, c.toString)
def separateRows(posicao: TableSeparator => ColumnSep)(colSizes: Seq[Int]): String =
separate(posicao)(colSizes.map(horizontal.toString * _))
def up: ColumnSep = (upLeft, upMiddle, upRight)
def middle: ColumnSep = (middleLeft, middleMiddle, middleRight)
def down: ColumnSep = (downLeft, downMiddle, downRight)
def verticals: ColumnSep = (vertical, vertical, vertical)
then we define the separators on the companion object
object TableSeparator:
lazy val simple = TableSeparator(
'-', '|',
'+', '+', '+',
'+', '+', '+',
'+', '+', '+'
)
lazy val light = TableSeparator(
'─', '│',
'┌', '┬', '┐',
'├', '┼', '┤',
'└', '┴', '┘'
)
lazy val heavy = TableSeparator(
'━', '┃',
'┏', '┳', '┓',
'┣', '╋', '┫',
'┗', '┻', '┛'
)
lazy val dottedLight = TableSeparator(
'┄', '┆',
'┌', '┬', '┐',
'├', '┼', '┤',
'└', '┴', '┘'
)
lazy val dottedHeavy = TableSeparator(
'┅', '┇',
'┏', '┳', '┓',
'┣', '╋', '┫',
'┗', '┻', '┛'
)
lazy val double = TableSeparator(
'═', '║',
'╔', '╦', '╗',
'╠', '╬', '╣',
'╚', '╩', '╝'
)
And finally the Tabulator:
class Tabulator(val separators: TableSeparator):
def format(table: Seq[Seq[Any]]): String = table match
case Seq() => ""
case _ =>
val sizes = for (row <- table) yield for (cell <- row) yield if cell == null then 0 else cell.toString.length
val colSizes = for (col <- sizes.transpose) yield col.max
val rows = for (row <- table) yield formatRow(row, colSizes)
formatRows(colSizes, rows)
private def centralize(text: String, width: Int): String =
val space: Int = width - text.length
val prefix: Int = space / 2
val suffix: Int = (space + 1) / 2
if width > text.length then " ".repeat(prefix) + text + " ".repeat(suffix) else text
def formatRows(colSizes: Seq[Int], rows: Seq[String]): String =
(separators.separateRows(_.up)(colSizes) ::
rows.head ::
separators.separateRows(_.middle)(colSizes) ::
rows.tail.toList ::
separators.separateRows(_.down)(colSizes) ::
List()).mkString("\n")
def formatRow(row: Seq[Any], colSizes: Seq[Int]): String =
val cells = for (item, size) <- row zip colSizes yield if size == 0 then "" else centralize(item.toString, size)
separators.separate(_.verticals)(cells)
Some output examples:
+---+-----+----+
| a | b | c |
+---+-----+----+
|abc|true |242 |
|xyz|false|1231|
|ijk|true |312 |
+---+-----+----+
┌───┬─────┬────┐
│ a │ b │ c │
├───┼─────┼────┤
│abc│true │242 │
│xyz│false│1231│
│ijk│true │312 │
└───┴─────┴────┘
┏━━━┳━━━━━┳━━━━┓
┃ a ┃ b ┃ c ┃
┣━━━╋━━━━━╋━━━━┫
┃abc┃true ┃242 ┃
┃xyz┃false┃1231┃
┃ijk┃true ┃312 ┃
┗━━━┻━━━━━┻━━━━┛
╔═══╦═════╦════╗
║ a ║ b ║ c ║
╠═══╬═════╬════╣
║abc║true ║242 ║
║xyz║false║1231║
║ijk║true ║312 ║
╚═══╩═════╩════╝