boutique-inventory

This commit is contained in:
Danil Negrienko 2023-12-19 23:54:39 -05:00
parent f324155864
commit ec644472b3
11 changed files with 564 additions and 0 deletions

View File

@ -0,0 +1,22 @@
{
"authors": [
"angelikatyborska"
],
"contributors": [
"neenjaw",
"fmmatheus"
],
"files": {
"solution": [
"lib/boutique_inventory.ex"
],
"test": [
"test/boutique_inventory_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
]
},
"language_versions": ">=1.10",
"blurb": "Learn about the Enum module by preparing your fashion boutique for the big annual sale."
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"boutique-inventory","id":"cdb368dfafa84deaaad78b0b83747be0","url":"https://exercism.org/tracks/elixir/exercises/boutique-inventory","handle":"negrienko","is_requester":true,"auto_approve":false}

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
elixir/boutique-inventory/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# 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").
nil-*.tar

View File

@ -0,0 +1,75 @@
# Help
## Running the tests
From the terminal, change to the base directory of the exercise then execute the tests with:
```bash
$ mix test
```
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
Documentation:
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
## Pending tests
In test suites of practice exercises, all but the first test have been tagged to be skipped.
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
For example:
```elixir
# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
```
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
```bash
$ mix test --include pending
```
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
```elixir
# ExUnit.configure(exclude: :pending, trace: true)
```
## Useful `mix test` options
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
* `--failed` - runs only tests that failed the last time they ran
* `--max-failures` - the suite stops evaluating tests when this number of test failures
is reached
* `--seed 0` - disables randomization so the tests in a single file will always be ran
in the same order they were defined in
## Submitting your solution
You can submit your solution using the `exercism submit lib/boutique_inventory.ex` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
- The [Elixir track's programming category on the forum](https://forum.exercism.org/c/programming/elixir)
- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.

View File

@ -0,0 +1,42 @@
# Hints
## General
- Read about the `Enum` module in the [official Getting Started guide][getting-started-enum] or on [elixirschool.com][elixir-school-enum].
- Take a look in the documentation for the full [list of functions in the `Enum` module][enum-functions].
## 1. Sort items by price
- There is a [built-in function][enum-sort-by] for sorting enumerables using a sorter function.
## 2. Find all items with missing prices
- There is a [built-in function][enum-filter] for filtering enumerables.
## 3. Update item names
- There is a [built-in function][enum-map] for transforming every element in an enumerable.
- There is a [built-in function][string-replace] that can replace all instances of one string with a different one.
## 4. Increment the item's quantity
- Maps implement the enumerable protocol.
- `Enum` functions convert maps to a list of `{key, value}` tuples.
- There are two different functions that can transform a list of `{key, value}` tuples back into a map using a transformation function. [One of them always returns a new map][map-new], while [the other lets you choose a collectible][enum-into].
## 5. Calculate the item's total quantity
- Maps implement the enumerable protocol.
- `Enum` functions convert maps to a list of `{key, value}` tuples.
- There is a [built-in function][enum-reduce] for reducing an enumerable to a single value.
[getting-started-enum]: https://elixir-lang.org/getting-started/enumerables-and-streams.html#enumerables
[elixir-school-enum]: https://elixirschool.com/en/lessons/basics/enum/
[enum-functions]: https://hexdocs.pm/elixir/Enum.html#functions
[enum-sort-by]: https://hexdocs.pm/elixir/Enum.html#sort_by/3
[enum-filter]: https://hexdocs.pm/elixir/Enum.html#filter/2
[enum-map]: https://hexdocs.pm/elixir/Enum.html#map/2
[enum-into]: https://hexdocs.pm/elixir/Enum.html#into/3
[enum-reduce]: https://hexdocs.pm/elixir/Enum.html#reduce/3
[map-new]: https://hexdocs.pm/elixir/Map.html#new/2
[string-replace]: https://hexdocs.pm/elixir/String.html#replace/4

View File

@ -0,0 +1,175 @@
# Boutique Inventory
Welcome to Boutique Inventory on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
## Introduction
## Enum
`Enum` is a very useful module that provides a set of algorithms for working with enumerables. It offers sorting, filtering, grouping, counting, searching, finding min/max values, and much more.
In general, an _enumerable_ is any data that can be iterated over, a collection. In Elixir, an enumerable is any data type that implements the `Enumerable` [protocol][exercism-protocols]. The most common of those are [lists][exercism-lists] and [maps][exercism-maps].
Many `Enum` functions accept a function as an argument.
```elixir
Enum.all?([1, 2, 3, 4, 5], fn x -> x > 3 end)
# => false
```
The most common `Enum` functions are `map` and `reduce`.
### `map/2`
`Enum.map/2` allows you to replace every element in an enumerable with another element. The second argument to `Enum.map/2` is a function that accepts the original element and returns its replacement.
### `reduce/3`
`Enum.reduce/3` allows you to _reduce_ the whole enumerable to a single value. To achieve this, a special variable called the _accumulator_ is used. The accumulator carries the intermediate state of the reduction between iterations.
The second argument to `Enum.reduce/3` is the initial value of the accumulator. The third argument is a function that accepts an element and an accumulator, and returns the new value for the accumulator.
### Working with maps
When using maps with `Enum` functions, the map gets automatically converted to a list of 2 `{key, value}` tuples.
To transform it back to a map, use `Enum.into/2`. `Enum.into/2` is a function that transforms an enumerable into a collectable - any data structure implementing the `Collectable` protocol. It can be thought of as the opposite of `Enum.reduce/3`.
`Enum` also has `Enum.into/3`. `Enum.into/3` is a variation of `Enum.into/2` that accepts a transformation function to be applied while transforming the enumerable into a collectable.
#### Mapping maps
Instead of using `Enum.into/3` or `Enum.map/2` plus `Enum.into/1` to apply a transformation (mapping) to a map, we can also use a dedicated `Map.new/2` function. It works exactly like `Enum.into/3` in that it accepts an enumerable and a transformation function, but it always returns a new map instead of letting us choose a collectible.
[exercism-protocols]: https://exercism.org/tracks/elixir/concepts/protocols
[exercism-lists]: https://exercism.org/tracks/elixir/concepts/lists
[exercism-maps]: https://exercism.org/tracks/elixir/concepts/maps
## Instructions
You are running an online fashion boutique. Your big annual sale is coming up, so you need to take stock of your inventory to make sure you're ready.
A single item in the inventory is represented by a map, and the whole inventory is a list of such maps.
```elixir
%{
name: "White Shirt",
price: 40,
quantity_by_size: %{s: 3, m: 7, l: 8, xl: 4}
}
```
## 1. Sort items by price
Implement the `sort_by_price/1` function. It should take the inventory and return it sorted by item price, ascending.
```elixir
BoutiqueInventory.sort_by_price([
%{price: 65, name: "Maxi Brown Dress", quantity_by_size: %{}},
%{price: 50, name: "Red Short Skirt", quantity_by_size: %{}},
%{price: 50, name: "Black Short Skirt", quantity_by_size: %{}},
%{price: 20, name: "Bamboo Socks Cats", quantity_by_size: %{}}
])
# => [
# %{price: 20, name: "Bamboo Socks Cats", quantity_by_size: %{}},
# %{price: 50, name: "Red Short Skirt", quantity_by_size: %{}},
# %{price: 50, name: "Black Short Skirt", quantity_by_size: %{}},
# %{price: 65, name: "Maxi Brown Dress", price: 65, quantity_by_size: %{}}
# ]
```
## 2. Find all items with missing prices
After sorting your inventory by price, you noticed that you must have made a mistake when you were taking stock and forgot to fill out prices for a few items.
Implement the `with_missing_price/1` function. It should take the inventory and return a list of items that do not have prices.
```elixir
BoutiqueInventory.with_missing_price([
%{price: 40, name: "Black T-shirt", quantity_by_size: %{}},
%{price: nil, name: "Denim Pants", quantity_by_size: %{}},
%{price: nil, name: "Denim Skirt", quantity_by_size: %{}},
%{price: 40, name: "Orange T-shirt", quantity_by_size: %{}}
])
# => [
# %{price: nil, name: "Denim Pants", quantity_by_size: %{}},
# %{price: nil, name: "Denim Skirt", quantity_by_size: %{}}
# ]
```
## 3. Update item names
You noticed that some item names have a word that you don't like to use anymore. Now you need to update all the item names with that word.
Implement the `update_names/3` function. It should take the inventory, the old word that you want to remove, and a new word that you want to use instead. It should return a list of items with updated names.
```elixir
BoutiqueInventory.update_names(
[
%{price: 40, name: "Black T-shirt", quantity_by_size: %{}},
%{price: 70, name: "Denim Pants", quantity_by_size: %{}},
%{price: 65, name: "Denim Skirt", quantity_by_size: %{}},
%{price: 40, name: "Orange T-shirt", quantity_by_size: %{}}
],
"T-shirt",
"Tee"
)
# => [
# %{price: 40, name: "Black Tee", quantity_by_size: %{}},
# %{price: 70, name: "Denim Pants", quantity_by_size: %{}},
# %{price: 65, name: "Denim Skirt", quantity_by_size: %{}},
# %{price: 40, name: "Orange Tee", quantity_by_size: %{}}
# ]
```
## 4. Increment the item's quantity
Some items were selling especially well, so you ordered more, in all sizes.
Implement the `increase_quantity/2` function. It should take a single item and a number `n`, and return that item with the quantity for each size increased by `n`.
```elixir
BoutiqueInventory.increase_quantity(
%{
name: "Polka Dot Skirt",
price: 68,
quantity_by_size: %{s: 3, m: 5, l: 3, xl: 4}
},
6
)
# => %{
# name: "Polka Dot Skirt",
# price: 68,
# quantity_by_size: %{l: 9, m: 11, s: 9, xl: 10}
# }
```
## 5. Calculate the item's total quantity
To know how much space you need in your storage, you need to know how many of each item you have in total.
Implement the `total_quantity/1` function. It should take a single item and return how many pieces you have in total, in any size.
```elixir
BoutiqueInventory.total_quantity(%{
name: "Red Shirt",
price: 62,
quantity_by_size: %{s: 3, m: 6, l: 5, xl: 2}
})
# => 16
```
## Source
### Created by
- @angelikatyborska
### Contributed to by
- @neenjaw
- @fmmatheus

View File

@ -0,0 +1,41 @@
defmodule BoutiqueInventory do
def sort_by_price(inventory) do
Enum.sort_by(inventory, & &1[:price])
end
def with_missing_price(inventory) do
Enum.filter(inventory, &(!&1[:price]))
end
def update_names(inventory, old_word, new_word) do
inventory
|> Enum.map(fn item ->
new_name = String.replace(item.name, old_word, new_word)
%{item | name: new_name}
end)
end
def increase_quantity(item, count) do
item
|> Map.get_and_update(:quantity_by_size, fn quantities ->
{quantities, update_quantities(quantities, count)}
end)
|> elem(1)
end
defp update_quantities(quantities, count) do
quantities
|> Map.to_list()
|> Enum.map(&increase_size_quantity(&1, count))
|> Enum.into(%{})
end
defp increase_size_quantity({key, value}, count), do: {key, value + count}
def total_quantity(item) do
item.quantity_by_size
|> Enum.reduce(0, &sum_total_quantity/2)
end
defp sum_total_quantity({_key, value}, acc), do: acc + value
end

View File

@ -0,0 +1,28 @@
defmodule BoutiqueInventory.MixProject do
use Mix.Project
def project do
[
app: :boutique_inventory,
version: "0.1.0",
# elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps()
]
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
end

View File

@ -0,0 +1,150 @@
defmodule BoutiqueInventoryTest do
use ExUnit.Case
describe "sort_by_price/1" do
@tag task_id: 1
test "works for an empty inventory" do
assert BoutiqueInventory.sort_by_price([]) == []
end
@tag task_id: 1
test "sorts items by price" do
assert BoutiqueInventory.sort_by_price([
%{price: 65, name: "Maxi Yellow Summer Dress", quantity_by_size: %{}},
%{price: 60, name: "Cream Linen Pants", quantity_by_size: %{}},
%{price: 33, name: "Straw Hat", quantity_by_size: %{}}
]) == [
%{price: 33, name: "Straw Hat", quantity_by_size: %{}},
%{price: 60, name: "Cream Linen Pants", quantity_by_size: %{}},
%{price: 65, name: "Maxi Yellow Summer Dress", quantity_by_size: %{}}
]
end
@tag task_id: 1
test "the order of items of equal price is preserved" do
assert BoutiqueInventory.sort_by_price([
%{price: 65, name: "Maxi Yellow Summer Dress", quantity_by_size: %{}},
%{price: 60, name: "Cream Linen Pants", quantity_by_size: %{}},
%{price: 33, name: "Straw Hat", quantity_by_size: %{}},
%{price: 60, name: "Brown Linen Pants", quantity_by_size: %{}}
]) == [
%{price: 33, name: "Straw Hat", quantity_by_size: %{}},
%{price: 60, name: "Cream Linen Pants", quantity_by_size: %{}},
%{price: 60, name: "Brown Linen Pants", quantity_by_size: %{}},
%{price: 65, name: "Maxi Yellow Summer Dress", quantity_by_size: %{}}
]
end
end
describe "with_missing_price/1" do
@tag task_id: 2
test "works for an empty inventory" do
assert BoutiqueInventory.with_missing_price([]) == []
end
@tag task_id: 2
test "filters out items that do have a price" do
assert BoutiqueInventory.with_missing_price([
%{name: "Red Flowery Top", price: 50, quantity_by_size: %{}},
%{name: "Purple Flowery Top", price: nil, quantity_by_size: %{}},
%{name: "Bamboo Socks Avocado", price: 10, quantity_by_size: %{}},
%{name: "Bamboo Socks Palm Trees", price: 10, quantity_by_size: %{}},
%{name: "Bamboo Socks Kittens", price: nil, quantity_by_size: %{}}
]) == [
%{name: "Purple Flowery Top", price: nil, quantity_by_size: %{}},
%{name: "Bamboo Socks Kittens", price: nil, quantity_by_size: %{}}
]
end
end
describe "update_names/3" do
@tag task_id: 3
test "works for an empty inventory" do
assert BoutiqueInventory.update_names([], "T-Shirt", "Tee") == []
end
@tag task_id: 3
test "replaces the word in all the names" do
assert BoutiqueInventory.update_names(
[
%{name: "Bambo Socks Avocado", price: 10, quantity_by_size: %{}},
%{name: "3x Bambo Socks Palm Trees", price: 26, quantity_by_size: %{}},
%{name: "Red Sequin Top", price: 87, quantity_by_size: %{}}
],
"Bambo",
"Bamboo"
) == [
%{name: "Bamboo Socks Avocado", price: 10, quantity_by_size: %{}},
%{name: "3x Bamboo Socks Palm Trees", price: 26, quantity_by_size: %{}},
%{name: "Red Sequin Top", price: 87, quantity_by_size: %{}}
]
end
@tag task_id: 3
test "replaces all the instances of the word within one name" do
assert BoutiqueInventory.update_names(
[
%{name: "GO! GO! GO! Tee", price: 8, quantity_by_size: %{}}
],
"GO!",
"Go!"
) == [
%{name: "Go! Go! Go! Tee", price: 8, quantity_by_size: %{}}
]
end
end
describe "increase_quantity/2" do
@tag task_id: 4
test "works for an empty quantity map" do
assert BoutiqueInventory.increase_quantity(
%{
name: "Long Black Evening Dress",
price: 105,
quantity_by_size: %{}
},
1
) == %{
name: "Long Black Evening Dress",
price: 105,
quantity_by_size: %{}
}
end
@tag task_id: 4
test "increases quantity of an item" do
assert BoutiqueInventory.increase_quantity(
%{
name: "Green Swimming Shorts",
price: 46,
quantity_by_size: %{s: 1, m: 2, l: 4, xl: 1}
},
3
) == %{
name: "Green Swimming Shorts",
price: 46,
quantity_by_size: %{s: 4, m: 5, l: 7, xl: 4}
}
end
end
describe "total_quantity/1" do
@tag task_id: 5
test "works for an empty quantity map" do
assert BoutiqueInventory.total_quantity(%{
name: "Red Denim Pants",
price: 77,
quantity_by_size: %{}
}) == 0
end
@tag task_id: 5
test "sums up total quantity" do
assert BoutiqueInventory.total_quantity(%{
name: "Black Denim Skirt",
price: 50,
quantity_by_size: %{s: 4, m: 11, l: 6, xl: 8}
}) == 29
end
end
end

View File

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true, seed: 0)