OuDia形式とは
鉄道やバスの時刻表を記述するファイルフォーマット。主にOuDia系のソフトウェアで使用されている。
OuDia形式の構文
OuDia形式のファイルは文字列で記述されているので、テキストエディタ等で開くことができる。
OuDia形式のファイルは以下の特徴を持つ。
- ドット
.
から次のドット.
までを1階層とした、階層構造になっている。 - ドット
.
による階層構造のほかに、key=value
形式でプロパティが定義できる。 - プロパティ(
=
の左側)は複数回登場することがある。
構文の一例
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=MS ゴシック
JikokuhyouFont=PointTextHeight=9;Facename=MS ゴシック;Bold=1
JikokuhyouFont=PointTextHeight=9;Facename=MS ゴシック;Itaric=1
JikokuhyouFont=PointTextHeight=9;Facename=MS ゴシック;Bold=1;Itaric=1
.
FileTypeAppComment=OuDia Ver. 1.02.05
これだけだと分かりにくいので、インデントを付けて見やすくしてみる。
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=MS ゴシック
├ JikokuhyouFont=PointTextHeight=9;Facename=MS ゴシック;Bold=1
├ JikokuhyouFont=PointTextHeight=9;Facename=MS ゴシック;Itaric=1
├ JikokuhyouFont=PointTextHeight=9;Facename=MS ゴシック;Bold=1;Itaric=1
.
FileTypeAppComment=OuDia Ver. 1.02.05
OuDia形式のパース/文字列化
OuDia形式のデータはただのテキストなので、そのままだとプログラムで扱いにくい。
JSON.parse/stringifyよろしく、OuDia.parse/stringifyを自作して、ObjectとArrayの組み合わせに変換してみる。
方針
- 基本はObjectの入れ子
- 同じプロパティ名が複数回登場する可能性があるので、値は配列で保持する。
-
key=value
形式のプロパティはkeyの先頭に$$
を追加して、.
区切りの階層名と区別する
実装
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
}
}
}
コメント書いてません、すいません。
使用例
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
関数を作成した。
const isObject = obj => Object.getPrototypeOf(JSON.parse(JSON.stringify(obj)))===Object.prototype
自作クラスなどが渡された時の挙動が不安だったので、一旦JSONを経由することで簡易的に型チェックしている。