swiftコンソールにprint()でテーブルを書きたい

# はじめに
特に実用性はない。深夜テンションで思いついて気が付いた頃には実行ボタン押してた。

> 実行結果
<img width="40%" alt="ascii-table.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/982650/6a33dba5-1f1c-4880-838c-3afe7b1ade8a.png">

# 使い方
### せっかくなので使ってみてね

あとで下に書くファイル一つ分をコピペするだけで使えるよ💕
 ```AsciiTable.swift```

実行コードもとっても簡単!

```Sample.swift
// サンプル用のデータ
let sample = Ascii.Table.Sample.jsonData

// 初期化
let table = Ascii.Table(json: sample)

// コンソール 出力
for text in table.texts {
    print(text)
}

// JSONに変換して取得
let json = table.jsonData
```

# コピペ用ファイル

丸々コピーして適当なファイルに貼り付けてね😊
**Ascii.Table**クラスが本体だよ🤗
```String``` の配列とか、```JSON``` とか色んな形式から初期化できるから試してみよう!

```AsciiTable.swift
//
// +------------------+
// | AsciiTable.swift |
// +------------------+
//
import Foundation

/*
 +------------+
 | How to Use |
 +------------+
 ``
 let sample = Ascii.Table.Sample.jsonData

 # 1. init
 let table = Ascii.Table(json: sample)

 # 2. print
 for text in table.texts {
 print(text)
 }

 # 3. Serialize
 let json = table.jsonData
 ``
 */
extension Ascii.Table {
    public enum Sample {
        /// this is a sample
        public static let rawTexts: (columns: [String], rows: [[String]]) = {
            return (
                columns: ["1", "2", "3"],
                rows: [
                    ["4", "5", "6"],
                    ["7", "8", "9"]
                ]
            )
        }()
        /// this is a sample
        public static let jsonData: Data = """
            {
              "columns": [
                {"key": "keyA", "width": 8},
                {"key": "keyB", "width": 10}
              ],
              "rows": [
                {"cells":[
                    {"key": "keyA", "value": "valueA0"},
                    {"key": "keyB", "value": "valueB0"}
                ]},
                {"cells": [
                    {"key": "keyA", "value": "valueA1"},
                    {"key": "keyB", "value": "valueB1"}
                ]}
              ]
            }
            """.data(using: .utf8)!
        /// this is a sample
        public static let rows: [Row] = [
            Row(cells: [
                Cell(key: "XXX",
                     value: "aaa"),
                Cell(key: "YYY",
                     value: "bbb")
            ]),
            Row(cells: [
                Cell(key: "XXX",
                     value: "ccc"),
                Cell(key: "YYY",
                     value: "ddd")
            ]),
        ]
    }
}
fileprivate protocol AsciiTable {

    /// [
    ///   "+----------+------------+",
    ///   "|     keyA |       keyB |",
    ///   "+----------+------------+",
    ///   "|  valueA0 |    valueB0 |",
    ///   "+----------+------------+",
    ///   "|  valueA1 |    valueB1 |",
    ///   "+----------+------------+"
    /// ]
    /// texts for output
    var texts: [String] { get }

    /// init with Swift Type
    init(columns: [Ascii.Table.Column]?, rows: [Ascii.Table.Row])

    /// init with String Array
    init(columns: [String], rows: [[String]])
}

public enum Ascii {
    public struct Table {
        public struct Column {
            let key: String
            var width: Int?
        }
        public struct Row {
            let cells: [Cell]
        }
        public struct Cell {
            let key: String
            let value: String
        }
        public let columns: [Column]
        public let rows: [Row]
    }
}
extension Ascii.Table: AsciiTable {
    init(columns: [Ascii.Table.Column]? = nil, rows: [Ascii.Table.Row]) {
        self.columns = (columns ?? rows.first?.cells.map { Column(key: $0.key) }) ?? []
        self.rows = rows
    }
    init(columns: [String], rows: [[String]]) {
        self.columns = columns.map {Column(key: $0)}
        self.rows = rows.map {(row) -> Row in Row(cells: row.enumerated().map {Cell(key: columns[$0.offset], value: $0.element)})}
    }
    var texts: [String] {
        let borderLines = [String](repeating: self.horizontalBorder, count: self.rows.count).map { [$0] }
        let rowLines = self.rows.map {self.stringRow(for: $0.cells)}
        let zipped = zip(borderLines, rowLines)
        let alts = [String](zipped.map { $0.0 + $0.1 }.joined())

        return [self.horizontalBorder]
            + [self.stringColumns]
            + alts
            + [self.horizontalBorder]
    }
}
extension Ascii.Table: JSONSerializable {}
extension Ascii.Table.Row: JSONSerializable {}
extension Ascii.Table.Column: JSONSerializable {}
extension Ascii.Table.Cell: JSONSerializable {}
fileprivate extension Ascii.Table.Cell {
    func lineBrokenValue(by count: Int) -> [String] {
        return self.value.split(by: count).map {$0.filled(to: count)}
    }
}
fileprivate extension Ascii.Table {
    func cells(at line: Int) -> [Ascii.Table.Cell]? {
        return self.rows[at: line]?.cells
    }
    var horizontalBorder: String {
        return "+-" + self.columns.map {[String](repeating: "-", count: $0.width ?? $0.key.count).joined()}.joined(separator: "-+-") + "-+"
    }
    var stringColumns: String {
        return "| " + self.columns.map {$0.key.filled(to: $0.width ?? $0.key.count)}.joined(separator: " | ") + " |"
    }
    func stringRow(for cells: [Ascii.Table.Cell]) -> [String] {
        let lineBrokens = self.columns.map { c -> [String] in
            return cells.first(where: {$0.key == c.key})!.lineBrokenValue(by: c.width ?? c.key.count)
        }
        let cellHeight = lineBrokens.max(by: {return $0.count < $1.count})!.count
        let filledToHeight = lineBrokens.map {$0 + [String](repeating: "", count: cellHeight - $0.count)}
        return filledToHeight.transpose().map {"| " + $0.joined(separator: " | ") + " |"}
    }
}
protocol JSONSerializable: Codable {
    init(json: Data)
    var jsonData: Data { get }
}
extension JSONSerializable {
    init(json: Data) {
        self = try! JSONDecoder().decode(Self.self, from: json)
    }
    var jsonData: Data {
        return try! JSONEncoder().encode(self)
    }
}
fileprivate extension String {
    func split(by count: Int) -> [String] {
        let arrayValue = Array(self)
        if arrayValue.isEmpty { return [] }
        var parent: [[Element]] = []
        for i in 1 ... arrayValue.count {
            if (i % count == 0) {
                parent.append(arrayValue[(i - count)...(i - 1)].map{$0})
            }
        }
        if arrayValue.count % count != 0 { parent.append(arrayValue.suffix(arrayValue.count % count)) }
        return parent.map({String($0)})
    }
    func filled(to count: Int, with pad: String = " ", isleftJustified: Bool = false) -> String {
        let main = String(self.prefix(count))
        let pads = String(repeating: pad, count: count - main.count)
        return isleftJustified ? (main + pads) : (pads + main)
    }
}
fileprivate extension Array {
    subscript (at index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}
extension Array where Element: RandomAccessCollection, Element.Index == Int {
    func transpose() -> [[Element.Element]] {
        return self.isEmpty ? [] : (0...(self.first!.endIndex - 1)).map { i -> [Element.Element] in self.map { $0[i] } }
    }
}
```

# さいごに
実用性はないと書きましたが、実は色んなコーディング要素が詰まっているので何かの題材に使えそうかも?!
せっかくだから他の機能も増やしてコマンドラインツールでも作ってみようかしら。 ちょうどテーブル表示なのでDB初期化用の ```init.sql``` と ```.json``` ファイルの相互変換とかが良いでしょうか。

...
... ...

はぁ、平日の夜中に何やってんだ俺。。。もう無理、マヂ寝ョ