CodeIQ:バスの料金を計算しよう(初級編)

私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「バスの料金を計算しよう(初級編)」(CodeIQ)を参照してください。

問題の概要

運賃は次のように定義されます。
幼児 子供 大人
記号 I C A
料金 後述 大人の半額
(10円未満切り上げ)
標準料金
幼児については同行の大人1人につき、2名まで無料。
無料にならなかった幼児は子供と同額。

【入力と出力】
入力は「210:A,C,I,I」、「210,220,230,210:A,A,C,I」のような形式で与えられます。
コロンの前が標準料金のリスト、コロンの後が同乗者の年齢区分のリストです。
同乗者のバス代の合計を出力してください。

私のプログラム

Pythonで解答しています。

#!/usr/local/bin/python3
import fileinput
import math

# 子供料金を計算する
def CalcChildPrice(lst):
	ret = []
	for i in lst:
		n = 10 * math.ceil((i // 10) / 2)
		ret.append(n)

	return ret

# 入力文字列をパースする
def parseLine(str):
	ret = []
	price, passenger = str.split(":")
	ret.append(passenger.split(","))
	ret.append(list(map(int, price.split(","))))
	ret.append(CalcChildPrice(ret[1]))
	return ret

# 乗客の大人、子供、幼児の数を数える
def countPassenger(lst):
	p = {"A":0, "C":0, "I":0}
	for k,v in p.items():
		p[k] = len(list(filter(lambda a:a==k, lst)))
	return p

# 金額計算
# p 乗客を数えたもの
# ap 大人の金額
# cp 子供の金額
def calcTotal(p, ap, cp):
	price_dic = {
		"A": sum(ap),
		"C": sum(cp),
		"I": sum(cp),
	}

	cnt_i =  p["I"] - (2*p["A"])
	if cnt_i < 0:
		cnt_i = 0

	target = {
		"A": p["A"],
		"C": p["C"],
		"I": cnt_i,
	}

	ret = 0
	for k, v in target.items():
		ret += v * price_dic[k]

	return ret

#==============================================================================
# main
#------------------------------------------------------------------------------

for line in fileinput.input():
	if not line.strip():
		continue

	PassengerList, PliceListA, PliceListC = parseLine(line.strip())
	PassengerCount = countPassenger(PassengerList)
	total = calcTotal(PassengerCount, PliceListA, PliceListC)
	print(total)

解説

ポイントは幼児の扱いと10円未満での切り上げ処理でしょうか。そこさえクリアすれば困るとことはありません。

入力をパースする

parseLine()ですが入力書式が複雑なのでパース処理も複雑です。
この関数は入力値を「乗員リスト」、「大人料金リスト」、「子供料金リスト」を返します。
乗員リストは入力値を「:」で分割した後半部分を「,」で分割したものです。
大人料金リストは入力値を「:」で分割した前半部分を「,」で分割し、数値に変換したもの。
子供料金リストは大人料金リストを半額にし10円未満を切り上げたものです。

子供料金の計算

CalcChildPrice()で計算しています。引数は大人料金のリストなのでループでリストの要素ごとに対応する子供料金を計算します。
ポイントは10円未満切り上げの処理で、次のような手順で行っています。
  1. 大人料金を1/10(小数点以下切り捨て)する
  2. 1の結果を1/2する(浮動小数点)
  3. math#ceil()で小数点以下を切り上げる
  4. 3を10倍する
210円なら次のようになります。
210 -> 21(1/10して切り捨て) -> 10.5(1/2) -> 11(小数点以下切り上げ) -> 110(10倍)

大体の言語では小数点を基準に切り上げ、切り捨てをする関数は用意されているのでそれを使えるようにするのがポイントです。

countPassenger()

乗員のリスト(文字のリスト)を大人X人、子供Y人、幼児X人というディクリョナリに変換します。

料金計算

calcTotal()で最終的な計算をします。

35〜37行目は大人、子供、幼児の料金を計算します。ループで単純に処理できるように幼児料金は子供料金と同じですがセットします。

41〜43行目で無料になる幼児を人数から引いています。最低値が0なのでif文ではその処理をします。 45〜49の処理で最終的に計算する人数を処理対象のディクショナリに設定しています。引数pのp["I"]を書き換えても同じですが引数を破壊的に扱いたくないので別変数に設定しています。

ここまでの処理でA、C、Iそれぞれの料金、計算対象の人数が決まったので最後に値段×人数をA、C、Iごとに計算して合計して終わりです。

雑感

こういうタイプの問題を見るとほっとします。
私がプログラムを書く上で気をつけていることの一つに、なるべくifやswitchを使わなくて良いようにするというのがあります。条件分岐があるとそこでコードを2つの流れに分けて考えなければならなくなるので可読性が悪くなるからです。
少々メモリ効率が悪くても処理が単純になるようにした方が良いというのが私の持論です(私が会社に入って時ですでにそれほどメモリが厳しいということはなかったので、今となってはさらにそうでしょう)。