diff --git a/elixir/boutique-suggestions/.exercism/config.json b/elixir/boutique-suggestions/.exercism/config.json new file mode 100644 index 0000000..34bcc79 --- /dev/null +++ b/elixir/boutique-suggestions/.exercism/config.json @@ -0,0 +1,22 @@ +{ + "authors": [ + "neenjaw" + ], + "contributors": [ + "angelikatyborska", + "kevpo" + ], + "files": { + "solution": [ + "lib/boutique_suggestions.ex" + ], + "test": [ + "test/boutique_suggestions_test.exs" + ], + "exemplar": [ + ".meta/exemplar.ex" + ] + }, + "language_versions": ">=1.10", + "blurb": "Learn about list comprehensions by generating outfit suggestions for the clients of your fashion boutique." +} diff --git a/elixir/boutique-suggestions/.exercism/metadata.json b/elixir/boutique-suggestions/.exercism/metadata.json new file mode 100644 index 0000000..8b91225 --- /dev/null +++ b/elixir/boutique-suggestions/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"elixir","exercise":"boutique-suggestions","id":"0efd24fddb7e4fe68fc2d5b3487d94e4","url":"https://exercism.org/tracks/elixir/exercises/boutique-suggestions","handle":"negrienko","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/elixir/boutique-suggestions/.formatter.exs b/elixir/boutique-suggestions/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/elixir/boutique-suggestions/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/elixir/boutique-suggestions/.gitignore b/elixir/boutique-suggestions/.gitignore new file mode 100644 index 0000000..f26d960 --- /dev/null +++ b/elixir/boutique-suggestions/.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"). +list_comprehensions-*.tar + diff --git a/elixir/boutique-suggestions/HELP.md b/elixir/boutique-suggestions/HELP.md new file mode 100644 index 0000000..39009a4 --- /dev/null +++ b/elixir/boutique-suggestions/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_suggestions.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-suggestions/HINTS.md b/elixir/boutique-suggestions/HINTS.md new file mode 100644 index 0000000..ad5736d --- /dev/null +++ b/elixir/boutique-suggestions/HINTS.md @@ -0,0 +1,28 @@ +# Hints + +## General + +- Read about [comprehensions][elixir-comprehensions] in the Getting Started guide. +- Read about [lists][elixir-lists] in the Getting Started guide. +- Read about [maps][elixir-maps] in the Getting Started guide. +- Read about [keyword Lists][elixir-kwlists] in the Getting Started guide. + +## 1. Suggest a combination + +- In the list comprehension, use two _generators_ to create the cartesian product. + +## 2. Filter out clashing outfits + +- You can use _pattern matching_ to deconstruct in the _generator_ to bind the fields to a variable. +- Use a _filter_ expression to return false when the base colors match. + +## 3. Filter by combination price + +- There a number of [keyword list functions][kw-module] available to use to retrieve options from a keyword list. +- Use a _filter_ expression to return false when the combined price is greater than the maximum price. + +[elixir-lists]: https://elixir-lang.org/getting-started/basic-types.html#linked-lists +[elixir-maps]: https://elixir-lang.org/getting-started/keywords-and-maps.html#maps +[elixir-kwlists]: https://elixir-lang.org/getting-started/keywords-and-maps.html#keyword-lists +[elixir-comprehensions]: https://elixir-lang.org/getting-started/comprehensions.html +[kw-module]: https://hexdocs.pm/elixir/Keyword.html \ No newline at end of file diff --git a/elixir/boutique-suggestions/README.md b/elixir/boutique-suggestions/README.md new file mode 100644 index 0000000..cabbe7f --- /dev/null +++ b/elixir/boutique-suggestions/README.md @@ -0,0 +1,136 @@ +# Boutique Suggestions + +Welcome to Boutique Suggestions 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 + +## List Comprehensions + +Comprehensions provide a facility for transforming _Enumerables_ easily and declaratively. + +To declare a very simple comprehension, we can use the `for` keyword followed by a _generator_ and a _do-block_ which creates the new values from the enumerated values. + +```elixir +for n <- [0, 1, 2, 3], do: n + 1 +# => [1, 2, 3, 4] +``` + +Comprehensions can also have _filters_. Values that do not pass the filter are removed from the final list: + +```elixir +for n <- [0, 1, 2, 3], n > 1, do: n + 1 +# => [3, 4] +``` + +We can declare more complicated comprehensions over several lines: + +```elixir +for {atom, number} <- [a: 1, b: 2, c: 3, d: 4], + rem(number, 2) == 0 do + atom +end +# => [:b, :d] +``` + +A _cartesian product_ can be created using multiple generators. That means that each value generated by the first generator will be paired once with each value generated by the second generator: + +```elixir +for x <- [0, 1], + y <- [0, 1] do + {x, y} +end +# => [{0, 0}, {0, 1}, {1, 0}, {1, 1}] +``` + +## Instructions + +Your work at the online fashion boutique store continues. You come up with the idea for a website feature where an outfit is suggested to the user. While you want to give lots of suggestions, you don't want to give bad suggestions, so you decide to use a list comprehension since you can easily _generate_ outfit combinations, then _filter_ them by some criteria. + +Clothing items are stored as a map: + +```elixir +%{ + item_name: "Descriptive Name", + price: 99.00, + base_color: "red" +} +``` + +## 1. Suggest a combination + +Implement `get_combinations/3` to take a list of tops, a list of bottoms, and keyword list of options. For now, set options to default to an empty keyword list. The function should return the cartesian product of the lists. + +```elixir +tops = [ + %{item_name: "Dress shirt"}, + %{item_name: "Casual shirt"} +] +bottoms = [ + %{item_name: "Jeans"}, + %{item_name: "Dress trousers"} +] +BoutiqueSuggestions.get_combinations(tops, bottoms) +# => [ +# {%{item_name: "Dress shirt"}, %{item_name: "Jeans"}}, +# {%{item_name: "Dress shirt"}, %{item_name: "Dress trousers"}}, +# {%{item_name: "Casual shirt"}, %{item_name: "Jeans"}}, +# {%{item_name: "Casual shirt"}, %{item_name: "Dress trousers"}} +# ] +``` + +## 2. Filter out clashing outfits + +Each piece of clothing has a `:base_color` field, use this field to filter out all combinations where the top and the bottom have the same base color. + +```elixir +tops = [ + %{item_name: "Dress shirt", base_color: "blue"}, + %{item_name: "Casual shirt", base_color: "black"} +] +bottoms = [ + %{item_name: "Jeans", base_color: "blue"}, + %{item_name: "Dress trousers", base_color: "black"} +] +BoutiqueSuggestions.get_combinations(tops, bottoms) +# => [ +# {%{item_name: "Dress shirt", base_color: "blue"}, +# %{item_name: "Dress trousers", base_color: "black"}}, +# {%{item_name: "Casual shirt", base_color: "black"}, +# %{item_name: "Jeans", base_color: "blue"}} +# ] +``` + +## 3. Filter by combination price + +Each piece of clothing has a `:price` field associated with it. While you want to give lots of suggestions, you want to be able to provide users an opportunity to select a price within their budget. From the keyword list of options, use `:maximum_price` to filter out combinations where the price of the top and bottom exceed the maximum price. + +If no maximum_price is specified, the default should be `100.00` + +```elixir +tops = [ + %{item_name: "Dress shirt", base_color: "blue", price: 35}, + %{item_name: "Casual shirt", base_color: "black", price: 20} +] +bottoms = [ + %{item_name: "Jeans", base_color: "blue", price: 30}, + %{item_name: "Dress trousers", base_color: "black", price: 75} +] +BoutiqueSuggestions.get_combinations(tops, bottoms, maximum_price: 50) +# => [ +# {%{item_name: "Casual shirt", base_color: "black", price: 20}, +# %{item_name: "Jeans", base_color: "blue", price: 30}} +# ] +``` + +## Source + +### Created by + +- @neenjaw + +### Contributed to by + +- @angelikatyborska +- @kevpo \ No newline at end of file diff --git a/elixir/boutique-suggestions/lib/boutique_suggestions.ex b/elixir/boutique-suggestions/lib/boutique_suggestions.ex new file mode 100644 index 0000000..5ffd47a --- /dev/null +++ b/elixir/boutique-suggestions/lib/boutique_suggestions.ex @@ -0,0 +1,23 @@ +defmodule BoutiqueSuggestions do + @moduledoc """ + Your work at the online fashion boutique store continues. You come up with the idea for a website feature where an outfit is suggested to the user. While you want to give lots of suggestions, you don't want to give bad suggestions, so you decide to use a list comprehension since you can easily generate outfit combinations, then filter them by some criteria. + """ + + @type boutique_item() :: %{item_name: String.t(), price: float(), base_color: String.t()} + + @doc """ + Generate carthesian list of recomended product pairs Filter out clashing outfits. Using maximum_price in the options keyword list for filter products by price + """ + + @spec get_combinations([boutique_item()], [boutique_item()], Keyword.t()) :: [{boutique_item(), boutique_item()}] + def get_combinations(tops, bottoms, options \\ [{:maximum_price, 100}]) do + maximum_price = Keyword.get(options, :maximum_price, 100.0) + + for top <- tops, + bottom <- bottoms, + top.base_color != bottom.base_color, + top.price + bottom.price <= maximum_price do + {top, bottom} + end + end +end diff --git a/elixir/boutique-suggestions/mix.exs b/elixir/boutique-suggestions/mix.exs new file mode 100644 index 0000000..7ae91f1 --- /dev/null +++ b/elixir/boutique-suggestions/mix.exs @@ -0,0 +1,28 @@ +defmodule BoutiqueSuggestions.MixProject do + use Mix.Project + + def project do + [ + app: :boutique_suggestions, + 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-suggestions/test/boutique_suggestions_test.exs b/elixir/boutique-suggestions/test/boutique_suggestions_test.exs new file mode 100644 index 0000000..e3a8a33 --- /dev/null +++ b/elixir/boutique-suggestions/test/boutique_suggestions_test.exs @@ -0,0 +1,204 @@ +defmodule BoutiqueSuggestionsTest do + use ExUnit.Case + + @tag task_id: 1 + test "the third argument, options, is optional" do + assert BoutiqueSuggestions.get_combinations([], []) + end + + @tag task_id: 1 + test "generates one pair from one top and one bottom" do + top = %{ + item_name: "Long Sleeve T-shirt", + price: 19.95, + color: "Deep Red", + base_color: "red" + } + + bottom = %{ + item_name: "Wonderwall Pants", + price: 48.97, + color: "French Navy", + base_color: "blue" + } + + assert BoutiqueSuggestions.get_combinations([top], [bottom]) == [{top, bottom}] + end + + @tag task_id: 1 + test "generates all pairs from two top and two bottom" do + top1 = %{ + item_name: "Long Sleeve T-shirt", + price: 19.95, + color: "Deep Red", + base_color: "red" + } + + top2 = %{ + item_name: "Brushwood Shirt", + price: 19.10, + color: "Camel-Sandstone Woodland Plaid", + base_color: "brown" + } + + bottom1 = %{ + item_name: "Wonderwall Pants", + price: 48.97, + color: "French Navy", + base_color: "blue" + } + + bottom2 = %{ + item_name: "Terrena Stretch Pants", + price: 79.95, + color: "Cast Iron", + base_color: "grey" + } + + tops = [top1, top2] + bottoms = [bottom1, bottom2] + expected = [{top1, bottom1}, {top1, bottom2}, {top2, bottom1}, {top2, bottom2}] + assert BoutiqueSuggestions.get_combinations(tops, bottoms) == expected + end + + @tag task_id: 2 + test "does not create suggestions that 'clash'" do + top = %{ + item_name: "Long Sleeve T-shirt", + price: 19.95, + color: "Deep Red", + base_color: "red" + } + + bottom = %{ + item_name: "Happy Hike Studio Pants", + price: 19.00, + color: "Ochre Red", + base_color: "red" + } + + assert BoutiqueSuggestions.get_combinations([top], [bottom]) == [] + end + + @tag task_id: 3 + test "accepts keyword list for third argument for options" do + assert BoutiqueSuggestions.get_combinations([], [], maximum_price: 200.00) + end + + @tag task_id: 3 + test "filter rejects combinations based on combined maximum price" do + top = %{ + item_name: "Sano Long Sleeve Shirt", + price: 45.47, + color: "Linen Chambray", + base_color: "yellow" + } + + bottom = %{ + item_name: "Happy Hike Studio Pants", + price: 99.00, + color: "Ochre Red", + base_color: "red" + } + + assert BoutiqueSuggestions.get_combinations([top], [bottom], maximum_price: 100.00) == [] + end + + @tag task_id: 3 + test "filter accepts combinations based on combined maximum price" do + top = %{ + item_name: "Sano Long Sleeve Shirt", + price: 45.47, + color: "Linen Chambray", + base_color: "yellow" + } + + bottom = %{ + item_name: "Happy Hike Studio Pants", + price: 99.00, + color: "Ochre Red", + base_color: "red" + } + + assert BoutiqueSuggestions.get_combinations([top], [bottom], maximum_price: 200.00) == [ + {top, bottom} + ] + end + + @tag task_id: 3 + test "provides default when maximum_price option not specified" do + top = %{ + item_name: "Sano Long Sleeve Shirt", + price: 45.47, + color: "Linen Chambray", + base_color: "yellow" + } + + bottom = %{ + item_name: "Happy Hike Studio Pants", + price: 99.00, + color: "Ochre Red", + base_color: "red" + } + + assert BoutiqueSuggestions.get_combinations([top], [bottom], other_option: "test") == [] + end + + @tag task_id: 3 + test "putting it all together" do + top1 = %{ + item_name: "Long Sleeve T-shirt", + price: 19.95, + color: "Deep Red", + base_color: "red" + } + + top2 = %{ + item_name: "Brushwood Shirt", + price: 19.10, + color: "Camel-Sandstone Woodland Plaid", + base_color: "brown" + } + + top3 = %{ + item_name: "Sano Long Sleeve Shirt", + price: 45.47, + color: "Linen Chambray", + base_color: "yellow" + } + + bottom1 = %{ + item_name: "Wonderwall Pants", + price: 48.97, + color: "French Navy", + base_color: "blue" + } + + bottom2 = %{ + item_name: "Terrena Stretch Pants", + price: 79.95, + color: "Cast Iron", + base_color: "grey" + } + + bottom3 = %{ + item_name: "Happy Hike Studio Pants", + price: 99.00, + color: "Ochre Red", + base_color: "red" + } + + tops = [top1, top2, top3] + bottoms = [bottom1, bottom2, bottom3] + + expected = [ + {top1, bottom1}, + {top1, bottom2}, + {top2, bottom1}, + {top2, bottom2}, + {top3, bottom1} + ] + + assert BoutiqueSuggestions.get_combinations(tops, bottoms) == expected + end +end diff --git a/elixir/boutique-suggestions/test/test_helper.exs b/elixir/boutique-suggestions/test/test_helper.exs new file mode 100644 index 0000000..e8677a3 --- /dev/null +++ b/elixir/boutique-suggestions/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true, seed: 0)