From ec644472b3074453fe49f67bd6208e32ce6de1b4 Mon Sep 17 00:00:00 2001 From: Danylo Negrienko Date: Tue, 19 Dec 2023 23:54:39 -0500 Subject: [PATCH] boutique-inventory --- .../boutique-inventory/.exercism/config.json | 22 +++ .../.exercism/metadata.json | 1 + elixir/boutique-inventory/.formatter.exs | 4 + elixir/boutique-inventory/.gitignore | 24 +++ elixir/boutique-inventory/HELP.md | 75 ++++++++ elixir/boutique-inventory/HINTS.md | 42 +++++ elixir/boutique-inventory/README.md | 175 ++++++++++++++++++ .../lib/boutique_inventory.ex | 41 ++++ elixir/boutique-inventory/mix.exs | 28 +++ .../test/boutique_inventory_test.exs | 150 +++++++++++++++ .../boutique-inventory/test/test_helper.exs | 2 + 11 files changed, 564 insertions(+) create mode 100644 elixir/boutique-inventory/.exercism/config.json create mode 100644 elixir/boutique-inventory/.exercism/metadata.json create mode 100644 elixir/boutique-inventory/.formatter.exs create mode 100644 elixir/boutique-inventory/.gitignore create mode 100644 elixir/boutique-inventory/HELP.md create mode 100644 elixir/boutique-inventory/HINTS.md create mode 100644 elixir/boutique-inventory/README.md create mode 100644 elixir/boutique-inventory/lib/boutique_inventory.ex create mode 100644 elixir/boutique-inventory/mix.exs create mode 100644 elixir/boutique-inventory/test/boutique_inventory_test.exs create mode 100644 elixir/boutique-inventory/test/test_helper.exs diff --git a/elixir/boutique-inventory/.exercism/config.json b/elixir/boutique-inventory/.exercism/config.json new file mode 100644 index 0000000..744fe25 --- /dev/null +++ b/elixir/boutique-inventory/.exercism/config.json @@ -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." +} diff --git a/elixir/boutique-inventory/.exercism/metadata.json b/elixir/boutique-inventory/.exercism/metadata.json new file mode 100644 index 0000000..0e3aa4f --- /dev/null +++ b/elixir/boutique-inventory/.exercism/metadata.json @@ -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} \ No newline at end of file diff --git a/elixir/boutique-inventory/.formatter.exs b/elixir/boutique-inventory/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/elixir/boutique-inventory/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/elixir/boutique-inventory/.gitignore b/elixir/boutique-inventory/.gitignore new file mode 100644 index 0000000..cb54963 --- /dev/null +++ b/elixir/boutique-inventory/.gitignore @@ -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 + diff --git a/elixir/boutique-inventory/HELP.md b/elixir/boutique-inventory/HELP.md new file mode 100644 index 0000000..0ac41b3 --- /dev/null +++ b/elixir/boutique-inventory/HELP.md @@ -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/.exs:LINENUM` - runs only a single test, the test from `.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. \ No newline at end of file diff --git a/elixir/boutique-inventory/HINTS.md b/elixir/boutique-inventory/HINTS.md new file mode 100644 index 0000000..0456824 --- /dev/null +++ b/elixir/boutique-inventory/HINTS.md @@ -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 \ No newline at end of file diff --git a/elixir/boutique-inventory/README.md b/elixir/boutique-inventory/README.md new file mode 100644 index 0000000..00f8f0a --- /dev/null +++ b/elixir/boutique-inventory/README.md @@ -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 \ No newline at end of file diff --git a/elixir/boutique-inventory/lib/boutique_inventory.ex b/elixir/boutique-inventory/lib/boutique_inventory.ex new file mode 100644 index 0000000..a022cf6 --- /dev/null +++ b/elixir/boutique-inventory/lib/boutique_inventory.ex @@ -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 diff --git a/elixir/boutique-inventory/mix.exs b/elixir/boutique-inventory/mix.exs new file mode 100644 index 0000000..86e51d0 --- /dev/null +++ b/elixir/boutique-inventory/mix.exs @@ -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 diff --git a/elixir/boutique-inventory/test/boutique_inventory_test.exs b/elixir/boutique-inventory/test/boutique_inventory_test.exs new file mode 100644 index 0000000..39dfce1 --- /dev/null +++ b/elixir/boutique-inventory/test/boutique_inventory_test.exs @@ -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 diff --git a/elixir/boutique-inventory/test/test_helper.exs b/elixir/boutique-inventory/test/test_helper.exs new file mode 100644 index 0000000..e8677a3 --- /dev/null +++ b/elixir/boutique-inventory/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true, seed: 0)