Displaying Tables
In console
In scripts or conosle, fansi formatting is strongly preferred.
import io.github.quafadas.table.*
def csv = CSV.fromString("col1,col2,col3\n1,2,7\n3,4,8\n5,6,9")
csv.toSeq.consoleFormatNt(fansi = false)
// res0: String = """| |col1|col2|col3|
// |-|----|----|----|
// |0| 1| 2| 7|
// |1| 3| 4| 8|
// |2| 5| 6| 9|
// |-|----|----|----|"""
// This much better in a console / script, but ugly in browser
csv.toSeq.consoleFormatNt(fansi = true)
// res1: String = """+------+------+------+
// | col1 | col2 | col3 |
// +------+------+------+
// | 1 | 2 | 7 |
// | 3 | 4 | 8 |
// | 5 | 6 | 9 |
// +------+------+------+"""
// but this is easier to type and is fansi;
csv.toSeq.ptbln
// +------+------+------+
// | [36mcol1[39m | [35mcol2[39m | [33mcol3[39m |
// +------+------+------+
// | [36m1 [39m | [35m2 [39m | [33m7 [39m |
// | [36m3 [39m | [35m4 [39m | [33m8 [39m |
// | [36m5 [39m | [35m6 [39m | [33m9 [39m |
// +------+------+------+
Formatting individual columns
By default every cell is rendered with toString, which is rarely what you want for
numbers - a price, a rate and a ratio are all Double, but none of them should look
the same.
formatColumn attaches a display tag to one column. The tag lives only in the type;
the value underneath is untouched, and attaching one costs nothing at runtime.
import io.github.quafadas.table.*
def csv = CSV.fromString("product,price,margin\nwidget,1234.5678,0.0425\ngizmo,99.5,0.113")
csv.toSeq
.formatColumn["price", Currency["$", 2]]
.formatColumn["margin", Percent[2]]
.ptbln
+---------+-----------+--------+
| product | price | margin |
+---------+-----------+--------+
| widget | $1,234.57 | 4.25% |
| gizmo | $99.50 | 11.30% |
+---------+-----------+--------+
The same tags drive html and desktopShowNt, so the browser and the terminal agree.
Styling many columns at once
formatColumn takes one column at a time. style takes a spec - a named tuple type whose
names are columns and whose values are tags - and applies the lot in one call:
import io.github.quafadas.table.*
type Report = (price: Currency["$", 2], margin: Percent[2])
def csv = CSV.fromString("product,price,margin\nwidget,1234.5678,0.0425\ngizmo,99.5,0.113")
csv.toSeq.style[Report].ptbln
+---------+-----------+--------+
| product | price | margin |
+---------+-----------+--------+
| widget | $1,234.57 | 4.25% |
| gizmo | $99.50 | 11.30% |
+---------+-----------+--------+
Identical to the formatColumn chain above - including at runtime, where both are casts.
What the spec buys you is that it is a type: it can be named, reused across every table
that has those columns, and kept next to the code that displays the data rather than the
code that builds it.
Columns the spec doesn't mention are left alone, so one spec can serve a family of tables, and the order of the spec is irrelevant.
Spreadsheet columns often have spaces in their names, which backticks handle:
type PriceSheet = (
`Bid Spread`: Decimals[2],
`Offer Spread`: Decimals[2],
`Change`: Decimals[4],
EL: Percent[1]
)
brokerInfo.style[PriceSheet].ptbln
A spec that names a column which isn't there, or hands a column a tag it can't carry, is a compile error that names the column:
style: no column named prise
style: the tag given for column product is not compatible with its type
Built-in tags
N is always the number of decimal places to round to.
| Tag | Applies to | 1234.5678 renders as |
|---|---|---|
Decimals[N] |
Double |
Decimals[2] → 1234.57 |
SigFigs[N] |
Double |
SigFigs[4] → 1235 |
Thousands |
Double |
1,234.57 |
Currency[Sym, N] |
Double |
Currency["$", 2] → $1,234.57 |
IntThousands |
Int |
9876543 → 9,876,543 |
LongThousands |
Long |
9876543210 → 9,876,543,210 |
Rates, where the value is a fraction and you want it read at a different scale:
| Tag | Applies to | Example |
|---|---|---|
Percent[N] |
Double |
Percent[2] of 0.0425 → 4.25% |
BasisPoints[N] |
Double |
BasisPoints[0] of 0.0425 → 425 bps |
Large magnitudes, where you want a compact number rather than a long one. Note these
rescale the value, unlike Thousands, which keeps it at full scale and only adds
grouping separators:
| Tag | Applies to | Example |
|---|---|---|
InThousands[N] |
Double |
InThousands[3] of 1234.0 → 1.234 k |
InMillions[N] |
Double |
InMillions[4] of 123400.0 → 0.1234 M |
InBillions[N] |
Double |
InBillions[2] of 2500000000.0 → 2.50 B |
Rescaling and percentage arithmetic are done in BigDecimal, not on the Double, so
InMillions[4] of 123400.0 is exactly 0.1234 M rather than 0.1233 M.
Columns read from a CSV with blank cells arrive as Option[Double]; tag them exactly the
same way, and None still renders as None.
Formatting is locale-independent - a . is always the decimal separator, on the JVM and
in the browser alike.
Defining your own
A tag is a bounded opaque type plus a given CellFormat for it:
import io.github.quafadas.table.*
object MyTags:
opaque type Bytes <: Long = Long
given CellFormat[Bytes] = (b: Bytes) =>
if b < 1024 then s"$b B" else f"${b / 1024.0}%.1f KiB"
import MyTags.{Bytes, given}
data.formatColumn["size", Bytes].ptbln
The bound (<: Long) is what keeps the column usable: numericCols, summary and
arithmetic all still see a Long. A plain marker trait would not work here, because
Long is final - the compiler proves Long & Bytes uninhabited and the column machinery
stops reducing.
Tag late
A tag is a presentation concern, so apply it at the end of a pipeline, just before
displaying. Because the column's declared type becomes Decimals[2] rather than Double,
anything that needs an invariant typeclass on that exact type - Numeric, ClassTag, a
CSV Decoder - will no longer resolve for it. Match-type machinery (numericCols,
columns, dropColumn, summary) is unaffected.
forceColumnType is the escape hatch if you need to strip a tag back off, and retype
does it for several columns at once:
tagged.forceColumnType["price", Double]
tagged.retype[(price: Double, margin: Double)]
Both are unchecked casts, but stripping a display tag is the case where that is always sound - a tag erases to the type underneath it, so nothing about the data changes.
In browser (currently untested with named tuples)
import io.github.quafadas.table.*
case class ScauTest(anInt: Int, aString: String)
val table = Seq(ScauTest(1, "one"), ScauTest(2, "two"), ScauTest(3, "booyakashaha!"))
println(table.consoleFormat(fancy = false))
On the JVM in particular, the ability to pop it open in the browser, see and search the actual data... can be useful. Particularly if you're working with a lot of messy, csv data for example.
import io.github.quafadas.table.*
case class ScauTest(anInt: Int, aString: String)
val soComplex = Seq(ScauTest(1, "one"), ScauTest(2, "two"))
scautable.desktopShow(soComplex)
Will pop open a browser... using https://datatables.net

And your case classes are now easily visible and searchable.