TDDBC東京1.5 のお題を Squeak Smalltalk で


楽しげなお題だったのでチャレンジしてみました。


ざっと思いつく実装を書いただけでろくにクタってもないため、ツッコミどころ満載かもですがどうぞお手柔らかに。^^; Squeak4.2J でファイルインして動かせます。


少し読みやすく圧縮整形したもの。

TestCase subclass: #'商品Test'

商品Test>>test01hash
   | 商品1 商品2 商品3 商品4 商品5 |
   商品1 := 商品 ID: 1 製品名: #test 価格: 100.
   商品2 := 商品 ID: 1 製品名: #test 価格: 100.
   商品3 := 商品 ID: 2 製品名: #test 価格: 100.
   商品4 := 商品 ID: 1 製品名: #test2 価格: 100.
   商品5 := 商品 ID: 1 製品名: #test 価格: 101.
   self assert: 商品1 hash = 商品2 hash.
   self deny: 商品1 hash = 商品3 hash.
   self deny: 商品1 hash = 商品4 hash.
   self deny: 商品1 hash = 商品5 hash.

商品Test>>test02等価
   | 商品1 商品2 商品3 商品4 商品5 |
   商品1 := 商品 ID: 1 製品名: #test 価格: 100.
   商品2 := 商品 ID: 1 製品名: #test 価格: 100.
   商品3 := 商品 ID: 2 製品名: #test 価格: 100.
   商品4 := 商品 ID: 1 製品名: #test2 価格: 100.
   商品5 := 商品 ID: 1 製品名: #test 価格: 101.
   self assert: 商品1 = 商品2.
   self deny: 商品1 = 商品3.
   self deny: 商品1 = 商品4.
   self deny: 商品1 = 商品5.

商品Test>>test03辞書のキーになることができる
   | 商品1 辞書 |
   商品1 := 商品 ID: 1 製品名: #test 価格: 100.
   辞書 := Dictionary new.
   辞書 at: 商品1 put: 1.
   self assert: (辞書 at: 商品1 ifAbsent: [0]) = 1
TestCase subclass: #'自動販売機Test'

自動販売機Test>>test01自動販売機を生成できる
   | 販売機 |
   販売機 := 自動販売機 new.
   self assert: 販売機 class == 自動販売機

自動販売機Test>>test02千円札を挿入することができる
   | 販売機 |
   販売機 := 自動販売機 new.
   self shouldnt: [販売機 投入: 1000] raise: Error

自動販売機Test>>test03x1円玉と5円玉以外の硬貨を投入できる
   | 販売機 |
   販売機 := 自動販売機 new.
   self shouldnt: [販売機 投入: 500] raise: Error.
   self shouldnt: [販売機 投入: 100] raise: Error.
   self shouldnt: [販売機 投入: 50] raise: Error.
   self shouldnt: [販売機 投入: 10] raise: Error

自動販売機Test>>test04x1円玉と5円玉は使うことができずエラーになる
   | 販売機 |
   販売機 := 自動販売機 new.
   self should: [販売機 投入: 5] raise: Error.
   self should: [販売機 投入: 1] raise: Error

自動販売機Test>>test05現在投入されている合計金額を算出できる
   | 販売機 |
   販売機 := 自動販売機 new.
   販売機 投入: 1000.
   販売機 投入: 500.
   self assert: 販売機 投入金額 = 1500

自動販売機Test>>test06ID1コーラ5120円を在庫として保持できる
   | 販売機 コーラ |
   販売機 := 自動販売機 new.
   コーラ := 商品 ID: 1 製品名: #コーラ 価格: 120.
   self assert: (販売機 在庫 at:コーラ) = 5

自動販売機Test>>test07お金を投入すると現在購入できるIDを算出する
   | 販売機 コーラ 水 |
   販売機 := 自動販売機 new.
   コーラ := 商品 ID: 1 製品名: #コーラ 価格: 120.
   水 := 商品 ID: 3 製品名: #水 価格: 100.
   販売機 投入: 50.
   self assert: 販売機 購入可能商品群 isEmpty.
   販売機 投入: 50.
   self assert: 販売機 購入可能商品群 asArray = {水 ID}.
   販売機 投入: 50.
   self assert: 販売機 購入可能商品群 asSet = {水 ID. コーラ ID} asSet.

自動販売機Test>>test08購入できるIDを指定してジュースを買うとコーラの在庫が減る
   | 販売機 コーラ |
   販売機 := 自動販売機 new.
   コーラ := 商品 ID: 1 製品名: #コーラ 価格: 120.
   販売機 投入: 100.
   販売機 投入: 50.
   販売機 購入: コーラ ID.
   self assert: (販売機 在庫 at: コーラ) = 4

自動販売機Test>>test09現在の売上金額が算出される
   | 販売機 コーラ |
   販売機 := 自動販売機 new.
   コーラ := 商品 ID: 1 製品名: #コーラ 価格: 120.
   3 timesRepeat: [販売機 投入: 100].
   6 timesRepeat: [販売機 投入: 10].
   販売機 購入: コーラ ID.
   self assert: (販売機 売上金額 = 120)

自動販売機Test>>test10現在の在庫数が算出される
   | 販売機 コーラ |
   販売機 := 自動販売機 new.
   コーラ := 商品 ID: 1 製品名: #コーラ 価格: 120.
   3 timesRepeat: [販売機 投入: 100].
   6 timesRepeat: [販売機 投入: 10].
   販売機 購入: コーラ ID.
   self assert: (販売機 在庫 at: コーラ) = 4

自動販売機Test>>test11在庫切れを考慮する
   | 販売機 コーラ |
   販売機 := 自動販売機 new.
   コーラ := 商品 ID: 1 製品名: #コーラ 価格: 120.
   5 timesRepeat: [販売機 投入: 100. 2 timesRepeat: [販売機 投入: 10]. 販売機 購入: コーラ ID].
   self assert: 販売機 在庫切れ商品群 = {コーラ ID}

自動販売機Test>>test12千円札と500円と100円と50円と10円硬貨を保持できる
   | 販売機 |
   販売機 := 自動販売機 new.
   self assert: 販売機 保持紙幣と硬貨 keys asSet = #(1000 500 100 50 10) asSet

自動販売機Test>>test13千円札5枚と硬貨はそれぞれ10枚保持する
   | 販売機 |
   販売機 := 自動販売機 new.
   self assert: (販売機 保持紙幣と硬貨 at: 1000) = 5.
   #(500 100 50 10)
      do: [:key | self assert: (販売機 保持紙幣と硬貨 at: key) = 10]

自動販売機Test>>test14x500円玉を投入すると保持硬貨数が増える
   | 販売機 投入前 差分 |
   販売機 := 自動販売機 new.
   投入前 := 販売機 保持紙幣と硬貨 copy.
   販売機 投入: 500.
   差分 := 販売機 保持紙幣と硬貨 keys
      collect: [:key | key -> ((販売機 保持紙幣と硬貨 at: key) - (投入前 at: key))]
      thenSelect: [:assoc | assoc value ~= 0].
   self assert: 差分 = {500->1}

自動販売機Test>>test15ジュースが1つ購入されるとお釣りとしてお金が減算される
   | 販売機 投入前 差分 コーラ |
   販売機 := 自動販売機 new.
   コーラ := 商品 ID: 1 製品名: #コーラ 価格: 120.
   投入前 := 販売機 保持紙幣と硬貨 copy.
   販売機 投入: 500.
   販売機 購入: コーラ ID.
   差分 := 販売機 保持紙幣と硬貨 keys asSet
      collect: [:key | key -> ((販売機 保持紙幣と硬貨 at: key) - (投入前 at: key))]
      thenSelect: [:assoc | assoc value ~= 0].
   self assert: 差分 = {500->1. 100-> -3. 50-> -1. 10-> -3} asSet

自動販売機Test>>test16お釣りが足りるか判断できる
   | 販売機 |
   販売機 := 自動販売機 new.
   販売機 投入: 1000.
   self assert: (販売機 保持紙幣と硬貨 at: 100) = 10.
   self assert: (販売機 保持紙幣と硬貨 at: 50) = 10.
   self assert: (販売機 保持紙幣と硬貨 at: 10) = 10.
   self assert: (販売機 購入後残金返却可か: 100).
   販売機 保持紙幣と硬貨 at: 100 put: 0.
   self assert: (販売機 購入後残金返却可か: 100).
   販売機 保持紙幣と硬貨 at: 50 put: 5.
   self deny: (販売機 購入後残金返却可か: 100)

自動販売機Test>>test17現在のお釣り用のお金が算出される
   | 販売機 |
   販売機 := 自動販売機 new.
   self assert: (販売機 保持紙幣と硬貨 at: 500) = 10.
   self assert: (販売機 保持紙幣と硬貨 at: 100) = 10.
   self assert: (販売機 保持紙幣と硬貨 at: 50) = 10.
   self assert: (販売機 保持紙幣と硬貨 at: 10) = 10

自動販売機Test>>test18xID2レッドブル2005本を追加
   | 販売機 レッドブル |
   販売機 := 自動販売機 new.
   レッドブル := 商品 ID: 2 製品名: #レッドブル 価格: 200.
   self assert: (販売機 在庫 at: レッドブル) = 5

自動販売機Test>>test19xID3水1005本を追加
   | 販売機 水 |
   販売機 := 自動販売機 new.
   水 := 商品 ID: 3 製品名: #水 価格: 100.
   self assert: (販売機 在庫 at: 水) = 5

自動販売機Test>>test20お釣りが足りないと購入できない
   | 販売機 コーラ |
   販売機 := 自動販売機 new.
   販売機 投入: 1000.
   self assert: (販売機 保持紙幣と硬貨 at: 100) = 10.
   self assert: (販売機 保持紙幣と硬貨 at: 50) = 10.
   self assert: (販売機 保持紙幣と硬貨 at: 10) = 10.
   販売機 保持紙幣と硬貨 at: 100 put: 0.
   販売機 保持紙幣と硬貨 at: 50 put: 5.
   コーラ := 商品 ID: 1 製品名: #コーラ 価格: 120.
   self deny: (販売機 購入可能商品群 includes: コーラ ID)
Object subclass: #'商品'
   instanceVariableNames: 'ID 製品名 価格'

商品>>ID
   ^ ID 

商品>>ID: anInteger
   ID := anInteger 

商品>>価格
  ^ 価格

商品>>価格: anInteger
   価格 := anInteger

商品>>製品名
   ^ 製品名

商品>>製品名: aSymbol
   製品名 := aSymbol

商品>>printOn: stream
   stream nextPutAll: 製品名 printString

商品>>= a商品
   a商品 class == self class ifFalse: [^false].
   a商品 ID = ID ifFalse: [^false].
   a商品 製品名 = 製品名 ifFalse: [^false].
   a商品 価格 = 価格 ifFalse: [^false].
   ^true

商品>>hash
   ^(ID hash bitXor: 製品名 hash) bitXor: 価格 hash

商品 class>>ID: ID 製品名: 製品名 価格: 価格 
   ^self new ID: ID; 製品名: 製品名; 価格: 価格; yourself
Object subclass: #'自動販売機'
   instanceVariableNames: '投入金額 在庫 売上金額 保持紙幣と硬貨'

自動販売機>>投入: 紙幣か硬貨
   (self 使用可能紙幣と硬貨 includes: 紙幣か硬貨)
      ifFalse: [self error: 'この紙幣または硬貨は使えません'].
   投入金額 := 投入金額 + 紙幣か硬貨.
   保持紙幣と硬貨 at: 紙幣か硬貨 put: (保持紙幣と硬貨 at: 紙幣か硬貨) + 1

自動販売機>>使用可能紙幣と硬貨
   ^保持紙幣と硬貨 keys asSet

自動販売機>>保持紙幣と硬貨
   ^保持紙幣と硬貨

自動販売機>>在庫
   ^在庫

自動販売機>>在庫切れ商品群
   ^在庫 keys select: [:key | (在庫 at: key) = 0] thenCollect: [:key | key ID]

自動販売機>>売上金額
   ^売上金額

自動販売機>>投入金額
   ^投入金額

自動販売機>>購入可能商品群
   ^在庫 keys
      select: [:key |
         (在庫 at: key) > 0
            and: [key 価格 <= 投入金額]
            and: [self 購入後残金返却可か: key 価格]]
      thenCollect: [:key | key ID]

自動販売機>>initialize
   super initialize.
   投入金額 := 売上金額 := 0.
   在庫 := Dictionary new.
   保持紙幣と硬貨 := {1000->5. 500->10. 100->10. 50->10. 10->10} as: Dictionary.
   5 timesRepeat: [self 補充: (商品 ID: 1 製品名: #コーラ 価格: 120)].
   5 timesRepeat: [self 補充: (商品 ID: 2 製品名: #レッドブル 価格: 200)].
   5 timesRepeat: [self 補充: (商品 ID: 3 製品名: #水 価格: 100)]

自動販売機>>購入: 商品ID
   | 購入可能商品群 購入商品 |
   購入可能商品群 := self 購入可能商品群.
   (購入可能商品群 includes: 商品ID) ifFalse: [self error: '購入できません'].
   購入商品 := 在庫 keys detect: [:each | each ID = 商品ID].
   在庫 at: 購入商品 put: (在庫 at: 購入商品) - 1.
   売上金額 := 売上金額 + 購入商品 価格.
   投入金額 := 投入金額 - 購入商品 価格.
   self 差額返却

自動販売機>>補充: a商品 
   ^在庫 at: a商品 put: (在庫 at: a商品 ifAbsentPut: [0]) + 1

自動販売機>>差額返却
   保持紙幣と硬貨 keys sort reverseDo: [:key |
      [key <= 投入金額] whileTrue: [
         投入金額 := 投入金額 - key.
         保持紙幣と硬貨 at: key put: (保持紙幣と硬貨 at: key) - 1]]

自動販売機>>購入後残金返却可か: 購入金額
   | 返却額 現状 |
   返却額 := 投入金額 - 購入金額.
   現状 := 保持紙幣と硬貨 copy.
   現状 keys sort reverseDo: [:key |
      [key <= 返却額 and: [(現状 at: key) > 0]] whileTrue: [
         返却額 := 返却額 - key.
         現状 at: key put: (現状 at: key) - 1]].
   ^返却額 = 0