“とある世界のタクシー料金”問題を Squeak/Pharo Smalltalk で

| cityOfPlace distOfSection additFare initFare fareFor |
cityOfPlace := Dictionary new.
#(円來 'ABC' 炭州 'DEFG') pairsDo: [:ci :pls | pls do: [:pl | cityOfPlace at: pl put: ci]].

distOfSection := {
   'AC'->180.
   'AB'->1090.
   'DA'->540.
   'BC'->960.
   'BG'->1270.
   'DC'->400.
   'CF'->200.
   'DE'->720.
   'DF'->510.
   'EG'->1050.
   'FG'->230
} as: Dictionary.

initFare := {#円來->#(-995 400). #炭州->#(-845 350)} as: Dictionary.
additFare := {#円來->#(-200 60). #炭州->#(-200 50)} as: Dictionary.

fareFor := [:path |
   | path1 distAndFare |
   path1 := path. "for Pharo"
   distAndFare := (initFare at: (cityOfPlace at: path1 first)) copy.
   [path1 size < 2] whileFalse: [
      | section nextDist city |
      section := path1 first: 2.
      nextDist := distOfSection at: section
         ifAbsent: [distOfSection at: (section := section reversed)].
      distAndFare at: 1 incrementBy: nextDist.
      city := cityOfPlace at: section first.
      [distAndFare first positive]
         whileTrue: [distAndFare := distAndFare + (additFare at: city)].
      path1 := path1 allButFirst.
   ].
   distAndFare last
].


#(
   (0  ADFC  510)
   (1  CFDA  500)
   (2  AB  460)
   (3  BA  460)
   (4  CD  400)
   (5  DC  350)
   (6  BG  520)
   (7  GB  530)
   (8  FDA  450)
   (9  ADF  450)
   (10  FDACB  750)
   (11  BCADF  710)
   (12  EDACB  800)
   (13  BCADE  810)
   (14  EGFCADE  920)
   (15  EDACFGE  910)
   (16  ABCDA  960)
   (17  ADCBA  1000)
   (18  BADCFGB  1180)
   (19  BGFCDAB  1180)
   (20  CDFC  460)
   (21  CFDC  450)
   (22  ABGEDA  1420)
   (23  ADEGBA  1470)
   (24  CFGB  640)
   (25  BGFC  630)
   (26  ABGEDFC  1480)
   (27  CFDEGBA  1520)
   (28  CDFGEDABG  1770)
   (29  GBADEGFDC  1680)
) do: [:data | self assert: (fareFor value: data second) = data last]


以下、解説。


cityOfPlace は乗降場所(A〜G)がどちらの市にあるか、の辞書です。

distOfSection は各区間がどのくらいの距離かを調べるための辞書です。キーである区間は乗降場所二カ所を表わす二文字の組み合わせの文字列で、距離を引けるようになっています。両端の乗降場所がある市が異なる場合については、最初の文字の市がその区間が属する市と同一になるようにしました。

initFare、additFare は、それぞれの市(円來、炭州)での初乗り、もしくは、加算の距離と運賃のタプル(配列ですが)を引くための辞書です。

これくらいを用意しておけば、料金体系の違う市が追加されることがあっても対応可能と考えました(無いと思いますが^^;)。


fareFor が料金計算機です。お題にあるように乗降場所のルート(パス)を表わす文字列を与えることでかかった料金の計算をします。


冒頭に

path1 := path. "for Pharo"

とあるのは、Squeak と違い、Pharo ではブロック引数への代入が禁じられているため、書き換え専用の一時変数(path1)を用意したからです。


まず、path1 の一文字目(path1 first)で cityOfPlace 辞書を引いてどちらの市からの乗車かを判定し、さらにその市の初乗り距離と料金のタプルを distAndFare の初期値として得ます。あとで処理を単純な加算ですませられるように、初乗り距離は負にしてあります。

distAndFare := (initFare at: (cityOfPlace at: path1 first)) copy.

あとで破壊的な操作をしているので、得られた値はここでコピーしておかないとひどい目に遭います。^^;


path1 が二文字より少なくなければ、最初の二文字から区間 section を得ます

section := path1 first: 2.


得られた区間をキーにして distOfSection 辞書から距離 nextDist を得ますが、見つからなければ section を逆順にして辞書を引き直します。

nextDist := distOfSection at: section
   ifAbsent: [distOfSection at: (section := section reversed)].


区間を Set のインスタンスとして管理しておけば逆順にして〜のくだりの処理は必要ないので、どうするかすこし迷ったところですが、区間が属する市、ひいては加算距離と料金を得るのに簡単で、distOfSection 辞書を複雑にしない方向にしました。

ここで得られた nextDist をもって、distAndFare の第一要素である初乗り距離(負数)に破壊的に加算します。

distAndFare at: 1 incrementBy: nextDist.

こういうむき出しの処理が入ると、distAndFare は料金メーターかなにかに抽象化したオブジェクトであったほうがコードの可読性が増すのでよいかなとも一瞬思いましたが、たとえば GUI をつけるとか、ゲームやシミュレーションにするとかの方向にこのお題が拡張されることはないと判断し、そういう仕組みを作り込むことはしませんでした。


続いて、必要なら加算距離・料金の計算をするために、区間が属する市を得ます。すでに section は第一文字目が示す乗降場所がその区間が属する市と一致するように変更されているので、キーは section first で事足ります。

city := cityOfPlace at: section first.


負数の初乗り距離に対して、正の区間距離を足していますので、それがゼロ以上になるのを待って(distAndFare first positive)、その間、距離を減算しつつ料金を加算します。

distAndFare := distAndFare + (additFare at: city))

距離と料金のタプル(配列)同士の加算(加算距離の方は負数なので減算)になっているのがミソとなっています。


最後に、path1 から最初の文字を省いて次の区間の計算を同様に繰り返し、

path1 := path1 allButFirst.


パスを消費しつくしたら、タプルの料金の方だけを返し終了する、

   distAndFare last


とそんな感じです。