http://nabetani.sakura.ne.jp/hena/ord9busfare/
「バス代」をErlangでやってみました。
- 大人、子供、幼児の料金を別々に計算して合計しています。
- 幼児は1人ずつ料金を求め、金額が高い方から大人×2人分を削除しています。
- 年齢区分、料金区分はそれぞれ大人料金からの割合として格納し、料金計算の時に掛け合わせています。例: Cw→子供(0.5)×福祉割引(0.5)→大人料金の0.25倍
1関数1行で書いたらどうなるだろうと思ってやってみました。(一応、すべての行が80桁以内になっています。)
いままで再帰を使うときは頭の中で手続き的なループを考えていました。今回は1行で書くため再帰すら使わず、ループが必要なときはmap, filter, foldlなどに関数を渡し、ひたすら大きな関数を小さな関数に分割することでプログラムを作成しました。手続き型だった脳が、少しずつ関数型に慣れてきたように思います。
なお、1関数1行はトップダウンで書くと書きやすかったですが、はたして読みやすいのかどうか?
bus.erl
-module(bus).
-compile(export_all).
solve(Data) -> adult_fee(Data) + child_fee(Data) + infant_fee(Data).
adult_fee(Data) -> lists:sum(fees($A, Data)).
child_fee(Data) -> lists:sum(fees($C, Data)).
infant_fee(Data) -> lists:sum(dropn(num_adult(Data) * 2, rev_sort(fees($I, Data)))).
num_adult(Data) -> length(fees($A, Data)).
fees(Mark, Data) -> [mark_to_fee(X, base_fee(Data)) || X <- only(Mark, Data)].
mark_to_fee(Mark, BaseFee) -> roundup10(marks_to_ratio(Mark) * BaseFee).
roundup10(X) -> trunc(X / 10 + 0.99) * 10.
marks_to_ratio(Marks) -> lists:foldl(fun(X, S) -> mark_to_ratio(X) * S end, 1, Marks).
mark_to_ratio(Mark) -> element(2, find_mark(Mark)).
find_mark(Mark) -> hd(lists:dropwhile(fun({M, _}) -> M =/= Mark end, ratio_data())).
ratio_data() -> [{$A, 1}, {$C, 0.5}, {$I, 0.5}, {$n, 1}, {$p, 0}, {$w, 0.5}].
base_fee(Data) -> list_to_integer(hd(string:tokens(Data, ":"))).
only(Mark, Data) -> lists:filter(fun(X) -> hd(X) =:= Mark end, marks(Data)).
marks(Data) -> string:tokens(lists:nth(2, string:tokens(Data, ":")), ",").
dropn(N, L) -> case length(L) >= N of true -> lists:nthtail(N, L); false -> [] end.
rev_sort(L) -> lists:reverse(lists:sort(L)).
tests() ->
test("210:Cn,In,Iw,Ap,Iw", 170), %0
test("220:Cp,In", 110), %1
test("440:In,Ip,Cp,Aw,Iw,In,An", 660), %39
test("1270:Ap,In,An,Ip,In,Ip,Ip", 1270). %40
test(Data, Expected) ->
Result = solve(Data),
OkNg = case Result =:= Expected of true -> ok; false -> ng end,
io:fwrite("~s: ~s -> ~w~n", [OkNg, Data, Result]).