Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add elixir version (without tests) #47

Merged
merged 4 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 26 additions & 0 deletions elixir/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
supermarket-*.tar

# Temporary files, for example, from tests.
/tmp/
3 changes: 3 additions & 0 deletions elixir/README-elixir.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# README (Elixir)

There are no external dependencies other than Elixir itself.
7 changes: 7 additions & 0 deletions elixir/lib/supermarket/model/discount.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Supermarket.Model.Discount do
defstruct [:product, :description, :discount_amount]

def new(product, description, discount_amount) do
%__MODULE__{product: product, description: description, discount_amount: discount_amount}
end
end
7 changes: 7 additions & 0 deletions elixir/lib/supermarket/model/offer.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Supermarket.Model.Offer do
defstruct [:offer_type, :product, :argument]

def new(offer_type, product, argument) do
%__MODULE__{offer_type: offer_type, argument: argument, product: product}
end
end
5 changes: 5 additions & 0 deletions elixir/lib/supermarket/model/product.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule Supermarket.Model.Product do
defstruct [:name, :unit]

def new(name, unit), do: %__MODULE__{name: name, unit: unit}
end
5 changes: 5 additions & 0 deletions elixir/lib/supermarket/model/product_quantity.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule Supermarket.Model.ProductQuantity do
defstruct [:product, :quantity]

def new(product, weight), do: %__MODULE__{product: product, quantity: weight}
end
27 changes: 27 additions & 0 deletions elixir/lib/supermarket/model/receipt.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
defmodule Supermarket.Model.Receipt do
alias Supermarket.Model.ReceiptItem

defstruct [:items, :discounts]

def new, do: %__MODULE__{items: [], discounts: []}

def add_product(receipt, product, quantity, price, total_price) do
item = ReceiptItem.new(product, quantity, price, total_price)
Map.update!(receipt, :items, &[item | &1])
end

def add_discount(receipt, discount) do
Map.update!(receipt, :discounts, &[discount | &1])
end

def total_price(receipt) do
item_total = Enum.reduce(receipt.items, 0.0, fn item, total -> item.total_price + total end)

discount_total =
Enum.reduce(receipt.discounts, 0.0, fn discount, total ->
discount.discount_amount + total
end)

item_total + discount_total
end
end
7 changes: 7 additions & 0 deletions elixir/lib/supermarket/model/receipt_item.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Supermarket.Model.ReceiptItem do
defstruct [:product, :quantity, :price, :total_price]

def new(product, quantity, price, total_price) do
%__MODULE__{product: product, quantity: quantity, price: price, total_price: total_price}
end
end
99 changes: 99 additions & 0 deletions elixir/lib/supermarket/model/shopping_cart.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
defmodule Supermarket.Model.ShoppingCart do
require IEx
alias Supermarket.Model.Discount
alias Supermarket.Model.ProductQuantity
alias Supermarket.Model.Receipt
alias Supermarket.Model.SupermarketCatalog

defstruct [:items, :product_quantities]

def new, do: %__MODULE__{items: [], product_quantities: %{}}

def add_item(cart, product) do
add_item_quantity(cart, product, 1.0)
end

def add_item_quantity(cart, product, quantity) do
cart
|> Map.update!(:items, &[ProductQuantity.new(product, quantity) | &1])
|> Map.update!(:product_quantities, fn product_quantities ->
if Map.has_key?(product_quantities, product) do
Map.put(product_quantities, product, product_quantities[product] + quantity)
else
Map.put(product_quantities, product, quantity)
end
end)
end

def handle_offers(cart, receipt, offers, catalog) do
cart.product_quantities
|> Map.keys()
|> Enum.reduce(receipt, fn p, receipt ->
quantity = cart.product_quantities[p]

if Map.has_key?(offers, p) do
offer = offers[p]
unit_price = SupermarketCatalog.get_unit_price(catalog, p)
quantity_as_int = trunc(quantity)
discount = nil
x = 1

{discount, x} =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately I do not know much about Elexir.... Is the cond expression strictly necessary here? In the other languages this is the first step people have to consolidate the code which is more imperative and having different updates in the branches.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that in Elixir variables are immutable (but can be rebound, and shadowed in nested scopes). So if we reproduced the shape of the java code along these lines (there’s no elseif, but cond is similar):

x = 1
cond do
  offer.offer_type == :three_for_two) ->
    x = 3
  offer.offer_type == :two_for_amount ->
    x = 2
    # etc
end

… then x would always end up with the value of 1, because the x = 3 and x = 2 are creating separate variables in their own block scopes. The only way to set a variable based on a conditional is to assign the result of the conditional to the variable.

cond do
offer.offer_type == :three_for_two ->
{discount, 3}

offer.offer_type == :two_for_amount ->
if quantity_as_int >= 2 do
x = 2
int_division = div(quantity_as_int, x)
price_per_unit = offer.argument * int_division
the_total = Integer.mod(quantity_as_int, 2) * unit_price
total = price_per_unit + the_total
discount_n = unit_price * quantity - total
{Discount.new(p, "2 for #{offer.argument}", -discount_n), 2}
else
{discount, x}
end

true ->
{discount, 2}
end

x = if offer.offer_type == :five_for_amount, do: 5, else: x
number_of_xs = div(quantity_as_int, x)

discount =
cond do
offer.offer_type == :three_for_two and quantity_as_int > 2 ->
discount_amount =
quantity * unit_price -
(number_of_xs * 2 * unit_price + Integer.mod(quantity_as_int, 3) * unit_price)

Discount.new(p, "3 for 2", -discount_amount)

offer.offer_type == :ten_percent_discount ->
Discount.new(
p,
"#{offer.argument}% off",
-quantity * unit_price * offer.argument / 100.0
)

offer.offer_type == :five_for_amount and quantity_as_int >= 5 ->
discount_total =
unit_price * quantity -
(offer.argument * number_of_xs + Integer.mod(quantity_as_int, 5) * unit_price)

Discount.new(p, "#{x} for #{offer.argument}", -discount_total)

true ->
discount
end

if !is_nil(discount), do: Receipt.add_discount(receipt, discount), else: receipt
else
receipt
end
end)
end
end
4 changes: 4 additions & 0 deletions elixir/lib/supermarket/model/supermarket_catalog.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
defprotocol Supermarket.Model.SupermarketCatalog do
def add_product(catalog, product, price)
def get_unit_price(catalog, product)
end
34 changes: 34 additions & 0 deletions elixir/lib/supermarket/model/teller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule Supermarket.Model.Teller do
alias Supermarket.Model.Offer
alias Supermarket.Model.Receipt
alias Supermarket.Model.ShoppingCart
alias Supermarket.Model.SupermarketCatalog

defstruct [:catalog, :offers]

def new(catalog) do
%__MODULE__{catalog: catalog, offers: %{}}
end

def add_special_offer(teller, offer_type, product, argument) do
Map.update!(teller, :offers, &Map.put(&1, product, Offer.new(offer_type, product, argument)))
end

def checks_out_articles_from(teller, the_cart) do
receipt = Receipt.new()
product_quantities = the_cart.items

receipt =
product_quantities
|> Enum.reverse()
|> Enum.reduce(receipt, fn pq, receipt ->
p = pq.product
quantity = pq.quantity
unit_price = SupermarketCatalog.get_unit_price(teller.catalog, p)
price = quantity * unit_price
Receipt.add_product(receipt, p, quantity, unit_price, price)
end)

ShoppingCart.handle_offers(the_cart, receipt, teller.offers, teller.catalog)
end
end
32 changes: 32 additions & 0 deletions elixir/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule Supermarket.MixProject do
use Mix.Project

def project do
[
app: :supermarket,
version: "0.1.0",
elixir: "~> 1.15",
start_permanent: Mix.env() == :prod,
deps: deps(),
elixirc_paths: elixirc_paths(Mix.env())
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end

defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_env), do: ["lib"]
end
41 changes: 41 additions & 0 deletions elixir/test/supermarket/model/supermarket_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule Supermarket.Model.SupermarketTest do
use ExUnit.Case, async: true

alias Supermarket.Model.Product
alias Supermarket.Model.Receipt
alias Supermarket.Model.ShoppingCart
alias Supermarket.Model.SupermarketCatalog
alias Supermarket.Model.Teller

# Todo: test all kinds of discounts are applied properly

test "ten percent discount" do
toothbrush = Product.new("toothbrush", :each)
apples = Product.new("apples", :kilo)

catalog =
FakeCatalog.new()
|> SupermarketCatalog.add_product(toothbrush, 0.99)
|> SupermarketCatalog.add_product(apples, 1.99)

teller =
catalog
|> Teller.new()
|> Teller.add_special_offer(:ten_percent_discount, toothbrush, 10.0)

the_cart = ShoppingCart.new() |> ShoppingCart.add_item_quantity(apples, 2.5)

# ACT
receipt = Teller.checks_out_articles_from(teller, the_cart)

# ASSERT
assert_in_delta Receipt.total_price(receipt), 4.975, 0.01
assert receipt.discounts == []
assert length(receipt.items) == 1
receipt_item = List.first(receipt.items)
assert receipt_item.product == apples
assert receipt_item.price == 1.99
assert receipt_item.total_price == 2.5 * 1.99
assert receipt_item.quantity == 2.5
end
end
17 changes: 17 additions & 0 deletions elixir/test/support/fake_catalog.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule FakeCatalog do
defstruct [:products, :prices]

def new, do: %__MODULE__{products: %{}, prices: %{}}

defimpl Supermarket.Model.SupermarketCatalog do
def add_product(catalog, product, price) do
catalog
|> Map.update!(:products, &Map.put(&1, product.name, product))
|> Map.update!(:prices, &Map.put(&1, product.name, price))
end

def get_unit_price(catalog, product) do
catalog.prices[product.name]
end
end
end
1 change: 1 addition & 0 deletions elixir/test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start()