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?

Go言語でローマ数字パーサーを作る

Last updated at Posted at 2024-12-15

ローマ数字ってなんやねん

ローマ数字(ローマすうじ)は、数を表す記号の一種である。ラテン文字の一部を用い、例えばアラビア数字における 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 をそれぞれ I, II, III, IV, V, VI, VII, VIII, IX, X のように表記する。I, V, X, L, C, D, Mはそれぞれ 1, 5, 10, 50, 100, 500, 1000 を表す。i, v, x などと小文字で書くこともある。現代の一般的な表記法では、1以上4000未満の数を表すことができる。

というやつです。時計の文字でたまに見るアイツらです

きっかけ

ローマ数字をパースしたかった...訳ではないです
アドカレなんか書きたいなーと思っていた時に大学の講義でローマ数字の話が出まして、
パーサー書いたら面白そうだしアドカレのネタにもできそうだなーと思ったのが理由です

この記事ではアラビア数字->ローマ数字の部分のみを扱います
(逆は作成中です。完成したら記事にするかもしれません)

インターフェイス

Go言語は静的型付け言語です
静的型付け言語は、同じ計算をする関数を型ごとに用意する必要があるという問題を持っていますが、Go言語ではこれをインターフェイスという仕組みで解決しています。そしてこの仕組みに乗っかることでGo言語っぽいいい感じのコードが書ける...はずです

具体的には、インターフェイスは必要なメソッドのリストとして定義しておき、ある型がそのメソッドすべてを持っていれば自動的にその「インターフェイスを実装した」ことになり、その「インターフェイスを引数に取る関数」にその型を渡すことができるようになります

今回作るパーサーでは、MS Copilot君が教えてくれたStringerインターフェースを使うことにします
Stringerインターフェイスはこういうやつです↓

type Stringer interface {
  String() string
}

Stringメソッドがあればいいんですね

レシーバー

type Roman uint16

func (r Roman) String() string {
  //☆
}

Roman型をuint16の別名として定義し、Roman型にStringメソッドを定義しています
String()の左側の(r Roman)は(値)レシーバーと呼ばれ、rom.String()として呼んだときのromのデータがコピーされこのrに代入されます。
(ポインタレシーバーというのもありますが省略)

Stringメソッド

☆の部分はこのようになっています

s := strconv.Itoa(int(r))
ord := len(s)
buf := bytes.NewBuffer(make([]byte, 0, 15))
for i, c := range s {
	buf.WriteString(getDigit(c-'0', ord-i))
}
return buf.String()

for文まで

  1. アラビア数字表記の文字列をsに代入
    • たとえば1234という数値が"1234"という文字列になります
  2. 桁数を取得
    • 文字のバイト数(=長さ)が桁数と等しいことを利用します
  3. 長さ0、容量15のバイト配列からbytes.Bufferを生成
    • 変換されたローマ数字を書き込むためのバッファを用意しています

for文からreturnまで

  1. インデックスと文字を取得
    • rangeはスライスからインデックスと対応する要素をループ毎に一つずつ渡してくれます
  2. バッファに高い桁から順に書き込んでいく
    • getDigitは数値と桁の位をローマ数字に変換する関数です
    • c-'0'とすることで'1'から1に変換しています。(runeの引き算)
    • ord-iで何位の桁とするかを計算できます
  3. バッファの中身を文字列にして返り値とする
    • 中のバイトスライスの長さ(容量ではない)分が文字列に変換されます
      make([]byte, 0, 15)でLenとCap両方を指定しているのはこれが理由です

getDigit

中身はこうなっています

var iv_digit =  [...]string{"", "M", "MM", "MMM"}
var iii_digit = [...]string{"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"}
var ii_digit =  [...]string{"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"}
var i_digit =   [...]string{"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"}

func getDigit(c rune, d int) string {
   switch d {
   case 4:
   	return iv_digit[c]
   case 3:
   	return iii_digit[c]
   case 2:
   	return ii_digit[c]
   case 1:
   	return i_digit[c]
   default:
   	panic("Unexpected error. the number exceeds 10000") //or 4000
   }
}

書いていた時はこれが最善だろうと思っていたのですが、
改めて見ると面白くないですね、これ

桁ごとに0-9と文字を対応つけるために配列を4つ用意します。(千の位は0-3)
switchで位ごとにケースを分けて配列から数値cに対応する文字列を返しています

どれにもあたらないのは想定しません。panicで落ちるようにします

NewRoman関数

フォントの話ではないです。
ここまででローマ数字への変換はできるようになったので最後にRoman型を作る関数を作ります

func NewRoman(n uint16) (Roman, error) {
	if n == 0 {
		return 1, errors.New("0 cannot be presented in roman number")
	}
	if n >= 4000 {
		return 1, errors.New("TOO Large Number. n must be <4000")
	}
	return Roman(n), nil
}

といってもこれだけです
ローマ数字は普通に書くと4000未満1以上の整数しか表せないのでこれを表現するようにします
エラーメッセージはよく考えずに書いてます

完成

全体を載せておきます(折りたたみ)
type Roman uint16

func NewRoman(n uint16) (Roman, error) {
	if n == 0 {
		return 1, errors.New("0 cannot be presented in roman number")
	}
	if n >= 4000 {
		return 1, errors.New("TOO Large Number. n must be <4000")
	}
	return Roman(n), nil
}

var iv_digit = [...]string{"", "M", "MM", "MMM"}
var iii_digit = [...]string{"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"}
var ii_digit = [...]string{"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"}
var i_digit = [...]string{"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"}

func getDigit(c rune, d int) string {
	switch d {
	case 4:
		return iv_digit[c]
	case 3:
		return iii_digit[c]
	case 2:
		return ii_digit[c]
	case 1:
		return i_digit[c]
	default:
		panic("Unexpected error. the number exceeds 10000") //or 4000
	}
}

func (r Roman) String() string {
	s := strconv.Itoa(int(r))
	ord := len(s)
	buf := bytes.NewBuffer(make([]byte, 0, 15))
	for i, c := range s {
		buf.WriteString(getDigit(c-'0', ord-i))
	}
	return buf.String()
}
これでこういうことができます
one, _ := NewRoman(1)
two, _ := NewRoman(2)
three, _ := NewRoman(3)
fmt.Print(one, two, three)// --> I II III
fmt.Print(one + two, two * three)// --> III VI
fmt.Print(two * three * three * three)// --> LIV

課題

書いていて気づいた課題点を挙げたいと思います

  • Roman(n)で範囲制限を回避できる
  • 範囲を超過したままStringを呼び出すとpanicで落ちる
  • 足し算、掛け算、引き算などでも範囲を超えられる

panicで落ちるのが大きな問題ですね。StringgetDigitでしっかりチェックして表現できない数字が来た時の対処を決めるべきでしょう

Roman型はuint16の別名として定義したのでuint16と同じように四則演算ができます。便利ではありますが、負の数や3999を超えると表現できないという制約を持ち込めないのでこれも問題になります

終わりに

何かについて記事を書いてネットに公開するというのは今回が初めてでした。
めちゃくちゃ読みづらかったと思いますし、コードも綺麗ではなかったと思いますが、初挑戦ということで許してください!!!

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?