🍁

Swift: 大抵のObjectをTree表示する

2022/08/03に公開
1

Swiftの場合、DictionaryをJSON形式でPrettyPrintするのはJSONSerializationを使えば簡単です。→オブジェクトをPrettyPrint

ただ、Tree形式で表示するとなると自前実装が必要そうだったので、書いてみました。

import Foundation

struct SwiftTree {
    enum RuledLine {
        static let root       = "."
        static let stem       = "│  "
        static let branch     = "├──"
        static let lastBranch = "└──"
        static let space      = "   "
    }

    enum Parent {
        case none
        case array
        case dictionary
    }

    static func print(_ obj: Any?) {
        let output = SwiftTree.makeTree(obj: obj).joined(separator: "\n")
        Swift.print(output)
    }

    private static func makeTree(obj: Any?, parent: Parent = .none, isLast: Bool = false, prefix: [String] = []) -> [String] {
        var result = [String]()
        if let dict = obj as? [String: Any?] { // Dictionay
            let array: [(key: String, value: Any?)] = dict.sorted { $0.key < $1.key }
            if parent == .none {
                result.append(RuledLine.root)
            } else if parent == .array {
                result.append(contentsOf: makeTree(obj: RuledLine.root,
                                                   parent: .array,
                                                   isLast: isLast,
                                                   prefix: prefix))
            }
            for i in (0 ..< array.count) {
                let isLastValue: Bool = (i == array.count - 1)
                var keyPrefix = prefix
                if parent == .array {
                    keyPrefix.append(isLast ? RuledLine.space : RuledLine.stem)
                }
                result.append(contentsOf: makeTree(obj: array[i].key,
                                                   parent: .dictionary,
                                                   isLast: isLastValue,
                                                   prefix: keyPrefix))
                let valuePrefix = keyPrefix + [isLastValue ? RuledLine.space : RuledLine.stem]
                result.append(contentsOf: makeTree(obj: array[i].value,
                                                   parent: .dictionary,
                                                   isLast: true,
                                                   prefix: valuePrefix))
            }
        } else if let array = obj as? [Any?] { // Array
            if array.isEmpty {
                let isLastValue: Bool = (parent == .dictionary ? true : isLast)
                result.append(contentsOf: makeTree(obj: "empty",
                                                   parent: .array,
                                                   isLast: isLastValue,
                                                   prefix: prefix))
            } else {
                if parent == .none {
                    result.append(RuledLine.root)
                } else if parent == .array {
                    result.append(contentsOf: makeTree(obj: RuledLine.root,
                                                       parent: .array,
                                                       isLast: isLast,
                                                       prefix: prefix))
                }
                for i in (0 ..< array.count) {
                    let isLastValue: Bool = i == (array.count - 1)
                    var valuePrefix = prefix
                    if parent == .array {
                        valuePrefix.append(isLast ? RuledLine.space : RuledLine.stem)
                    }
                    result.append(contentsOf: makeTree(obj: array[i],
                                                       parent: .array,
                                                       isLast: isLastValue,
                                                       prefix: valuePrefix))
                }
            }
        } else { // Element
            let valuePrefix = prefix + [isLast ? RuledLine.lastBranch : RuledLine.branch]
            let line = (valuePrefix + ["\(obj ?? "nil")"]).joined(separator: " ")
            result.append(line)
        }
        return result
    }
}

このSwiftTree.print(_:)を使えば、たとえば以下のような複雑なオブジェクトでもTree形式で表示できます。(もちろんnilでも大丈夫)

複雑なオブジェクトの例
let dict: [String: Any?] = [
    "Drink": [
        "RedBull",
        "Monster",
        "RealGold",
    ],
    "Price": [
        108,
        216,
        300.33
    ],
    "Food": [
        "Sushi": [
            "Maguro",
            "Sake",
            "Ikura",
            "Uni",
        ],
        "Yakiniku": [
            "Harami",
            "Karubi",
            nil
        ],
    ],
    "Piyo": [
        [
            "Meu": 1,
            "Foo": 2,
            "Gao": 3
        ],
        [
            "Jake": 4,
            "Gomi": 5
        ]
    ],
    "Hoge": nil
]
出力結果の例
.
├── Drink
│   ├── RedBull
│   ├── Monster
│   └── RealGold
├── Food
│   ├── Sushi
│   │   ├── Maguro
│   │   ├── Sake
│   │   ├── Ikura
│   │   └── Uni
│   └── Yakiniku
│       ├── Harami
│       ├── Karubi
│       └── nil
├── Hoge
│   └── nil
├── Piyo
│   ├── .
│   │   ├── Foo
│   │   │   └── 2
│   │   ├── Gao
│   │   │   └── 3
│   │   └── Meu
│   │       └── 1
│   └── .
│       ├── Gomi
│       │   └── 5
│       └── Jake
│           └── 4
└── Price
    ├── 108.0
    ├── 216.0
    └── 300.33

Discussion

KyomeKyome

配列の要素が一つしかないときや辞書のキーが一つしかないときに、Treeをさらにコンパクトにまとめたい場合。

private static func makeTree(obj: Any?, parent: Parent = .none, isLast: Bool = false, prefix: [String] = []) -> [String] {
        var result = [String]()
        if let dict = obj as? [String: Any?] { // Dictionay
            let array: [(key: String, value: Any?)] = dict.sorted { $0.key < $1.key }
            if parent == .array, array.count == 1, let first = array.first {
                result.append(contentsOf: makeTree(obj: first.key,
                                                   parent: .array,
                                                   isLast: isLast,
                                                   prefix: prefix))
                let valuePrefix = prefix + [isLast ? RuledLine.space : RuledLine.stem]
                result.append(contentsOf: makeTree(obj: first.value,
                                                   parent: .dictionary,
                                                   isLast: true,
                                                   prefix: valuePrefix))
            } else {
                if parent == .none {
                    result.append(RuledLine.root)
                } else if parent == .array {
                    result.append(contentsOf: makeTree(obj: RuledLine.root,
                                                       parent: .array,
                                                       isLast: isLast,
                                                       prefix: prefix))
                }
                for i in (0 ..< array.count) {
                    let isLastValue: Bool = (i == array.count - 1)
                    var keyPrefix = prefix
                    if parent == .array {
                        keyPrefix.append(isLast ? RuledLine.space : RuledLine.stem)
                    }
                    result.append(contentsOf: makeTree(obj: array[i].key,
                                                       parent: .dictionary,
                                                       isLast: isLastValue,
                                                       prefix: keyPrefix))
                    let valuePrefix = keyPrefix + [isLastValue ? RuledLine.space : RuledLine.stem]
                    result.append(contentsOf: makeTree(obj: array[i].value,
                                                       parent: .dictionary,
                                                       isLast: true,
                                                       prefix: valuePrefix))
                }
            }
        } else if let array = obj as? [Any?] { // Array
            if array.isEmpty {
                let isLastValue: Bool = (parent == .dictionary ? true : isLast)
                result.append(contentsOf: makeTree(obj: "empty",
                                                   parent: .array,
                                                   isLast: isLastValue,
                                                   prefix: prefix))
            } else if parent == .array, array.count == 1, let first = array.first {
                result.append(contentsOf: makeTree(obj: first,
                                                   parent: .array,
                                                   isLast: isLast,
                                                   prefix: prefix))
            } else {
                if parent == .none {
                    result.append(RuledLine.root)
                } else if parent == .array {
                    result.append(contentsOf: makeTree(obj: RuledLine.root,
                                                       parent: .array,
                                                       isLast: isLast,
                                                       prefix: prefix))
                }
                for i in (0 ..< array.count) {
                    let isLastValue: Bool = i == (array.count - 1)
                    var valuePrefix = prefix
                    if parent == .array {
                        valuePrefix.append(isLast ? RuledLine.space : RuledLine.stem)
                    }
                    result.append(contentsOf: makeTree(obj: array[i],
                                                       parent: .array,
                                                       isLast: isLastValue,
                                                       prefix: valuePrefix))
                }
            }
        } else { // Element
            let valuePrefix = prefix + [isLast ? RuledLine.lastBranch : RuledLine.branch]
            let line = (valuePrefix + ["\(obj ?? "nil")"]).joined(separator: " ")
            result.append(line)
        }
        return result
    }