ローマ数字ってなんやねん
ローマ数字(ローマすうじ)は、数を表す記号の一種である。ラテン文字の一部を用い、例えばアラビア数字における 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文まで
- アラビア数字表記の文字列を
s
に代入- たとえば
1234
という数値が"1234"
という文字列になります
- たとえば
- 桁数を取得
- 文字のバイト数(=長さ)が桁数と等しいことを利用します
- 長さ0、容量15のバイト配列から
bytes.Buffer
を生成- 変換されたローマ数字を書き込むためのバッファを用意しています
for文からreturnまで
- インデックスと文字を取得
-
range
はスライスからインデックスと対応する要素をループ毎に一つずつ渡してくれます
-
- バッファに高い桁から順に書き込んでいく
-
getDigit
は数値と桁の位をローマ数字に変換する関数です -
c-'0'
とすることで'1'
から1
に変換しています。(rune
の引き算) -
ord-i
で何位の桁とするかを計算できます
-
- バッファの中身を文字列にして返り値とする
- 中のバイトスライスの長さ(容量ではない)分が文字列に変換されます
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で落ちるのが大きな問題ですね。String
やgetDigit
でしっかりチェックして表現できない数字が来た時の対処を決めるべきでしょう
Roman
型はuint16
の別名として定義したのでuint16
と同じように四則演算ができます。便利ではありますが、負の数や3999を超えると表現できないという制約を持ち込めないのでこれも問題になります
終わりに
何かについて記事を書いてネットに公開するというのは今回が初めてでした。
めちゃくちゃ読みづらかったと思いますし、コードも綺麗ではなかったと思いますが、初挑戦ということで許してください!!!