CodeIQ:【ムサンガーくん(ワークスアプリケーションズ)からの挑戦状!】源泉徴収額の計算!

私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「【ムサンガーくん(ワークスアプリケーションズ)からの挑戦状!】源泉徴収額の計算!」(CodeIQ)を参照してください。

問題の概要

問題を引用します。
賞与支給における源泉徴収額の計算

ある会社では、社員に支給する賞与を、社会保険料・源泉所得税を除いた実支給額ベースで算出しています。一方で、社会保険料・源泉所得税の計算には総支給額を用います。

賞与支給における各種計算式は次の通りです。
実支給額 = 総支給額 - 源泉徴収額
源泉徴収額 = 社会保険料 + 源泉所得税
社会保険料 = 総支給額 (1000円未満切り捨て) × 13.7% (1円未満、0.5円以下切り捨て、0.5円超過で切り上げ)
課税対象額 = 総支給額 - 社会保険料
源泉所得税 = 課税対象額 × 税率 (1円未満切り捨て)

税率は課税対象額に応じて、以下の表で定めるものとします。

税率(%)以上(円)未満(円)
0.00-68
2.0426879
4.08479252
6.126252-

※ 問題を簡単にするため、ボーナス支給額としては安くなっています。

実支給額に対応する総支給額が複数存在する場合は、そのうちの最も高い総支給額を支給するものとします。

プログラム仕様
データは標準入力を介して渡されます。
1行毎に1人分の実支給(予定)額(円)が渡されます。
60000
1000
100000

結果は、1行毎に源泉徴収額(円)を出力します。
9453
137
20697

私のプログラム

Rubyで解答しています。

#!/usr/bin/ruby

# 社会保険料の計算
# @param n 総支払金額
# @return 社会保険料
def syakaihoken(n)
	return (n / 1000) * 137
end

def zeiritu(j)
	# 実支払金額では次の値が境界値になる
	if j < 66612 then r = 0.0
	elsif j < 75774 then r = 0.02042
	elsif j < 236563 then r = 0.04084
	else r = 0.06126
	end

	return r
end

# 源泉所得税の計算
# @param n 総支払金額
# @param s 社会保険料
# @return 源泉所得税額
def gensenshotokuzei(n, s)
	m = n - s

	if m < 68000 then r = 0
	elsif m < 79000 then r = 2042
	elsif m < 252000 then r = 4084
	else r = 6126
	end

	return m * r / 100000
end

# 実支払金額から総支払金額より若干大きな値を求め、その値以下でもっとも近く、実支払金額が入力
# 値になる総支払金額を探索する
# @param n 実支払金額
# @return 源泉徴収額
def solve(n)
	mx = (n.to_f / (1-zeiritu(n)) / (1-0.137)).ceil

	mx.downto(n){|i|
		s = syakaihoken(i)
		z = gensenshotokuzei(i, s)

		return (s+z) if i - (s+z) == n
	}
end

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

	p solve(line.to_i)
end

解説

一見方程式を解けば良さそうに見えますがそれではできないようです。

考え方

方程式を解けば良さそうなのですが、上に書いた通りそれではできないようです。
なので適当な金額を探索することになります。ここでネックになるのが「実支給額に対応する総支給額が複数存在する場合は、そのうちの最も高い総支給額を支給するものとします。」です。「もっとも低い額」なら容易ですが、「もっとも高い額」はどこまで探索すれば良いのか判断がつかないのでなかなか難しいです。

とりあえず、私は支給総額が1〜300,000円までの時に実支給額、社会保険料、源泉所得税がいくらになるか調べて見ました。目的は、(1)どういう時に同じ実支給額に対して複数の総支給額が現れるか、(2)実支給額がいくらの時に源泉所得税額が変化するか、を知るためです。

(1)に関しては、
・実支給金額が源泉所得税率の境界値未満で近い場合(これは予測できます)
・1円総支給金額が増えても実支給金額が変わらない場合
の2通りがあることがわかりました。

(2)は総支給金額の境界値と1対1対応する実支給金額の境界値が判明しました。

次に探索方法ですが、答えとなる総支給金額以上でなるべく答えに近い金額を実支給金額から導ければ、仮定した総支給金額から金額を減らしながらチェックしてゆけば答えにたどり着けます。
実支給金額をnとすると、
 総支給金額候補値 = n / {(1-源泉所得税率) * (1-0.137)}(端数切り上げ)
が答えとなる総支給金額以上でなるべく答えに近い金額になります。
(1-0.137)で割っている部分が社会保険料なのですが本来1000円未満切り捨てなのを考慮していないので1000円未満の分だけ必ず大きな額になります。
そして(2)が明らかになったので、実支給金額から源泉所得税率を決定できます。

この考え方で実装しました。

main

入力値を1行ずつ数値に変換し、solve()に渡して結果を印字します。

solve(n)

mxが総支給金額候補値の初期値で、そこから単純に1円ずつ金額を減らしながら実支給額が入力値と一致する値を探し、見つかったらその時の社会保険料と源泉徴収税額の和を返却します。

zeiritu(j)

実支給金額jから源泉徴収税率を求めます。
この値は支給総額が1〜300,000円までの時に実支給額、社会保険料、源泉所得税がいくらになるか調べた時の結果から決定しています(総当たりで調べると源泉所得税額に連続性がなくなる部分があるのでその場所をピックアップすればわかります)。

syakaihoken(n)

総支給金額nから社会保険料を計算します。
問題文には端数の扱いが記述されていますが、1000円未満切り捨てなので端数は出ません。

gensenshotokuzei(n, s)

総支給金額nと社会保険料sから源泉所得税額を計算します。
端数処理のため整数で計算しています。

雑感

源泉所得税率を実支払金額から求める手段が出題者の意図通りではないように思えます。
とはいえK&Rの『プログラミング言語C』にも「あらかじめ計算できるものはしておけ」と書かれていますし、定数なので無駄な計算を減らすためにも有用だと思います。
頭悪く1円単位で探索していますが、候補値の初期値が近いのでプログラムは探索はすぐに終わります。ローカルで1,000,000,000円までテストしましたが0.05秒未満で終わります。