Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
1
Help us understand the problem. What is going on with this article?
@Rubydog

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

はじめに

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

実行結果
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 ファイルの相互変換とかが良いでしょうか。

...
... ...

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

1
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Rubydog
ミニマリストなプログラマ

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
1
Help us understand the problem. What is going on with this article?