1
0

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 1 year has passed since last update.

【Swift】OuDiaファイルのパース

Last updated at Posted at 2023-03-03

OuDiaとは

OuDiaは、時刻表のデータからダイヤグラムを描画する、Windowsのフリーソフトウェアです。

OuDiaについて

OuDiaのファイル形式

OuDiaファイルの拡張子は.oudで、中身はShift-JISのテキストになっています。
そのため、メモ帳アプリなどで簡単にファイルの中身を覗くことができます。

ファイルの構造は、具体的に以下のようになっています。

  • ドット(.)で区切られた階層構造となっている。
  • 要素はkey=value方式で記述されている。

簡単な構文の一例については、以下の記事の「OuDia形式の構文」項目内にある「構文の一例」という折りたたみ内に記述があります。
インデントつきで見やすくなっているので確認してみてください。

また、ファイル全体の内容に関する詳しい解説については、大井さかなさんが制作された「CloudDia」のGitHubリポジトリ内にあるOuDiaFileMemo.rtfというリッチテキストファイルに記述があります。
ファイルの構造と、それぞれの要素に入りうる値の例などが詳しく書かれているので、こちらも参考にしてみてください。

OuDiaファイルのパース/文字列化

先で述べたとおり、OuDia形式のファイルはテキストデータです。
しかし、テキストのままではプログラムで扱いづらいです。

そこで、構造体と配列の組み合わせとして扱えるように、ファイルをパース/文字列化するプログラムをSwiftで書きました。

構造体の定義

まずは、データを格納するための構造体についてみていきます。
構造体は、以下のように定義されています。

構造体の定義 (長くなるので折りたたんでおきます。)
構造体の定義
//MARK: - 構造体の定義
struct OudData: Equatable {
    var fileType: String
    var rosen: Rosen
    var dispProp: DispProp
    var fileTypeAppComment: String
}

struct Rosen: Equatable { //インデント数: 1
    var rosenmei: String
    var eki: [Eki]
    var ressyasyubetsu: [Ressyasyubetsu]
    var dia: [Dia]
    var kitenJikoku: String
    var diagramDgrYZahyouKyoriDefault: String
    var comment: String
}

struct DispProp: Equatable { //インデント数: 1
    var jikokuhyouFont: [String]
    var jikokuhyouVFont: String
    var diaEkimeiFont: String
    var diaJikokuFont: String
    var diaRessyaFont: String
    var commentFont: String
    var diaMojiColor: String
    var diaHaikeiColor: String
    var diaRessyaColor: String
    var diaJikuColor: String
    var ekimeiLength: String
    var jikokuhyouRessyaWidth: String
}

struct Eki: Hashable, Equatable { //インデント数: 2
    var ekimei: String
    var ekijikokukeisiki: Ekijikokukeisiki
    var ekikibo: Ekikibo
    var kyoukaisen: String //任意
    var diagramRessyajouhouHyoujiKudari: String //任意
    var diagramRessyajouhouHyoujiNobori: String //任意
}

struct Ressyasyubetsu: Equatable { //インデント数: 2
    var syubetsumei: String
    var ryakusyou: String
    var jikokuhyouMojiColor: String
    var jikokuhyouFontIndex: String
    var diagramSenColor: String
    var diagramSenStyle: DiagramSenStyle
    var diagramSenIsBold: String //任意
    var stopMarkDrawType: String //任意
}

struct Dia: Equatable { //インデント数: 2
    var diaName: String
    var kudari: Kudari
    var nobori: Nobori
}

struct Kudari: Equatable { //インデント数: 3
    var ressya: [Ressya]
}

struct Nobori: Equatable { //インデント数: 3
    var ressya: [Ressya]
}

struct Ressya: Hashable, Equatable { //インデント数: 4
    var houkou: String
    var syubetsu: Int
    var ressyabangou: String //任意
    var ressyamei: String //任意
    var gousuu: String //任意
    var ekiJikoku: [String]
    var bikou: String //任意
}

//MARK: - enum
enum Ekijikokukeisiki: String {
    case hatsu = "Jikokukeisiki_Hatsu"
    case hatsuchaku = "Jikokukeisiki_Hatsuchaku"
    case kudariChaku = "Jikokukeisiki_KudariChaku"
    case noboriChaku = "Jikokukeisiki_NoboriChaku"
}

enum Ekikibo: String {
    case ippan = "Ekikibo_Ippan"
    case syuyou = "Ekikibo_Syuyou"
}

enum DiagramSenStyle: String {
    case jissen = "SenStyle_Jissen"
    case hasen = "SenStyle_Hasen"
    case tensen = "SenStyle_Tensen"
    case ittensasen = "SenStyle_Ittensasen"
}

「インデント数」の分だけ、入れ子が深くなっていっています。最も深いところのインデント数は4です。

なお、「任意」というコメントがついている箇所は、データが入っていなくても特に問題ないです。
(データが入っていない場合、要素の中身は""のように空の文字列になります。)

parse

文字列を構造体に変換する関数です。

parse (長くなるので折りたたんでおきます。)
文字列を構造体に
static func parse(_ text: String) -> OudData {
    enum ProcessState {
        case none
        case kudari
        case nobori
    }

    //このoudDataプロパティに値が代入、追加されていく
    var oudData = OudData(fileType: "",
                          rosen: Rosen(rosenmei: "",
                                       eki: [],
                                       ressyasyubetsu: [],
                                       dia: [],
                                       kitenJikoku: "",
                                       diagramDgrYZahyouKyoriDefault: "",
                                       comment: ""
                                      ),
                          dispProp: DispProp(jikokuhyouFont: [],
                                             jikokuhyouVFont: "",
                                             diaEkimeiFont: "",
                                             diaJikokuFont: "",
                                             diaRessyaFont: "",
                                             commentFont: "",
                                             diaMojiColor: "",
                                             diaHaikeiColor: "",
                                             diaRessyaColor: "",
                                             diaJikuColor: "",
                                             ekimeiLength: "",
                                             jikokuhyouRessyaWidth: ""
                                            ),
                          fileTypeAppComment: ""
    )

    var isRessya = false
    var processingHoukouState: ProcessState = .none //どの構成要素を処理しているかを示す

    for lineRow in text.components(separatedBy: .newlines) { //textを1行づつ処理
        let line: String = lineRow.trimmingCharacters(in: .whitespaces) //行の端にある空白を削除
        if line.isEmpty {
            continue
        } else if line == "." { //行がピリオドの場合
            resetProcessingDiaState()
        } else if line.hasSuffix(".") { //行がピリオドで終わっている場合
            handleScopeEntry(line: line)
        } else if line.contains("=") { // 行にイコールが含まれている場合
            setValueFromKey(line: line)
        }
    }
    return oudData

    func resetProcessingDiaState() {
        if isRessya {
            isRessya = false
        } else {
            processingHoukouState = .none
        }
    }

    func handleScopeEntry(line: String) {
        switch line {
        case "Kudari.":
            processingHoukouState = .kudari //Kudari.の処理中であることを示すBool
        case "Nobori.":
            processingHoukouState = .nobori
        case "Ressya.":
            isRessya = true
            if var diaTarget = oudData.rosen.dia.lastElement {
                if case .kudari = processingHoukouState {
                    //空の要素をひとつ追加
                    diaTarget.kudari.ressya.append( Ressya(houkou: "", syubetsu: 0, ressyabangou: "", ressyamei: "", gousuu: "", ekiJikoku: [], bikou: "") )
                    oudData.rosen.dia.lastElement = diaTarget
                }
                if case .nobori = processingHoukouState {
                    diaTarget.nobori.ressya.append( Ressya(houkou: "", syubetsu: 0, ressyabangou: "", ressyamei: "", gousuu: "", ekiJikoku: [], bikou: "") )
                    oudData.rosen.dia.lastElement = diaTarget
                }
            }
        case "Eki.":
            oudData.rosen.eki.append( Eki(ekimei: "", ekijikokukeisiki: .hatsu, ekikibo: .ippan, kyoukaisen: "", diagramRessyajouhouHyoujiKudari: "", diagramRessyajouhouHyoujiNobori: "") )
        case "Ressyasyubetsu.":
            oudData.rosen.ressyasyubetsu.append( Ressyasyubetsu(syubetsumei: "", ryakusyou: "", jikokuhyouMojiColor: "", jikokuhyouFontIndex: "", diagramSenColor: "", diagramSenStyle: .jissen, diagramSenIsBold: "", stopMarkDrawType: "") )
        case "Dia.":
            oudData.rosen.dia.append( Dia(diaName: "", kudari: Kudari(ressya: []), nobori: Nobori(ressya: [])) )
        default:
            break
        }
        return
    }

    func setValueFromKey(line: String) {
        var keyAndValue: [String] = line.components(separatedBy: "=")
        let key: String = keyAndValue.removeFirst() //イコールの左側
        let value: String = keyAndValue.joined(separator: "=") //イコールの右側
        updateElement()
        return

        func updateElement() {
            if case .kudari = processingHoukouState, var kudariRessyaTarget = oudData.rosen.dia.lastElement?.kudari.ressya.lastElement {
                updateRessya(in: &kudariRessyaTarget, withKey: key, value: value)
                oudData.rosen.dia.lastElement?.kudari.ressya.lastElement = kudariRessyaTarget
            } else if case .nobori = processingHoukouState, var noboriRessyaTarget = oudData.rosen.dia.lastElement?.nobori.ressya.lastElement {
                updateRessya(in: &noboriRessyaTarget, withKey: key, value: value)
                oudData.rosen.dia.lastElement?.nobori.ressya.lastElement = noboriRessyaTarget
            }
            if var ekiTarget = oudData.rosen.eki.lastElement {
                updateEki(in: &ekiTarget, withKey: key, value: value)
                oudData.rosen.eki.lastElement = ekiTarget
            }
            if var ressyasyubetsuTarget = oudData.rosen.ressyasyubetsu.lastElement {
                updateRessyasyubetsu(in: &ressyasyubetsuTarget, withKey: key, value: value)
                oudData.rosen.ressyasyubetsu.lastElement = ressyasyubetsuTarget
            }
            if var diaTarget = oudData.rosen.dia.lastElement {
                updateDia(in: &diaTarget, withKey: key, value: value)
                oudData.rosen.dia.lastElement = diaTarget
            }
            updateRosen(key: key, value: value)
            updateDispProp(key: key, value: value)
            updateOudData(key: key, value: value)
            return

            func updateRessya(in ressya: inout Ressya, withKey key: String, value: String) {
                switch key {
                case "Houkou":
                    ressya.houkou = value
                case "Syubetsu":
                    if let valueInt = Int(value) {
                        ressya.syubetsu = valueInt
                    }
                case "Ressyabangou":
                    ressya.ressyabangou = value
                case "Ressyamei":
                    ressya.ressyamei = value
                case "Gousuu":
                    ressya.gousuu = value
                case "EkiJikoku":
                    ressya.ekiJikoku = EkiJikoku.parse(value) //String -> [String]に変換して代入
                case "Bikou":
                    ressya.bikou = value
                default:
                    break
                }
            }

            func updateEki(in eki: inout Eki, withKey key: String, value: String) {
                switch key {
                case "Ekimei":
                    eki.ekimei = value
                case "Ekijikokukeisiki":
                    switch value {
                    case let jikokukeisiki:
                        eki.ekijikokukeisiki = Ekijikokukeisiki(rawValue: jikokukeisiki) ?? .hatsu
                    }
                case "Ekikibo":
                    switch value {
                    case let kibo:
                        eki.ekikibo = Ekikibo(rawValue: kibo) ?? .ippan
                    }
                case "Kyoukaisen":
                    eki.kyoukaisen = value
                case "DiagramRessyajouhouHyoujiKudari":
                    eki.diagramRessyajouhouHyoujiKudari = value
                case "DiagramRessyajouhouHyoujiNobori":
                    eki.diagramRessyajouhouHyoujiNobori = value
                default:
                    break
                }
            }

            func updateRessyasyubetsu(in ressyasyubetsu: inout Ressyasyubetsu, withKey key: String, value: String) {
                switch key {
                case "Syubetsumei":
                    ressyasyubetsu.syubetsumei = value
                case "Ryakusyou":
                    ressyasyubetsu.ryakusyou = value
                case "JikokuhyouMojiColor":
                    ressyasyubetsu.jikokuhyouMojiColor = value
                case "JikokuhyouFontIndex":
                    ressyasyubetsu.jikokuhyouFontIndex = value
                case "DiagramSenColor":
                    ressyasyubetsu.diagramSenColor = value
                case "DiagramSenStyle":
                    switch value {
                    case let senStyle:
                        ressyasyubetsu.diagramSenStyle = DiagramSenStyle(rawValue: senStyle) ?? .jissen
                    }
                case "DiagramSenIsBold":
                    ressyasyubetsu.diagramSenIsBold = value
                case "StopMarkDrawType":
                    ressyasyubetsu.stopMarkDrawType = value
                default:
                    break
                }
            }

            func updateDia(in dia: inout Dia, withKey key: String, value: String) {
                switch key {
                case "DiaName":
                    dia.diaName = value
                default:
                    break
                }
            }

            func updateRosen(key: String, value: String) {
                switch key {
                case "Rosenmei":
                    oudData.rosen.rosenmei = value
                case "KitenJikoku":
                    oudData.rosen.kitenJikoku = value
                case "DiagramDgrYZahyouKyoriDefault":
                    oudData.rosen.diagramDgrYZahyouKyoriDefault = value
                case "Comment":
                    oudData.rosen.comment = value
                default:
                    break
                }
            }

            func updateDispProp(key: String, value: String) {
                switch key {
                case "JikokuhyouFont":
                    oudData.dispProp.jikokuhyouFont.append(value) //この要素は配列で定義されているのでappend()を用いる
                case "JikokuhyouVFont":
                    oudData.dispProp.jikokuhyouVFont = value
                case "DiaEkimeiFont":
                    oudData.dispProp.diaEkimeiFont = value
                case "DiaJikokuFont":
                    oudData.dispProp.diaJikokuFont = value
                case "DiaRessyaFont":
                    oudData.dispProp.diaRessyaFont = value
                case "CommentFont":
                    oudData.dispProp.commentFont = value
                case "DiaMojiColor":
                    oudData.dispProp.diaMojiColor = value
                case "DiaHaikeiColor":
                    oudData.dispProp.diaHaikeiColor = value
                case "DiaRessyaColor":
                    oudData.dispProp.diaRessyaColor = value
                case "DiaJikuColor":
                    oudData.dispProp.diaJikuColor = value
                case "EkimeiLength":
                    oudData.dispProp.ekimeiLength = value
                case "JikokuhyouRessyaWidth":
                    oudData.dispProp.jikokuhyouRessyaWidth = value
                default:
                    break
                }
            }

            func updateOudData(key: String, value: String) {
                switch key {
                case "FileType":
                    oudData.fileType = value
                case "FileTypeAppComment":
                    oudData.fileTypeAppComment = value //ここは各Appが名付ける要素
                default:
                    break
                }
            }
        }
    }
}

改行を目印に文字列のデータを配列にし、その一つ一つの要素の構造や値を調べています。

stringify

次に、構造体を文字列にする関数です。

stringify (長くなるので折りたたんでおきます。)
構造体を文字列に
static func stringify(_ data: OudData) -> String {
    var result: String = ""
    result.append("FileType=\(data.fileType)\n") //OudDataの情報を順番に追加していく
    stringifyRosen(rosen: data.rosen)
    stringifyDispProp(dispProp: data.dispProp)
    result.append("FileTypeAppComment=" + "Diagram Editor Ver. Alpha 1.0.0") //ここは各Appが名付ける要素
    return result

    func stringifyRosen(rosen: Rosen) {
        result.append("Rosen.\n")
        result.append("Rosenmei=\(rosen.rosenmei)\n")
        stringifyEki(ekiArr: rosen.eki)
        stringifyRessyasyubetsu(ressyasyubetsuArr: rosen.ressyasyubetsu)
        stringifyDia(diaArr: rosen.dia)
        result.append("KitenJikoku=\(rosen.kitenJikoku)\n")
        result.append("DiagramDgrYZahyouKyoriDefault=\(rosen.diagramDgrYZahyouKyoriDefault)\n")
        result.append("Comment=\(rosen.comment)\n")
        result.append(".\n") //Rosen End
        return

        func stringifyEki(ekiArr: [Eki]) {
            for eki in ekiArr {
                result.append("Eki.\n")
                result.append("Ekimei=\(eki.ekimei)\n")
                result.append("Ekijikokukeisiki=\(eki.ekijikokukeisiki.rawValue)\n")
                result.append("Ekikibo=\(eki.ekikibo.rawValue)\n")
                if !eki.kyoukaisen.isEmpty {
                    result.append("Kyoukaisen=\(eki.kyoukaisen)\n")
                }
                if !eki.diagramRessyajouhouHyoujiKudari.isEmpty {
                    result.append("DiagramRessyajouhouHyoujiKudari=\(eki.diagramRessyajouhouHyoujiKudari)\n")
                }
                if !eki.diagramRessyajouhouHyoujiNobori.isEmpty {
                    result.append("DiagramRessyajouhouHyoujiNobori=\(eki.diagramRessyajouhouHyoujiNobori)\n")
                }
                result.append(".\n") //Eki. End
            }
            return
        }

        func stringifyRessyasyubetsu(ressyasyubetsuArr: [Ressyasyubetsu]) {
            for ressyasyubetsu in ressyasyubetsuArr {
                result.append("Ressyasyubetsu.\n")
                result.append("Syubetsumei=\(ressyasyubetsu.syubetsumei)\n")
                result.append("Ryakusyou=\(ressyasyubetsu.ryakusyou)\n")
                result.append("JikokuhyouMojiColor=\(ressyasyubetsu.jikokuhyouMojiColor)\n")
                result.append("JikokuhyouFontIndex=\(ressyasyubetsu.jikokuhyouFontIndex)\n")
                result.append("DiagramSenColor=\(ressyasyubetsu.diagramSenColor)\n")
                result.append("DiagramSenStyle=\(ressyasyubetsu.diagramSenStyle.rawValue)\n")
                if !ressyasyubetsu.diagramSenIsBold.isEmpty {
                    result.append("DiagramSenIsBold=\(ressyasyubetsu.diagramSenIsBold)\n")
                }
                if !ressyasyubetsu.stopMarkDrawType.isEmpty {
                    result.append("StopMarkDrawType=\(ressyasyubetsu.stopMarkDrawType)\n")
                }
                result.append(".\n") //Ressyasyubetsu. End
            }
            return
        }

        func stringifyDia(diaArr: [Dia]) {
            for dia in diaArr {
                result.append("Dia.\n")
                result.append("DiaName=\(dia.diaName)\n")
                result.append("Kudari.\n")
                stringifyRessya(ressyaArr: dia.kudari.ressya)
                result.append(".\n") //Kudari. End
                result.append("Nobori.\n")
                stringifyRessya(ressyaArr: dia.nobori.ressya)
                result.append(".\n") //Nobori. End
                result.append(".\n") //Dia. End
            }
            return

            func stringifyRessya(ressyaArr: [Ressya]) {
                for ressya in ressyaArr {
                    result.append("Ressya.\n")
                    if !ressya.houkou.isEmpty {
                        result.append("Houkou=\(ressya.houkou)\n")
                        result.append("Syubetsu=\(ressya.syubetsu)\n")
                    }
                    if !ressya.ressyabangou.isEmpty {
                        result.append("Ressyabangou=\(ressya.ressyabangou)\n")
                    }
                    if !ressya.ressyamei.isEmpty {
                        result.append("Ressyamei=\(ressya.ressyamei)\n")
                    }
                    if !ressya.gousuu.isEmpty {
                        result.append("Gousuu=\(ressya.gousuu)\n")
                    }
                    if !ressya.ekiJikoku.isEmpty {
                        result.append("EkiJikoku=\( EkiJikoku.stringify(ressya.ekiJikoku) )\n")
                    }
                    if !ressya.bikou.isEmpty {
                        result.append("Bikou=\(ressya.bikou)\n")
                    }
                    result.append(".\n") //Ressya. End
                }
                return
            }
        }
    }

    func stringifyDispProp(dispProp: DispProp) {
        result.append("DispProp.\n")
        for jikokuhyouFont in dispProp.jikokuhyouFont {
            result.append("JikokuhyouFont=\(jikokuhyouFont)\n")
        }
        result.append("JikokuhyouVFont=\(dispProp.jikokuhyouVFont)\n")
        result.append("DiaEkimeiFont=\(dispProp.diaEkimeiFont)\n")
        result.append("DiaJikokuFont=\(dispProp.diaJikokuFont)\n")
        result.append("DiaRessyaFont=\(dispProp.diaRessyaFont)\n")
        result.append("CommentFont=\(dispProp.commentFont)\n")
        result.append("DiaMojiColor=\(dispProp.diaMojiColor)\n")
        result.append("DiaHaikeiColor=\(dispProp.diaHaikeiColor)\n")
        result.append("DiaRessyaColor=\(dispProp.diaRessyaColor)\n")
        result.append("DiaJikuColor=\(dispProp.diaJikuColor)\n")
        result.append("EkimeiLength=\(dispProp.ekimeiLength)\n")
        result.append("JikokuhyouRessyaWidth=\(dispProp.jikokuhyouRessyaWidth)\n")
        result.append(".\n") //DispProp End
        return
    }
}

結果となるString型の文字列resultを用意し、そこに一つ一つの要素をappendでコツコツと追加しています。
構造体が複数個にわたって定義されている可能性のあるところ(ekiressyasyubetsudiaなど)は、for文で余すことなく処理しています。

なお、コードの最後の方に登場するFileTypeAppCommentのところは、そのファイルを作成したアプリの名前を記述するところなので、ここの値は各開発者さんで決めてください。

EkiJikokuの扱い

メンテナンスのしやすさなども考慮して、EkiJikokuをパース/文字列化する処理は、大元の処理とは分けて書きました。

EkiJikokuは、1;800,1;810/815,1;830/のように、コンマ,やセミコロン;区切りの文字列で記述されています。
ただ、やはり文字列のままだとプログラムで扱いづらいので、["1;800", "1;810/815", "1;830/"]のように配列として扱えるようにします。

EkiJikokuのパース/文字列化
class EkiJikoku {
    static func parse(_ text: String) -> [String] {
        return text.components(separatedBy: ",")
    }

    static func stringify(_ jikokuArr: [String]) -> String {
        return jikokuArr.joined(separator: ",")
    }
}

lastElementメソッド

swiftのlastメソッドは、get-onlyのプロパティであるため、それ自体に代入することができません。そのため、array.lastElement = hogeのように書けるように、lastElementメソッドをArrayextensionとして定義しました。

extension
extension Array {
    var lastElement: Element? {
        get {
            return self.last
        }
        set {
            if let newValue = newValue {
                self[self.endIndex - 1] = newValue
            }
        }
    }
}

使用例

let oudData = OuDia.parse(oudText)
// -> OudData(fileType: "OuDia.1.02", rosen: Rosen(rosenmei: "", eki: [Eki(ekimei: "A駅", ekijikokukeisiki: "Jikokukeisiki_NoboriChaku",…(中略)…, fileTypeAppComment: "OuDia Ver. 1.02.05")

let oudText = OuDia.stringify(oudData)
// -> FileType=OuDia.1.02\nRosen.\nRosenmei=\nEki.\nEkimei=A\nEkijikokukeisiki=Jikokukeisiki_NoboriChaku\n…(以下略)

print(oudData.rosen.dia[0].kudari.ressya[0].ekiJikoku)
// -> ["1;800", "1;810/815", "1;830/"]

print(oudData.rosen.eki[0].ekimei)
// -> A駅

構造体が複数個にわたって定義されている可能性のあるところ(diaressyaekiなど)では、dia[0]のようにインデックスを指定する必要があります。

おわりに

何か改善すべき点などがあれば、コメントいただけると幸いです。

コードの全文を載せたGitHubのリポジトリは以下の通りです。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?