OuDia形式ファイルのパース

# OuDia形式とは

鉄道やバスの時刻表を記述するファイルフォーマット。主にOuDia系のソフトウェアで使用されている。

# OuDia形式の構文

OuDia形式のファイルは文字列で記述されているので、テキストエディタ等で開くことができる。
OuDia形式のファイルは以下の特徴を持つ。

 - ドット`.`から次のドット`.`までを1階層とした、階層構造になっている。
 - ドット`.`による階層構造のほかに、`key=value`形式でプロパティが定義できる。
 - プロパティ(`=`の左側)は複数回登場することがある。

<details><summary>構文の一例</summary><div>

```OuDia:oudiaの構文例
FileType=OuDia.1.02
Rosen.
Rosenmei=〇〇線
Eki.
Ekimei=A
Ekijikokukeisiki=Jikokukeisiki_NoboriChaku
Ekikibo=Ekikibo_Ippan
.
Eki.
Ekimei=B
Ekijikokukeisiki=Jikokukeisiki_Hatsu
Ekikibo=Ekikibo_Ippan
.
Ressyasyubetsu.
Syubetsumei=普通
JikokuhyouMojiColor=00000000
.
Dia.
DiaName=diaName
Kudari.
Ressya.
Houkou=Kudari
Syubetsu=0
EkiJikoku=1,1;001,1;010/
.
.
Nobori.
Ressya.
Houkou=Nobori
Syubetsu=0
EkiJikoku=1;001/,2,1;300/300
.
.
.
.
DispProp.
JikokuhyouFont=PointTextHeight=9;Facename=ＭＳ ゴシック
JikokuhyouFont=PointTextHeight=9;Facename=ＭＳ ゴシック;Bold=1
JikokuhyouFont=PointTextHeight=9;Facename=ＭＳ ゴシック;Itaric=1
JikokuhyouFont=PointTextHeight=9;Facename=ＭＳ ゴシック;Bold=1;Itaric=1
.
FileTypeAppComment=OuDia Ver. 1.02.05
```

これだけだと分かりにくいので、インデントを付けて見やすくしてみる。


```OuDia:oudiaの構文例
FileType=OuDia.1.02
Rosen.
 ├ Rosenmei=〇〇線
 ├ Eki.
 │  ├ Ekimei=A
 │  ├ Ekijikokukeisiki=Jikokukeisiki_NoboriChaku
 │  ├ Ekikibo=Ekikibo_Ippan
 ├ .
 ├ Eki.
 │  ├ Ekimei=B
 │  ├ Ekijikokukeisiki=Jikokukeisiki_Hatsu
 │  ├ Ekikibo=Ekikibo_Ippan
 ├ .
 ├ Ressyasyubetsu.
 │  ├ Syubetsumei=普通
 │  ├ JikokuhyouMojiColor=00000000
 ├ .
 ├ Dia.
 │  ├ DiaName=diaName
 │  ├ Kudari.
 │  │  ├ Ressya.
 │  │  │  ├ Houkou=Kudari
 │  │  │  ├ Syubetsu=0
 │  │  │  ├ EkiJikoku=1,1;001,1;010/
 │  │  ├ .
 │  ├ .
 │  ├ Nobori.
 │  │  ├ Ressya.
 │  │  │  ├ Houkou=Nobori
 │  │  │  ├ Syubetsu=0
 │  │  │  ├ EkiJikoku=1;001/,2,1;300/300
 │  │  ├ .
 │  ├ .
 ├ .
.
DispProp.
 ├ JikokuhyouFont=PointTextHeight=9;Facename=ＭＳ ゴシック
 ├ JikokuhyouFont=PointTextHeight=9;Facename=ＭＳ ゴシック;Bold=1
 ├ JikokuhyouFont=PointTextHeight=9;Facename=ＭＳ ゴシック;Itaric=1
 ├ JikokuhyouFont=PointTextHeight=9;Facename=ＭＳ ゴシック;Bold=1;Itaric=1
.
FileTypeAppComment=OuDia Ver. 1.02.05
```
</div></details>

## OuDia形式のパース/文字列化

OuDia形式のデータはただのテキストなので、そのままだとプログラムで扱いにくい。
JSON.parse/stringifyよろしく、OuDia.parse/stringifyを自作して、ObjectとArrayの組み合わせに変換してみる。

#### 方針
 - 基本はObjectの入れ子
 - 同じプロパティ名が複数回登場する可能性があるので、値は配列で保持する。
 - `key=value`形式のプロパティはkeyの先頭に`$$`を追加して、`.`区切りの階層名と区別する

### 実装
```javascript
const OuDia = {
	parse (text) {
		let current = {}
		for (const lineRaw of text.split(/\r\n|\n/)) {
			const line = lineRaw.trim()
			if (line==='') {
				continue
			} else if (line==='.') {
				const parent = current.parent
				delete current.parent
				current = parent
			} else if (line.endsWith('.')) {
				const type = line.slice(0, -1)
				const newCurrent = {parent: current}
				if (!current[type]) {current[type] = []}
				current[type].push(newCurrent)
				current = newCurrent
			} else if (line.includes('=')) {
				let [key, ...value] = line.split('=')
				key = `$$${key}`
				value = value.join('=')
				if (value.includes('=')) {
					value = Object.fromEntries(value.split(';').map(v=>v.split('=').map(v=>v.trim())))
				}
				if (!current[key]) {current[key] = []}
				current[key].push(value)
			}
		}
		return current
	},
	stringify (obj, isReturnText=true) {
		const isObject = obj => Object.getPrototypeOf(JSON.parse(JSON.stringify(obj)))===Object.prototype
		let result = []
		for (const [key, values] of Object.entries(obj)) {
			for (let value of values) {
				if (value==null) {continue}
				if (key.startsWith('$$')) {
					if (isObject(value)) {
						value = Object.entries(value).map(v=>v.join('=')).join(';')
					}
					result.push(`${key.slice(2)}=${value}`)
				} else {
					result.push(`${key}.`, ...this.stringify(value, false), '.')
				}
			}
		}
		if (isReturnText) {
			return result.join('\n')
		} else {
			return result
		}
	}
}
```


コメント書いてません、すいません。

### 使用例
```javascript
const oudData = OuDia.parse(oudText) //=> {$$FileType: ["OuDiaSecond.1.09"], $$FileTypeAppComment: ["OuDiaSecondV2 Ver. 2.04.04"], DispProp: [{…}], Rosen: [{…}]}
const oudText = OuDia.stringify(oudData) //=> FileType=OuDiaSecond.1.09\nRosen.\nRosenmei=...
console.log(oudData.Rosen[0].$$Rosenmei[0]) //=> 〇〇線
```

値が配列に入っているでので、\$\$Rosenmei[0]のように毎回[0]で取りに行く必要がある。

OuDia.stringify()内でObjectとStringを判別する必要があったので、自作の`isObject`関数を作成した。

```javascript
const isObject = obj => Object.getPrototypeOf(JSON.parse(JSON.stringify(obj)))===Object.prototype
```

自作クラスなどが渡された時の挙動が不安だったので、一旦JSONを経由することで簡易的に型チェックしている。