CodeIQ:変進小数の足し算

私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「変進小数の足し算」(CodeIQ)を参照してください。

問題の概要

問題を引用します。
【概要】
小数の足し算をして下さい。ただしこの小数は、
小数第 n 位 が 11-n 進数
という不思議なルールになっています。例えば、小数第1位は 10進数、小数第9位は2進数です。
このルールを「変進小数」と呼びます。

【入出力】
入力は
8.622+3.177
こんな感じです。
2個の変進小数が「+」で区切られて並んでいます。

出力は、
11.811 のように、足し算の結果を変進小数で出力して下さい。

【例】
入力出力
8.622+3.17711.811
2.677+1.3114

【具体的な課題】
下記リンク先にdata.txtというファイルがあります。
data.txt
このファイルの各行は
データID, 変進小数の足し算, 変進小数で表された足し算の結果
を TAB区切りで並べたものになっています。
ほとんどのデータは正しい情報になっていますが、 足し算の結果が正しくないデータが数件あります。

それらのデータの IDを
1234,5678,9012
のようにコンマ区切りで登場順に並べ、コメント欄に書いて下さい。

【補足】
変進小数の仕組み上、小数第10位はありません。最大でも小数第9位までです。
整数部は 10 進数です。
入力となる数には、小数部が必ず含まれます。
入力は正の数で、10万を超えることはありません。
出力に小数部が含まれない場合は、小数点を出力しません。
間違っているデータの件数は、5件以上、10件以下です。

私のプログラム

Rubyで解答しています。

#!/usr/bin/ruby

# 変進小数クラス
class DescDecimal
	# int: 整数部分
	# dec: 小数部分の各桁ごとの配列
	# ex: 1.987654321 -> @int=1, @dec=[9.8.7.6.5.4.3.2.1]
	attr_reader :int, :dec

	# コンストラクタ
	# 引数が1つの場合は文字列(ex:1.987654321)でパースして保持する
	# 引数が2つの場合はintとdecがそれぞれ整数、各桁の値の整数の配列で与えられたものとする
	def initialize(*args)
		if args.size == 1 then		# 引数が1つの場合
			v = args[0].split(".")
			@int = v[0].to_i
			if v[1] != nil then
				@dec = v[1].split("").map{|a| a.to_i}
			else
				@dec = []
			end
		elsif args.size == 2 then	# 引数が2つの場合
			@int = args[0]

			# [1,2,0,0,0]のような場合、末尾の0を除く
			while (args[1].size > 0) && (args[1].last == 0)
				args[1].pop
			end
			@dec = args[1]
		else	# ここにはこないはず
			@int = 0
			@dec = []
			puts "Bad args!!"
		end
	end

	# 足し算
	def +(o)
		i = @int + o.int	# 整数部分
		ed = (@dec.size < o.dec.size) ? o.dec.size : @dec.size	# ループ回数を求める(小数点以下の桁数が多い方)

		d = []		# 各桁の計算結果(順番が逆になる)
		over = 0	# 繰り上がる数
		(ed-1).downto(0){|j|
			r = 10 - j	# 現在何進数か

			# 一方のdec[]にしか値がない場合は値のある方だけを適用する
			# 両方のdec[]に値があれば足し算する
			if @dec[j] == nil && o.dec[j] != nil then
				d << o.dec[j]
			elsif @dec[j] != nil && o.dec[j] == nil then
				d << @dec[j]
			elsif @dec[j] == nil && o.dec[j] == nil then	# ありえないはず
				d << 0
			else
				# 10進数として足す
				v = @dec[j] + o.dec[j] + over

				# 各桁の進数の文字列に変換し、桁ごとに分解する
				s = v.to_s(r).split("").map{|a| a.to_i}

				# s[]の要素が2個ある場合はくり上がりが発生している
				if s.size == 1 then
					d << s[0]
					over = 0
				else
					d << s[1]
					over = s[0]
				end
			end
		}

		i = i+over	# 整数部分にくり上がりを適用
		return DescDecimal.new(i, d.reverse)
	end

	# 表示用に文字列表現を返す
	def to_s()
		# 小数部分が0の場合は整数部分だけを表示する
		if @dec.size != 0 then
			s = @int.to_s + "." + @dec.map{|i| i.to_s}.join
		else
			s = @int.to_s
		end

		return s
	end
end

# 入力値を足し算して返す
def calc(line)
	form = line.split("+")
	v1 = DescDecimal.new(form[0])
	v2 = DescDecimal.new(form[1])

	return v1 + v2
end

# main
while line = gets
	line.strip!
	if line.empty? then next end

	r = calc(line)
	puts r.to_s
end

解説

ロジック的に難しい部分はなく、性能を稼ぐために工夫する必要もありません。
頑張ればできるタイプの問題です。
ただし、手動採点だったので気楽に実行してみて間違っていたら直せば良いという気楽さはありません。

考え方

小数点以下の計算をどうするかという問題です。
桁ごとに進数が違うのでどうするかということですが、足し算だけなのでそれほど難しくはありません。
具体例を示します。

8.622+3.177の場合を考えてみます。
まず1番下の桁を普通に10進数で足し合わせます。2+7=9ですが、この桁は8進数なので11になります。
1繰り上がって、次の桁は2+7+1=10となり、9進数なので11になります。
さらに次の桁は6+1+1=8です。ここは10進数なので繰り上がりはありません。
整数部分は繰り上がりがなければそのまま足し合わせればよく、8+3=11になります。
結果、11.811になります。

つまり、下の桁から1桁ずつ10進数として足し合わせて、その桁の進数に変換し、繰り上がりがあったら上の桁でそれを考慮するだけです。

DescDecimalクラス

変進小数を表すクラスです。
足し算を+演算子で表現するためクラス化しました。
メンバ変数としてint(整数部分の値)とdec(小数部分を1桁ずつの配列にしたもの)を持ちます。

DescDecimal#initialize()

コンストラクタは2種類の引数をとります。
一つはDescDecimal.new("12.345")のように文字列表現をとる場合、もう一つはDescDecimal.new(12, [3,4,5])のように整数部分と小数部分の配列をとる場合です。
文字列表現の場合は、整数部分と小数部分に分け、さらに小数部分を桁ごとに数値の配列に変換します。入力値は変進小数として正しい表現が保障されるのでこれだけで十分です。
整数部分と小数部分の配列をとる場合はそれぞれの値をintとdecにとるだけですが、小数部分の末尾の0が連続する部分を除いています。これは結果出力の際に末尾の0は表示しないので最初からその部分を持たないようにするためです。
else節はパラメータがおかしかった場合の処理ですが、デバッグ用です。

DescDecimal#+

変進小数の足し算を実装します。やり方は考え方の通りです。
39行目で整数部分を足していますが、これはもっと後(73行目の繰り上がり処理)でやった方が見通しがよかったかもしれません。
40行目の処理はA+BのAとBを比較し、小数点以下の桁数が長い方の桁数を取得しています。
その桁数だけ降順にループし(44〜71行目)、各桁を計算します。
49〜50行目と51〜52行目はAとBの桁数が違う場合に長い方だけを処理するためのものです。例えば1.1+2.22の場合、小数点以下2桁目は2をそのまま結果とすれば良いのでそのための処理をしています。
53〜54行目は桁チェックがおかしかった場合の処理です。桁を多く数えすぎた場合ですが、0にしておけば問題ないはずです。
55〜69行目がAB共に値がある場合の処理です。
普通に10進数として足し合わせて(57行目)、進数変換します(60行目)。各桁が何進数かは45行目で計算しています。小数点以下の値の配列の要素番号を10から引いた値がその桁の進数になります。進数変換はRubyの場合、文字列表現にするときに基数を指定すると簡単にできるのでそれを利用します。
ついでに文字列になっているので1文字ずつ分解すると要素数が2になったら繰り上がりが発生したことになるので簡単です。繰り上がりがあった場合、変数overにそれをとっておいて次の桁を計算する際に反映します。
73行目の処理は整数部分に繰り上がりを適用するための処理です。
結果として、DescDecimalクラスのインスタンスを返します。

DescDecimal#to_s()

返信小数の文字列表現を返すための関数です。
小数部分が0の場合は整数部分だけを印字しなければならないのでそのための処理を入れています。

calc()

入力値を計算する関数ですが、処理の大半をDescDecimalクラスが行うので、入力値を"+"で分割して足し算するだけです。

雑感

解答のコメントにも書いたのですが、小数部分は10桁固定の配列にして初期値を0で埋めておいた方がよかった気がします。そうすると40行目と53〜54行目の処理が不要になります。代わりに表示時に末尾の0を除くことになりますが、こちらは処理の場所が変わるだけで手間は変わりません。
出題者のコメントには「# ここにはこないはず」、「# ありえないはず」の部分は例外を投げるようにした方が良いとありました。確かにその通りで仕事で書くプログラムでは例外を投げるようにしたと思います(そのような場合、DescDecimalクラスは部品の一つで全体はもっと巨大なプログラムのはずですから)。
あと、提出するコートは1件分の計算を行うものですが、解答として間違っている項目を列挙しなければならないので、提出するコードとは別にdata.txtをチェックするためのプログラムを書く必要があります(1000件もあるので手動ではできません)が、これが結構面倒臭かったです。