2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-05-06

はじめに

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

実行結果

ascii-table.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 ファイルの相互変換とかが良いでしょうか。

...
... ...

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?