Skip to content

Instantly share code, notes, and snippets.

@pauldambra
Created December 29, 2015 15:33
Show Gist options
  • Save pauldambra/05577be969f351c39c9c to your computer and use it in GitHub Desktop.
Save pauldambra/05577be969f351c39c9c to your computer and use it in GitHub Desktop.
the checkout kata in elixir
defmodule Checkout do
defstruct [
basket: %{A: 0, B: 0, C: 0, D: 0}
]
def scan(checkout, code) do
new_value = checkout.basket[code] + 1
%{checkout | basket: Map.put(checkout.basket, code, new_value)}
end
def total(checkout) do
(for item <- checkout.basket,
do: get_price item)
|> Enum.sum
end
defp get_price({:A, amount}), do: get_special_price(%{amount: 3, price: 130}, 50, amount, 0)
defp get_price({:B, amount}), do: get_special_price(%{amount: 2, price: 45}, 30, amount, 0)
defp get_price({:C, amount}), do: get_price(20, amount)
defp get_price({:D, amount}), do: get_price(15, amount)
defp get_price(cost, amount) do
cost * amount
end
defp get_special_price(offer, normal_cost, amount, current_total) do
if amount >= offer.amount do
get_special_price(offer, normal_cost, amount - offer.amount, current_total + offer.price)
else # amount is now either less than the offer anmount or 0
current_total + get_price(normal_cost, amount)
end
end
end
defmodule CheckoutTest do
use ExUnit.Case
doctest Checkout
# Item Unit Special
# Price Price
# --------------------------
# A 50 3 for 130
# B 30 2 for 45
# C 20
# D 15
setup do
scans = [
%{items: [:A], total: 50},
%{items: [:B], total: 30},
%{items: [:C], total: 20},
%{items: [:D], total: 15},
%{items: [:A, :A], total: 100},
%{items: [:B, :B], total: 45},
%{items: [:A, :B], total: 80},
%{items: [:A, :A, :A], total: 130},
%{items: [:A, :A, :A, :A, :A], total: 230},
%{items: [:A, :A, :A, :B, :B], total: 175},
]
{:ok, data: scans}
end
test "scanning an item adds it to the basket" do
checkout = Checkout.scan %Checkout{}, :A
assert checkout.basket == %{A: 1, B: 0, C: 0, D: 0}
end
test "totalling a checkout can give a result" do
checkout = Checkout.scan %Checkout{}, :A
assert Checkout.total(checkout) == 50
end
test "scanning a basket gives expected total cost", %{data: scans} do
Enum.each(scans,
fn(scan) ->
total = Enum.reduce(
scan.items,
%Checkout{},
fn(item, chkot) -> Checkout.scan(chkot, item) end
)
|> Checkout.total
assert total == scan.total
end
)
end
end
@tomliversidge
Copy link

Ok, came up with:

defmodule Checkout do

  def scan(item) when is_map(item), do: scan(item, [])
  #convenience for passing in multiple items
  def scan(items) when is_list(items), do: Enum.reduce(items, [], &(scan(&1, &2)))
  def scan(item, order) when is_list(order), do: [item | order]

  def total(cart, offers) do
    subtotal(cart) - discount(cart, offers)
  end

  def discount(cart, offers) do
    Enum.reduce(offers, 0, fn(offer, total) -> calculate_discount(cart, offer) + total end)
  end

  defp calculate_discount(cart, offer) do
    cart
      |> filter_by(offer.sku)
      |> count
      |> calculate_discount(offer.discount_qualification_quantity, offer.discount)
  end

  defp filter_by(cart, sku) do
    Enum.filter(cart, &(&1.sku == sku))
  end

  defp count(items) do
    Enum.reduce(items, 0, fn(item, total) -> item.quantity + total end)
  end

  defp calculate_discount(items_count, discount_qualification_quantity, discount_value) do
    Float.floor(items_count / discount_qualification_quantity, 0) * discount_value
  end

  def subtotal(cart) do
    Enum.reduce cart, 0, fn(item, running_total) ->
      (item.price * item.quantity) + running_total
    end
  end

end

defmodule CheckoutTest do
  use ExUnit.Case

  setup do
    items = [
      %{sku: :A, price: 50.00, quantity: 3},
      %{sku: :A, price: 50.00, quantity: 2},
      %{sku: :A, price: 50.00, quantity: 1},
      %{sku: :B, price: 30.00, quantity: 1},
      %{sku: :B, price: 30.00, quantity: 1},
      %{sku: :B, price: 30.00, quantity: 1},
      %{sku: :C, price: 20.00, quantity: 1},
    ]
    {:ok, cart: items}
  end

  test "The cart is subtotalled without discount", %{cart: items} do
    total = Checkout.scan(items) |> Checkout.subtotal
    assert total == 410.00
  end

  test "The cart is totalled with discounts based on default offers", %{cart: items} do
    offers = [
      %{sku: :A, discount_qualification_quantity: 3, discount: 20},
      %{sku: :B, discount_qualification_quantity: 2, discount: 15}
    ]
    total = Checkout.scan(items) |> Checkout.total(offers)
    assert total == 355.00
  end

  test "The cart is totalled with discounts based on different offers", %{cart: items} do
    offers = [
      %{sku: :A, discount_qualification_quantity: 3, discount: 25},
      %{sku: :B, discount_qualification_quantity: 2, discount: 20}
    ]
    total = Checkout.scan(items) |> Checkout.total(offers)
    assert total == 340.00
  end

end

Any suggestions welcome - this is my first attempt at writing any elixir, along with the Red:4 course! It's great fun so far!

Think the filter_by and count functions should possibly be one thing as what I want to know is how many of a certain sku are in the cart, just wasn't sure on the best way to write it. Could just stick it in one function, but wondered if Enum has anything build in that might do this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment