Skip to content

Instantly share code, notes, and snippets.

@pocketberserker
Last active December 11, 2015 13:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pocketberserker/4605144 to your computer and use it in GitHub Desktop.
Save pocketberserker/4605144 to your computer and use it in GitHub Desktop.
TDDBCの自販機課題を実装してみた残骸

自販機コードの残骸

このコード群はTDDBC演習題材の一つである自動販売機を実装しようとしたときの残骸です。

バージョン管理がかなりテキトーだったため、gistのほうで晒しておきます。

開発環境

  • Visual Studio 2010系列
  • F# 2.0

使用ライブラリ

注意事項

  • このコードには負債が詰まっています(特に変数名など)
  • ユニットテストとランダムテストを混ぜて記述しています
  • 飲み物を購入する関数は特にリファクタリング対象です
  • 言い訳「夜に書いてはいけませんね・・・」
module DrinkStock
type Drink = {
name : string
price : int
}
type DrinkStock = Drink list
let init = [{name = "コーラ"; price = 120}; {name = "レッドブル"; price = 200}; {name = "水"; price = 100}] |> List.map (Array.create 5) |> Seq.concat |> Seq.toList
let getInfo drinkStock =
drinkStock
|> Seq.countBy id
|> Seq.fold (fun info (d,c) ->
let x = sprintf "%s:%d円:%d本\n" d.name d.price c
info + x ) ""
module DrinkStockTest
open NUnit.Framework
open Swensen.Unquote
module 初期状態 =
let stock = DrinkStock.init
[<Test>]
let ``格納されている飲み物の在庫情報が取得できる``() =
test <@ stock |> DrinkStock.getInfo = "コーラ:120円:5本\nレッドブル:200円:5本\n水:100円:5本\n" @>
module Money
type Money =
| One
| Five
| Ten
| Fifty
| Hundred
| FiveHundred
| Thousand
| FiveThousand
| TenThousand
let private Success = Choice1Of2
let private Failure = Choice2Of2
let toInt = function
| One -> 1
| Five -> 5
| Ten -> 10
| Fifty -> 50
| Hundred -> 100
| FiveHundred -> 500
| Thousand -> 1000
| FiveThousand -> 5000
| TenThousand -> 10000
let ofInt = function
| 1 -> Success One
| 5 -> Success Five
| 10 -> Success Ten
| 50 -> Success Fifty
| 100 -> Success Hundred
| 500 -> Success FiveHundred
| 1000 -> Success Thousand
| 5000 -> Success FiveThousand
| 10000 -> Success TenThousand
| _ -> Failure (System.ArgumentException "お金に変換できません")
let total moneys = moneys |> List.map toInt |> List.sum
module MoneyTest
open System
open FsCheck.NUnit
open FSharpx.Validation
open NUnit.Framework
open Swensen.Unquote
open Money
let fsCheck t = fsCheck "" t
let success = Choice1Of2
[<Test>]
let ``お金を数値に変換して更にお金に変換すると元にもどる``() =
fsCheck <| fun money -> money |> Money.toInt |> Money.ofInt = success money
[<Test>]
let ``お金に合致しない数値はお金に変換できない``() =
test <|
<@
match 2 |> Money.ofInt with
| Success _ -> false
| Failure e -> e.Message = "お金に変換できません"
@>
module VendingMachine
open Money
open DrinkStock
open FSharpx.Validation
type VendingMachine = {
allowMoneys : Money list
totalAmount : Money list
drinkStock : DrinkStock
saleAmount : Money list
change : Money list
}
let private success = Choice1Of2
let private failure = Choice2Of2
let init =
let allowMoneys = [Thousand;FiveHundred;Hundred;Fifty;Ten]
let change = allowMoneys |> List.tail |> List.map (Array.create 10) |> Seq.concat |> Seq.toList
{ allowMoneys = allowMoneys
totalAmount = []
drinkStock = DrinkStock.init
saleAmount = []
change = change }
let getTotalAmount vendingMachine = vendingMachine.totalAmount |> Money.total
let insert m vendingMachine =
match vendingMachine.allowMoneys |> List.exists ((=) m) with
| true -> success { vendingMachine with totalAmount = m :: vendingMachine.totalAmount }
| false -> failure(vendingMachine, m)
// 外部公開関数の戻り値をタプルのままにするかレコードに変更すべきか…
let payback vendingMachine = (vendingMachine.totalAmount, { vendingMachine with totalAmount = [] })
let getDrinkStockInfo vendingMachine =
vendingMachine.drinkStock |> DrinkStock.getInfo
// TODO:単にfalseでは"飲み物が存在しない"のか"お金が足りない"のか判別できない。判別共用体に変更すべきである。
let canBuy drinkName vendingMachine =
let amount = getTotalAmount vendingMachine
match vendingMachine.drinkStock |> List.tryFind (fun x -> x.name = drinkName) with
| Some x -> amount >= x.price
| None -> false
let buy name vendingMachine =
let rec remove n acc = function
| [] -> failwith "指定した飲み物が見つかりませんでした"
| x::xs when x.name = n -> (x, List.append acc xs)
| x::xs -> remove n (x::acc) xs
let substractMoney moneys price =
let rec removes acc moneys n m = function
| [] -> failure (exn "お金を指定のリストから取り除くことができません")
| x::xs when x = m && n - 1 = 0 -> success (x::acc,List.append xs moneys)
| x::xs when x = m -> removes (x::acc) moneys (n-1) m xs
| x::xs -> removes acc (x::moneys) n m xs
let rec inner acc moneys price = function
| [] -> failure (exn "お金を減らすことができません")
| x::xs ->
let m = Money.toInt x
let count = price / m
let moneyCount = moneys |> List.filter ((=) x) |> List.length
if count > 0 && moneyCount >= count then
match removes [] [] count x moneys with
| Success(s, moneys) when price % m = 0 -> success (List.append acc s, moneys)
| Success(s, moneys) -> inner (List.append s acc) moneys (price % m) xs
| fail -> fail
elif moneyCount > 0 && count > 0 then
match removes [] [] moneyCount x moneys with
| Success(s, moneys) -> inner (List.append acc s) moneys (price - (moneyCount * m)) xs
| fail -> fail
else
inner acc moneys price xs
inner [] moneys price (List.tail vendingMachine.allowMoneys)
match canBuy name vendingMachine with
| true ->
let (d, stock) = remove name [] vendingMachine.drinkStock
match substractMoney vendingMachine.totalAmount d.price with
| Success (saleAmount, totalAmount) ->
let saleAmount = List.append saleAmount vendingMachine.saleAmount
let vendingMachine = { vendingMachine with drinkStock = stock; totalAmount = totalAmount; saleAmount = saleAmount }
success(d, vendingMachine)
| Failure _ ->
FSharpx.Choice.choose {
let! (saleAmount, change) = substractMoney vendingMachine.change d.price
let saleAmount = List.append saleAmount vendingMachine.saleAmount
let change = List.append vendingMachine.totalAmount change
let amount = Money.total vendingMachine.totalAmount - d.price
let! (totalAmount, change) = substractMoney change amount
let vendingMachine = { vendingMachine with drinkStock = stock; totalAmount = totalAmount; saleAmount = saleAmount; change = change }
return (d, vendingMachine)
}
| false ->
let message = sprintf "%sを購入できませんでした" name
failure (exn message)
let getSaleAmount vendingMachine = vendingMachine.saleAmount |> Money.total
let purchasableDrink vendingMachine =
vendingMachine.drinkStock
|> Seq.distinct
|> Seq.filter (fun x -> canBuy x.name vendingMachine)
|> Seq.toList
module VendingMachineTest
open NUnit.Framework
open FsCheck
open FsCheck.NUnit
open Swensen.Unquote
open FSharpx.Choice
open FSharpx.Validation
open Money
open VendingMachine
open DrinkStock
let fsCheck t = fsCheck "" t
let chooseFromList xs = gen {
let! i = Gen.choose (0, List.length xs-1)
return (List.nth xs i)
}
module 初期状態 =
let vendingMachine = VendingMachine.init
[<Test>]
let ``お金を投入しなければ合計金額は0円``() =
test <@ vendingMachine |> VendingMachine.getTotalAmount = 0 @>
[<Test>]
let ``10円を投入したら合計金額は10円``() =
test <|
<@
match vendingMachine |> VendingMachine.insert Ten with
| Success vendingMachine -> vendingMachine |> VendingMachine.getTotalAmount = 10
| Failure _ -> false
@>
[<Test>]
let ``お金を投入せずに払い戻し操作をしたら釣り銭は空``() =
test <@ vendingMachine |> VendingMachine.payback |> fst = [] @>
[<Test>]
let ``想定外のお金を投入したらそのままつり銭として返される``() =
test <|
<@
match vendingMachine |> VendingMachine.insert One with
| Success vendingMachine -> false
| Failure(vendingMachine, m) -> m = One
@>
[<Test>]
let ``利用可能なお金を1回投入して払い戻し操作をしたら釣り銭は投入したお金が戻ってくる``() =
let allowMoney = chooseFromList vendingMachine.allowMoneys
fsCheck <| Prop.forAll (Arb.fromGen allowMoney) (fun x ->
match vendingMachine |> VendingMachine.insert x with
| Success vendingMachine -> vendingMachine |> VendingMachine.payback |> fst = [x]
| Failure(vendingMachine, m) -> false)
[<Test>]
let ``お金を投入していないのでコーラは購入可能ではない``() =
test <@ vendingMachine |> VendingMachine.canBuy "コーラ" = false @>
[<Test>]
let ``お金を投入しなければ売り上げ金額は0円``() =
test <@ vendingMachine |> VendingMachine.getSaleAmount = 0 @>
module ``10円が投入された状態`` =
let vendingMachine = VendingMachine.init |> VendingMachine.insert Ten |> function Success vendingMachine -> vendingMachine | Failure _ -> failwith "10円の投入に失敗しました"
[<Test>]
let ``追加で100円を投入したら合計金額は110円``() =
test <|
<@
match vendingMachine |> VendingMachine.insert Hundred with
| Success vendingMachine -> vendingMachine |> VendingMachine.getTotalAmount = 110
| Failure _ -> false
@>
[<Test>]
let ``払い戻し操作をしたら釣り銭は10円``() =
test <@ vendingMachine |> VendingMachine.payback |> fst = [Ten] @>
[<Test>]
let ``払い戻し操作をしたら投入金額の合計は0円``() =
test <@ vendingMachine |> VendingMachine.payback |> snd |> VendingMachine.getTotalAmount = 0 @>
[<Test>]
let ``投入金額が足りないのでコーラを購入できない``() =
match vendingMachine |> VendingMachine.buy "コーラ" with
| Success _ -> Assert.Fail("投入金額が足りないのにコーラが購入できました")
| Failure e -> test <@ e.Message = "コーラを購入できませんでした" @>
module ``120円が投入された状態`` =
let vendingMachine =
let x = FSharpx.Choice.choose {
let v = VendingMachine.init
let! v = v |> VendingMachine.insert Ten
let! v = v |> VendingMachine.insert Ten
return! v |> VendingMachine.insert Hundred
}
match x with
| Success vendingMachine -> vendingMachine
| Failure _ -> failwith "120円の投入に失敗しました"
[<Test>]
let ``コーラは購入可能である``() =
test <@ vendingMachine |> VendingMachine.canBuy "コーラ" = true @>
[<Test>]
let ``存在しないドリンクは購入不可能``() =
test <@ vendingMachine |> VendingMachine.canBuy "---" = false @>
[<Test>]
let ``コーラの購入を実行するとコーラの購入に成功する``() =
test <|
<@
match vendingMachine |> VendingMachine.buy "コーラ" with
| Success(drink, _) -> drink.name = "コーラ"
| Failure _ -> false
@>
[<Test>]
let ``コーラの購入に成功するとコーラの在庫が一つ減る``() =
match vendingMachine |> VendingMachine.buy "コーラ" with
| Success(_, vendingMachine) -> test <@ vendingMachine |> VendingMachine.getDrinkStockInfo = "コーラ:120円:4本\nレッドブル:200円:5本\n水:100円:5本\n" @>
| Failure e -> raise e
[<Test>]
let ``コーラの購入に成功するとコーラの値段分だけ投入金額が減少する``() =
match vendingMachine |> VendingMachine.buy "コーラ" with
| Success(_, vendingMachine) -> test <@ vendingMachine |> VendingMachine.getTotalAmount = 0 @>
| Failure e -> raise e
[<Test>]
let ``コーラの購入に成功するとコーラの値段分だけ売り上げ金額が増加する``() =
match vendingMachine |> VendingMachine.buy "コーラ" with
| Success(_, vendingMachine) -> test <@ vendingMachine |> VendingMachine.getSaleAmount = 120 @>
| Failure e -> raise e
[<Test>]
let ``コーラの購入して払い戻し操作を行うと釣り銭は0円``() =
match vendingMachine |> VendingMachine.buy "コーラ" with
| Success(_, vendingMachine) -> test <@ vendingMachine |> VendingMachine.payback |> fst = [] @>
| Failure e -> raise e
[<Test>]
let ``コーラの在庫がないので購入できない``() =
let vendingMachine = { vendingMachine with drinkStock = [] }
match vendingMachine |> VendingMachine.buy "コーラ" with
| Success _ -> Assert.Fail("コーラの在庫がないのにコーラが購入できました")
| Failure e -> test <@ e.Message = "コーラを購入できませんでした" @>
[<Test>]
let ``購入可能な飲み物として水とコーラがリストとして取得できる``() =
test <@ vendingMachine |> VendingMachine.purchasableDrink = [{name = "コーラ"; price = 120}; {name = "水"; price = 100}] @>
module ``140円が投入された状態`` =
let vendingMachine =
let x = FSharpx.Choice.choose {
let v = VendingMachine.init
let! v = v |> VendingMachine.insert Ten
let! v = v |> VendingMachine.insert Ten
let! v = v |> VendingMachine.insert Ten
let! v = v |> VendingMachine.insert Ten
return! v |> VendingMachine.insert Hundred
}
match x with
| Success vendingMachine -> vendingMachine
| Failure _ -> failwith "140円の投入に失敗しました"
[<Test>]
let ``コーラの購入に成功するとコーラの値段分だけ投入金額が減少する``() =
match vendingMachine |> VendingMachine.buy "コーラ" with
| Success(_, vendingMachine) -> test <@ vendingMachine |> VendingMachine.getTotalAmount = 20 @>
| Failure e -> raise e
module ``1000円が投入された状態`` =
let vendingMachine =
VendingMachine.init
|> VendingMachine.insert Thousand
|> function Success vendingMachine -> vendingMachine | Failure _ -> failwith "1000円の投入に失敗しました"
let drinkList = vendingMachine.drinkStock |> Seq.distinct |> Seq.toList |> chooseFromList
[<Test>]
let ``飲み物の購入に成功すると飲み物の値段分だけ投入金額が減少する``() =
fsCheck <| Prop.forAll (Arb.fromGen drinkList) (fun x ->
let expected = VendingMachine.getTotalAmount vendingMachine - x.price
match vendingMachine |> VendingMachine.buy x.name with
| Success(_, vendingMachine) -> vendingMachine |> VendingMachine.getTotalAmount = expected
| Failure e -> raise e)
[<Test>]
let ``飲み物の購入に成功すると飲み物の値段分だけ売り上げ金額が増加する``() =
fsCheck <| Prop.forAll (Arb.fromGen drinkList) (fun x ->
match vendingMachine |> VendingMachine.buy x.name with
| Success(_, vendingMachine) -> vendingMachine |> VendingMachine.getSaleAmount = x.price
| Failure e -> raise e)
[<Test>]
let ``購入可能な飲み物として水,コーラ,レッドブルがリストとして取得できる``() =
test <@ vendingMachine |> VendingMachine.purchasableDrink = [{name = "コーラ"; price = 120}; {name = "レッドブル"; price = 200}; {name = "水"; price = 100}] @>
module 釣り銭用の小銭が10円100枚のみのとき =
let vendingMachine = { vendingMachine with change = Array.create 100 Ten |> Array.toList }
[<Test>]
let ``コーラの購入して払い戻し操作を行うと釣り銭は10円玉が88枚``() =
match vendingMachine |> VendingMachine.buy "コーラ" with
| Success(_, vendingMachine) -> test <@ vendingMachine |> VendingMachine.payback |> fst = (Array.create 88 Ten |> Array.toList) @>
| Failure e -> raise e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment