diff --git a/elixir/remote-control-car/.exercism/config.json b/elixir/remote-control-car/.exercism/config.json new file mode 100644 index 0000000..1c837b9 --- /dev/null +++ b/elixir/remote-control-car/.exercism/config.json @@ -0,0 +1,25 @@ +{ + "authors": [ + "neenjaw" + ], + "contributors": [ + "angelikatyborska" + ], + "files": { + "solution": [ + "lib/remote_control_car.ex" + ], + "test": [ + "test/remote_control_car_test.exs" + ], + "exemplar": [ + ".meta/exemplar.ex" + ] + }, + "language_versions": ">=1.10", + "forked_from": [ + "csharp/elons-toys" + ], + "icon": "elons-toys", + "blurb": "Learn about structs by playing around with a remote controlled car." +} diff --git a/elixir/remote-control-car/.exercism/metadata.json b/elixir/remote-control-car/.exercism/metadata.json new file mode 100644 index 0000000..a001f03 --- /dev/null +++ b/elixir/remote-control-car/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"elixir","exercise":"remote-control-car","id":"6223ef7a1ae142c1a18b31995b0b742e","url":"https://exercism.org/tracks/elixir/exercises/remote-control-car","handle":"negrienko","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/elixir/remote-control-car/.formatter.exs b/elixir/remote-control-car/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/elixir/remote-control-car/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/elixir/remote-control-car/.gitignore b/elixir/remote-control-car/.gitignore new file mode 100644 index 0000000..b97fe0c --- /dev/null +++ b/elixir/remote-control-car/.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"). +structs-*.tar + diff --git a/elixir/remote-control-car/HELP.md b/elixir/remote-control-car/HELP.md new file mode 100644 index 0000000..aad26b2 --- /dev/null +++ b/elixir/remote-control-car/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/remote_control_car.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/remote-control-car/HINTS.md b/elixir/remote-control-car/HINTS.md new file mode 100644 index 0000000..ae520cc --- /dev/null +++ b/elixir/remote-control-car/HINTS.md @@ -0,0 +1,44 @@ +# Hints + +## General + +- Read about [structs][getting-started-structs] in the Getting Started guide. +- Read about [`defstruct`][docs-defstruct] in the documentation. +- Watch [Elixir Casts - #106: Intro to Structs][elixir-casts]. + +## 1. Create a brand-new remote controlled car + +- The module attribute `@enforce_keys` can be used to specify required values. It needs to be defined before calling `defstruct`. +- The `nickname` field should not have a default value specified. +- The `new/0` function should initialize the `nickname` with the value `"none"`. + +## 2. Create a brand-new remote controlled car with a nickname + +- Use [`multiple-function-clauses`][multiple-fn-clauses] to reuse the function name but accept different arguments. +- Consider a [`default-argument`][default-arg] for the function. + +## 3. Display the distance + +- For functions which accept only a specific type of struct, make sure you perform a pattern match to check the argument. +- Use the _static access operator_ to obtain field values. + +## 4. Display the battery percentage + +- For functions which accept only a specific type of struct, make sure you perform a pattern match to check the argument. +- Use the _static access operator_ to obtain field values. + +## 5. Driving changes the battery and distance driven + +- For functions which accept only a specific type of struct, make sure you perform a pattern match to check the argument. +- Use the _static access operator_ to obtain field values. +- Review the introduction for updating structs. + +## 6. Account for driving with a dead battery + +- If the battery is dead, it should return the struct unchanged. + +[getting-started-structs]: https://elixir-lang.org/getting-started/structs.html +[docs-defstruct]: https://hexdocs.pm/elixir/Kernel.html#defstruct/1 +[elixir-casts]: https://elixircasts.io/intro-to-structs +[multiple-fn-clauses]: https://elixir-lang.org/getting-started/modules-and-functions.html#named-functions +[default-arg]: https://elixir-lang.org/getting-started/modules-and-functions.html#default-arguments \ No newline at end of file diff --git a/elixir/remote-control-car/README.md b/elixir/remote-control-car/README.md new file mode 100644 index 0000000..8dbe769 --- /dev/null +++ b/elixir/remote-control-car/README.md @@ -0,0 +1,171 @@ +# Remote Control Car + +Welcome to Remote Control Car 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 + +## Structs + +Structs are an extension built on top of [maps][exercism-maps] which provide compile-time checks and default values. A struct is named after the module it is defined in. To define a struct use the `defstruct` construct. The construct usually immediately follows after the module definition. `defstruct` accepts either a list of atoms (for `nil` default values) or a keyword list (for specified default values). The fields without defaults must precede the fields with default values. + +```elixir +defmodule Plane do + defstruct [:engine, wings: 2] +end + +plane = %Plane{} +# => %Plane{engine: nil, wings: 2} +``` + +### Accessing fields and updating + +Since structs are built on maps, we can use most map functions to get and manipulate values. The _Access Behaviour_ is **not** implemented for structs. It is recommended to use the _static access operator_ `.` to access struct fields instead. + +- get/fetch field values: + + ```elixir + plane = %Plane{} + plane.engine + # => nil + Map.fetch(plane, :wings) + # => {:ok, 2} + ``` + +- update field values + + ```elixir + plane = %Plane{} + %{plane | wings: 4} + # => %Plane{engine: nil, wings: 4} + ``` + +### Enforcing field value initialization + +We can use the `@enforce_keys` module attribute with a list of the field keys to ensure that the values are initialized when the struct is created. If a key is not listed, its value will be `nil` as seen in the above example. If an enforced key is not initialized, an error is raised. + +```elixir +defmodule User do + @enforce_keys [:username] + defstruct [:username] +end + +%User{} +# => (ArgumentError) the following keys must also be given when building struct User: [:username] +``` + +[exercism-maps]: https://exercism.org/tracks/elixir/concepts/maps + +## Instructions + +In this exercise you'll be playing around with a remote controlled car, which you've finally saved enough money for to buy. + +Cars start with full (100%) batteries. Each time you drive the car using the remote control, it covers 20 meters and drains one percent of the battery. The car's nickname is not known until it is created. + +The remote controlled car has a fancy LED display that shows two bits of information: + +- The total distance it has driven, displayed as: `" meters"`. +- The remaining battery charge, displayed as: `"Battery at %"`. + +If the battery is at 0%, you can't drive the car anymore and the battery display will show `"Battery empty"`. + +## 1. Create a brand-new remote controlled car + +Implement the `RemoteControlCar.new/0` function to return a brand-new remote controlled car struct: + +```elixir +RemoteControlCar.new() +# => %RemoteControlCar{ +# battery_percentage: 100, +# distance_driven_in_meters: 0, +# nickname: "none" +# } +``` + +The nickname is required by the struct, make sure that a value is initialized in the `new` function, but not in the struct. + +## 2. Create a brand-new remote controlled car with a nickname + +Implement the `RemoteControlCar.new/1` function to return a brand-new remote controlled car struct with a provided nickname: + +```elixir +RemoteControlCar.new("Blue") +# => %RemoteControlCar{ +# battery_percentage: 100, +# distance_driven_in_meters: 0, +# nickname: "Blue" +# } +``` + +## 3. Display the distance + +Implement the `RemoteControlCar.display_distance/1` function to return the distance as displayed on the LED display: + +```elixir +car = RemoteControlCar.new() +RemoteControlCar.display_distance(car) +# => "0 meters" +``` + +Make sure the function only accepts a `RemoteControlCar` struct as the argument. + +## 4. Display the battery percentage + +Implement the `RemoteControlCar.display_battery/1` function to return the battery percentage as displayed on the LED display: + +```elixir +car = RemoteControlCar.new() +RemoteControlCar.display_battery(car) +# => "Battery at 100%" +``` + +Make sure the function only accepts a `RemoteControlCar` struct as the argument. If the battery is at 0%, the battery display will show "Battery empty". + +## 5. Driving changes the battery and distance driven + +Implement the `RemoteControlCar.drive/1` function that: + +- updates the number of meters driven by 20 +- drains 1% of the battery + +```elixir +RemoteControlCar.new("Red") +|> RemoteControlCar.drive() +# => %RemoteControlCar{ +# battery_percentage: 99, +# distance_driven_in_meters: 20, +# nickname: "Red" +# } + +``` + +Make sure the function only accepts a `RemoteControlCar` struct as the argument. + +## 6. Account for driving with a dead battery + +Update the `RemoteControlCar.drive/1` function to not increase the distance driven nor decrease the battery percentage when the battery is drained (at 0%): + +```elixir +%RemoteControlCar{ + battery_percentage: 0, + distance_driven_in_meters: 1980, + nickname: "Red" +} +|> RemoteControlCar.drive() +# => %RemoteControlCar{ +# battery_percentage: 0, +# distance_driven_in_meters: 1980, +# nickname: "Red" +# } +``` + +## Source + +### Created by + +- @neenjaw + +### Contributed to by + +- @angelikatyborska \ No newline at end of file diff --git a/elixir/remote-control-car/lib/remote_control_car.ex b/elixir/remote-control-car/lib/remote_control_car.ex new file mode 100644 index 0000000..25a4084 --- /dev/null +++ b/elixir/remote-control-car/lib/remote_control_car.ex @@ -0,0 +1,27 @@ +defmodule RemoteControlCar do + @enforce_keys [:nickname] + defstruct [:nickname, battery_percentage: 100, distance_driven_in_meters: 0] + + def new(nickname \\ "none"), do: %__MODULE__{nickname: nickname} + + def display_distance(%__MODULE__{distance_driven_in_meters: distance_driven_in_meters} = _remote_car) do + "#{distance_driven_in_meters} meters" + end + + def display_battery(%__MODULE__{battery_percentage: 0} = _remote_car) do + "Battery empty" + end + + def display_battery(%__MODULE__{battery_percentage: battery_percentage} = _remote_car) do + "Battery at #{battery_percentage}%" + end + + def drive(%__MODULE__{battery_percentage: 0} = remote_car), do: remote_car + def drive(%__MODULE__{} = remote_car) do + %{ + remote_car | + battery_percentage: remote_car.battery_percentage - 1, + distance_driven_in_meters: remote_car.distance_driven_in_meters + 20, + } + end +end diff --git a/elixir/remote-control-car/mix.exs b/elixir/remote-control-car/mix.exs new file mode 100644 index 0000000..6d49c1d --- /dev/null +++ b/elixir/remote-control-car/mix.exs @@ -0,0 +1,28 @@ +defmodule RemoteControlCar.MixProject do + use Mix.Project + + def project do + [ + app: :remote_control_car, + 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/remote-control-car/test/remote_control_car_test.exs b/elixir/remote-control-car/test/remote_control_car_test.exs new file mode 100644 index 0000000..de20d0f --- /dev/null +++ b/elixir/remote-control-car/test/remote_control_car_test.exs @@ -0,0 +1,169 @@ +defmodule FakeRemoteControlCar do + defstruct battery_percentage: 100, + distance_driven_in_meters: 0, + nickname: nil +end + +defmodule RemoteControlCarTest do + use ExUnit.Case + + @tag task_id: 1 + test "required key 'nickname' should not have a default value" do + assert_raise ArgumentError, fn -> + quote do + %RemoteControlCar{} + end + |> Code.eval_quoted() + end + end + + @tag task_id: 1 + test "new" do + car = RemoteControlCar.new() + + assert car.__struct__ == RemoteControlCar + assert car.battery_percentage == 100 + assert car.distance_driven_in_meters == 0 + assert car.nickname == "none" + end + + @tag task_id: 2 + test "new with nickname" do + nickname = "Red" + car = RemoteControlCar.new(nickname) + + assert car.__struct__ == RemoteControlCar + assert car.battery_percentage == 100 + assert car.distance_driven_in_meters == 0 + assert car.nickname == nickname + end + + @tag task_id: 3 + test "display distance raises error when not given struct" do + fake_car = %{ + battery_percentage: 100, + distance_driven_in_meters: 0, + nickname: "Fake" + } + + assert_raise(FunctionClauseError, fn -> + RemoteControlCar.display_distance(fake_car) + end) + end + + @tag task_id: 3 + test "display distance raises error when given unexpected struct" do + fake_car = %FakeRemoteControlCar{ + battery_percentage: 100, + distance_driven_in_meters: 0, + nickname: "Fake" + } + + assert_raise(FunctionClauseError, fn -> + RemoteControlCar.display_distance(fake_car) + end) + end + + @tag task_id: 3 + test "display distance of new" do + car = RemoteControlCar.new() + + assert RemoteControlCar.display_distance(car) == "0 meters" + end + + @tag task_id: 3 + test "display distance of driven" do + car = RemoteControlCar.new() + car = %{car | distance_driven_in_meters: 20} + + assert RemoteControlCar.display_distance(car) == "20 meters" + end + + @tag task_id: 4 + test "display battery raises error when not given struct" do + fake_car = %{ + battery_percentage: 100, + distance_driven_in_meters: 0, + nickname: "Fake" + } + + assert_raise(FunctionClauseError, fn -> + RemoteControlCar.display_battery(fake_car) + end) + end + + @tag task_id: 4 + test "display battery raises error when given unexpected struct" do + fake_car = %FakeRemoteControlCar{ + battery_percentage: 100, + distance_driven_in_meters: 0, + nickname: "Fake" + } + + assert_raise(FunctionClauseError, fn -> + RemoteControlCar.display_battery(fake_car) + end) + end + + @tag task_id: 4 + test "display battery of new" do + car = RemoteControlCar.new() + + assert RemoteControlCar.display_battery(car) == "Battery at 100%" + end + + @tag task_id: 4 + test "display battery of dead battery" do + car = RemoteControlCar.new() + car = %{car | battery_percentage: 0} + + assert RemoteControlCar.display_battery(car) == "Battery empty" + end + + @tag task_id: 5 + test "drive raises error when not given struct" do + fake_car = %{ + battery_percentage: 100, + distance_driven_in_meters: 0, + nickname: "Fake" + } + + assert_raise(FunctionClauseError, fn -> + RemoteControlCar.drive(fake_car) + end) + end + + @tag task_id: 5 + test "drive raises error when given unexpected struct" do + fake_car = %FakeRemoteControlCar{ + battery_percentage: 100, + distance_driven_in_meters: 0, + nickname: "Fake" + } + + assert_raise(FunctionClauseError, fn -> + RemoteControlCar.drive(fake_car) + end) + end + + @tag task_id: 5 + test "drive with battery" do + car = RemoteControlCar.new() |> RemoteControlCar.drive() + + assert car.__struct__ == RemoteControlCar + assert car.battery_percentage == 99 + assert car.distance_driven_in_meters == 20 + end + + @tag task_id: 6 + test "drive with dead battery" do + car = + RemoteControlCar.new() + |> Map.put(:battery_percentage, 0) + |> RemoteControlCar.drive() + + assert car.__struct__ == RemoteControlCar + assert car.battery_percentage == 0 + assert car.distance_driven_in_meters == 0 + end +end diff --git a/elixir/remote-control-car/test/test_helper.exs b/elixir/remote-control-car/test/test_helper.exs new file mode 100644 index 0000000..e8677a3 --- /dev/null +++ b/elixir/remote-control-car/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true, seed: 0)