boutique-suggestions

This commit is contained in:
Danil Negrienko 2023-12-23 00:17:15 -05:00
parent cd5d83851d
commit e2e3430ef8
11 changed files with 547 additions and 0 deletions

View File

@ -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."
}

View File

@ -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}

View File

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

24
elixir/boutique-suggestions/.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").
list_comprehensions-*.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_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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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