OuDiaとは
OuDiaは、時刻表のデータからダイヤグラムを描画する、Windowsのフリーソフトウェアです。
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
でコツコツと追加しています。
構造体が複数個にわたって定義されている可能性のあるところ(eki
やressyasyubetsu
、dia
など)は、for
文で余すことなく処理しています。
なお、コードの最後の方に登場するFileTypeAppComment
のところは、そのファイルを作成したアプリの名前を記述するところなので、ここの値は各開発者さんで決めてください。
EkiJikoku
の扱い
メンテナンスのしやすさなども考慮して、EkiJikoku
をパース/文字列化する処理は、大元の処理とは分けて書きました。
EkiJikoku
は、1;800,1;810/815,1;830/
のように、コンマ,
やセミコロン;
区切りの文字列で記述されています。
ただ、やはり文字列のままだとプログラムで扱いづらいので、["1;800", "1;810/815", "1;830/"]
のように配列として扱えるようにします。
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
メソッドをArray
の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駅
構造体が複数個にわたって定義されている可能性のあるところ(dia
やressya
、eki
など)では、dia[0]
のようにインデックスを指定する必要があります。
おわりに
何か改善すべき点などがあれば、コメントいただけると幸いです。
コードの全文を載せたGitHubのリポジトリは以下の通りです。