Easily print textual tables in Swift. Inspired by the Python tabulate library.
See details on an upcoming change.
- Easily generate textual tables from collections of any type
- Auto-calculates column width
- Custom width for individual columns
- Align individual columns
- Supports per-column Formatters
- Multiple table output styles including Markdown, Emacs, HTML, Latex etc...
- Extensible table styles allow you to easily create your own table style
- Column-specific truncation modes (truncate head or tail of column content or optionally fail if content overflows).
This package was written for and tested with the following version of Swift:
Apple Swift version 5.0
Use the Swift Package Manager to install TextTable
as a dependency of your project.
dependencies: [
.package(
url: "https://github.com/cfilipov/TextTable",
.branch("master"))
]
First, import TextTable
:
import TextTable
Throughout the examples, the following data structure will be used:
struct Person {
let name: String
let age: Int
let birhtday: Date
}
Let's say we have a collection of Person
we wish to display as a table.
let data = [
Person(name: "Alice", age: 42, birhtday: Date()),
Person(name: "Bob", age: 22, birhtday: Date()),
Person(name: "Eve", age: 142, birhtday: Date())
]
Configure a TextTable
instance for the type you wish to print as a table. The minimum configuration will require a mapping of values to columns for your type.
let table = TextTable<Person> {
[Column("Name" <- $0.name),
Column("Age" <- $0.age),
Column("Birthday" <- $0.birhtday)]
}
The <-
operator is just syntactic sugar. If you don't want to use the operator, you can use labeled arguments instead.
let table = TextTable<Person> {
[Column(title: "Name", value: $0.name),
Column(title: "Age", value: $0.age),
Column(title: "Birthday", value: $0.birhtday)]
}
Simply call the print()
method, passing in a collection of Person
.
table.print(data)
Name Age Birthday
----- --- -------------------------
Alice 42 2016-08-13 19:21:54 +0000
Bob 22 2016-08-13 19:21:54 +0000
Eve 142 2016-08-13 19:21:54 +0000
You can also just get the rendered table as a string by calling the string(for:)
method.
let s = table.string(for: data)
You can optionally specify a table style using the style:
argument.
table.print(data, style: Style.psql)
+-------+-----+---------------------------+
| Name | Age | Birthday |
+-------+-----+---------------------------+
| Alice | 42 | 2016-08-13 08:12:58 +0000 |
| Bob | 22 | 2016-08-13 08:12:58 +0000 |
| Eve | 142 | 2016-08-13 08:12:58 +0000 |
+-------+-----+---------------------------+
The style can also be specified when using the string(for:)
method.
let s = table.string(for: data, style: Style.psql)
For a full list of supported styles see Supported Styles below.
You can specify a Formatter on a per-column basis. In this example a DateFormatter is used to customize the display of the Birthday
column.
let df = DateFormatter()
dateFormatter.dateStyle = .medium
let table = TextTable<Person> {
[Column("Name" <- $0.name),
Column("Age" <- $0.age),
Column("Birthday" <- $0.birhtday, formatter: df)]
}
table.print(data, style: Style.psql)
+-------+-----+--------------+
| Name | Age | Birthday |
+-------+-----+--------------+
| Alice | 42 | Aug 13, 2016 |
| Bob | 22 | Aug 13, 2016 |
| Eve | 142 | Aug 13, 2016 |
+-------+-----+--------------+
By default TextTable will calculate the necessary width for each column. You can also explictely set the width with the width:
argument.
let table = TextTable<Person> {
[Column("Name" <- $0.name, width: 10),
Column("Age" <- $0.age, width: 10)]
}
table.print(data, style: Style.psql)
+------------+------------+
| Name | Age |
+------------+------------+
| Alice | 42 |
| Bob | 22 |
| Eve | 142 |
+------------+------------+
Some table styles may include padding to the left and/or right of the content, the width argument does not effect include this padding.
By default, if a width is specified and the contents of the column are wider than the width, the text will be truncated at the tail (.tail
truncation mode).
let table = TextTable<Person> {
[Column("Name" <- $0.name, width: 4), // defaults to truncation: .tail
Column("Age" <- $0.age)]
}
table.print(data, style: testStyle)
+------+-----+
| Name | Age |
+------+-----+
| Ali… | 42 |
| Bob | 22 |
| Eve | 142 |
+------+-----+
You can also truncate at the head of the text string by specifying .head
for the truncation:
argument. If no width
argument is present, the truncation
argument has no effect.
let table = TextTable<Person> {
[Column("Name" <- $0.name, width: 4, truncate: .head),
Column("Age" <- $0.age)]
}
table.print(data, style: testStyle)
+------+-----+
| Name | Age |
+------+-----+
| …ice | 42 |
| Bob | 22 |
| Eve | 142 |
+------+-----+
If you prefer to have an fatal error triggered when the contents of a column do not fit the specified width you can specify the .error
truncation mode.
let table = TextTable<Person> {
[Column("Name" <- $0.name, width: 4, truncate: .error),
Column("Age" <- $0.age)]
}
table.print(data, style: testStyle)
// fatal error: Truncation error
By default, all columns will be left-aligned. You can set the alignment on each column individually.
let table = TextTable<Person> {
[Column("Name" <- $0.name), // default: .left
Column("Age" <- $0.age, align: .right)]
}
table.print(data, style: Style.psql)
+-------+-----+
| Name | Age |
+-------+-----+
| Alice | 42 |
| Bob | 22 |
| Eve | 142 |
+-------+-----+
By default TextTable will calculate the necessary width for each column that does not specify an explicit width using the width:
column argument. This calculation is accomplished by iterating over each row of the table twice: the first time to calculate the max width, the second time to actually render the table. The overhead of this calculation may not be appropriate for very large tables. If you wish to avoid this calculation you must specify the width for every column.
In the example below table1
will incur the overhead of the width calculation because one of the columns does not have an explicit width. On the other hand, table2
will not incur the overhead because all columns have an explicit width.
// This will still result in extra calculations
let table1 = TextTable<Person> {
[Column("Name" <- $0.name),
Column("Age" <- $0.age, width: 10)]
}
// This will skip the width calculations
let table2 = TextTable<Person> {
[Column("Name" <- $0.name, width: 9),
Column("Age" <- $0.age, width: 10)]
}
TextTable supports most of the styles as tabulate.
table.print(data, style: Style.plain)
Name Age Birthday
Alice 42 8/13/16
Bob 22 8/13/16
Eve 142 8/13/16
simple
is the default Style. It corresponds to simple_tables
in Pandoc Markdown extensions.
table.print(data) // or:
table.print(data, style: Style.simple)
Name Age Birthday
----- --- --------
Alice 42 8/13/16
Bob 22 8/13/16
Eve 142 8/13/16
grid
follows the conventions of Emacs’ table.el package. It corresponds to grid_tables
in Pandoc Markdown extensions.
table.print(data, style: Style.grid)
+-------+-----+----------+
| Name | Age | Birthday |
+=======+=====+==========+
| Alice | 42 | 8/13/16 |
+-------+-----+----------+
| Bob | 22 | 8/13/16 |
+-------+-----+----------+
| Eve | 142 | 8/13/16 |
+-------+-----+----------+
table.print(data, style: Style.fancyGrid)
╒═══════╤═════╤══════════╕
│ Name │ Age │ Birthday │
╞═══════╪═════╪══════════╡
│ Alice │ 42 │ 8/13/16 │
├───────┼─────┼──────────┤
│ Bob │ 22 │ 8/13/16 │
├───────┼─────┼──────────┤
│ Eve │ 142 │ 8/13/16 │
╘═══════╧═════╧══════════╛
table.print(data, style: Style.psql)
+-------+-----+----------+
| Name | Age | Birthday |
+-------+-----+----------+
| Alice | 42 | 8/13/16 |
| Bob | 22 | 8/13/16 |
| Eve | 142 | 8/13/16 |
+-------+-----+----------+
pipe
follows the conventions of PHP Markdown Extra extension. It corresponds to pipe_tables
in Pandoc. This style uses colons to indicate column alignment.
table.print(data, style: Style.pipe)
| Name | Age | Birthday |
|:------|----:|---------:|
| Alice | 42 | 8/13/16 |
| Bob | 22 | 8/13/16 |
| Eve | 142 | 8/13/16 |
org
follows the conventions of Emacs org-mode.
table.print(data, style: Style.org)
| Name | Age | Birthday |
|-------+-----+----------|
| Alice | 42 | 8/13/16 |
| Bob | 22 | 8/13/16 |
| Eve | 142 | 8/13/16 |
rst
follows the conventions of simple table of the reStructuredText Style.
table.print(data, style: Style.rst)
===== === ========
Name Age Birthday
===== === ========
Alice 42 8/13/16
Bob 22 8/13/16
Eve 142 8/13/16
===== === ========
html
produces HTML table markup.
table.print(data, style: Style.html)
<table>
<tr>
<th style="text-align:left;">Name</th>
<th style="text-align:center;">Age</th>
<th style="text-align:right;">Birthday</th>
</tr>
<tr>
<td>Alice</td>
<td>42</td>
<td>8/14/16</td>
</tr>
<tr>
<td>Bob</td>
<td>22</td>
<td>8/14/16</td>
</tr>
<tr>
<td>Eve</td>
<td>142</td>
<td>8/14/16</td>
</tr>
</table>
latex
produces a tabular environment for Latex.
table.print(data, style: Style.latex)
\begin{tabular}{lll}
\hline
Name & Age & Birthday \\
\hline
Alice & 42 & 8/13/16 \\
Bob & 22 & 8/13/16 \\
Eve & 142 & 8/13/16 \\
\hline
\end{tabular}
You can create a custom table style by conforming to the TextTableStyle
protocol.
enum MyCustomStyle: TextTableStyle {
// implement methods from TextTableStyle...
}
extension Style {
static let custom = MyCustomStyle.self
}
table.print(data, style: Style.custom)
A list of changes can be found in the CHANGELOG.
Other Swift libraries that serve a similar purpose.
MIT License
Copyright (c) 2016 Cristian Filipov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.