超初心者が	
ローマ数字を	
	いろいろパースしてみる	
よねざわいずみ	
(プロプログラマー連続20年、通算26年なのに超初心者)	
h&ps://github.com/yonezawaizumi	
h&p://qiita.com/yonezawaizumi
•  こういうやつ	
•  ラテン文字の一部のみで表現	
•  ヤギやヒツジの数え上げに利用	
•  確定したのは18世紀、割と最近	
•  CP932やMacJapaneseでは	
機種依存文字として	
I(1)〜XII(12)までが	
「1文字」として使えた	
•  パソコン通信や初期のeメールで	
この文字を使うと	
罵倒対象になった(笑)	
ローマ数字とは
ローマ数字の定義	
•  上の7文字を並べてその総和が表したい数	
•  基本、大きい数を表す文字から先に並べる	
•  V/L/Dは1文字まで、他は3文字まで	
•  4とか9が表せないが、「1つ大きいV/L/Dの	
手前に1文字、I/X/Cを書くことで表現(減算則)	
•  0は表現不能(ヒツジの数え上げですからね…)	
•  最大3999(ヒツジそんなに数えませんからね…)	
	I	 V	 X	 L	 C	 D	 M	
1	 5	 10	 50	 100	 500	 1000
ローマ数字を算用数字に変換したい	
•  エビデンスはM$	ExcelのROMAN関数(笑)	
•  一見して単純なルール	
•  1文字1要素なので字句解析不要に見える	
•  が、実は減算則があるので、字句解析	
したほうがベターかな?	
•  まずは状態遷移図書くかな…
ローマ数字の状態遷移図	
h&ps://github.com/hideshi/pyagram を利用
だめだこりゃ…
ということで机上で整理	
•  実はこの方法、算用数字と何ら変わりがない	
–  例:	MCMXLVIII	(1948)	
•  M	==	1000	
CM	==	900	
XL	==	40	
VIII	==	8	
•  左からの読み出し時に算用数字=10進法の	
桁区切りができればよい	
•  特殊ケースの減算則は必ず2文字	
•  1文字先読みでべた書きできるはず!
Pythonでベタベタ実装(1)	
•  roman1.py	
– 何をやってるかさっぱりわからんコード	
– 1文字先読みを桁ごとに繰り返してみた	
– list(実質stack)で左読みを実現するreverse()	
•  Pythonのlistにはappend()/pop()しかない	
•  list(str)で文字リストに変換し、reverse()で逆にすると	
先頭からの読み書きのように実装できる	
– パースロジック245行
あまりにひどいべた書き	
(百の位のみでもこの量)	
def eat_hundred(chrs):
if len(chrs) == 0:
print('empty')
return -1
c = chrs.pop()
if c == 'C':
if len(chrs) == 0:
return 100
c2 = chrs.pop()
if c2 == 'M':
return 900
elif c2 == 'D':
return 400
elif c2 == 'C':
if len(chrs) == 0:
return 200
c3 = chrs.pop()
if c3 != 'C':
chrs.append(c3)
return 200
if len(chrs) == 0:
return 300
c4 = chrs.pop()
if c4 != 'C':
chrs.append(c4)
return 300
print('too many C')
return -1
else:
chrs.append(c2)
return 100
elif c == 'D':
if len(chrs) == 0:
return 500
c2 = chrs.pop()
if c2 == 'C':
if len(chrs) == 0:
return 600
c3 = chrs.pop()
if c3 != 'C':
chrs.append(c3)
return 600
if len(chrs) == 0:
return 700
c4 = chrs.pop()
if c4 != 'C':
chrs.append(c4)
return 700
if len(chrs) == 0:
return 800
c5 = chrs.pop()
if c5 != 'C':
chrs.append(c5)
return 800
print('too many C')
return -1
else:
chrs.append(c2)
return 500
else:
chrs.append(c)
return 0
何をやっているのか	
•  先頭から1文字読む	
•  C(100)なら次を読む	
–  M(1000)なら900とみなして終了(→十の位解析へ)	
–  D(500)なら400とみなして終了	
–  C(100)なら3文字連続まで読んでみて個数×100	
–  それ以外なら100とみなして終了	
•  D(500)なら	
–  Cの3文字連続まで読んでみて個数×100+500	
•  それ以外なら終了
Pythonでベタベタ実装(2)	
•  roman2.py	
– よく考えると、各桁の規則は同じ	
(千の位で更に強い制約があるが問題ない)	
– それらと、繰り返しのロジックをまとめてみた	
– パースロジック82行
3文字読む関数と1桁読む関数	
def eat_repeat(chrs, c, max):
count = 0
while len(chrs) > 0:
cc = chrs.pop()
if cc != c:
chrs.append(cc)
break
count += 1
if count > max:
return -1
return count
def eat_digit(chrs, one, five, ten,
mul):
if len(chrs) == 0:
print('empty')
return -1
c = chrs.pop()
if c == one:
v = eat_repeat(chrs, one, 2)
if v > 0:
return mul * (v + 1)
elif v < 0:
return v
if len(chrs) == 0:
return mul
c2 = chrs.pop()
if c2 == ten:
return mul * 9
elif c2 == five:
return mul * 4
else:
chrs.append(c2)
return mul
elif c == five:
v = eat_repeat(chrs, one, 3)
if v >= 0:
return mul * (v + 5)
else:
return v
else:
chrs.append(c)
return 0
Scalaでベタベタ実装	
•  Roman1.scala	
– Pythonだとスタック操作で先読みを実現していた	
– ので、Scalaでimmutableに実装してみた
結果クラスと連続文字パース	
sealed case class ParseResult(chrs: Seq[Char], num: Int) {
def +(add: Int) = ParseResult(chrs, num + add)
}
private def parseRepeated(chrs: Seq[Char], chr: Char, mul: Int):
Option[ParseResult] = {
val next = chrs.indexWhere(_ != chr)
val drop = if (next >= 0) next else chrs.length
if (drop <= 3) {
Some(ParseResult(chrs.drop(drop), drop * mul))
} else {
None
}
}
各桁のパース	
private def parseThousand(chrs: Seq[Char]): Option[ParseResult] = parseRepeated(chrs,
'M', 1000)
private def parseDigit(chrs: Seq[Char], chr1: Char, chr5: Char, chr10: Char, mul:
Int): Option[ParseResult] =
chrs.headOption match {
case Some(c) if c == chr1 =>
chrs.tail.headOption match {
case Some(cc) if cc == chr10 =>
Some(ParseResult(chrs.drop(2), mul * 9))
case Some(cc) if cc == chr5 =>
Some(ParseResult(chrs.drop(2), mul * 4))
case Some(cc) if cc == chr1 =>
parseRepeated(chrs, chr1, mul)
case _ =>
Some(ParseResult(chrs.tail, mul))
}
case Some(c) if c == chr5 =>
parseRepeated(chrs.tail, chr1, mul).map(_ + mul * 5)
case _ =>
Some(ParseResult(chrs, 0))
統合	
override def parseRoman(str: String): Int = (for {
t <- parseThousand(str)
h <- parseDigit(t.chrs, 'C', 'D', 'M', 100)
e <- parseDigit(h.chrs, 'X', 'L', 'C', 10)
o <- parseDigit(e.chrs, 'I', 'V', 'X', 1)
} yield {
val value = if (o.chrs.isEmpty) t.num + h.num + e.num + o.num
else -1
if (value > 0) value else -1
}).getOrElse(-1)
とここまでやって考えた…	
•  これ、明らかに、文脈自由文法じゃなくて	
正規表現の範囲じゃね?	
•  だってツリー構造じゃないし	
– 実はor条件はツリーなんだけど孫枝はない	
•  桁ごとに独立しててしかも法則は共通だし	
	
•  (しばし考える…)ピコーン(	゚∀゚)o彡°
ローマ数字の(ほぼ)正規表現	
^(M{0,3})

(CM|DC{0,3}|CD|C{0,3})

(XC|LX{0,3}|XL|X{0,3})

(IX|VI{0,3}|IV|I{0,3})$
※ただし、空文字列は除く
ていうか、	
20年やってんなら	
初めから気づけよ	
m(__)m
というわけでPythonで正規表現	
•  桁ごとにグループ化してキャプチャー	
•  グループ内はたかだか4文字なのでベタ判定	
•  それでもパースロジック50行で済んだ
千の位と百の位のパース	
r = re.compile("""^(M{0,3})(CM|DC{0,3}|CD|C{0,3})

(XC|LX{0,3}|XL|X{0,3})(IX|VI{0,3}|IV|I{0,3})$""")
def parse_thousand(str):
return len(str) * 1000
def parse_hundred(str):
if not str:
return 0
elif str == 'CM':
return 900
elif str == 'CD':
return 400
elif str[0] == 'D':
return 500 + (len(str) - 1) * 100
else:
return len(str) * 100
数値に変換=足し算	
def parse(str):
m = r.match(str)
if m is None:
return -1
else:
return parse_thousand(m.group(1)) + 
parse_hundred(m.group(2)) + 
parse_ten(m.group(3)) + 
parse_one(m.group(4))
Scalaのパーサーコンビネーター	
•  というわけで真打ち登場	
– なんやこの出来レース感…	
•  正規表現でパースできるものを内容に	
応じてさらに変換する、というのにピッタリ!	
•  正規表現の記載と、それを数値に変換する	
部分が一致しているため、読みやすい	
•  速度は出ません…たぶん…
パーサーコンビネーターで	
ローマ数字をパース(1)	
•  Roman2.scala	
– すべての文法をベタ書きして整数に変換	
– 桁どうしの干渉がなく、各桁の結果を足すだけ	
– 空文字列はデフォルト値、すなわち整数なら0	
– べた書きなので、各パーサーはlazy	val	
– バックトラックがないので、パーサー連結は	~!
RegexParsersでべた書き	
 lazy val hundred9: Parser[Int] = "CM" ^^ (_ => 900)
lazy val hundred5: Parser[Int] = "DC{0,3}".r ^^ (_.length() * 100 -
100 + 500)
lazy val hundred4: Parser[Int] = "CD" ^^ (_ => 400)
lazy val hundred1: Parser[Int] = "C{0,3}".r ^^ (_.length * 100)
lazy val hundred: Parser[Int] = hundred9 | hundred5 | hundred4 |
hundred1
  lazy val romanNumerals: Parser[Int] = thousand ~! hundred ~! ten ~!
one ^^ {

  case t ~ h ~ e ~ o => t + h + e + o

  }

 def parseRoman(str:String): Int = parseAll(romanNumerals, str) match
{
case Success(num, next) => if (num > 0) num else -1
case NoSuccess(errorMessage, next) => -1
}
パーサーコンビネーターで	
ローマ数字をパース(2)	
•  Roman3.scala	
– 例によって千の位以外の共通ロジックをまとめた	
– 共通ロジックが各位用のパーサーを生成する
RegexParsersでパーサー生成(1)	
    private def createParser(chr1: Char, chro5: Option[Char],
chro10: Option[Char], mul: Int): Parser[Int] = {
val p1 = (chr1.toString + "{0,3}").r ^^ (_.length * mul)
(for {
chr5 <- chro5
chr10 <- chro10
} yield {
(chr1.toString + chr10) ^^ (_ => 9 * mul) |
(chr5.toString + chr1 + "{0,3}").r ^^
(s => (s.length() + 4) * mul) |
(chr1.toString + chr5) ^^ (_ => 4 * mul) |
p1
}).getOrElse(p1)
}
RegexParsersでパーサー生成(2)	
lazy val romanNumerals: Parser[Int] =
createParser('M', None, None, 1000) ~!
createParser('C', Some('D'), Some('M'), 100) ~!
createParser('X', Some('L'), Some('C'), 10) ~!
createParser('I', Some('V'), Some('X'), 1) ^^ {
case t ~ h ~ e ~ o => t + h + e + o
}
def parseRoman(str:String): Int = parseAll(romanNumerals, str)
match {
case Success(num, next) => if (num > 0) num else -1
case NoSuccess(errorMessage, next) => -1
}
やってみての感想	
•  はじめはyacc使おうとか壮大な計画だった…	
•  しかし計画を精査している段階で、これ正規表現で	
行けちゃうんじゃないかと気づく((((;゚Д゚))))	
•  しかし他のネタを思い浮かばず、強行m(__)m	
•  久々にべた書きのパースをやってみて懐かしさを	
覚えたが、こんな小技には現代的意味はないよね…	
•  実務でパーサーコンビネーターが役立つ場面が	
どれだけあるのか疑問だが、こういうのが用意されて	
いるのがさすが関数型言語(初心者まるだしの雑感)	
•  immutableにしなければ、という意識は業務でScala	
いじって2年目でようやく脳に貼り付いてきた
ソースコードはGitHubにあります	
h&ps://github.com/yonezawaizumi/procon20

超初心者がローマ数字をいろいろパースしてみる