diff --git a/elixir/basketball-website/.exercism/config.json b/elixir/basketball-website/.exercism/config.json new file mode 100644 index 0000000..ec5f089 --- /dev/null +++ b/elixir/basketball-website/.exercism/config.json @@ -0,0 +1,22 @@ +{ + "authors": [ + "neenjaw" + ], + "contributors": [ + "angelikatyborska", + "NobbZ" + ], + "files": { + "solution": [ + "lib/basketball_website.ex" + ], + "test": [ + "test/basketball_website_test.exs" + ], + "exemplar": [ + ".meta/exemplar.ex" + ] + }, + "language_versions": ">=1.10", + "blurb": "Learn about Access Behaviour by helping extract deeply nested data for the basketball team's website." +} diff --git a/elixir/basketball-website/.exercism/metadata.json b/elixir/basketball-website/.exercism/metadata.json new file mode 100644 index 0000000..c30e617 --- /dev/null +++ b/elixir/basketball-website/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"elixir","exercise":"basketball-website","id":"740e1842211445898b26c211cbf40f0c","url":"https://exercism.org/tracks/elixir/exercises/basketball-website","handle":"negrienko","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/elixir/basketball-website/.formatter.exs b/elixir/basketball-website/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/elixir/basketball-website/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/elixir/basketball-website/.gitignore b/elixir/basketball-website/.gitignore new file mode 100644 index 0000000..d5af801 --- /dev/null +++ b/elixir/basketball-website/.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"). +access-*.tar + diff --git a/elixir/basketball-website/HELP.md b/elixir/basketball-website/HELP.md new file mode 100644 index 0000000..b58f92e --- /dev/null +++ b/elixir/basketball-website/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/basketball_website.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/basketball-website/HINTS.md b/elixir/basketball-website/HINTS.md new file mode 100644 index 0000000..e0a5044 --- /dev/null +++ b/elixir/basketball-website/HINTS.md @@ -0,0 +1,18 @@ +# Hints + +## General + +- Read about the [`Access` behaviour][access-behaviour] in the documentation. + +## 1. Extract data from a nested map structure + +- First tokenize the string path to a usable state. You can make use of [`String` module functions][string-module]. +- Write a recursive function to traverse the nested-map structure to retrieve the value or return `nil`. + +## 2. Refactor using included functions + +- Read through the various [`Kernel` module functions][kernel-module], to find one that might shorten/simplify your approach. + +[kernel-module]: https://hexdocs.pm/elixir/Kernel.html#functions +[string-module]: https://hexdocs.pm/elixir/String.html#functions +[access-behaviour]: https://hexdocs.pm/elixir/Access.html \ No newline at end of file diff --git a/elixir/basketball-website/README.md b/elixir/basketball-website/README.md new file mode 100644 index 0000000..a784236 --- /dev/null +++ b/elixir/basketball-website/README.md @@ -0,0 +1,82 @@ +# Basketball Website + +Welcome to Basketball Website 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 + +## Access Behaviour + +Elixir uses code _Behaviours_ to provide common generic interfaces while facilitating specific implementations for each module which implements it. One such common example is the _Access Behaviour_. + +The _Access Behaviour_ provides a common interface for retrieving data from a key-based data structure. The _Access Behaviour_ is implemented for maps and keyword lists, but let's look at its use for maps to get a feel for it. _Access Behaviour_ specifies that when you have a map, you may follow it with _square brackets_ and then use the key to retrieve the value associated with that key. + +```elixir +# Suppose we have these two maps defined (note the difference in the key type) +my_map = %{key: "my value"} +your_map = %{"key" => "your value"} + +# Obtain the value using the Access Behaviour +my_map[:key] == "my value" +your_map[:key] == nil +your_map["key"] == "your value" +``` + +If the key does not exist in the data structure, then `nil` is returned. This can be a source of unintended behavior, because it does not raise an error. Note that `nil` itself implements the Access Behaviour and always returns `nil` for any key. + +## Instructions + +You are working with a web development team to maintain a website for a local basketball team. The web development team is less familiar with Elixir and is asking for a function to be able to extract data from a series of nested maps to facilitate rapid development. + +## 1. Extract data from a nested map structure + +Implement the `extract_from_path/2` function to take two arguments: + +- `data`: a nested map structure with data about the basketball team. +- `path`: a string consisting of period-delimited keys to obtain the value associated with the last key. + +If the value or the key does not exist at any point in the path, `nil` should be returned + +```elixir +data = %{ + "team_mascot" => %{ + "animal" => "bear", + "actor" => %{ + "first_name" => "Noel" + } + } +} +BasketballWebsite.extract_from_path(data, "team_mascot.animal") +# => "bear" +BasketballWebsite.extract_from_path(data, "team_mascot.colors") +# => nil +``` + +Use the _Access Behaviour_ when implementing this function. + +Do not use any `Map` or `Kernel` module functions for working with the nested map data structure. + +## 2. Refactor using included functions + +Your coworker reviewing your code tells you about a `Kernel` module function which does something very similar to your implementation. + +Implement `get_in_path/2` to use this `Kernel` module function. + +The arguments expected are the same as part 1. + +```elixir +BasketballWebsite.get_in_path(data, "team_mascot.actor.first_name") +# => "Noel" +``` + +## Source + +### Created by + +- @neenjaw + +### Contributed to by + +- @angelikatyborska +- @NobbZ \ No newline at end of file diff --git a/elixir/basketball-website/lib/basketball_website.ex b/elixir/basketball-website/lib/basketball_website.ex new file mode 100644 index 0000000..7e498e7 --- /dev/null +++ b/elixir/basketball-website/lib/basketball_website.ex @@ -0,0 +1,15 @@ +defmodule BasketballWebsite do + def extract_from_path(data, path) do + path + |> String.split(".") + |> get_path(data) + end + + defp get_path(_list, nil), do: nil + defp get_path([], data), do: data + defp get_path([head | tail], data), do: get_path(tail, data[head]) + + def get_in_path(data, path) do + get_in(data, String.split(path, ".")) + end +end diff --git a/elixir/basketball-website/mix.exs b/elixir/basketball-website/mix.exs new file mode 100644 index 0000000..e653239 --- /dev/null +++ b/elixir/basketball-website/mix.exs @@ -0,0 +1,28 @@ +defmodule BasketballWebsite.MixProject do + use Mix.Project + + def project do + [ + app: :basketball_website, + 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/basketball-website/test/basketball_website_test.exs b/elixir/basketball-website/test/basketball_website_test.exs new file mode 100644 index 0000000..5224806 --- /dev/null +++ b/elixir/basketball-website/test/basketball_website_test.exs @@ -0,0 +1,335 @@ +defmodule BasketballWebsiteTest do + use ExUnit.Case + + describe "extract_from_path retrieves from" do + @tag task_id: 1 + test "first layer" do + team_data = %{ + "coach" => %{}, + "team_name" => "Hoop Masters", + "players" => %{} + } + + assert BasketballWebsite.extract_from_path(team_data, "team_name") == "Hoop Masters" + end + + @tag task_id: 1 + test "second layer" do + team_data = %{ + "coach" => %{ + "first_name" => "Jane", + "last_name" => "Brown" + }, + "team_name" => "Hoop Masters", + "players" => %{} + } + + assert BasketballWebsite.extract_from_path(team_data, "coach.first_name") == "Jane" + end + + @tag task_id: 1 + test "third layer" do + team_data = %{ + "coach" => %{}, + "team_name" => "Hoop Masters", + "players" => %{ + "99" => %{ + "first_name" => "Amalee", + "last_name" => "Tynemouth", + "email" => "atynemouth0@yellowpages.com", + "statistics" => %{} + }, + "98" => %{ + "first_name" => "Tiffie", + "last_name" => "Derle", + "email" => "tderle1@vimeo.com", + "statistics" => %{} + } + } + } + + assert BasketballWebsite.extract_from_path(team_data, "players.99.first_name") == "Amalee" + end + + @tag task_id: 1 + test "fourth layer" do + team_data = %{ + "coach" => %{}, + "team_name" => "Hoop Masters", + "players" => %{ + "42" => %{ + "first_name" => "Conchita", + "last_name" => "Elham", + "email" => "celham4@wikia.com", + "statistics" => %{ + "average_points_per_game" => 4.6, + "free_throws_made" => 7, + "free_throws_attempted" => 10 + } + }, + "61" => %{ + "first_name" => "Noel", + "last_name" => "Fawlkes", + "email" => "nfawlkes5@yahoo.co.jp", + "statistics" => %{ + "average_points_per_game" => 5.0, + "free_throws_made" => 5, + "free_throws_attempted" => 5 + } + } + } + } + + assert BasketballWebsite.extract_from_path( + team_data, + "players.61.statistics.average_points_per_game" + ) === 5.0 + end + end + + describe "extract_from_path returns nil from nonexistent last key in" do + @tag task_id: 1 + test "first layer" do + team_data = %{ + "coach" => %{}, + "team_name" => "Hoop Masters", + "players" => %{} + } + + assert BasketballWebsite.extract_from_path(team_data, "team_song") == nil + end + + @tag task_id: 1 + test "second layer" do + team_data = %{ + "coach" => %{ + "first_name" => "Jane", + "last_name" => "Brown" + }, + "team_name" => "Hoop Masters", + "players" => %{} + } + + assert BasketballWebsite.extract_from_path(team_data, "coach.age") == nil + end + + @tag task_id: 1 + test "third layer" do + team_data = %{ + "coach" => %{}, + "team_name" => "Hoop Masters", + "players" => %{ + "32" => %{ + "first_name" => "Deni", + "last_name" => "Lidster", + "email" => nil, + "statistics" => %{} + } + } + } + + assert BasketballWebsite.extract_from_path(team_data, "players.32.height") == nil + end + + @tag task_id: 1 + test "fourth layer" do + team_data = %{ + "coach" => %{}, + "team_name" => "Hoop Masters", + "players" => %{ + "12" => %{ + "first_name" => "Andy", + "last_name" => "Napoli", + "email" => "anapoli7@goodreads.com", + "statistics" => %{ + "average_points_per_game" => 7 + } + } + } + } + + assert BasketballWebsite.extract_from_path( + team_data, + "players.12.statistics.personal_fouls" + ) == nil + end + end + + @tag task_id: 1 + test "extract_from_path returns nil from nonexistent path" do + team_data = %{ + "coach" => %{}, + "team_name" => "Hoop Masters", + "players" => %{} + } + + assert BasketballWebsite.extract_from_path( + team_data, + "support_personnel.physiotherapy.first_name" + ) == nil + end + + describe "get_in_path retrieves from" do + @tag task_id: 2 + test "first layer" do + team_data = %{ + "coach" => %{}, + "team_name" => "Hoop Masters", + "players" => %{} + } + + assert BasketballWebsite.get_in_path(team_data, "team_name") == "Hoop Masters" + end + + @tag task_id: 2 + test "second layer" do + team_data = %{ + "coach" => %{ + "first_name" => "Jane", + "last_name" => "Brown" + }, + "team_name" => "Hoop Masters", + "players" => %{} + } + + assert BasketballWebsite.get_in_path(team_data, "coach.first_name") == "Jane" + end + + @tag task_id: 2 + test "third layer" do + team_data = %{ + "coach" => %{}, + "team_name" => "Hoop Masters", + "players" => %{ + "99" => %{ + "first_name" => "Amalee", + "last_name" => "Tynemouth", + "email" => "atynemouth0@yellowpages.com", + "statistics" => %{} + }, + "98" => %{ + "first_name" => "Tiffie", + "last_name" => "Derle", + "email" => "tderle1@vimeo.com", + "statistics" => %{} + } + } + } + + assert BasketballWebsite.get_in_path(team_data, "players.99.first_name") == "Amalee" + end + + @tag task_id: 2 + test "fourth layer" do + team_data = %{ + "coach" => %{}, + "team_name" => "Hoop Masters", + "players" => %{ + "42" => %{ + "first_name" => "Conchita", + "last_name" => "Elham", + "email" => "celham4@wikia.com", + "statistics" => %{ + "average_points_per_game" => 4.6, + "free_throws_made" => 7, + "free_throws_attempted" => 10 + } + }, + "61" => %{ + "first_name" => "Noel", + "last_name" => "Fawlkes", + "email" => "nfawlkes5@yahoo.co.jp", + "statistics" => %{ + "average_points_per_game" => 5.0, + "free_throws_made" => 5, + "free_throws_attempted" => 5 + } + } + } + } + + assert BasketballWebsite.get_in_path( + team_data, + "players.61.statistics.average_points_per_game" + ) === 5.0 + end + end + + describe "get_in_path returns nil from nonexistent last key in" do + @tag task_id: 2 + test "first layer" do + team_data = %{ + "coach" => %{}, + "team_name" => "Hoop Masters", + "players" => %{} + } + + assert BasketballWebsite.get_in_path(team_data, "team_song") == nil + end + + @tag task_id: 2 + test "second layer" do + team_data = %{ + "coach" => %{ + "first_name" => "Jane", + "last_name" => "Brown" + }, + "team_name" => "Hoop Masters", + "players" => %{} + } + + assert BasketballWebsite.get_in_path(team_data, "coach.age") == nil + end + + @tag task_id: 2 + test "third layer" do + team_data = %{ + "coach" => %{}, + "team_name" => "Hoop Masters", + "players" => %{ + "32" => %{ + "first_name" => "Deni", + "last_name" => "Lidster", + "email" => nil, + "statistics" => %{} + } + } + } + + assert BasketballWebsite.get_in_path(team_data, "players.32.height") == nil + end + + @tag task_id: 2 + test "fourth layer" do + team_data = %{ + "coach" => %{}, + "team_name" => "Hoop Masters", + "players" => %{ + "12" => %{ + "first_name" => "Andy", + "last_name" => "Napoli", + "email" => "anapoli7@goodreads.com", + "statistics" => %{ + "average_points_per_game" => 7 + } + } + } + } + + assert BasketballWebsite.get_in_path(team_data, "players.12.statistics.personal_fouls") == + nil + end + end + + @tag task_id: 2 + test "get_in_path returns nil from nonexistent path" do + team_data = %{ + "coach" => %{}, + "team_name" => "Hoop Masters", + "players" => %{} + } + + assert BasketballWebsite.get_in_path(team_data, "support_personnel.physiotherapy.first_name") == + nil + end +end diff --git a/elixir/basketball-website/test/test_helper.exs b/elixir/basketball-website/test/test_helper.exs new file mode 100644 index 0000000..e8677a3 --- /dev/null +++ b/elixir/basketball-website/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true, seed: 0)