Initial upload
This commit is contained in:
parent
c67653ddee
commit
57bc7b0289
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
elixir/**/_build/
|
22
elixir/basketball-website/.exercism/config.json
Normal file
22
elixir/basketball-website/.exercism/config.json
Normal file
@ -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."
|
||||
}
|
1
elixir/basketball-website/.exercism/metadata.json
Normal file
1
elixir/basketball-website/.exercism/metadata.json
Normal file
@ -0,0 +1 @@
|
||||
{"track":"elixir","exercise":"basketball-website","id":"62c61d3995094446ad2e8d13594f8e4f","url":"https://exercism.org/tracks/elixir/exercises/basketball-website","handle":"halfdan","is_requester":true,"auto_approve":false}
|
4
elixir/basketball-website/.formatter.exs
Normal file
4
elixir/basketball-website/.formatter.exs
Normal file
@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
24
elixir/basketball-website/.gitignore
vendored
Normal file
24
elixir/basketball-website/.gitignore
vendored
Normal 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").
|
||||
access-*.tar
|
||||
|
75
elixir/basketball-website/HELP.md
Normal file
75
elixir/basketball-website/HELP.md
Normal 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/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)
|
||||
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
|
||||
- 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.
|
||||
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).
|
18
elixir/basketball-website/HINTS.md
Normal file
18
elixir/basketball-website/HINTS.md
Normal file
@ -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
|
82
elixir/basketball-website/README.md
Normal file
82
elixir/basketball-website/README.md
Normal file
@ -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
|
15
elixir/basketball-website/lib/basketball_website.ex
Normal file
15
elixir/basketball-website/lib/basketball_website.ex
Normal file
@ -0,0 +1,15 @@
|
||||
defmodule BasketballWebsite do
|
||||
def extract_from_path(data, path) do
|
||||
paths = String.split(path, ".")
|
||||
root = Enum.at(paths, 0)
|
||||
if length(paths) > 1 and data[root] do
|
||||
extract_from_path(data[root], Enum.slice(paths, 1..-1) |> Enum.join("."))
|
||||
else
|
||||
data[root]
|
||||
end
|
||||
end
|
||||
|
||||
def get_in_path(data, path) do
|
||||
Kernel.get_in(data, String.split(path, "."))
|
||||
end
|
||||
end
|
28
elixir/basketball-website/mix.exs
Normal file
28
elixir/basketball-website/mix.exs
Normal file
@ -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
|
335
elixir/basketball-website/test/basketball_website_test.exs
Normal file
335
elixir/basketball-website/test/basketball_website_test.exs
Normal file
@ -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
|
2
elixir/basketball-website/test/test_helper.exs
Normal file
2
elixir/basketball-website/test/test_helper.exs
Normal file
@ -0,0 +1,2 @@
|
||||
ExUnit.start()
|
||||
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
21
elixir/captains-log/.exercism/config.json
Normal file
21
elixir/captains-log/.exercism/config.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"authors": [
|
||||
"angelikatyborska"
|
||||
],
|
||||
"contributors": [
|
||||
"neenjaw"
|
||||
],
|
||||
"files": {
|
||||
"solution": [
|
||||
"lib/captains_log.ex"
|
||||
],
|
||||
"test": [
|
||||
"test/captains_log_test.exs"
|
||||
],
|
||||
"exemplar": [
|
||||
".meta/exemplar.ex"
|
||||
]
|
||||
},
|
||||
"language_versions": ">=1.10",
|
||||
"blurb": "Learn about randomness and using Erlang libraries from Elixir by helping Mary generate stardates and starship registry numbers for her Star Trek themed pen-and-paper role playing sessions."
|
||||
}
|
1
elixir/captains-log/.exercism/metadata.json
Normal file
1
elixir/captains-log/.exercism/metadata.json
Normal file
@ -0,0 +1 @@
|
||||
{"track":"elixir","exercise":"captains-log","id":"42e6cfb7bfa94354a2597ab82ac8a8ba","url":"https://exercism.org/tracks/elixir/exercises/captains-log","handle":"halfdan","is_requester":true,"auto_approve":false}
|
4
elixir/captains-log/.formatter.exs
Normal file
4
elixir/captains-log/.formatter.exs
Normal file
@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
24
elixir/captains-log/.gitignore
vendored
Normal file
24
elixir/captains-log/.gitignore
vendored
Normal 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").
|
||||
match_binary-*.tar
|
||||
|
75
elixir/captains-log/HELP.md
Normal file
75
elixir/captains-log/HELP.md
Normal 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/captains_log.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)
|
||||
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
|
||||
- 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.
|
||||
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).
|
38
elixir/captains-log/HINTS.md
Normal file
38
elixir/captains-log/HINTS.md
Normal file
@ -0,0 +1,38 @@
|
||||
# Hints
|
||||
|
||||
## General
|
||||
|
||||
- Read about using Erlang libraries in the [official Getting Started guide][getting-started-erlang-libraries], and in particular about [formatting strings][getting-started-formatted-text-output].
|
||||
|
||||
## 1. Generate a random planet
|
||||
|
||||
- Use the provided module attribute with a list of letters representing planetary classes.
|
||||
- There is a [built-in function][enum-random] for choosing an element from a list at random.
|
||||
|
||||
## 2. Generate a random starship registry number
|
||||
|
||||
- There is a [build-in function][enum-random] for choosing an element from a range at random.
|
||||
|
||||
## 3. Generate a random stardate
|
||||
|
||||
- There is no Elixir function that would return a random float.
|
||||
- There is a [built-in Erlang function][erl-rand-uniform] that returns a random float x where `0.0 <= x < 1.0`.
|
||||
- If `x` belongs to a range `0.0 <= x < 1.0`, but you need a number from a different range `a <= x < b`, you can shift x's range by multiplying it by the range's width (`b - a`) and adding the range's start (`a`). That is: `x * (b - a) + a`.
|
||||
|
||||
## 4. Format the stardate
|
||||
|
||||
- There is no Elixir function that would be able to, in a single step, format a float as a string with a given precision.
|
||||
- There is a [built-in Erlang function][erl-io-lib-format] that takes a format string and a list of data, and returns a charlist.
|
||||
- There is a [built-in function][to-string] that changes a charlist to a string.
|
||||
- The format string of that function contains control sequences.
|
||||
- A control sequence starts with `~` and has the pattern `~F.P.PadModC`, where `F` stands for the width of the output, `P` stands for the precision, `Pad` stands for the padding character, `Mod` stands for the control sequence modifier, and `C` is the type of the control sequence.
|
||||
- To format a float with a desired precision, a control sequence with the pattern `~.PC` will suffice.
|
||||
- The control sequence type for floats is `f`.
|
||||
- The exact format string you need is `~.1f`.
|
||||
|
||||
[getting-started-erlang-libraries]: https://elixir-lang.org/getting-started/erlang-libraries.html
|
||||
[getting-started-formatted-text-output]: https://elixir-lang.org/getting-started/erlang-libraries.html#formatted-text-output
|
||||
[enum-random]: https://hexdocs.pm/elixir/Enum.html#random/1
|
||||
[erl-rand-uniform]: http://www.erlang.org/doc/man/rand.html#uniform-0
|
||||
[erl-io-lib-format]: http://www.erlang.org/doc/man/io_lib.html#format-2
|
||||
[to-string]: https://hexdocs.pm/elixir/Kernel.html#to_string/1
|
103
elixir/captains-log/README.md
Normal file
103
elixir/captains-log/README.md
Normal file
@ -0,0 +1,103 @@
|
||||
# Captain's Log
|
||||
|
||||
Welcome to Captain's Log 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
|
||||
|
||||
## Randomness
|
||||
|
||||
In Elixir, to choose a random element from an enumerable data structure (e.g. list, range), we use `Enum.random`. This function will pick a single element, with every element having equal probability of being picked.
|
||||
|
||||
Elixir does not have its own functions for picking a random float. To do that, we have to use Erlang directly.
|
||||
|
||||
## Erlang Libraries
|
||||
|
||||
Elixir code runs in the BEAM virtual machine. BEAM is part of the Erlang Run-Time System. Being inspired by Erlang, and sharing its run environment, Elixir provides great interoperability with Erlang libraries. This means that Elixir developers can use Erlang libraries from within their Elixir code. In fact, writing Elixir libraries for functionality already provided by Erlang libraries is discouraged in the Elixir community.
|
||||
|
||||
As a result, certain functionality, like mathematical operations or timer functions, is only available in Elixir via Erlang.
|
||||
|
||||
Erlang's standard library is available for use in our Elixir code without any extra steps necessary.
|
||||
|
||||
Erlang functions can be called in the same way we call Elixir functions, with one small difference. Erlang module names are `snake_case` atoms. For example, to call the Erlang `pi/0` function from the `math` module, one would write:
|
||||
|
||||
```elixir
|
||||
:math.pi()
|
||||
# => 3.141592653589793
|
||||
```
|
||||
|
||||
The most commonly used Erlang functions that do not have an Elixir equivalent are:
|
||||
|
||||
- `:timer.sleep/1` which suspends a process for the given amount of milliseconds.
|
||||
- `:rand.uniform/0` which generates a random float `x`, where `0.0 <= x < 1.0`.
|
||||
- `:io_lib.format/2` which provides C-style string formatting (using control sequences). Using this function, we could for example print an integer in any base between 2 and 36 or format a float with desired precision. Note that this function, like many Erlang functions, returns a charlist.
|
||||
- The `math` module that provides mathematical functions such as `sin/1`, `cos/1`, `log2/1`, `log10/1`, `pow/2`, and more.
|
||||
|
||||
To discover Erlang's standard library, explore the [STDLIB Reference Manual][erl-stdlib-ref].
|
||||
|
||||
[erl-stdlib-ref]: http://www.erlang.org/doc/apps/stdlib/index.html
|
||||
|
||||
## Instructions
|
||||
|
||||
Mary is a big fan of the TV series _Star Trek: The Next Generation_. She often plays pen-and-paper role playing games, where she and her friends pretend to be the crew of the _Starship Enterprise_. Mary's character is Captain Picard, which means she has to keep the captain's log. She loves the creative part of the game, but doesn't like to generate random data on the spot.
|
||||
|
||||
Help Mary by creating random generators for data commonly appearing in the captain's log.
|
||||
|
||||
## 1. Generate a random planet
|
||||
|
||||
The _Starship Enterprise_ encounters many planets in its travels. Planets in the Star Trek universe are split into categories based on their properties. For example, Earth is a class M planet. All possible planetary classes are: D, H, J, K, L, M, N, R, T, and Y.
|
||||
|
||||
Implement the `random_planet_class/0` function. It should return one of the planetary classes at random.
|
||||
|
||||
```elixir
|
||||
CaptainsLog.random_planet_class()
|
||||
# => "K"
|
||||
```
|
||||
|
||||
## 2. Generate a random starship registry number
|
||||
|
||||
Enterprise (registry number NCC-1701) is not the only starship flying around! When it rendezvous with another starship, Mary needs to log the registry number of that starship.
|
||||
|
||||
Registry numbers start with the prefix "NCC-" and then use a number from 1000 to 9999 (inclusive).
|
||||
|
||||
Implement the `random_ship_registry_number/0` function that returns a random starship registry number.
|
||||
|
||||
```elixir
|
||||
CaptainsLog.random_ship_registry_number()
|
||||
# => "NCC-1947"
|
||||
```
|
||||
|
||||
## 3. Generate a random stardate
|
||||
|
||||
What's the use of a log if it doesn't include dates?
|
||||
|
||||
A stardate is a floating point number. The adventures of the _Starship Enterprise_ from the first season of _The Next Generation_ take place between the stardates 41000.0 and 42000.0. The "4" stands for the 24th century, the "1" for the first season.
|
||||
|
||||
Implement the function `random_stardate/0` that returns a floating point number between 41000.0 (inclusive) and 42000.0 (exclusive).
|
||||
|
||||
```elixir
|
||||
CaptainsLog.random_stardate()
|
||||
# => 41458.15721310934
|
||||
```
|
||||
|
||||
## 4. Format the stardate
|
||||
|
||||
In the captain's log, stardates are usually rounded to a single decimal place.
|
||||
|
||||
Implement the `format_stardate/1` function that will take a floating point number and return a string with the number rounded to a single decimal place.
|
||||
|
||||
```elixir
|
||||
CaptainsLog.format_stardate(41458.15721310934)
|
||||
# => "41458.2"
|
||||
```
|
||||
|
||||
## Source
|
||||
|
||||
### Created by
|
||||
|
||||
- @angelikatyborska
|
||||
|
||||
### Contributed to by
|
||||
|
||||
- @neenjaw
|
21
elixir/captains-log/lib/captains_log.ex
Normal file
21
elixir/captains-log/lib/captains_log.ex
Normal file
@ -0,0 +1,21 @@
|
||||
defmodule CaptainsLog do
|
||||
@planetary_classes ["D", "H", "J", "K", "L", "M", "N", "R", "T", "Y"]
|
||||
|
||||
def random_planet_class() do
|
||||
Enum.random(@planetary_classes)
|
||||
end
|
||||
|
||||
def random_ship_registry_number() do
|
||||
n = Enum.random(1000..9999)
|
||||
"NCC-#{n}"
|
||||
end
|
||||
|
||||
def random_stardate() do
|
||||
41000.0 + :rand.uniform() * 1000
|
||||
end
|
||||
|
||||
def format_stardate(stardate) when is_float(stardate) do
|
||||
:io_lib.format("~.1f", [stardate]) |> String.Chars.to_string()
|
||||
end
|
||||
def format_stardate(_), do: raise ArgumentError
|
||||
end
|
28
elixir/captains-log/mix.exs
Normal file
28
elixir/captains-log/mix.exs
Normal file
@ -0,0 +1,28 @@
|
||||
defmodule CaptainsLog.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :captains_log,
|
||||
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
|
119
elixir/captains-log/test/captains_log_test.exs
Normal file
119
elixir/captains-log/test/captains_log_test.exs
Normal file
@ -0,0 +1,119 @@
|
||||
defmodule CaptainsLogTest do
|
||||
use ExUnit.Case
|
||||
|
||||
describe "random_planet_class" do
|
||||
@tag task_id: 1
|
||||
test "it always returns one of the letters: D, H, J, K, L, M, N, R, T, Y" do
|
||||
planetary_classes = ["D", "H", "J", "K", "L", "M", "N", "R", "T", "Y"]
|
||||
|
||||
Enum.each(0..100, fn _ ->
|
||||
assert CaptainsLog.random_planet_class() in planetary_classes
|
||||
end)
|
||||
end
|
||||
|
||||
@tag task_id: 1
|
||||
test "it will eventually return each of the letters at least once" do
|
||||
planetary_classes = ["D", "H", "J", "K", "L", "M", "N", "R", "T", "Y"]
|
||||
|
||||
never_returned_planetary_classes =
|
||||
Enum.reduce_while(0..1000, planetary_classes, fn _, remaining_planetary_classes ->
|
||||
if remaining_planetary_classes == [] do
|
||||
{:halt, remaining_planetary_classes}
|
||||
else
|
||||
{:cont, remaining_planetary_classes -- [CaptainsLog.random_planet_class()]}
|
||||
end
|
||||
end)
|
||||
|
||||
assert never_returned_planetary_classes == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "random_ship_registry_number" do
|
||||
@tag task_id: 2
|
||||
test "start with \"NCC-\"" do
|
||||
assert String.starts_with?(CaptainsLog.random_ship_registry_number(), "NCC-")
|
||||
end
|
||||
|
||||
@tag task_id: 2
|
||||
test "ends with a random integer between 1000 and 9999" do
|
||||
Enum.each(0..100, fn _ ->
|
||||
random_ship_registry_number = CaptainsLog.random_ship_registry_number()
|
||||
just_the_number = String.replace(random_ship_registry_number, "NCC-", "")
|
||||
|
||||
case Integer.parse(just_the_number) do
|
||||
{integer, ""} ->
|
||||
assert integer >= 1000
|
||||
assert integer <= 9999
|
||||
|
||||
_ ->
|
||||
flunk("Expected #{just_the_number} to be an integer")
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "random_stardate" do
|
||||
@tag task_id: 3
|
||||
test "is a float" do
|
||||
assert is_float(CaptainsLog.random_stardate())
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "is equal to or greater than 41_000.0" do
|
||||
Enum.each(0..100, fn _ ->
|
||||
assert CaptainsLog.random_stardate() >= 41_000.0
|
||||
end)
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "is less than 42_000.0" do
|
||||
Enum.each(0..100, fn _ ->
|
||||
assert CaptainsLog.random_stardate() < 42_000.0
|
||||
end)
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "consecutive calls return floats with different fractional parts" do
|
||||
decimal_parts =
|
||||
Enum.map(0..10, fn _ ->
|
||||
random_stardate = CaptainsLog.random_stardate()
|
||||
Float.ceil(random_stardate) - random_stardate
|
||||
end)
|
||||
|
||||
assert Enum.count(Enum.uniq(decimal_parts)) > 3
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "returns floats with fractional parts with more than one decimal place" do
|
||||
decimal_parts =
|
||||
Enum.map(0..10, fn _ ->
|
||||
random_stardate = CaptainsLog.random_stardate()
|
||||
Float.ceil(random_stardate * 10) - random_stardate * 10
|
||||
end)
|
||||
|
||||
assert Enum.count(Enum.uniq(decimal_parts)) > 3
|
||||
end
|
||||
end
|
||||
|
||||
describe "format_stardate" do
|
||||
@tag task_id: 4
|
||||
test "returns a string" do
|
||||
assert is_bitstring(CaptainsLog.format_stardate(41010.7))
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "formats floats" do
|
||||
assert CaptainsLog.format_stardate(41543.3) == "41543.3"
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "rounds floats to one decimal place" do
|
||||
assert CaptainsLog.format_stardate(41032.4512) == "41032.5"
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "does not accept integers" do
|
||||
assert_raise ArgumentError, fn -> CaptainsLog.format_stardate(41411) end
|
||||
end
|
||||
end
|
||||
end
|
2
elixir/captains-log/test/test_helper.exs
Normal file
2
elixir/captains-log/test/test_helper.exs
Normal file
@ -0,0 +1,2 @@
|
||||
ExUnit.start()
|
||||
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
22
elixir/city-office/.exercism/config.json
Normal file
22
elixir/city-office/.exercism/config.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"authors": [
|
||||
"angelikatyborska"
|
||||
],
|
||||
"contributors": [
|
||||
"neenjaw",
|
||||
"michallepicki"
|
||||
],
|
||||
"files": {
|
||||
"solution": [
|
||||
"lib/form.ex"
|
||||
],
|
||||
"test": [
|
||||
"test/form_test.exs"
|
||||
],
|
||||
"exemplar": [
|
||||
".meta/exemplar.ex"
|
||||
]
|
||||
},
|
||||
"language_versions": ">=1.10",
|
||||
"blurb": "Learn about writing documentation and typespecs by getting your code ready for the arrival of a new colleague at the city office."
|
||||
}
|
1
elixir/city-office/.exercism/metadata.json
Normal file
1
elixir/city-office/.exercism/metadata.json
Normal file
@ -0,0 +1 @@
|
||||
{"track":"elixir","exercise":"city-office","id":"f8d9471439664a2092d435381c8af052","url":"https://exercism.org/tracks/elixir/exercises/city-office","handle":"halfdan","is_requester":true,"auto_approve":false}
|
4
elixir/city-office/.formatter.exs
Normal file
4
elixir/city-office/.formatter.exs
Normal file
@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
24
elixir/city-office/.gitignore
vendored
Normal file
24
elixir/city-office/.gitignore
vendored
Normal 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").
|
||||
match_binary-*.tar
|
||||
|
75
elixir/city-office/HELP.md
Normal file
75
elixir/city-office/HELP.md
Normal 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/form.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)
|
||||
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
|
||||
- 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.
|
||||
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).
|
63
elixir/city-office/HINTS.md
Normal file
63
elixir/city-office/HINTS.md
Normal file
@ -0,0 +1,63 @@
|
||||
# Hints
|
||||
|
||||
## General
|
||||
|
||||
- Read the official documentation for [typespecs][typespecs].
|
||||
- Read the official documentation about [writing documentation][writing-documentation].
|
||||
- Read about using module attributes as annotations in the [official Getting Started guide][getting-started-module-attributes].
|
||||
- Read about using typespecs in the [official Getting Started guide][getting-started-typespecs].
|
||||
|
||||
## 1. Document the purpose of the form tools
|
||||
|
||||
- The module attribute `@moduledoc` can be used to write documentation for a module.
|
||||
|
||||
## 2. Document filling out fields with blank values
|
||||
|
||||
- The module attribute `@doc` can be used to write documentation for a function.
|
||||
- The module attribute `@spec` can be used to write a typespec for a function.
|
||||
- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe.
|
||||
- Refer to the [typespecs documentation][typespecs-types] for a list of all available types.
|
||||
- The correct type for strings is [defined in the `String` module][string-t].
|
||||
|
||||
## 3. Document splitting values into lists of uppercase letters
|
||||
|
||||
- The module attribute `@doc` can be used to write documentation for a function.
|
||||
- The module attribute `@spec` can be used to write a typespec for a function.
|
||||
- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe.
|
||||
- Refer to the [typespecs documentation][typespecs-types] for a list of all available types.
|
||||
- The correct type for strings is [defined in the `String` module][string-t].
|
||||
- A list is a parametrized type.
|
||||
|
||||
## 4. Document checking if a value fits a field with a max length
|
||||
|
||||
- The module attribute `@doc` can be used to write documentation for a function.
|
||||
- The module attribute `@spec` can be used to write a typespec for a function.
|
||||
- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe.
|
||||
- Refer to the [typespecs documentation][typespecs-types] for a list of all available types.
|
||||
- The correct type for strings is [defined in the `String` module][string-t].
|
||||
- Literal values can be used in a typespec.
|
||||
- The pipe `|` can be used to represent a union of types.
|
||||
|
||||
## 5. Document different address formats
|
||||
|
||||
- The module attribute `@type` can be use to define a custom public type.
|
||||
- Types can be compound, e.g. when specifying a type that's a map, you can also specify the types of the values under the specific keys.
|
||||
- [The type operator `::`][type-operator] can also be used to prepend a variable name to a type.
|
||||
- Custom types can be used to define other custom types.
|
||||
|
||||
## 6. Document formatting the address
|
||||
|
||||
- The module attribute `@doc` can be used to write documentation for a function.
|
||||
- The module attribute `@spec` can be used to write a typespec for a function.
|
||||
- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe.
|
||||
- Refer to the [typespecs documentation][typespecs-types] for a list of all available types.
|
||||
- The correct type for strings is [defined in the `String` module][string-t].
|
||||
- Custom types can be used in a typespec.
|
||||
|
||||
[writing-documentation]: https://hexdocs.pm/elixir/writing-documentation.html
|
||||
[typespecs]: https://hexdocs.pm/elixir/typespecs.html
|
||||
[typespecs-types]: https://hexdocs.pm/elixir/typespecs.html#types-and-their-syntax
|
||||
[getting-started-module-attributes]: https://elixir-lang.org/getting-started/module-attributes.html#as-annotations
|
||||
[getting-started-typespecs]: https://elixir-lang.org/getting-started/typespecs-and-behaviours.html#types-and-specs
|
||||
[string-t]: https://hexdocs.pm/elixir/String.html#t:t/0
|
||||
[type-operator]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#::/2
|
173
elixir/city-office/README.md
Normal file
173
elixir/city-office/README.md
Normal file
@ -0,0 +1,173 @@
|
||||
# City Office
|
||||
|
||||
Welcome to City Office 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
|
||||
|
||||
## Docs
|
||||
|
||||
Documentation in Elixir is a first-class citizen.
|
||||
|
||||
There are two module attributes commonly used to document your code - `@moduledoc` for documenting a module and `@doc` for documenting a function that follows the attribute. The `@moduledoc` attribute usually appears on the first line of the module, and the `@doc` attribute usually appears right before a function definition, or the function's typespec if it has one. The documentation is commonly written in a multiline string using the heredoc syntax.
|
||||
|
||||
Elixir documentation is written in [**Markdown**][markdown].
|
||||
|
||||
```elixir
|
||||
defmodule String do
|
||||
@moduledoc """
|
||||
Strings in Elixir are UTF-8 encoded binaries.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Converts all characters in the given string to uppercase according to `mode`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> String.upcase("abcd")
|
||||
"ABCD"
|
||||
|
||||
iex> String.upcase("olá")
|
||||
"OLÁ"
|
||||
"""
|
||||
def upcase(string, mode \\ :default)
|
||||
end
|
||||
```
|
||||
|
||||
## Typespecs
|
||||
|
||||
Elixir is a dynamically typed language, which means it doesn't provide compile-time type checks. Still, type specifications can be used as a form of documentation.
|
||||
|
||||
A type specification can be added to a function using the `@spec` module attribute right before the function definition. `@spec` is followed by the function name and a list of all of its arguments' types, in parentheses, separated by commas. The type of the return value is separated from the function's arguments with a double colon `::`.
|
||||
|
||||
```elixir
|
||||
@spec longer_than?(String.t(), non_neg_integer()) :: boolean()
|
||||
def longer_than?(string, length), do: String.length(string) > length
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
Most commonly used types include:
|
||||
|
||||
- booleans: `boolean()`
|
||||
- strings: `String.t()`
|
||||
- numbers: `integer()`, `non_neg_integer()`, `pos_integer()`, `float()`
|
||||
- lists: `list()`
|
||||
- a value of any type: `any()`
|
||||
|
||||
Some types can also be parameterized, for example `list(integer)` is a list of integers.
|
||||
|
||||
Literal values can also be used as types.
|
||||
|
||||
A union of types can be written using the pipe `|`. For example, `integer() | :error` means either an integer or the atom literal `:error`.
|
||||
|
||||
A full list of all types can be found in the ["Typespecs" section in the official documentation][types].
|
||||
|
||||
### Naming arguments
|
||||
|
||||
Arguments in the typespec could also be named which is useful for distinguishing multiple arguments of the same type. The argument name, followed by a double colon, goes before the argument's type.
|
||||
|
||||
```elixir
|
||||
@spec to_hex({hue :: integer, saturation :: integer, lightness :: integer}) :: String.t()
|
||||
```
|
||||
|
||||
### Custom types
|
||||
|
||||
Typespecs aren't limited to just the built-in types. Custom types can be defined using the `@type` module attribute. A custom type definition starts with the type's name, followed by a double colon and then the type itself.
|
||||
|
||||
```elixir
|
||||
@type color :: {hue :: integer, saturation :: integer, lightness :: integer}
|
||||
|
||||
@spec to_hex(color()) :: String.t()
|
||||
```
|
||||
|
||||
A custom type can be used from the same module where it's defined, or from another module.
|
||||
|
||||
[types]: https://hexdocs.pm/elixir/typespecs.html#types-and-their-syntax
|
||||
[markdown]: https://docs.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax
|
||||
|
||||
## Instructions
|
||||
|
||||
You have been working in the city office for a while, and you have developed a set of tools that speed up your day-to-day work, for example with filling out forms.
|
||||
|
||||
Now, a new colleague is joining you, and you realized your tools might not be self-explanatory. There are a lot of weird conventions in your office, like always filling out forms with uppercase letters and avoiding leaving fields empty.
|
||||
|
||||
You decide to write some documentation so that it's easier for your new colleague to hop right in and start using your tools.
|
||||
|
||||
## 1. Document the purpose of the form tools
|
||||
|
||||
Add documentation to the `Form` module that describes its purpose. It should read:
|
||||
|
||||
```
|
||||
A collection of loosely related functions helpful for filling out various forms at the city office.
|
||||
```
|
||||
|
||||
## 2. Document filling out fields with blank values
|
||||
|
||||
Add documentation and a typespec to the `Form.blanks/1` function. The documentation should read:
|
||||
|
||||
```
|
||||
Generates a string of a given length.
|
||||
|
||||
This string can be used to fill out a form field that is supposed to have no value.
|
||||
Such fields cannot be left empty because a malicious third party could fill them out with false data.
|
||||
```
|
||||
|
||||
The typespec should explain that the function accepts a single argument, a non-negative integer, and returns a string.
|
||||
|
||||
## 3. Document splitting values into lists of uppercase letters
|
||||
|
||||
Add documentation and a typespec to the `Form.letters/1` function. The documentation should read:
|
||||
|
||||
```
|
||||
Splits the string into a list of uppercase letters.
|
||||
|
||||
This is needed for form fields that don't offer a single input for the whole string,
|
||||
but instead require splitting the string into a predefined number of single-letter inputs.
|
||||
```
|
||||
|
||||
The typespec should explain that the function accepts a single argument, a string, and returns a list of strings.
|
||||
|
||||
## 4. Document checking if a value fits a field with a max length
|
||||
|
||||
Add documentation and a typespec to the `Form.check_length/2` function. The documentation should read:
|
||||
|
||||
```
|
||||
Checks if the value has no more than the maximum allowed number of letters.
|
||||
|
||||
This is needed to check that the values of fields do not exceed the maximum allowed length.
|
||||
It also tells you by how much the value exceeds the maximum.
|
||||
```
|
||||
|
||||
The typespec should explain that the function accepts two arguments, a string and a non-negative integer, and returns one of two possible values. It returns either the `:ok` atom or a 2-tuple with the first element being the `:error` atom, and the second a positive integer.
|
||||
|
||||
## 5. Document different address formats
|
||||
|
||||
For some unknown to you reason, the city office's internal system uses two different ways of representing addresses - either as a map or as a tuple.
|
||||
|
||||
Document this fact by defining three custom public types:
|
||||
- `address_map` - a map with the keys `:street`, `:postal_code`, and `:city`. Each key holds a value of type string.
|
||||
- `address_tuple` - a tuple with three values - `street`, `postal_code`, and `city`. Each value is of type string. Differentiate the values by giving them names in the typespec.
|
||||
- `address` - can be either an `address_map` or an `address_tuple`.
|
||||
|
||||
## 6. Document formatting the address
|
||||
|
||||
Add documentation and a typespec to the `Form.format_address/1` function. The documentation should read:
|
||||
|
||||
```
|
||||
Formats the address as an uppercase multiline string.
|
||||
```
|
||||
|
||||
The typespec should explain that the function accepts one argument, an address, and returns a string.
|
||||
|
||||
## Source
|
||||
|
||||
### Created by
|
||||
|
||||
- @angelikatyborska
|
||||
|
||||
### Contributed to by
|
||||
|
||||
- @neenjaw
|
||||
- @michallepicki
|
65
elixir/city-office/lib/form.ex
Normal file
65
elixir/city-office/lib/form.ex
Normal file
@ -0,0 +1,65 @@
|
||||
defmodule Form do
|
||||
@moduledoc """
|
||||
A collection of loosely related functions helpful for filling out various forms at the city office.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Generates a string of a given length.
|
||||
|
||||
This string can be used to fill out a form field that is supposed to have no value.
|
||||
Such fields cannot be left empty because a malicious third party could fill them out with false data.
|
||||
"""
|
||||
@spec blanks(non_neg_integer) :: String.t()
|
||||
def blanks(n) do
|
||||
String.duplicate("X", n)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Splits the string into a list of uppercase letters.
|
||||
|
||||
This is needed for form fields that don't offer a single input for the whole string,
|
||||
but instead require splitting the string into a predefined number of single-letter inputs.
|
||||
"""
|
||||
@spec letters(String.t()) :: [String.t()]
|
||||
def letters(word) do
|
||||
word
|
||||
|> String.upcase()
|
||||
|> String.split("", trim: true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the value has no more than the maximum allowed number of letters.
|
||||
|
||||
This is needed to check that the values of fields do not exceed the maximum allowed length.
|
||||
It also tells you by how much the value exceeds the maximum.
|
||||
"""
|
||||
@spec check_length(String.t(), non_neg_integer()) :: :ok | {:error, pos_integer()}
|
||||
def check_length(word, length) do
|
||||
diff = String.length(word) - length
|
||||
|
||||
if diff <= 0 do
|
||||
:ok
|
||||
else
|
||||
{:error, diff}
|
||||
end
|
||||
end
|
||||
|
||||
@type address_map :: %{street: String.t(), postal_code: String.t(), city: String.t()}
|
||||
@type address_tuple :: {street :: String.t(), postal_code :: String.t(), city :: String.t()}
|
||||
@type address :: address_map() | address_tuple()
|
||||
|
||||
@doc """
|
||||
Formats the address as an uppercase multiline string.
|
||||
"""
|
||||
@spec format_address(address()) :: String.t()
|
||||
def format_address(%{street: street, postal_code: postal_code, city: city}) do
|
||||
format_address({street, postal_code, city})
|
||||
end
|
||||
|
||||
def format_address({street, postal_code, city}) do
|
||||
"""
|
||||
#{String.upcase(street)}
|
||||
#{String.upcase(postal_code)} #{String.upcase(city)}
|
||||
"""
|
||||
end
|
||||
end
|
28
elixir/city-office/mix.exs
Normal file
28
elixir/city-office/mix.exs
Normal file
@ -0,0 +1,28 @@
|
||||
defmodule Form.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :city_office,
|
||||
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
|
292
elixir/city-office/test/form_test.exs
Normal file
292
elixir/city-office/test/form_test.exs
Normal file
@ -0,0 +1,292 @@
|
||||
defmodule FormTest do
|
||||
use ExUnit.Case
|
||||
|
||||
# Dear Elixir learner,
|
||||
# If you're reading this test suite to gain some insights,
|
||||
# please be advised that it is somewhat unusual.
|
||||
#
|
||||
# Don't worry if you don't understand this test suite at this stage of your learning journey.
|
||||
# We had to use some advanced features to be able to write assertions about docs and typespecs.
|
||||
# You wouldn't normally write assertions for that in a typical codebase.
|
||||
# We're doing it here strictly for educational purposes.
|
||||
|
||||
defmacrop assert_moduledoc(expected_moduledoc) do
|
||||
quote do
|
||||
{:docs_v1, _, _, _, module_doc, _, _} = Code.fetch_docs(Form)
|
||||
|
||||
if module_doc == :none do
|
||||
flunk("expected the module Form to have documentation")
|
||||
else
|
||||
actual_moduledoc = module_doc["en"]
|
||||
assert actual_moduledoc == unquote(expected_moduledoc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacrop assert_doc({function_name, function_arity}, expected_doc) do
|
||||
quote do
|
||||
{:docs_v1, _, _, _, _, _, docs} = Code.fetch_docs(Form)
|
||||
|
||||
{_, _, _, doc_content, _} =
|
||||
Enum.find(docs, fn {{kind, function_name, arity}, _, _, _, _} ->
|
||||
{kind, function_name, arity} ==
|
||||
{:function, unquote(function_name), unquote(function_arity)}
|
||||
end)
|
||||
|
||||
if doc_content == :none do
|
||||
flunk(
|
||||
"expected the function Form.#{unquote(function_name)}/#{unquote(function_arity)} to have documentation"
|
||||
)
|
||||
else
|
||||
actual_doc = doc_content["en"]
|
||||
assert actual_doc == unquote(expected_doc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacrop assert_spec({function_name, function_arity}, arguments_specs, return_spec) do
|
||||
quote do
|
||||
{:ok, specs} = Code.Typespec.fetch_specs(Form)
|
||||
|
||||
spec =
|
||||
Enum.find(specs, fn {{function_name, arity}, _} ->
|
||||
{function_name, arity} == {unquote(function_name), unquote(function_arity)}
|
||||
end)
|
||||
|
||||
assert spec,
|
||||
"expected the function Form.#{unquote(function_name)}/#{unquote(function_arity)} to have a typespec"
|
||||
|
||||
{{unquote(function_name), unquote(function_arity)}, [{:type, _, :fun, _} = function_spec]} =
|
||||
spec
|
||||
|
||||
{:"::", _, [arguments, return]} =
|
||||
Code.Typespec.spec_to_quoted(unquote(function_name), function_spec)
|
||||
|
||||
accepted_arguments_specs =
|
||||
Enum.map(unquote(arguments_specs), fn arguments_spec ->
|
||||
"#{unquote(function_name)}(#{arguments_spec})"
|
||||
end)
|
||||
|
||||
actual_arguments_spec = Macro.to_string(arguments)
|
||||
assert actual_arguments_spec in accepted_arguments_specs
|
||||
|
||||
expected_return_spec = unquote(return_spec)
|
||||
actual_return_spec = Macro.to_string(return)
|
||||
assert actual_return_spec == expected_return_spec
|
||||
end
|
||||
end
|
||||
|
||||
defmacrop assert_type({module_name, type_name}, expected_type_definition) do
|
||||
quote do
|
||||
{:ok, types} = Code.Typespec.fetch_types(unquote(module_name))
|
||||
|
||||
type =
|
||||
Enum.find(types, fn {declaration, {type_name, _, _}} ->
|
||||
declaration == :type && type_name == unquote(type_name)
|
||||
end)
|
||||
|
||||
assert type,
|
||||
"expected the module #{unquote(module_name)} to have a public type named #{unquote(type_name)}"
|
||||
|
||||
{:type, type} = type
|
||||
|
||||
{:"::", _, [_, type_definition]} = Code.Typespec.type_to_quoted(type)
|
||||
|
||||
actual_type_definition = Macro.to_string(type_definition)
|
||||
|
||||
if is_list(unquote(expected_type_definition)) do
|
||||
if actual_type_definition in unquote(expected_type_definition) do
|
||||
assert true
|
||||
else
|
||||
# we know this will fail at this point, but we're using it to provide a nice failure message
|
||||
assert actual_type_definition == hd(unquote(expected_type_definition))
|
||||
end
|
||||
else
|
||||
assert actual_type_definition == unquote(expected_type_definition)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "the Form module" do
|
||||
@tag task_id: 1
|
||||
test "has documentation" do
|
||||
expected_moduledoc = """
|
||||
A collection of loosely related functions helpful for filling out various forms at the city office.
|
||||
"""
|
||||
|
||||
assert_moduledoc(expected_moduledoc)
|
||||
end
|
||||
end
|
||||
|
||||
describe "blanks/1" do
|
||||
@tag task_id: 2
|
||||
test "returns a string with Xs of a given length" do
|
||||
assert Form.blanks(5) == "XXXXX"
|
||||
end
|
||||
|
||||
@tag task_id: 2
|
||||
test "returns an empty string when given length is 0" do
|
||||
assert Form.blanks(0) == ""
|
||||
end
|
||||
|
||||
@tag task_id: 2
|
||||
test "has documentation" do
|
||||
expected_doc = """
|
||||
Generates a string of a given length.
|
||||
|
||||
This string can be used to fill out a form field that is supposed to have no value.
|
||||
Such fields cannot be left empty because a malicious third party could fill them out with false data.
|
||||
"""
|
||||
|
||||
assert_doc({:blanks, 1}, expected_doc)
|
||||
end
|
||||
|
||||
@tag task_id: 2
|
||||
test "has a correct spec" do
|
||||
assert_spec({:blanks, 1}, ["n :: non_neg_integer()", "non_neg_integer()"], "String.t()")
|
||||
end
|
||||
end
|
||||
|
||||
describe "letters/1" do
|
||||
@tag task_id: 3
|
||||
test "returns a list of upcase letters" do
|
||||
assert Form.letters("Sao Paulo") == ["S", "A", "O", " ", "P", "A", "U", "L", "O"]
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "returns an empty list when given an empty string" do
|
||||
assert Form.letters("") == []
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "has documentation" do
|
||||
expected_doc = """
|
||||
Splits the string into a list of uppercase letters.
|
||||
|
||||
This is needed for form fields that don't offer a single input for the whole string,
|
||||
but instead require splitting the string into a predefined number of single-letter inputs.
|
||||
"""
|
||||
|
||||
assert_doc({:letters, 1}, expected_doc)
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "has a typespec" do
|
||||
assert_spec({:letters, 1}, ["word :: String.t()", "String.t()"], "[String.t()]")
|
||||
end
|
||||
end
|
||||
|
||||
describe "check_length/2" do
|
||||
@tag task_id: 4
|
||||
test "returns :ok is value is below max length" do
|
||||
assert Form.check_length("Ruiz", 6) == :ok
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "returns :ok is value is of exactly max length" do
|
||||
assert Form.check_length("Martinez-Cooper", 15) == :ok
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "returns an error tuple with the difference between max length and actual length" do
|
||||
assert Form.check_length("Martinez-Campbell", 10) == {:error, 7}
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "has documentation" do
|
||||
expected_doc = """
|
||||
Checks if the value has no more than the maximum allowed number of letters.
|
||||
|
||||
This is needed to check that the values of fields do not exceed the maximum allowed length.
|
||||
It also tells you by how much the value exceeds the maximum.
|
||||
"""
|
||||
|
||||
assert_doc({:check_length, 2}, expected_doc)
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "has a typespec" do
|
||||
assert_spec(
|
||||
{:check_length, 2},
|
||||
["word :: String.t(), length :: non_neg_integer()", "String.t(), non_neg_integer()"],
|
||||
":ok | {:error, pos_integer()}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "custom types in the Form module" do
|
||||
@tag task_id: 5
|
||||
test "has a custom 'address_map' type" do
|
||||
expected_type_definitions = [
|
||||
"%{street: String.t(), postal_code: String.t(), city: String.t()}",
|
||||
"%{street: String.t(), city: String.t(), postal_code: String.t()}",
|
||||
"%{postal_code: String.t(), street: String.t(), city: String.t()}",
|
||||
"%{postal_code: String.t(), city: String.t(), street: String.t()}",
|
||||
"%{city: String.t(), street: String.t(), postal_code: String.t()}",
|
||||
"%{city: String.t(), postal_code: String.t(), street: String.t()}"
|
||||
]
|
||||
|
||||
assert_type({Form, :address_map}, expected_type_definitions)
|
||||
end
|
||||
|
||||
@tag task_id: 5
|
||||
test "has a custom 'address_tuple' type with named arguments" do
|
||||
expected_type_definition =
|
||||
"{street :: String.t(), postal_code :: String.t(), city :: String.t()}"
|
||||
|
||||
assert_type({Form, :address_tuple}, expected_type_definition)
|
||||
end
|
||||
|
||||
@tag task_id: 5
|
||||
test "has a custom 'address' type that is a union of 'address_map' and 'address_tuple'" do
|
||||
expected_type_definitions = [
|
||||
"address_map() | address_tuple()",
|
||||
"address_tuple() | address_map()"
|
||||
]
|
||||
|
||||
assert_type({Form, :address}, expected_type_definitions)
|
||||
end
|
||||
end
|
||||
|
||||
describe "format_address/1" do
|
||||
@tag task_id: 6
|
||||
test "accepts a map" do
|
||||
input = %{
|
||||
street: "Wiejska 4/6/8",
|
||||
postal_code: "00-902",
|
||||
city: "Warsaw"
|
||||
}
|
||||
|
||||
result = """
|
||||
WIEJSKA 4/6/8
|
||||
00-902 WARSAW
|
||||
"""
|
||||
|
||||
assert Form.format_address(input) == result
|
||||
end
|
||||
|
||||
@tag task_id: 6
|
||||
test "accepts a 3 string tuple" do
|
||||
result = """
|
||||
PLATZ DER REPUBLIK 1
|
||||
11011 BERLIN
|
||||
"""
|
||||
|
||||
assert Form.format_address({"Platz der Republik 1", "11011", "Berlin"}) == result
|
||||
end
|
||||
|
||||
@tag task_id: 6
|
||||
test "has documentation" do
|
||||
expected_doc = """
|
||||
Formats the address as an uppercase multiline string.
|
||||
"""
|
||||
|
||||
assert_doc({:format_address, 1}, expected_doc)
|
||||
end
|
||||
|
||||
@tag task_id: 6
|
||||
test "has a typespec" do
|
||||
assert_spec({:format_address, 1}, ["address :: address()", "address()"], "String.t()")
|
||||
end
|
||||
end
|
||||
end
|
2
elixir/city-office/test/test_helper.exs
Normal file
2
elixir/city-office/test/test_helper.exs
Normal file
@ -0,0 +1,2 @@
|
||||
ExUnit.start()
|
||||
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
26
elixir/collatz-conjecture/.exercism/config.json
Normal file
26
elixir/collatz-conjecture/.exercism/config.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"authors": [
|
||||
"Tuxified"
|
||||
],
|
||||
"contributors": [
|
||||
"angelikatyborska",
|
||||
"Cohen-Carlisle",
|
||||
"devonestes",
|
||||
"neenjaw",
|
||||
"sotojuan"
|
||||
],
|
||||
"files": {
|
||||
"solution": [
|
||||
"lib/collatz_conjecture.ex"
|
||||
],
|
||||
"test": [
|
||||
"test/collatz_conjecture_test.exs"
|
||||
],
|
||||
"example": [
|
||||
".meta/example.ex"
|
||||
]
|
||||
},
|
||||
"blurb": "Calculate the number of steps to reach 1 using the Collatz conjecture.",
|
||||
"source": "An unsolved problem in mathematics named after mathematician Lothar Collatz",
|
||||
"source_url": "https://en.wikipedia.org/wiki/3x_%2B_1_problem"
|
||||
}
|
1
elixir/collatz-conjecture/.exercism/metadata.json
Normal file
1
elixir/collatz-conjecture/.exercism/metadata.json
Normal file
@ -0,0 +1 @@
|
||||
{"track":"elixir","exercise":"collatz-conjecture","id":"af0a52fbaf7d40e8b28828aff758c922","url":"https://exercism.org/tracks/elixir/exercises/collatz-conjecture","handle":"halfdan","is_requester":true,"auto_approve":false}
|
4
elixir/collatz-conjecture/.formatter.exs
Normal file
4
elixir/collatz-conjecture/.formatter.exs
Normal file
@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
24
elixir/collatz-conjecture/.gitignore
vendored
Normal file
24
elixir/collatz-conjecture/.gitignore
vendored
Normal 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").
|
||||
collatz_conjecture-*.tar
|
||||
|
75
elixir/collatz-conjecture/HELP.md
Normal file
75
elixir/collatz-conjecture/HELP.md
Normal 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/collatz_conjecture.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)
|
||||
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
|
||||
- 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.
|
||||
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).
|
50
elixir/collatz-conjecture/README.md
Normal file
50
elixir/collatz-conjecture/README.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Collatz Conjecture
|
||||
|
||||
Welcome to Collatz Conjecture on Exercism's Elixir Track.
|
||||
If you need help running the tests or submitting your code, check out `HELP.md`.
|
||||
|
||||
## Instructions
|
||||
|
||||
The Collatz Conjecture or 3x+1 problem can be summarized as follows:
|
||||
|
||||
Take any positive integer n. If n is even, divide n by 2 to get n / 2. If n is
|
||||
odd, multiply n by 3 and add 1 to get 3n + 1. Repeat the process indefinitely.
|
||||
The conjecture states that no matter which number you start with, you will
|
||||
always reach 1 eventually.
|
||||
|
||||
Given a number n, return the number of steps required to reach 1.
|
||||
|
||||
## Examples
|
||||
|
||||
Starting with n = 12, the steps would be as follows:
|
||||
|
||||
0. 12
|
||||
1. 6
|
||||
2. 3
|
||||
3. 10
|
||||
4. 5
|
||||
5. 16
|
||||
6. 8
|
||||
7. 4
|
||||
8. 2
|
||||
9. 1
|
||||
|
||||
Resulting in 9 steps. So for input n = 12, the return value would be 9.
|
||||
|
||||
## Source
|
||||
|
||||
### Created by
|
||||
|
||||
- @Tuxified
|
||||
|
||||
### Contributed to by
|
||||
|
||||
- @angelikatyborska
|
||||
- @Cohen-Carlisle
|
||||
- @devonestes
|
||||
- @neenjaw
|
||||
- @sotojuan
|
||||
|
||||
### Based on
|
||||
|
||||
An unsolved problem in mathematics named after mathematician Lothar Collatz - https://en.wikipedia.org/wiki/3x_%2B_1_problem
|
20
elixir/collatz-conjecture/lib/collatz_conjecture.ex
Normal file
20
elixir/collatz-conjecture/lib/collatz_conjecture.ex
Normal file
@ -0,0 +1,20 @@
|
||||
defmodule CollatzConjecture do
|
||||
@doc """
|
||||
calc/1 takes an integer and returns the number of steps required to get the
|
||||
number to 1 when following the rules:
|
||||
- if number is odd, multiply with 3 and add 1
|
||||
- if number is even, divide by 2
|
||||
"""
|
||||
@spec calc(input :: pos_integer()) :: non_neg_integer()
|
||||
|
||||
def calc(input) when is_integer(input) do
|
||||
collatz(input, 0)
|
||||
end
|
||||
|
||||
defp collatz(1, steps), do: steps
|
||||
|
||||
defp collatz(input, steps) when input > 0 and rem(input, 2) == 0,
|
||||
do: collatz(div(input, 2), steps + 1)
|
||||
|
||||
defp collatz(input, steps) when input > 0, do: collatz(3 * input + 1, steps + 1)
|
||||
end
|
28
elixir/collatz-conjecture/mix.exs
Normal file
28
elixir/collatz-conjecture/mix.exs
Normal file
@ -0,0 +1,28 @@
|
||||
defmodule CollatzConjecture.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :collatz_conjecture,
|
||||
version: "0.1.0",
|
||||
# elixir: "~> 1.8",
|
||||
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
|
39
elixir/collatz-conjecture/test/collatz_conjecture_test.exs
Normal file
39
elixir/collatz-conjecture/test/collatz_conjecture_test.exs
Normal file
@ -0,0 +1,39 @@
|
||||
defmodule CollatzConjectureTest do
|
||||
use ExUnit.Case
|
||||
|
||||
test "zero steps for one" do
|
||||
assert CollatzConjecture.calc(1) == 0
|
||||
end
|
||||
|
||||
test "zero is an error" do
|
||||
assert_raise FunctionClauseError, fn -> CollatzConjecture.calc(0) end
|
||||
end
|
||||
|
||||
test "divide if even" do
|
||||
assert CollatzConjecture.calc(16) == 4
|
||||
end
|
||||
|
||||
test "even and odd steps" do
|
||||
assert CollatzConjecture.calc(12) == 9
|
||||
end
|
||||
|
||||
test "Large number of even and odd steps" do
|
||||
assert CollatzConjecture.calc(1_000_000) == 152
|
||||
end
|
||||
|
||||
test "start with odd step" do
|
||||
assert CollatzConjecture.calc(21) == 7
|
||||
end
|
||||
|
||||
test "more steps than starting number" do
|
||||
assert CollatzConjecture.calc(7) == 16
|
||||
end
|
||||
|
||||
test "negative value is an error " do
|
||||
assert_raise FunctionClauseError, fn -> CollatzConjecture.calc(-15) end
|
||||
end
|
||||
|
||||
test "string as input value is an error " do
|
||||
assert_raise FunctionClauseError, fn -> CollatzConjecture.calc("fubar") end
|
||||
end
|
||||
end
|
2
elixir/collatz-conjecture/test/test_helper.exs
Normal file
2
elixir/collatz-conjecture/test/test_helper.exs
Normal file
@ -0,0 +1,2 @@
|
||||
ExUnit.start()
|
||||
ExUnit.configure(exclude: :pending, trace: true)
|
22
elixir/dna-encoding/.exercism/config.json
Normal file
22
elixir/dna-encoding/.exercism/config.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"authors": [
|
||||
"neenjaw"
|
||||
],
|
||||
"contributors": [
|
||||
"angelikatyborska",
|
||||
"NobbZ"
|
||||
],
|
||||
"files": {
|
||||
"solution": [
|
||||
"lib/dna.ex"
|
||||
],
|
||||
"test": [
|
||||
"test/dna_test.exs"
|
||||
],
|
||||
"exemplar": [
|
||||
".meta/exemplar.ex"
|
||||
]
|
||||
},
|
||||
"language_versions": ">=1.10",
|
||||
"blurb": "Learn about bitstrings and tail call recursion by encoding DNA sequences as binary data."
|
||||
}
|
1
elixir/dna-encoding/.exercism/metadata.json
Normal file
1
elixir/dna-encoding/.exercism/metadata.json
Normal file
@ -0,0 +1 @@
|
||||
{"track":"elixir","exercise":"dna-encoding","id":"b36d8706517343e99bd8187edf2b3be6","url":"https://exercism.org/tracks/elixir/exercises/dna-encoding","handle":"halfdan","is_requester":true,"auto_approve":false}
|
4
elixir/dna-encoding/.formatter.exs
Normal file
4
elixir/dna-encoding/.formatter.exs
Normal file
@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
24
elixir/dna-encoding/.gitignore
vendored
Normal file
24
elixir/dna-encoding/.gitignore
vendored
Normal 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").
|
||||
bitstrings-*.tar
|
||||
|
75
elixir/dna-encoding/HELP.md
Normal file
75
elixir/dna-encoding/HELP.md
Normal 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/dna.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)
|
||||
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
|
||||
- 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.
|
||||
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).
|
45
elixir/dna-encoding/HINTS.md
Normal file
45
elixir/dna-encoding/HINTS.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Hints
|
||||
|
||||
## General
|
||||
|
||||
- Use `?` to work with the character [code points][codepoint].
|
||||
- `\s` can be used to represent a space.
|
||||
- Use [integer binary notation][integer-literal] for working with the codes.
|
||||
- Try to use the tail call recursion strategy.
|
||||
|
||||
## 1. Encode nucleic acid to binary value
|
||||
|
||||
- This function needs to map one integer to another.
|
||||
- Making use of multiple clause functions may make this easier by breaking it down.
|
||||
|
||||
## 2. Decode the binary value to the nucleic acid
|
||||
|
||||
- This function is the opposite of part 1's function.
|
||||
- Making use of multiple clause functions may make this easier by breaking it down.
|
||||
|
||||
## 3. Encode a DNA charlist
|
||||
|
||||
- Create a recursive function which takes a code point from the charlist and recursively builds the bitstring result.
|
||||
- Remember, a [charlist][charlist] is a list of [integer code points][codepoint].
|
||||
- You can get the first and remaining items from a list using a build in [`Kernel` module][kernel] function.
|
||||
- You can also pattern match on a list using the [`[head | tail]`][list] notation.
|
||||
- Use multiple clause functions to separate the base case from the recursive cases.
|
||||
- Do not forget to specify the types of bitstring segments using the `::` operator.
|
||||
|
||||
## 4. Decode a DNA bitstring
|
||||
|
||||
- Create a recursive function which [matches the first 4 bits][bitstring-matching] from the [bitstring][bitstring] and recursively builds the [charlist][charlist] result.
|
||||
- Remember the [bitstring special form][bitstring-form] can be used for matching on bitstrings.
|
||||
- Do not forget to specify the types of bitstring segments using the `::` operator.
|
||||
- You will need to reverse the accumulator at the end. Write a private tail-recursive `reverse` function to do that and use it in the base-case of the `decode` function.
|
||||
|
||||
[integer-literal]: https://hexdocs.pm/elixir/master/syntax-reference.html#integers-in-other-bases-and-unicode-code-points
|
||||
[codepoint]: https://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html#unicode-and-code-points
|
||||
[charlist]: https://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html#charlists
|
||||
[bitstring]: https://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html#bitstrings
|
||||
[bitstring-form]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1
|
||||
[bitstring-matching]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1-binary-bitstring-matching
|
||||
[type-operator]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#::/2
|
||||
[recursion-tco]: https://en.wikipedia.org/wiki/Tail_call
|
||||
[list]: https://hexdocs.pm/elixir/List.html#content
|
||||
[kernel]: https://hexdocs.pm/elixir/Kernel.html#functions
|
149
elixir/dna-encoding/README.md
Normal file
149
elixir/dna-encoding/README.md
Normal file
@ -0,0 +1,149 @@
|
||||
# DNA Encoding
|
||||
|
||||
Welcome to DNA Encoding 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
|
||||
|
||||
## Bitstrings
|
||||
|
||||
Working with binary data is an important concept in any language, and Elixir provides an elegant syntax to write, match, and construct binary data.
|
||||
|
||||
In Elixir, binary data is referred to as the bitstring type. The binary data*type* (not to be confused with binary data in general) is a specific form of a bitstring, which we will discuss in a later exercise.
|
||||
|
||||
Bitstring literals are defined using the bitstring special form `<<>>`. When defining a bitstring literal, it is defined in segments. Each segment has a value and type, separated by the `::` operator. The type specifies how many bits will be used to encode the value. If the value of the segment overflows the capacity of the segment's type, it will be truncated from the left.
|
||||
|
||||
```elixir
|
||||
# This defines a bitstring with three segments of a single bit each
|
||||
<<0::1, 1::1, 0::1>> == <<0::size(1), 1::size(1), 0::size(1)>>
|
||||
# => true
|
||||
<<0::1, 1::1, 0::1>> == <<2::size(3)>>
|
||||
# => true
|
||||
<<2::1>> == <<0::1>>
|
||||
# => true because of value overflow
|
||||
```
|
||||
|
||||
When writing binary integer literals, we can write them directly in base-2 notation by prefixing the literal with `0b`.
|
||||
|
||||
```elixir
|
||||
value = 0b11111011011 = 2011
|
||||
```
|
||||
|
||||
By default, bitstrings are displayed in chunks of 8 bits (a byte).
|
||||
|
||||
```
|
||||
<<value::11>>
|
||||
# => <<251, 3::size(3)>>
|
||||
```
|
||||
|
||||
### Constructing
|
||||
|
||||
We can combine bitstrings stored in variables using the special form:
|
||||
|
||||
```elixir
|
||||
first = <<0b110::3>>
|
||||
second = <<0b001::3>>
|
||||
combined = <<first::bitstring, second::bitstring>>
|
||||
# => <<49::size(6)>>
|
||||
```
|
||||
|
||||
### Pattern matching
|
||||
|
||||
Pattern matching can also be done to obtain the value from within the special form:
|
||||
|
||||
```elixir
|
||||
<<value::4, rest::bitstring>> = <<0b01101001::8>>
|
||||
value == 0b0110
|
||||
# => true
|
||||
```
|
||||
|
||||
## Tail Call Recursion
|
||||
|
||||
When [recursing][exercism-recursion] through enumerables (lists, bitstrings, strings), there are often two concerns:
|
||||
|
||||
- how much memory is required to store the trail of recursive function calls
|
||||
- how to build the solution efficiently
|
||||
|
||||
To deal with these concerns an _accumulator_ may be used.
|
||||
|
||||
An accumulator is a variable that is passed along in addition to the data. It is used to pass the current state of the function's execution, from function call to function call, until the _base case_ is reached. In the base case, the accumulator is used to return the final value of the recursive function call.
|
||||
|
||||
Accumulators should be initialized by the function's author, not the function's user. To achieve this, declare two functions - a public function that takes just the necessary data as arguments and initializes the accumulator, and a private function that also takes an accumulator. In Elixir, it is a common pattern to prefix the private function's name with `do_`.
|
||||
|
||||
```elixir
|
||||
# Count the length of a list without an accumulator
|
||||
def count([]), do: 0
|
||||
def count([_head | tail]), do: 1 + count(tail)
|
||||
|
||||
# Count the length of a list with an accumulator
|
||||
def count(list), do: do_count(list, 0)
|
||||
|
||||
defp do_count([], count), do: count
|
||||
defp do_count([_head | tail], count), do: do_count(tail, count + 1)
|
||||
```
|
||||
|
||||
The usage of an accumulator allows us to turn recursive functions into _tail-recursive_ functions. A function is tail-recursive if the _last_ thing executed by the function is a call to itself.
|
||||
|
||||
[exercism-recursion]: https://exercism.org/tracks/elixir/concepts/recursion
|
||||
|
||||
## Instructions
|
||||
|
||||
In your DNA research lab, you have been working through various ways to compress your research data to save storage space. One teammate suggests converting the DNA data to a binary representation:
|
||||
|
||||
| Nucleic Acid | Code |
|
||||
| ------------ | ------ |
|
||||
| a space | `0000` |
|
||||
| A | `0001` |
|
||||
| C | `0010` |
|
||||
| G | `0100` |
|
||||
| T | `1000` |
|
||||
|
||||
You ponder this, as it will potentially halve the required data storage costs, but at the expense of human readability. You decide to write a module to encode and decode your data to benchmark your savings.
|
||||
|
||||
## 1. Encode nucleic acid to binary value
|
||||
|
||||
Implement `encode_nucleotide/1` to accept the code point for the nucleic acid and return the integer value of the encoded code.
|
||||
|
||||
```elixir
|
||||
DNA.encode_nucleotide(?A)
|
||||
# => 0b0001
|
||||
```
|
||||
|
||||
## 2. Decode the binary value to the nucleic acid
|
||||
|
||||
Implement `decode_nucleotide/1` to accept the integer value of the encoded code and return the code point for the nucleic acid.
|
||||
|
||||
```elixir
|
||||
DNA.decode_nucleotide(0b0001)
|
||||
# => ?A
|
||||
```
|
||||
|
||||
## 3. Encode a DNA charlist
|
||||
|
||||
Implement `encode/1` to accept a charlist representing nucleic acids and gaps and return a bitstring of the encoded data.
|
||||
|
||||
```elixir
|
||||
DNA.encode('AC GT')
|
||||
# => <<18, 4, 8::size(4)>>
|
||||
```
|
||||
|
||||
## 4. Decode a DNA bitstring
|
||||
|
||||
Implement `decode/1` to accept a bitstring representing nucleic acids and gaps and return the decoded data as a charlist.
|
||||
|
||||
```elixir
|
||||
DNA.decode(<<132, 2, 1::size(4)>>)
|
||||
# => 'TG CA'
|
||||
```
|
||||
|
||||
## Source
|
||||
|
||||
### Created by
|
||||
|
||||
- @neenjaw
|
||||
|
||||
### Contributed to by
|
||||
|
||||
- @angelikatyborska
|
||||
- @NobbZ
|
17
elixir/dna-encoding/lib/dna.ex
Normal file
17
elixir/dna-encoding/lib/dna.ex
Normal file
@ -0,0 +1,17 @@
|
||||
defmodule DNA do
|
||||
def encode_nucleotide(code_point) do
|
||||
# Please implement the encode_nucleotide/1 function
|
||||
end
|
||||
|
||||
def decode_nucleotide(encoded_code) do
|
||||
# Please implement the decode_nucleotide/1 function
|
||||
end
|
||||
|
||||
def encode(dna) do
|
||||
# Please implement the encode/1 function
|
||||
end
|
||||
|
||||
def decode(dna) do
|
||||
# Please implement the decode/1 function
|
||||
end
|
||||
end
|
28
elixir/dna-encoding/mix.exs
Normal file
28
elixir/dna-encoding/mix.exs
Normal file
@ -0,0 +1,28 @@
|
||||
defmodule DNA.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :dna,
|
||||
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
|
71
elixir/dna-encoding/test/dna_test.exs
Normal file
71
elixir/dna-encoding/test/dna_test.exs
Normal file
@ -0,0 +1,71 @@
|
||||
defmodule DNATest do
|
||||
use ExUnit.Case
|
||||
|
||||
describe "encode to 4-bit encoding" do
|
||||
@tag task_id: 1
|
||||
test "?\\s to 0b0000", do: assert(DNA.encode_nucleotide(?\s) == 0b0000)
|
||||
@tag task_id: 1
|
||||
test "?A to 0b0001", do: assert(DNA.encode_nucleotide(?A) == 0b0001)
|
||||
@tag task_id: 1
|
||||
test "?C to 0b0010", do: assert(DNA.encode_nucleotide(?C) == 0b0010)
|
||||
@tag task_id: 1
|
||||
test "?G to 0b0100", do: assert(DNA.encode_nucleotide(?G) == 0b0100)
|
||||
@tag task_id: 1
|
||||
test "?T to 0b1000", do: assert(DNA.encode_nucleotide(?T) == 0b1000)
|
||||
end
|
||||
|
||||
describe "decode to code point" do
|
||||
@tag task_id: 2
|
||||
test "0b0000 to ?\\s", do: assert(DNA.decode_nucleotide(0b0000) == ?\s)
|
||||
@tag task_id: 2
|
||||
test "0b0001 to ?A", do: assert(DNA.decode_nucleotide(0b0001) == ?A)
|
||||
@tag task_id: 2
|
||||
test "0b0010 to ?C", do: assert(DNA.decode_nucleotide(0b0010) == ?C)
|
||||
@tag task_id: 2
|
||||
test "0b0100 to ?G", do: assert(DNA.decode_nucleotide(0b0100) == ?G)
|
||||
@tag task_id: 2
|
||||
test "0b1000 to ?T", do: assert(DNA.decode_nucleotide(0b1000) == ?T)
|
||||
end
|
||||
|
||||
describe "encoding" do
|
||||
@tag task_id: 3
|
||||
test "' '", do: assert(DNA.encode(' ') == <<0b0000::4>>)
|
||||
@tag task_id: 3
|
||||
test "'A'", do: assert(DNA.encode('A') == <<0b0001::4>>)
|
||||
@tag task_id: 3
|
||||
test "'C'", do: assert(DNA.encode('C') == <<0b0010::4>>)
|
||||
@tag task_id: 3
|
||||
test "'G'", do: assert(DNA.encode('G') == <<0b0100::4>>)
|
||||
@tag task_id: 3
|
||||
test "'T'", do: assert(DNA.encode('T') == <<0b1000::4>>)
|
||||
|
||||
@tag task_id: 3
|
||||
test "' ACGT'",
|
||||
do: assert(DNA.encode(' ACGT') == <<0b0000::4, 0b0001::4, 0b0010::4, 0b0100::4, 0b1000::4>>)
|
||||
|
||||
@tag task_id: 3
|
||||
test "'TGCA '",
|
||||
do: assert(DNA.encode('TGCA ') == <<0b1000::4, 0b0100::4, 0b0010::4, 0b0001::4, 0b0000::4>>)
|
||||
end
|
||||
|
||||
describe "decoding" do
|
||||
@tag task_id: 4
|
||||
test "' '", do: assert(DNA.decode(<<0b0000::4>>) == ' ')
|
||||
@tag task_id: 4
|
||||
test "'A'", do: assert(DNA.decode(<<0b0001::4>>) == 'A')
|
||||
@tag task_id: 4
|
||||
test "'C'", do: assert(DNA.decode(<<0b0010::4>>) == 'C')
|
||||
@tag task_id: 4
|
||||
test "'G'", do: assert(DNA.decode(<<0b0100::4>>) == 'G')
|
||||
@tag task_id: 4
|
||||
test "'T'", do: assert(DNA.decode(<<0b1000::4>>) == 'T')
|
||||
|
||||
@tag task_id: 4
|
||||
test "' ACGT'",
|
||||
do: assert(DNA.decode(<<0b0000::4, 0b0001::4, 0b0010::4, 0b0100::4, 0b1000::4>>) == ' ACGT')
|
||||
|
||||
@tag task_id: 4
|
||||
test "'TGCA '",
|
||||
do: assert(DNA.decode(<<0b1000::4, 0b0100::4, 0b0010::4, 0b0001::4, 0b0000::4>>) == 'TGCA ')
|
||||
end
|
||||
end
|
2
elixir/dna-encoding/test/test_helper.exs
Normal file
2
elixir/dna-encoding/test/test_helper.exs
Normal file
@ -0,0 +1,2 @@
|
||||
ExUnit.start()
|
||||
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
24
elixir/guessing-game/.exercism/config.json
Normal file
24
elixir/guessing-game/.exercism/config.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"authors": [
|
||||
"neenjaw"
|
||||
],
|
||||
"contributors": [
|
||||
"angelikatyborska"
|
||||
],
|
||||
"files": {
|
||||
"solution": [
|
||||
"lib/guessing_game.ex"
|
||||
],
|
||||
"test": [
|
||||
"test/guessing_game_test.exs"
|
||||
],
|
||||
"exemplar": [
|
||||
".meta/exemplar.ex"
|
||||
]
|
||||
},
|
||||
"language_versions": ">=1.10",
|
||||
"forked_from": [
|
||||
"fsharp/guessing-game"
|
||||
],
|
||||
"blurb": "Learn about multiple clause functions, guards, and default arguments by implementing a simple game in which the player needs to guess a secret number."
|
||||
}
|
1
elixir/guessing-game/.exercism/metadata.json
Normal file
1
elixir/guessing-game/.exercism/metadata.json
Normal file
@ -0,0 +1 @@
|
||||
{"track":"elixir","exercise":"guessing-game","id":"f70876e9539441d6b6ec0053b3682657","url":"https://exercism.org/tracks/elixir/exercises/guessing-game","handle":"halfdan","is_requester":true,"auto_approve":false}
|
4
elixir/guessing-game/.formatter.exs
Normal file
4
elixir/guessing-game/.formatter.exs
Normal file
@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
24
elixir/guessing-game/.gitignore
vendored
Normal file
24
elixir/guessing-game/.gitignore
vendored
Normal 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").
|
||||
multiple_clause_functions-*.tar
|
||||
|
75
elixir/guessing-game/HELP.md
Normal file
75
elixir/guessing-game/HELP.md
Normal 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/guessing_game.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)
|
||||
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
|
||||
- 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.
|
||||
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).
|
33
elixir/guessing-game/HINTS.md
Normal file
33
elixir/guessing-game/HINTS.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Hints
|
||||
|
||||
## General
|
||||
|
||||
- In Elixir's ['Getting Started Guide'][guide] there is a nice refresher about named functions.
|
||||
|
||||
## 1. Make the response when the guess matches the secret number
|
||||
|
||||
- You can use a [guard][guard] to check if the numbers are the same with `===/2`.
|
||||
|
||||
## 2. Make the response when the guess is greater than the secret number
|
||||
|
||||
- You can add a [function clause][multiple-fn-clauses] and [guards][guard] to check if the guess is greater `>/2` than the secret number.
|
||||
|
||||
## 3. Make the response when the guess is less than the secret number
|
||||
|
||||
- You can add a [function clause][multiple-fn-clauses] and [guards][guard] to check if the guess is less than `</2` the secret number.
|
||||
|
||||
## 4. Make the responses when the guess is one more or one less than the secret number
|
||||
|
||||
- You can add a [function clause][multiple-fn-clauses] and [guards][guard] to check if the guess is one less or one more than the secret number.
|
||||
- Guards expressions can use `and/2`, or `or/2` to combine boolean expressions.
|
||||
- Pay attention to the order of the function clauses.
|
||||
|
||||
## 5. Make the response when there is no guess
|
||||
|
||||
- You can make use of a [default argument][default-arg] for a guess. The default value does not have to be an integer.
|
||||
- Use a function header before all the other function clauses to define the default argument.
|
||||
|
||||
[default-arg]: https://elixir-lang.org/getting-started/modules-and-functions.html#default-arguments
|
||||
[guard]: https://hexdocs.pm/elixir/master/Kernel.html#guards
|
||||
[guide]: https://elixir-lang.org/getting-started/modules-and-functions.html#named-functions
|
||||
[multiple-fn-clauses]: https://elixir-lang.org/getting-started/modules-and-functions.html#named-functions
|
131
elixir/guessing-game/README.md
Normal file
131
elixir/guessing-game/README.md
Normal file
@ -0,0 +1,131 @@
|
||||
# Guessing Game
|
||||
|
||||
Welcome to Guessing Game 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
|
||||
|
||||
## Multiple Clause Functions
|
||||
|
||||
Elixir facilitates **Open-Close Principle** practices by allowing functions to have multiple clauses, so instead of sprawling and hard-coded control-logic, pointed functions can be written to add/remove behavior easily.
|
||||
|
||||
Elixir offers _multiple function clauses_ and _guards_ to write:
|
||||
|
||||
```elixir
|
||||
def number(n) when n == 7 do
|
||||
"Awesome, that's my favorite"
|
||||
end
|
||||
def number(_n) do
|
||||
"That's not my favorite"
|
||||
end
|
||||
```
|
||||
|
||||
At run-time, Elixir will test, from top to bottom of the source file, which function clause to invoke.
|
||||
|
||||
Variables that are unused should be prefixed with an underscore.
|
||||
|
||||
## Guards
|
||||
|
||||
Guards are used to prevent Elixir from invoking functions based on evaluation of the arguments by guard functions. Guards begin with the `when` keyword, followed by a boolean expression. Guard functions are special functions which:
|
||||
|
||||
- Must be pure and not mutate any global states.
|
||||
- Must return strict `true` or `false` values.
|
||||
|
||||
A list of common guards can be found in the [Elixir documentation][kernel-guards]. It includes type checks, basic arithmetic, comparisons, and strictly boolean operators.
|
||||
|
||||
## Default Arguments
|
||||
|
||||
Functions may declare default values for one or more arguments. Let's consider this function:
|
||||
|
||||
```elixir
|
||||
def number(n \\ 13), do: "That's not my favorite"
|
||||
```
|
||||
|
||||
When compiled, Elixir creates a function definition for `number/0` (no arguments), and `number/1` (one argument).
|
||||
|
||||
If more than one argument has default values, the default values will be applied to the function from left to right to fill in for missing arguments.
|
||||
|
||||
If the function has more than one clause, the default arguments should be defined in a function header (a function without a body) before the function clauses:
|
||||
|
||||
```elixir
|
||||
def number(n \\ 13)
|
||||
def number(n) when n < 10, do: "Dream bigger!"
|
||||
def number(n) when n > 100, do: "Not that big..."
|
||||
```
|
||||
|
||||
[kernel-guards]: https://hexdocs.pm/elixir/master/Kernel.html#guards
|
||||
|
||||
## Instructions
|
||||
|
||||
You are creating a trivial online game where a friend can guess a secret number. You want to give some feedback, but not give away the answer with a guess. You need to devise a function to provide different responses depending on how the guess relates to the secret number.
|
||||
|
||||
| Condition | Response |
|
||||
| ------------------------------------------------------------- | -------------- |
|
||||
| When the guess matches the secret number | "Correct" |
|
||||
| When the guess is one more or one less than the secret number | "So close" |
|
||||
| When the guess is greater than the secret number | "Too high" |
|
||||
| When the guess is less than the secret number | "Too low" |
|
||||
| When a guess isn't made | "Make a guess" |
|
||||
|
||||
All guesses and secret numbers are integer numbers.
|
||||
|
||||
## 1. Make the response when the guess matches the secret number
|
||||
|
||||
Implement the `compare/2` function which takes two arguments, `secret_number` and `guess`, which are both integers.
|
||||
|
||||
```elixir
|
||||
GuessingGame.compare(5, 5)
|
||||
# => "Correct"
|
||||
```
|
||||
|
||||
## 2. Make the response when the guess is greater than the secret number
|
||||
|
||||
Modify the `compare` function to respond to guesses that are higher than the secret number.
|
||||
|
||||
```elixir
|
||||
GuessingGame.compare(5, 8)
|
||||
# => "Too high"
|
||||
```
|
||||
|
||||
## 3. Make the response when the guess is less than the secret number
|
||||
|
||||
Modify the `compare` function to respond to guesses that are lower than the secret number.
|
||||
|
||||
```elixir
|
||||
GuessingGame.compare(5, 2)
|
||||
# => "Too low"
|
||||
```
|
||||
|
||||
## 4. Make the responses when the guess is one more or one less than the secret number
|
||||
|
||||
Modify the `compare` function to respond to guesses that are close to the secret number.
|
||||
|
||||
```elixir
|
||||
GuessingGame.compare(5, 6)
|
||||
# => "So close"
|
||||
GuessingGame.compare(5, 4)
|
||||
# => "So close"
|
||||
```
|
||||
|
||||
## 5. Make the response when there is no guess
|
||||
|
||||
Modify the `compare` function to respond to a lack of guess.
|
||||
|
||||
```elixir
|
||||
GuessingGame.compare(5)
|
||||
# => "Make a guess"
|
||||
|
||||
GuessingGame.compare(5, :no_guess)
|
||||
# => "Make a guess"
|
||||
```
|
||||
|
||||
## Source
|
||||
|
||||
### Created by
|
||||
|
||||
- @neenjaw
|
||||
|
||||
### Contributed to by
|
||||
|
||||
- @angelikatyborska
|
12
elixir/guessing-game/lib/guessing_game.ex
Normal file
12
elixir/guessing-game/lib/guessing_game.ex
Normal file
@ -0,0 +1,12 @@
|
||||
defmodule GuessingGame do
|
||||
|
||||
def compare(_secret_number, :no_guess \\ :no_guess), do: "Make a guess"
|
||||
|
||||
def compare(secret_number, guess) when secret_number == guess, do: "Correct"
|
||||
|
||||
def compare(secret_number, guess) when abs(guess-secret_number) == 1, do: "So close"
|
||||
|
||||
def compare(secret_number, guess) when guess > secret_number, do: "Too high"
|
||||
|
||||
def compare(secret_number, guess) when guess < secret_number, do: "Too low"
|
||||
end
|
28
elixir/guessing-game/mix.exs
Normal file
28
elixir/guessing-game/mix.exs
Normal file
@ -0,0 +1,28 @@
|
||||
defmodule GuessingGame.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :guessing_game,
|
||||
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
|
38
elixir/guessing-game/test/guessing_game_test.exs
Normal file
38
elixir/guessing-game/test/guessing_game_test.exs
Normal file
@ -0,0 +1,38 @@
|
||||
defmodule GuessingGameTest do
|
||||
use ExUnit.Case
|
||||
|
||||
@tag task_id: 1
|
||||
test "correct when the guessed number equals secret" do
|
||||
assert GuessingGame.compare(7, 7) == "Correct"
|
||||
end
|
||||
|
||||
@tag task_id: 2
|
||||
test "too high when guessed number is greater than the secret" do
|
||||
assert GuessingGame.compare(9, 18) == "Too high"
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "too low when guessed number is less than the secret" do
|
||||
assert GuessingGame.compare(42, 30) == "Too low"
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "so close when guess differs from secret by -1" do
|
||||
assert GuessingGame.compare(64, 63) == "So close"
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "so close when guess differs from secret by +1" do
|
||||
assert GuessingGame.compare(52, 53) == "So close"
|
||||
end
|
||||
|
||||
@tag task_id: 5
|
||||
test "when no guess is supplied, ask the player to make a guess" do
|
||||
assert GuessingGame.compare(15) == "Make a guess"
|
||||
end
|
||||
|
||||
@tag task_id: 5
|
||||
test "when the atom :no_guess is supplied, ask the player to make a guess" do
|
||||
assert GuessingGame.compare(16, :no_guess) == "Make a guess"
|
||||
end
|
||||
end
|
2
elixir/guessing-game/test/test_helper.exs
Normal file
2
elixir/guessing-game/test/test_helper.exs
Normal file
@ -0,0 +1,2 @@
|
||||
ExUnit.start()
|
||||
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
21
elixir/kitchen-calculator/.exercism/config.json
Normal file
21
elixir/kitchen-calculator/.exercism/config.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"authors": [
|
||||
"neenjaw"
|
||||
],
|
||||
"contributors": [
|
||||
"angelikatyborska"
|
||||
],
|
||||
"files": {
|
||||
"solution": [
|
||||
"lib/kitchen_calculator.ex"
|
||||
],
|
||||
"test": [
|
||||
"test/kitchen_calculator_test.exs"
|
||||
],
|
||||
"exemplar": [
|
||||
".meta/exemplar.ex"
|
||||
]
|
||||
},
|
||||
"language_versions": ">=1.10",
|
||||
"blurb": "Learn about tuples and pattern matching by converting common US baking measurements to the metric system."
|
||||
}
|
1
elixir/kitchen-calculator/.exercism/metadata.json
Normal file
1
elixir/kitchen-calculator/.exercism/metadata.json
Normal file
@ -0,0 +1 @@
|
||||
{"track":"elixir","exercise":"kitchen-calculator","id":"fba1265584b9449783f6074ce0c8ceb8","url":"https://exercism.org/tracks/elixir/exercises/kitchen-calculator","handle":"halfdan","is_requester":true,"auto_approve":false}
|
4
elixir/kitchen-calculator/.formatter.exs
Normal file
4
elixir/kitchen-calculator/.formatter.exs
Normal file
@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
24
elixir/kitchen-calculator/.gitignore
vendored
Normal file
24
elixir/kitchen-calculator/.gitignore
vendored
Normal 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").
|
||||
tuples-*.tar
|
||||
|
75
elixir/kitchen-calculator/HELP.md
Normal file
75
elixir/kitchen-calculator/HELP.md
Normal 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/kitchen_calculator.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)
|
||||
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
|
||||
- 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.
|
||||
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).
|
30
elixir/kitchen-calculator/HINTS.md
Normal file
30
elixir/kitchen-calculator/HINTS.md
Normal file
@ -0,0 +1,30 @@
|
||||
# Hints
|
||||
|
||||
## General
|
||||
|
||||
- [Tuples][tuple-module] are data structures which are arranged in contiguous memory and can hold any data-type.
|
||||
- Atoms may be used to denote finite states, as this exercise uses `:cup`, `:fluid_ounce`, `:teaspoon`, `:tablespoon`, `:milliliter` to denote the units used.
|
||||
- You may use [`Kernel`][elem] or [`Tuple`][tuple-module] functions or pattern matching to manipulate the tuples.
|
||||
|
||||
## 1. Get the numeric component from a volume-pair
|
||||
|
||||
- Consider using [a `Kernel` module function][elem] to return the volume-pair's numeric component.
|
||||
|
||||
## 2. Convert the volume-pair to milliliters
|
||||
|
||||
- Use [multiple clause functions][multi-clause] and [pattern matching][pattern-matching] to reduce conditional control flow logic.
|
||||
- Implement the function for all units to milliliters, including milliliters to milliliters.
|
||||
|
||||
## 3. Convert the milliliter volume-pair to another unit
|
||||
|
||||
- Use [multiple clause functions][multi-clause] and [pattern matching][pattern-matching] to reduce conditional control flow logic.
|
||||
- Implement the function for all units to milliliters, including milliliters to milliliters.
|
||||
|
||||
## 4. Convert from any unit to any unit
|
||||
|
||||
- Reuse the functions already created to perform the conversion in the `convert/2` function.
|
||||
|
||||
[elem]: https://hexdocs.pm/elixir/Kernel.html#elem/2
|
||||
[multi-clause]: https://elixir-lang.org/getting-started/modules-and-functions.html#named-functions
|
||||
[tuple-module]: https://hexdocs.pm/elixir/Tuple.html
|
||||
[pattern-matching]: https://medium.com/rebirth-delivery/how-to-use-elixir-pattern-matched-functions-arguments-a793733acc6d
|
143
elixir/kitchen-calculator/README.md
Normal file
143
elixir/kitchen-calculator/README.md
Normal file
@ -0,0 +1,143 @@
|
||||
# Kitchen Calculator
|
||||
|
||||
Welcome to Kitchen Calculator 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
|
||||
|
||||
## Tuples
|
||||
|
||||
In Elixir, a tuple is a data structure which organizes data, holding a fixed number of items of any type, but without explicit names for each element. Tuples are often used in Elixir for memory read-intensive operations, since read-access of an element is a constant-time operation. They are not usually used when elements may need to be added/removed dynamically because rather than modifying the existing tuple, a new tuple is created which requires memory to be allocated upfront.
|
||||
|
||||
In practice, tuples are created in Elixir using curly braces. Elements in a tuple can be individually accessed using the `elem/2` function using 0-based indexing:
|
||||
|
||||
```elixir
|
||||
empty_tuple = {}
|
||||
one_element_tuple = {1}
|
||||
multiple_element_tuple = {1, :a, "hello"}
|
||||
|
||||
elem(multiple_element_tuple, 2)
|
||||
# => "hello"
|
||||
```
|
||||
|
||||
### Tuples as grouped information
|
||||
|
||||
Tuples are often used in practice to represent grouped information.
|
||||
|
||||
```elixir
|
||||
Float.ratio(0.25)
|
||||
# => {1, 4} indicating the numerator and denominator of the fraction ¼
|
||||
```
|
||||
|
||||
## Pattern Matching
|
||||
|
||||
The use of pattern matching is dominant in assertive, idiomatic Elixir code. You might recall that `=/2` is described as a match operator rather than as an assignment operator. When using the match operator, if the pattern on the left matches the right, any variables on the left are bound, and the value of the right side is returned. A `MatchError` is raised if there is no match.
|
||||
|
||||
```elixir
|
||||
2 = 2
|
||||
# => 2
|
||||
# Literals can be matched if they are the same
|
||||
|
||||
2 = 3
|
||||
# => ** (MatchError) no match of right hand side value: 3
|
||||
|
||||
{_, denominator} = Float.ratio(0.25)
|
||||
# => {1, 4}
|
||||
# the variable `denominator` is bound to the value 4
|
||||
```
|
||||
|
||||
Remember, matches occur from the right side to the left side.
|
||||
|
||||
In the last example if we don't need a variable in a pattern match, we can discard it by referencing `_`. Any variable starting with an `_` is not tracked by the runtime.
|
||||
|
||||
### Pattern matching in named functions
|
||||
|
||||
Pattern matching is also a useful tool when creating multiple function clauses. Pattern matching can be used on the functions' arguments which then determines which function clause to invoke -- starting from the top of the file down until the first match. Variables may be bound in a function head and used in the function body.
|
||||
|
||||
```elixir
|
||||
defmodule Example do
|
||||
def named_function(:a = variable_a) do
|
||||
{variable_a, 1}
|
||||
end
|
||||
|
||||
def named_function(:b = variable_b) do
|
||||
{variable_b, 2}
|
||||
end
|
||||
end
|
||||
|
||||
Example.named_function(:a)
|
||||
# => {:a, 1}
|
||||
|
||||
Example.named_function(:b)
|
||||
# => {:b, 2}
|
||||
|
||||
Example.named_function(:c)
|
||||
# => ** (FunctionClauseError) no function clause matching in Example.named_function/1
|
||||
```
|
||||
|
||||
## Instructions
|
||||
|
||||
While preparing to bake cookies for your friends, you have found that you have to convert some of the measurements used in the recipe. Being only familiar with the metric system, you need to come up with a way to convert common US baking measurements to milliliters (mL) for your own ease.
|
||||
|
||||
Use this conversion chart for your solution:
|
||||
|
||||
| Unit to convert | volume | in milliliters (mL) |
|
||||
| --------------- | ------ | ------------------- |
|
||||
| mL | 1 | 1 |
|
||||
| US cup | 1 | 240 |
|
||||
| US fluid ounce | 1 | 30 |
|
||||
| US teaspoon | 1 | 5 |
|
||||
| US tablespoon | 1 | 15 |
|
||||
|
||||
Being a talented programmer in training, you decide to use milliliters as a transition unit to facilitate the conversion from any unit listed to any other (even itself).
|
||||
|
||||
## 1. Get the numeric component from a volume-pair
|
||||
|
||||
Implement the `KitchenCalculator.get_volume/1` function. Given a volume-pair tuple, it should return just the numeric component.
|
||||
|
||||
```elixir
|
||||
KitchenCalculator.get_volume({:cup, 2.0})
|
||||
# => 2.0
|
||||
```
|
||||
|
||||
## 2. Convert the volume-pair to milliliters
|
||||
|
||||
Implement the `KitchenCalculator.to_milliliter/1` function. Given a volume-pair tuple, it should convert the volume to milliliters using the conversion chart.
|
||||
|
||||
Use multiple function clauses and pattern matching to create the functions for each unit. The atoms used to denote each unit are: `:cup`, `:fluid_ounce`, `:teaspoon`, `:tablespoon`, `:milliliter`. Return the result of the conversion wrapped in a tuple.
|
||||
|
||||
```elixir
|
||||
KitchenCalculator.to_milliliter({:cup, 2.5})
|
||||
# => {:milliliter, 600.0}
|
||||
```
|
||||
|
||||
## 3. Convert the milliliter volume-pair to another unit
|
||||
|
||||
Implement the `KitchenCalculator.from_milliliter/2` function. Given a volume-pair tuple and the desired unit, it should convert the volume to the desired unit using the conversion chart.
|
||||
|
||||
Use multiple function clauses and pattern matching to create the functions for each unit. The atoms used to denote each unit are: `:cup`, `:fluid_ounce`, `:teaspoon`, `:tablespoon`, `:milliliter`
|
||||
|
||||
```elixir
|
||||
KitchenCalculator.from_milliliter({:milliliter, 1320.0}, :cup)
|
||||
# => {:cup, 5.5}
|
||||
```
|
||||
|
||||
## 4. Convert from any unit to any unit
|
||||
|
||||
Implement the `KitchenCalculator.convert/2` function. Given a volume-pair tuple and the desired unit, it should convert the given volume to the desired unit.
|
||||
|
||||
```elixir
|
||||
KitchenCalculator.convert({:teaspoon, 9.0}, :tablespoon)
|
||||
# => {:tablespoon, 3.0}
|
||||
```
|
||||
|
||||
## Source
|
||||
|
||||
### Created by
|
||||
|
||||
- @neenjaw
|
||||
|
||||
### Contributed to by
|
||||
|
||||
- @angelikatyborska
|
31
elixir/kitchen-calculator/lib/kitchen_calculator.ex
Normal file
31
elixir/kitchen-calculator/lib/kitchen_calculator.ex
Normal file
@ -0,0 +1,31 @@
|
||||
defmodule KitchenCalculator do
|
||||
def get_volume(volume_pair) do
|
||||
elem(volume_pair, 1)
|
||||
end
|
||||
|
||||
def to_milliliter(volume_pair) do
|
||||
volume = get_volume(volume_pair)
|
||||
case elem(volume_pair, 0) do
|
||||
:milliliter -> volume
|
||||
:fluid_ounce -> 30 * volume
|
||||
:cup -> 240 * volume
|
||||
:teaspoon -> 5 * volume
|
||||
:tablespoon -> 15 * volume
|
||||
_ -> -1
|
||||
end
|
||||
end
|
||||
|
||||
def from_milliliter({:milliliter, volume}, unit) do
|
||||
case unit do
|
||||
:cup -> volume / 240
|
||||
:fluid_ounce -> volume / 30
|
||||
:teaspoon -> volume / 5
|
||||
:tablespoon -> volume / 15
|
||||
_ -> volume
|
||||
end
|
||||
end
|
||||
|
||||
def convert({unit1, volume}, unit2) do
|
||||
from_milliliter({:milliliter, to_milliliter({unit1, volume})}, unit2)
|
||||
end
|
||||
end
|
28
elixir/kitchen-calculator/mix.exs
Normal file
28
elixir/kitchen-calculator/mix.exs
Normal file
@ -0,0 +1,28 @@
|
||||
defmodule KitchenCalculator.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :kitchen_calculator,
|
||||
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
|
109
elixir/kitchen-calculator/test/kitchen_calculator_test.exs
Normal file
109
elixir/kitchen-calculator/test/kitchen_calculator_test.exs
Normal file
@ -0,0 +1,109 @@
|
||||
defmodule KitchenCalculatorTest do
|
||||
use ExUnit.Case
|
||||
|
||||
describe "get volume from tuple pair" do
|
||||
@tag task_id: 1
|
||||
test "get cups" do
|
||||
assert KitchenCalculator.get_volume({:cup, 1}) == 1
|
||||
end
|
||||
|
||||
@tag task_id: 1
|
||||
test "get fluid ounces" do
|
||||
assert KitchenCalculator.get_volume({:fluid_ounce, 2}) == 2
|
||||
end
|
||||
|
||||
@tag task_id: 1
|
||||
test "get teaspoons" do
|
||||
assert KitchenCalculator.get_volume({:teaspoon, 3}) == 3
|
||||
end
|
||||
|
||||
@tag task_id: 1
|
||||
test "get tablespoons" do
|
||||
assert KitchenCalculator.get_volume({:tablespoon, 4}) == 4
|
||||
end
|
||||
|
||||
@tag task_id: 1
|
||||
test "get milliliters" do
|
||||
assert KitchenCalculator.get_volume({:milliliter, 5}) == 5
|
||||
end
|
||||
end
|
||||
|
||||
describe "convert to milliliters from" do
|
||||
@tag task_id: 2
|
||||
test "milliliters" do
|
||||
assert KitchenCalculator.to_milliliter({:milliliter, 3}) == {:milliliter, 3}
|
||||
end
|
||||
|
||||
@tag task_id: 2
|
||||
test "cups" do
|
||||
assert KitchenCalculator.to_milliliter({:cup, 3}) == {:milliliter, 720}
|
||||
end
|
||||
|
||||
@tag task_id: 2
|
||||
test "fluid ounces" do
|
||||
assert KitchenCalculator.to_milliliter({:fluid_ounce, 100}) == {:milliliter, 3000}
|
||||
end
|
||||
|
||||
@tag task_id: 2
|
||||
test "teaspoon" do
|
||||
assert KitchenCalculator.to_milliliter({:teaspoon, 3}) == {:milliliter, 15}
|
||||
end
|
||||
|
||||
@tag task_id: 2
|
||||
test "tablespoon" do
|
||||
assert KitchenCalculator.to_milliliter({:tablespoon, 3}) == {:milliliter, 45}
|
||||
end
|
||||
end
|
||||
|
||||
describe "convert from milliliters to" do
|
||||
@tag task_id: 3
|
||||
test "milliliters" do
|
||||
assert KitchenCalculator.from_milliliter({:milliliter, 4}, :milliliter) == {:milliliter, 4}
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "cups" do
|
||||
assert KitchenCalculator.from_milliliter({:milliliter, 840}, :cup) == {:cup, 3.5}
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "fluid ounces" do
|
||||
assert KitchenCalculator.from_milliliter({:milliliter, 4522.5}, :fluid_ounce) ==
|
||||
{:fluid_ounce, 150.75}
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "teaspoon" do
|
||||
assert KitchenCalculator.from_milliliter({:milliliter, 61.25}, :teaspoon) ==
|
||||
{:teaspoon, 12.25}
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "tablespoon" do
|
||||
assert KitchenCalculator.from_milliliter({:milliliter, 71.25}, :tablespoon) ==
|
||||
{:tablespoon, 4.75}
|
||||
end
|
||||
end
|
||||
|
||||
describe "convert from x to y:" do
|
||||
@tag task_id: 4
|
||||
test "teaspoon to tablespoon" do
|
||||
assert KitchenCalculator.convert({:teaspoon, 15}, :tablespoon) == {:tablespoon, 5}
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "cups to fluid ounces" do
|
||||
assert KitchenCalculator.convert({:cup, 4}, :fluid_ounce) == {:fluid_ounce, 32}
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "fluid ounces to teaspoons" do
|
||||
assert KitchenCalculator.convert({:fluid_ounce, 4}, :teaspoon) == {:teaspoon, 24}
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "tablespoons to cups" do
|
||||
assert KitchenCalculator.convert({:tablespoon, 320}, :cup) == {:cup, 20}
|
||||
end
|
||||
end
|
||||
end
|
2
elixir/kitchen-calculator/test/test_helper.exs
Normal file
2
elixir/kitchen-calculator/test/test_helper.exs
Normal file
@ -0,0 +1,2 @@
|
||||
ExUnit.start()
|
||||
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
35
elixir/leap/.exercism/config.json
Normal file
35
elixir/leap/.exercism/config.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"authors": [
|
||||
"rubysolo"
|
||||
],
|
||||
"contributors": [
|
||||
"angelikatyborska",
|
||||
"Cohen-Carlisle",
|
||||
"dalexj",
|
||||
"devonestes",
|
||||
"glennular",
|
||||
"jinyeow",
|
||||
"korbin",
|
||||
"kytrinyx",
|
||||
"lpil",
|
||||
"neenjaw",
|
||||
"parkerl",
|
||||
"sotojuan",
|
||||
"Teapane",
|
||||
"waiting-for-dev"
|
||||
],
|
||||
"files": {
|
||||
"solution": [
|
||||
"lib/year.ex"
|
||||
],
|
||||
"test": [
|
||||
"test/year_test.exs"
|
||||
],
|
||||
"example": [
|
||||
".meta/example.ex"
|
||||
]
|
||||
},
|
||||
"blurb": "Given a year, report if it is a leap year.",
|
||||
"source": "JavaRanch Cattle Drive, exercise 3",
|
||||
"source_url": "http://www.javaranch.com/leap.jsp"
|
||||
}
|
1
elixir/leap/.exercism/metadata.json
Normal file
1
elixir/leap/.exercism/metadata.json
Normal file
@ -0,0 +1 @@
|
||||
{"track":"elixir","exercise":"leap","id":"0621ad4832e546b6b1a1e5e07af0b3f0","url":"https://exercism.org/tracks/elixir/exercises/leap","handle":"halfdan","is_requester":true,"auto_approve":false}
|
4
elixir/leap/.formatter.exs
Normal file
4
elixir/leap/.formatter.exs
Normal file
@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
24
elixir/leap/.gitignore
vendored
Normal file
24
elixir/leap/.gitignore
vendored
Normal 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").
|
||||
leap-*.tar
|
||||
|
75
elixir/leap/HELP.md
Normal file
75
elixir/leap/HELP.md
Normal 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/year.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)
|
||||
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
|
||||
- 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.
|
||||
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).
|
56
elixir/leap/README.md
Normal file
56
elixir/leap/README.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Leap
|
||||
|
||||
Welcome to Leap on Exercism's Elixir Track.
|
||||
If you need help running the tests or submitting your code, check out `HELP.md`.
|
||||
|
||||
## Instructions
|
||||
|
||||
Given a year, report if it is a leap year.
|
||||
|
||||
The tricky thing here is that a leap year in the Gregorian calendar occurs:
|
||||
|
||||
```text
|
||||
on every year that is evenly divisible by 4
|
||||
except every year that is evenly divisible by 100
|
||||
unless the year is also evenly divisible by 400
|
||||
```
|
||||
|
||||
For example, 1997 is not a leap year, but 1996 is. 1900 is not a leap
|
||||
year, but 2000 is.
|
||||
|
||||
## Notes
|
||||
|
||||
Though our exercise adopts some very simple rules, there is more to
|
||||
learn!
|
||||
|
||||
For a delightful, four minute explanation of the whole leap year
|
||||
phenomenon, go watch [this youtube video][video].
|
||||
|
||||
[video]: http://www.youtube.com/watch?v=xX96xng7sAE
|
||||
|
||||
## Source
|
||||
|
||||
### Created by
|
||||
|
||||
- @rubysolo
|
||||
|
||||
### Contributed to by
|
||||
|
||||
- @angelikatyborska
|
||||
- @Cohen-Carlisle
|
||||
- @dalexj
|
||||
- @devonestes
|
||||
- @glennular
|
||||
- @jinyeow
|
||||
- @korbin
|
||||
- @kytrinyx
|
||||
- @lpil
|
||||
- @neenjaw
|
||||
- @parkerl
|
||||
- @sotojuan
|
||||
- @Teapane
|
||||
- @waiting-for-dev
|
||||
|
||||
### Based on
|
||||
|
||||
JavaRanch Cattle Drive, exercise 3 - http://www.javaranch.com/leap.jsp
|
16
elixir/leap/lib/year.ex
Normal file
16
elixir/leap/lib/year.ex
Normal file
@ -0,0 +1,16 @@
|
||||
defmodule Year do
|
||||
@doc """
|
||||
Returns whether 'year' is a leap year.
|
||||
|
||||
A leap year occurs:
|
||||
|
||||
on every year that is evenly divisible by 4
|
||||
except every year that is evenly divisible by 100
|
||||
unless the year is also evenly divisible by 400
|
||||
"""
|
||||
@spec leap_year?(non_neg_integer) :: boolean
|
||||
def leap_year?(year) when rem(year, 400) == 0, do: true
|
||||
def leap_year?(year) when rem(year, 100) == 0, do: false
|
||||
def leap_year?(year) when rem(year, 4) == 0, do: true
|
||||
def leap_year?(_year), do: false
|
||||
end
|
28
elixir/leap/mix.exs
Normal file
28
elixir/leap/mix.exs
Normal file
@ -0,0 +1,28 @@
|
||||
defmodule Year.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :year,
|
||||
version: "0.1.0",
|
||||
# elixir: "~> 1.8",
|
||||
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
|
2
elixir/leap/test/test_helper.exs
Normal file
2
elixir/leap/test/test_helper.exs
Normal file
@ -0,0 +1,2 @@
|
||||
ExUnit.start()
|
||||
ExUnit.configure(exclude: :pending, trace: true)
|
48
elixir/leap/test/year_test.exs
Normal file
48
elixir/leap/test/year_test.exs
Normal file
@ -0,0 +1,48 @@
|
||||
defmodule YearTest do
|
||||
use ExUnit.Case
|
||||
|
||||
# @tag :pending
|
||||
test "year not divisible by 4 is common year" do
|
||||
refute Year.leap_year?(2015)
|
||||
end
|
||||
|
||||
@tag :pending
|
||||
test "year divisible by 2, not divisible by 4 is common year" do
|
||||
refute Year.leap_year?(1970)
|
||||
end
|
||||
|
||||
@tag :pending
|
||||
test "year divisible by 4, not divisible by 100 is leap year" do
|
||||
assert Year.leap_year?(1996)
|
||||
end
|
||||
|
||||
@tag :pending
|
||||
test "year divisible by 4 and 5 is still a leap year" do
|
||||
assert Year.leap_year?(1960)
|
||||
end
|
||||
|
||||
@tag :pending
|
||||
test "year divisible by 100, not divisible by 400 is common year" do
|
||||
refute Year.leap_year?(2100)
|
||||
end
|
||||
|
||||
@tag :pending
|
||||
test "year divisible by 100 but not by 3 is still not a leap year" do
|
||||
refute Year.leap_year?(1900)
|
||||
end
|
||||
|
||||
@tag :pending
|
||||
test "year divisible by 400 is leap year" do
|
||||
assert Year.leap_year?(2000)
|
||||
end
|
||||
|
||||
@tag :pending
|
||||
test "year divisible by 400 but not by 125 is still a leap year" do
|
||||
assert Year.leap_year?(2400)
|
||||
end
|
||||
|
||||
@tag :pending
|
||||
test "year divisible by 200, not divisible by 400 in common year" do
|
||||
refute Year.leap_year?(1800)
|
||||
end
|
||||
end
|
21
elixir/library-fees/.exercism/config.json
Normal file
21
elixir/library-fees/.exercism/config.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"authors": [
|
||||
"angelikatyborska"
|
||||
],
|
||||
"contributors": [
|
||||
"neenjaw"
|
||||
],
|
||||
"files": {
|
||||
"solution": [
|
||||
"lib/library_fees.ex"
|
||||
],
|
||||
"test": [
|
||||
"test/library_fees_test.exs"
|
||||
],
|
||||
"exemplar": [
|
||||
".meta/exemplar.ex"
|
||||
]
|
||||
},
|
||||
"language_versions": ">=1.10",
|
||||
"blurb": "Learn about dates and time by calculating late fees for the local library."
|
||||
}
|
1
elixir/library-fees/.exercism/metadata.json
Normal file
1
elixir/library-fees/.exercism/metadata.json
Normal file
@ -0,0 +1 @@
|
||||
{"track":"elixir","exercise":"library-fees","id":"f919bcf7474041d6b5dd2b3990192dd2","url":"https://exercism.org/tracks/elixir/exercises/library-fees","handle":"halfdan","is_requester":true,"auto_approve":false}
|
4
elixir/library-fees/.formatter.exs
Normal file
4
elixir/library-fees/.formatter.exs
Normal file
@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
24
elixir/library-fees/.gitignore
vendored
Normal file
24
elixir/library-fees/.gitignore
vendored
Normal 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").
|
||||
match_binary-*.tar
|
||||
|
75
elixir/library-fees/HELP.md
Normal file
75
elixir/library-fees/HELP.md
Normal 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/library_fees.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)
|
||||
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
|
||||
- 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.
|
||||
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).
|
47
elixir/library-fees/HINTS.md
Normal file
47
elixir/library-fees/HINTS.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Hints
|
||||
|
||||
## General
|
||||
|
||||
- Review the functions available in the [`NaiveDateTime` module][naive-date-time], the [`Date` module][date], and the [`Time` module][time].
|
||||
|
||||
## 1. Parse the stored datetimes
|
||||
|
||||
- There is a [built-in function][naive-date-time-from-iso8601] that parses an ISO8601 datetime string and returns a `NaiveDateTime` struct.
|
||||
|
||||
## 2. Determine if a book was checked out before noon
|
||||
|
||||
- You can define a `Time` literal using the [`~T` sigil][time-sigil].
|
||||
- There is a [built-in function][naive-date-time-to-time] that changes a `NaiveDateTime` struct to a `Time` struct.
|
||||
- There is a [built-in function][time-compare] that checks which one of two `Time` structs is bigger.
|
||||
|
||||
## 3. Calculate the return date
|
||||
|
||||
- A day has `24 * 60 * 60` seconds.
|
||||
- There is a [built-in function][naive-date-time-add] that adds a given number of seconds to a `NaiveDateTime` struct.
|
||||
- There is a [built-in function][naive-date-time-to-date] that changes a `NaiveDateTime` struct to a `Date` struct.
|
||||
|
||||
## 4. Determine how late the return of the book was
|
||||
|
||||
- There is a [built-in function][naive-date-time-to-date] that changes a `NaiveDateTime` struct to a `Date` struct.
|
||||
- There is a [built-in function][date-diff] that calculates the difference in days between two `Date` structs.
|
||||
|
||||
## 5. Determine if the book was returned on a Monday
|
||||
|
||||
- There is a [built-in function][naive-date-time-to-date] that changes a `NaiveDateTime` struct to a `Date` struct.
|
||||
- There is a [built-in function][date-day-of-week] that returns the day of week for a given `Date` struct.
|
||||
|
||||
## 6. Calculate the late fee
|
||||
|
||||
- Combine together all of the functions that you defined in previous steps.
|
||||
|
||||
[naive-date-time]: https://hexdocs.pm/elixir/NaiveDateTime.html
|
||||
[time]: https://hexdocs.pm/elixir/Time.html
|
||||
[date]: https://hexdocs.pm/elixir/Date.html
|
||||
[naive-date-time-from-iso8601]: https://hexdocs.pm/elixir/NaiveDateTime.html#from_iso8601!/2
|
||||
[naive-date-time-to-time]: https://hexdocs.pm/elixir/NaiveDateTime.html#to_time/1
|
||||
[naive-date-time-to-date]: https://hexdocs.pm/elixir/NaiveDateTime.html#to_date/1
|
||||
[naive-date-time-add]: https://hexdocs.pm/elixir/NaiveDateTime.html#add/3
|
||||
[time-sigil]: https://hexdocs.pm/elixir/Kernel.html#sigil_T/2
|
||||
[time-compare]: https://hexdocs.pm/elixir/Time.html#compare/2
|
||||
[date-diff]: https://hexdocs.pm/elixir/Date.html#diff/2
|
||||
[date-day-of-week]: https://hexdocs.pm/elixir/Date.html#day_of_week/2
|
124
elixir/library-fees/README.md
Normal file
124
elixir/library-fees/README.md
Normal file
@ -0,0 +1,124 @@
|
||||
# Library Fees
|
||||
|
||||
Welcome to Library Fees 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
|
||||
|
||||
## Dates and Time
|
||||
|
||||
Elixir's standard library offers 4 different modules for working with dates and time, each with its own struct.
|
||||
|
||||
- The `Date` module. A `Date` struct can be created with the `~D` sigil.
|
||||
|
||||
```elixir
|
||||
~D[2021-01-01]
|
||||
```
|
||||
|
||||
- The `Time` module. A `Time` struct can be created with the `~T` sigil.
|
||||
|
||||
```elixir
|
||||
~T[12:00:00]
|
||||
```
|
||||
|
||||
- The `NaiveDateTime` module for datetimes without a timezone. A `NaiveDateTime` struct can be created with the `~N` sigil.
|
||||
|
||||
```elixir
|
||||
~N[2021-01-01 12:00:00]
|
||||
```
|
||||
|
||||
- The `DateTime` module for datetimes with a timezone. Using this module for timezones other than UTC requires an external dependency, a timezone database.
|
||||
|
||||
### Comparisons
|
||||
|
||||
To compare dates or times to one another, look for a `compare` or `diff` function in the corresponding module. Comparison operators such as `==`, `>`, and `<` _seem_ to work, but they don't do a correct semantic comparison for those structs.
|
||||
|
||||
## Instructions
|
||||
|
||||
Your librarian friend has asked you to extend her library software to automatically calculate late fees.
|
||||
Her current system stores the exact date and time of a book checkout as an [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) datetime string.
|
||||
She runs a local library in a small town in Ghana, which uses the GMT timezone (UTC +0), doesn't use daylight saving time, and doesn't need to worry about other timezones.
|
||||
|
||||
## 1. Parse the stored datetimes
|
||||
|
||||
Implement the `LibraryFees.datetime_from_string/1` function. It should take an ISO8601 datetime string as an argument, and return a `NaiveDateTime` struct.
|
||||
|
||||
```elixir
|
||||
LibraryFees.datetime_from_string("2021-01-01T13:30:45Z")
|
||||
# => ~N[2021-01-01 13:30:45]
|
||||
```
|
||||
|
||||
## 2. Determine if a book was checked out before noon
|
||||
|
||||
If a book was checked out before noon, the reader has 28 days to return it. If it was checked out at or after noon, it's 29 days.
|
||||
|
||||
Implement the `LibraryFees.before_noon?/1` function. It should take a `NaiveDateTime` struct and return a boolean.
|
||||
|
||||
```elixir
|
||||
LibraryFees.before_noon?(~N[2021-01-12 08:23:03])
|
||||
# => true
|
||||
```
|
||||
|
||||
## 3. Calculate the return date
|
||||
|
||||
Based on the checkout datetime, calculate the return date.
|
||||
|
||||
Implement the `LibraryFees.return_date/1` function. It should take a `NaiveDateTime` struct and return a `Date` struct, either 28 or 29 days later.
|
||||
|
||||
```elixir
|
||||
LibraryFees.return_date(~N[2020-11-28 15:55:33])
|
||||
# => ~D[2020-12-27]
|
||||
```
|
||||
|
||||
## 4. Determine how late the return of the book was
|
||||
|
||||
The library has a flat rate for late returns. To be able to calculate the fee, we need to know how many days after the return date the book was actually returned.
|
||||
|
||||
Implement the `LibraryFees.days_late/2` function. It should take a `Date` struct - the planned return datetime, and a `NaiveDateTime` struct - the actual return datetime.
|
||||
|
||||
If the actual return date is on an earlier or the same day as the planned return datetime, the function should return 0. Otherwise, the function should return the difference between those two dates in days.
|
||||
|
||||
The library tracks both the date and time of the actual return of the book for statistical purposes, but doesn't use the time when calculating late fees.
|
||||
|
||||
```elixir
|
||||
LibraryFees.days_late(~D[2020-12-27], ~N[2021-01-03 09:23:36])
|
||||
# => 7
|
||||
```
|
||||
|
||||
## 5. Determine if the book was returned on a Monday
|
||||
|
||||
The library has a special offer for returning books on Mondays.
|
||||
|
||||
Implement the `LibraryFees.monday?/1` function. It should take a `NaiveDateTime` struct and return a boolean.
|
||||
|
||||
```elixir
|
||||
LibraryFees.monday?(~N[2021-01-03 13:30:45Z])
|
||||
# => false
|
||||
```
|
||||
|
||||
## 6. Calculate the late fee
|
||||
|
||||
Implement the `LibraryFees.calculate_late_fee/3` function. It should take three arguments - two ISO8601 datetime strings, checkout datetime and actual return datetime, and the late fee for one day. It should return the total late fee according to how late the actual return of the book was.
|
||||
|
||||
Include the special Monday offer. If you return the book on Monday, your late fee is 50% off, rounded down.
|
||||
|
||||
```elixir
|
||||
# Sunday, 7 days late
|
||||
LibraryFees.calculate_late_fee("2020-11-28T15:55:33Z", "2021-01-03T13:30:45Z", 100)
|
||||
# => 700
|
||||
|
||||
# one day later, Monday, 8 days late
|
||||
LibraryFees.calculate_late_fee("2020-11-28T15:55:33Z", "2021-01-04T09:02:11Z", 100)
|
||||
# => 400
|
||||
```
|
||||
|
||||
## Source
|
||||
|
||||
### Created by
|
||||
|
||||
- @angelikatyborska
|
||||
|
||||
### Contributed to by
|
||||
|
||||
- @neenjaw
|
39
elixir/library-fees/lib/library_fees.ex
Normal file
39
elixir/library-fees/lib/library_fees.ex
Normal file
@ -0,0 +1,39 @@
|
||||
defmodule LibraryFees do
|
||||
def datetime_from_string(string) do
|
||||
{:ok, dt} = NaiveDateTime.from_iso8601(string)
|
||||
dt
|
||||
end
|
||||
|
||||
def before_noon?(datetime) do
|
||||
datetime.hour < 12
|
||||
end
|
||||
|
||||
def return_date(checkout_datetime) do
|
||||
dt = NaiveDateTime.to_date(checkout_datetime)
|
||||
case before_noon?(checkout_datetime) do
|
||||
true -> Date.add(dt, 28)
|
||||
_ -> Date.add(dt, 29)
|
||||
end
|
||||
end
|
||||
|
||||
def days_late(planned_return_date, actual_return_datetime) do
|
||||
actual_return_datetime
|
||||
|> Date.diff(planned_return_date)
|
||||
|> max(0)
|
||||
end
|
||||
|
||||
def monday?(datetime) do
|
||||
Date.day_of_week(NaiveDateTime.to_date(datetime)) == 1
|
||||
end
|
||||
|
||||
def calculate_late_fee(checkout, return, rate) do
|
||||
checkout_datetime = datetime_from_string(checkout)
|
||||
return_datetime = datetime_from_string(return)
|
||||
raw_fee = days_late(return_date(checkout_datetime), return_datetime) * rate
|
||||
if monday?(return_datetime) do
|
||||
floor(raw_fee * 0.5)
|
||||
else
|
||||
raw_fee
|
||||
end
|
||||
end
|
||||
end
|
28
elixir/library-fees/mix.exs
Normal file
28
elixir/library-fees/mix.exs
Normal file
@ -0,0 +1,28 @@
|
||||
defmodule CaptainsLog.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :library_fees,
|
||||
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
|
171
elixir/library-fees/test/library_fees_test.exs
Normal file
171
elixir/library-fees/test/library_fees_test.exs
Normal file
@ -0,0 +1,171 @@
|
||||
defmodule LibraryFeesTest do
|
||||
use ExUnit.Case
|
||||
|
||||
describe "datetime_from_string/1" do
|
||||
@tag task_id: 1
|
||||
test "returns NaiveDateTime" do
|
||||
result = LibraryFees.datetime_from_string("2021-01-01T12:00:00Z")
|
||||
assert result.__struct__ == NaiveDateTime
|
||||
end
|
||||
|
||||
@tag task_id: 1
|
||||
test "parses an ISO8601 string" do
|
||||
result = LibraryFees.datetime_from_string("2019-12-24T13:15:45Z")
|
||||
assert result == ~N[2019-12-24 13:15:45Z]
|
||||
end
|
||||
end
|
||||
|
||||
describe "before_noon?/1" do
|
||||
@tag task_id: 2
|
||||
test "returns true if the given NaiveDateTime is before 12:00" do
|
||||
assert LibraryFees.before_noon?(~N[2020-06-06 11:59:59Z]) == true
|
||||
end
|
||||
|
||||
@tag task_id: 2
|
||||
test "returns false if the given NaiveDateTime is after 12:00" do
|
||||
assert LibraryFees.before_noon?(~N[2021-01-03 12:01:01Z]) == false
|
||||
end
|
||||
|
||||
@tag task_id: 2
|
||||
test "returns false if the given NaiveDateTime is exactly at 12:00" do
|
||||
assert LibraryFees.before_noon?(~N[2018-11-17 12:00:00Z]) == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "return_date/1" do
|
||||
@tag task_id: 3
|
||||
test "adds 28 days if the given NaiveDateTime is before 12:00" do
|
||||
result = LibraryFees.return_date(~N[2020-02-14 11:59:59Z])
|
||||
assert result == ~D[2020-03-13]
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "adds 29 days if the given NaiveDateTime is after 12:00" do
|
||||
result = LibraryFees.return_date(~N[2021-01-03 12:01:01Z])
|
||||
assert result == ~D[2021-02-01]
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "adds 29 days if the given NaiveDateTime is exactly at 12:00" do
|
||||
result = LibraryFees.return_date(~N[2018-12-01 12:00:00Z])
|
||||
assert result == ~D[2018-12-30]
|
||||
end
|
||||
end
|
||||
|
||||
describe "days_late/2" do
|
||||
@tag task_id: 4
|
||||
test "returns 0 when identical datetimes" do
|
||||
result = LibraryFees.days_late(~D[2021-02-01], ~N[2021-02-01 12:00:00Z])
|
||||
assert result == 0
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "returns 0 when identical dates, but different times" do
|
||||
result = LibraryFees.days_late(~D[2019-03-11], ~N[2019-03-11 12:00:00Z])
|
||||
assert result == 0
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "returns 0 when planned return date is later than actual return date" do
|
||||
result = LibraryFees.days_late(~D[2020-12-03], ~N[2020-11-29 16:00:00Z])
|
||||
assert result == 0
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "returns date difference in numbers of days when planned return date is earlier than actual return date" do
|
||||
result = LibraryFees.days_late(~D[2020-06-12], ~N[2020-06-21 16:00:00Z])
|
||||
assert result == 9
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "a new day starts at midnight" do
|
||||
result = LibraryFees.days_late(~D[2020-06-12], ~N[2020-06-12 23:59:59Z])
|
||||
assert result == 0
|
||||
|
||||
result = LibraryFees.days_late(~D[2020-06-12], ~N[2020-06-13 00:00:00Z])
|
||||
assert result == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "monday?" do
|
||||
@tag task_id: 5
|
||||
test "2021-02-01 was a Monday" do
|
||||
assert LibraryFees.monday?(~N[2021-02-01 14:01:00Z]) == true
|
||||
end
|
||||
|
||||
@tag task_id: 5
|
||||
test "2020-03-16 was a Monday" do
|
||||
assert LibraryFees.monday?(~N[2020-03-16 09:23:52Z]) == true
|
||||
end
|
||||
|
||||
@tag task_id: 5
|
||||
test "2020-04-22 was a Monday" do
|
||||
assert LibraryFees.monday?(~N[2019-04-22 15:44:03Z]) == true
|
||||
end
|
||||
|
||||
@tag task_id: 5
|
||||
test "2021-02-02 was a Tuesday" do
|
||||
assert LibraryFees.monday?(~N[2021-02-02 15:07:00Z]) == false
|
||||
end
|
||||
|
||||
@tag task_id: 5
|
||||
test "2020-03-14 was a Friday" do
|
||||
assert LibraryFees.monday?(~N[2020-03-14 08:54:51Z]) == false
|
||||
end
|
||||
|
||||
@tag task_id: 5
|
||||
test "2019-04-28 was a Sunday" do
|
||||
assert LibraryFees.monday?(~N[2019-04-28 11:37:12Z]) == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "calculate_late_fee/2" do
|
||||
@tag task_id: 6
|
||||
test "returns 0 if the book was returned less than 28 days after a morning checkout" do
|
||||
result = LibraryFees.calculate_late_fee("2018-11-01T09:00:00Z", "2018-11-13T14:12:00Z", 123)
|
||||
assert result == 0
|
||||
end
|
||||
|
||||
@tag task_id: 6
|
||||
test "returns 0 if the book was returned exactly 28 days after a morning checkout" do
|
||||
result = LibraryFees.calculate_late_fee("2018-11-01T09:00:00Z", "2018-11-29T14:12:00Z", 123)
|
||||
assert result == 0
|
||||
end
|
||||
|
||||
@tag task_id: 6
|
||||
test "returns the rate for one day if the book was returned exactly 29 days after a morning checkout" do
|
||||
result = LibraryFees.calculate_late_fee("2018-11-01T09:00:00Z", "2018-11-30T14:12:00Z", 320)
|
||||
assert result == 320
|
||||
end
|
||||
|
||||
@tag task_id: 6
|
||||
test "returns 0 if the book was returned less than 29 days after an afternoon checkout" do
|
||||
result = LibraryFees.calculate_late_fee("2019-05-01T16:12:00Z", "2019-05-17T14:32:45Z", 400)
|
||||
assert result == 0
|
||||
end
|
||||
|
||||
@tag task_id: 6
|
||||
test "returns 0 if the book was returned exactly 29 days after an afternoon checkout" do
|
||||
result = LibraryFees.calculate_late_fee("2019-05-01T16:12:00Z", "2019-05-30T14:32:45Z", 313)
|
||||
assert result == 0
|
||||
end
|
||||
|
||||
@tag task_id: 6
|
||||
test "returns the rate for one day if the book was returned exactly 30 days after an afternoon checkout" do
|
||||
result = LibraryFees.calculate_late_fee("2019-05-01T16:12:00Z", "2019-05-31T14:32:45Z", 234)
|
||||
assert result == 234
|
||||
end
|
||||
|
||||
@tag task_id: 6
|
||||
test "multiplies the number of days late by the rate for one day" do
|
||||
result = LibraryFees.calculate_late_fee("2021-01-01T08:00:00Z", "2021-02-13T08:00:00Z", 111)
|
||||
assert result == 111 * 15
|
||||
end
|
||||
|
||||
@tag task_id: 6
|
||||
test "late fees are 50% off (rounded down) when the book is returned on a Monday" do
|
||||
result = LibraryFees.calculate_late_fee("2021-01-01T08:00:00Z", "2021-02-15T08:00:00Z", 111)
|
||||
assert result == trunc(111 * 17 * 0.5)
|
||||
end
|
||||
end
|
||||
end
|
2
elixir/library-fees/test/test_helper.exs
Normal file
2
elixir/library-fees/test/test_helper.exs
Normal file
@ -0,0 +1,2 @@
|
||||
ExUnit.start()
|
||||
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
22
elixir/log-level/.exercism/config.json
Normal file
22
elixir/log-level/.exercism/config.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"authors": [
|
||||
"neenjaw"
|
||||
],
|
||||
"files": {
|
||||
"solution": [
|
||||
"lib/log_level.ex"
|
||||
],
|
||||
"test": [
|
||||
"test/log_level_test.exs"
|
||||
],
|
||||
"exemplar": [
|
||||
".meta/exemplar.ex"
|
||||
]
|
||||
},
|
||||
"language_versions": ">=1.10",
|
||||
"forked_from": [
|
||||
"csharp/logs-logs-logs"
|
||||
],
|
||||
"icon": "log-levels",
|
||||
"blurb": "Learn about atoms and the cond conditional expression by aggregating application logs."
|
||||
}
|
1
elixir/log-level/.exercism/metadata.json
Normal file
1
elixir/log-level/.exercism/metadata.json
Normal file
@ -0,0 +1 @@
|
||||
{"track":"elixir","exercise":"log-level","id":"cfabdd7d8b3045aa9515e5fcc87ffaf6","url":"https://exercism.org/tracks/elixir/exercises/log-level","handle":"halfdan","is_requester":true,"auto_approve":false}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user