mirror of
https://github.com/rjNemo/exercism-elixir
synced 2026-06-06 02:16:48 +00:00
rpn
This commit is contained in:
parent
3c882052df
commit
a99aaccbc9
22 changed files with 820 additions and 0 deletions
23
rpn-calculator/.exercism/config.json
Normal file
23
rpn-calculator/.exercism/config.json
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"blurb": "Learn about errors and rescuing them by working on an experimental Reverse Polish Notation calculator.",
|
||||||
|
"icon": "instruments-of-texas",
|
||||||
|
"authors": [
|
||||||
|
"neenjaw"
|
||||||
|
],
|
||||||
|
"contributors": [
|
||||||
|
"angelikatyborska",
|
||||||
|
"cjmaxik"
|
||||||
|
],
|
||||||
|
"files": {
|
||||||
|
"solution": [
|
||||||
|
"lib/rpn_calculator.ex"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"test/rpn_calculator_test.exs"
|
||||||
|
],
|
||||||
|
"exemplar": [
|
||||||
|
".meta/exemplar.ex"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"language_versions": ">=1.10"
|
||||||
|
}
|
||||||
1
rpn-calculator/.exercism/metadata.json
Normal file
1
rpn-calculator/.exercism/metadata.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"track":"elixir","exercise":"rpn-calculator","id":"ed514077caa14d95be2a2ffb74f4d5c1","url":"https://exercism.org/tracks/elixir/exercises/rpn-calculator","handle":"rjNemo","is_requester":true,"auto_approve":false}
|
||||||
4
rpn-calculator/.formatter.exs
Normal file
4
rpn-calculator/.formatter.exs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
||||||
24
rpn-calculator/.gitignore
vendored
Normal file
24
rpn-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").
|
||||||
|
errors-*.tar
|
||||||
|
|
||||||
75
rpn-calculator/HELP.md
Normal file
75
rpn-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/rpn_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).
|
||||||
23
rpn-calculator/HINTS.md
Normal file
23
rpn-calculator/HINTS.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Hints
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
- Read about [errors][errors] in the Getting Started guide.
|
||||||
|
- Read about [`try`][docs-try] in the documentation.
|
||||||
|
|
||||||
|
## 1. Warn the team
|
||||||
|
|
||||||
|
- Allow the operation function to raise its error.
|
||||||
|
- To invoke a function in a variable, use the `.` operator.
|
||||||
|
|
||||||
|
## 2. Wrap the error
|
||||||
|
|
||||||
|
- Make use of try .. rescue to return the intended result.
|
||||||
|
|
||||||
|
## 3. Pass on the message
|
||||||
|
|
||||||
|
- Make use of try .. rescue to return the intended result.
|
||||||
|
- The rescue block allows you to pattern match on the error's Module name and also bind the error to a variable.
|
||||||
|
|
||||||
|
[errors]: https://elixir-lang.org/getting-started/try-catch-and-rescue.html#errors
|
||||||
|
[docs-try]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#try/1
|
||||||
117
rpn-calculator/README.md
Normal file
117
rpn-calculator/README.md
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
# RPN Calculator
|
||||||
|
|
||||||
|
Welcome to RPN 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
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
Errors happen. In Elixir, while people often say to "let it crash", there are times when we need to rescue the function call to a known good state to fulfill a software contract. In some languages, errors are used as a method of control flow, but in Elixir, this pattern is discouraged. We can often recognize functions that may raise an error just by their name: functions that raise errors are to have `!` at the end of their name. This is in comparison with functions that return `{:ok, value}` or `:error`. Look at these library examples:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Map.fetch(%{a: 1}, :b)
|
||||||
|
# => :error
|
||||||
|
Map.fetch!(%{a: 1}, :b)
|
||||||
|
# => raises KeyError
|
||||||
|
```
|
||||||
|
|
||||||
|
## Try Rescue
|
||||||
|
|
||||||
|
Elixir provides a construct for rescuing from errors using `try .. rescue`
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
try do #1
|
||||||
|
raise RuntimeError, "error" #2
|
||||||
|
rescue
|
||||||
|
e in RuntimeError -> :error #3
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's examine this construct:
|
||||||
|
|
||||||
|
- **Line 1**, the block is declared with `try`.
|
||||||
|
- **Line 2**, the function call which may error is placed here, in this case we are calling `raise/2`.
|
||||||
|
- **Line 3**, in the `rescue` section, we pattern match on the _Module_ name of the error raised
|
||||||
|
- on the left side of `->`:
|
||||||
|
- `e` is matched to the error struct.
|
||||||
|
- `in` is a keyword.
|
||||||
|
- `RuntimeError` is the error that we want to rescue.
|
||||||
|
- If we wanted to rescue from all errors, we could use `_` instead of the module name or omit the `in` keyword entirely.
|
||||||
|
- on the right side:
|
||||||
|
- the instructions to be executed if the error matches.
|
||||||
|
|
||||||
|
### Error structs
|
||||||
|
|
||||||
|
Errors (sometimes also called "exceptions") that you rescue this way are structs.
|
||||||
|
Rescuing errors in Elixir is done very rarely.
|
||||||
|
Usually the rescued error is logged or sent to an external monitoring service, and then reraised.
|
||||||
|
This means we usually don't care about the internal structure of the specific error struct.
|
||||||
|
|
||||||
|
In the [Exceptions concept](/tracks/elixir/concepts/exceptions) you will learn more about error structs, including how to define your own custom error.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
While working at _Instruments of Texas_, you are tasked to work on an experimental Reverse Polish Notation [RPN] calculator written in Elixir. Your team is having a problem with some operations raising errors and crashing the process. You have been tasked to write a function which wraps the operation function so that the errors can be handled more elegantly with idiomatic Elixir code.
|
||||||
|
|
||||||
|
## 1. Warn the team
|
||||||
|
|
||||||
|
Implement the function `calculate!/2` to call the operation function with the stack as the only argument. The operation function is defined elsewhere, but you know that it can either complete successfully or raise an error.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
stack = []
|
||||||
|
operation = fn _ -> :ok end
|
||||||
|
RPNCalculator.calculate!(stack, operation)
|
||||||
|
# => :ok
|
||||||
|
|
||||||
|
stack = []
|
||||||
|
operation = fn _ -> raise ArgumentError, "An error occurred" end
|
||||||
|
RPNCalculator.calculate!(stack, operation)
|
||||||
|
# => ** (ArgumentError) An error occurred
|
||||||
|
```
|
||||||
|
|
||||||
|
> Function names that end in `!` are a warning to programmers that this function may raise an error
|
||||||
|
|
||||||
|
## 2. Wrap the error
|
||||||
|
|
||||||
|
When doing more research you notice that many functions use atoms and tuples to indicate their success/failure. Implement `calculate/2` using this strategy.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
stack = []
|
||||||
|
operation = fn _ -> "operation completed" end
|
||||||
|
RPNCalculator.calculate(stack, operation)
|
||||||
|
# => {:ok, "operation completed"}
|
||||||
|
|
||||||
|
stack = []
|
||||||
|
operation = fn _ -> raise ArgumentError, "An error occurred" end
|
||||||
|
RPNCalculator.calculate(stack, operation)
|
||||||
|
# => :error
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Pass on the message
|
||||||
|
|
||||||
|
Some of the errors contain important information that your coworkers need to have to ensure the correct operation of the system. Implement `calculate_verbose/2` to pass on the error message. The error is a struct that has a `:message` field.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
stack = []
|
||||||
|
operation = fn _ -> "operation completed" end
|
||||||
|
RPNCalculator.calculate_verbose(stack, operation)
|
||||||
|
# => {:ok, "operation completed"}
|
||||||
|
|
||||||
|
stack = []
|
||||||
|
operation = fn _ -> raise ArgumentError, "An error occurred" end
|
||||||
|
RPNCalculator.calculate_verbose(stack, operation)
|
||||||
|
# => {:error, "An error occurred"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
### Created by
|
||||||
|
|
||||||
|
- @neenjaw
|
||||||
|
|
||||||
|
### Contributed to by
|
||||||
|
|
||||||
|
- @angelikatyborska
|
||||||
|
- @cjmaxik
|
||||||
19
rpn-calculator/lib/rpn_calculator.ex
Normal file
19
rpn-calculator/lib/rpn_calculator.ex
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
defmodule RPNCalculator do
|
||||||
|
def calculate!(stack, operation), do: operation.(stack)
|
||||||
|
|
||||||
|
def calculate(stack, operation) do
|
||||||
|
try do
|
||||||
|
{:ok, calculate!(stack, operation)}
|
||||||
|
rescue
|
||||||
|
_ in RuntimeError -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_verbose(stack, operation) do
|
||||||
|
try do
|
||||||
|
{:ok, calculate!(stack, operation)}
|
||||||
|
rescue
|
||||||
|
e in ArgumentError -> {:error, e.message}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
28
rpn-calculator/mix.exs
Normal file
28
rpn-calculator/mix.exs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
defmodule RPNCalculator.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :rpn_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
|
||||||
38
rpn-calculator/test/rpn_calculator_test.exs
Normal file
38
rpn-calculator/test/rpn_calculator_test.exs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
defmodule RPNCalculatorTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
|
||||||
|
@tag task_id: 1
|
||||||
|
test "calculate! returns an :ok atom" do
|
||||||
|
assert RPNCalculator.calculate!([], fn _ -> :ok end) == :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 1
|
||||||
|
test "let it crash" do
|
||||||
|
assert_raise(RuntimeError, fn ->
|
||||||
|
RPNCalculator.calculate!([], fn _ -> raise "test error" end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 2
|
||||||
|
test "calculate returns a tuple" do
|
||||||
|
assert RPNCalculator.calculate([], fn _ -> "operation completed" end) ==
|
||||||
|
{:ok, "operation completed"}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 2
|
||||||
|
test "rescue the crash, no message" do
|
||||||
|
assert RPNCalculator.calculate([], fn _ -> raise "test error" end) == :error
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "calculate_verbose returns a tuple" do
|
||||||
|
assert RPNCalculator.calculate_verbose([], fn _ -> "operation completed" end) ==
|
||||||
|
{:ok, "operation completed"}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "rescue the crash, get error tuple with message" do
|
||||||
|
assert RPNCalculator.calculate_verbose([], fn _ -> raise ArgumentError, "test error" end) ==
|
||||||
|
{:error, "test error"}
|
||||||
|
end
|
||||||
|
end
|
||||||
2
rpn-calculator/test/test_helper.exs
Normal file
2
rpn-calculator/test/test_helper.exs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
ExUnit.start()
|
||||||
|
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
||||||
22
stack-underflow/.exercism/config.json
Normal file
22
stack-underflow/.exercism/config.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"blurb": "Learn about defining custom exceptions by continuing work on your experimental Reverse Polish Notation calculator.",
|
||||||
|
"icon": "error-handling",
|
||||||
|
"authors": [
|
||||||
|
"neenjaw"
|
||||||
|
],
|
||||||
|
"contributors": [
|
||||||
|
"angelikatyborska"
|
||||||
|
],
|
||||||
|
"files": {
|
||||||
|
"solution": [
|
||||||
|
"lib/rpn_calculator/exception.ex"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"test/rpn_calculator/exception_test.exs"
|
||||||
|
],
|
||||||
|
"exemplar": [
|
||||||
|
".meta/exemplar.ex"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"language_versions": ">=1.10"
|
||||||
|
}
|
||||||
1
stack-underflow/.exercism/metadata.json
Normal file
1
stack-underflow/.exercism/metadata.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"track":"elixir","exercise":"stack-underflow","id":"7c23c23937f64262845fde8806001dd6","url":"https://exercism.org/tracks/elixir/exercises/stack-underflow","handle":"rjNemo","is_requester":true,"auto_approve":false}
|
||||||
4
stack-underflow/.formatter.exs
Normal file
4
stack-underflow/.formatter.exs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
||||||
24
stack-underflow/.gitignore
vendored
Normal file
24
stack-underflow/.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").
|
||||||
|
errors-*.tar
|
||||||
|
|
||||||
75
stack-underflow/HELP.md
Normal file
75
stack-underflow/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/rpn_calculator/exception.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).
|
||||||
28
stack-underflow/HINTS.md
Normal file
28
stack-underflow/HINTS.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Hints
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
- Read about [errors][getting-started-errors] in the Getting Started guide.
|
||||||
|
- Read about [`defexception`][defexception] in the documentation.
|
||||||
|
- Read about the [`Exception` behaviour][exception-behaviour] in the documentation.
|
||||||
|
- Read the code snippets from the introduction.
|
||||||
|
|
||||||
|
## 1. Error for Division by Zero
|
||||||
|
|
||||||
|
- Implement the module, specifying the message using a special [built-in macro for defining exceptions][defexception].
|
||||||
|
- Modules can be nested inside of other modules.
|
||||||
|
|
||||||
|
## 2. Error when encountering stack underflow
|
||||||
|
|
||||||
|
- Implement the module, specifying the message using a special [built-in macro for defining exceptions][defexception].
|
||||||
|
- You can use one of the Exception Behaviour callbacks to define an exception whose message changes based on the arguments passed to `raise/2`.
|
||||||
|
- Modules can be nested inside of other modules.
|
||||||
|
|
||||||
|
## 3. Write a dividing function
|
||||||
|
|
||||||
|
- Write a multi-clause function using guards for control-flow.
|
||||||
|
- You can pattern match in the function argument list to bind the stack's values to variables.
|
||||||
|
|
||||||
|
[getting-started-errors]: https://elixir-lang.org/getting-started/try-catch-and-rescue.html#errors
|
||||||
|
[defexception]: https://hexdocs.pm/elixir/Kernel.html#defexception/1
|
||||||
|
[exception-behaviour]: https://hexdocs.pm/elixir/Exception.html
|
||||||
109
stack-underflow/README.md
Normal file
109
stack-underflow/README.md
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
# Stack Underflow
|
||||||
|
|
||||||
|
Welcome to Stack Underflow 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
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
All errors in Elixir implement the _Exception Behaviour_. Just like the _Access Behaviour_, the _Exception Behaviour_ defines callback functions that a module must implement to fulfill the software contract of the behaviour. Once an error is defined, it has the following properties:
|
||||||
|
|
||||||
|
- The module's name defines the error's name.
|
||||||
|
- The module defines an error-struct.
|
||||||
|
- The struct will have a `:message` field.
|
||||||
|
- The module can be be used with `raise/1` and `raise/2` to raise the intended error
|
||||||
|
|
||||||
|
The _Exception Behaviour_ also specifies two callbacks: `message/1` and `exception/1`. If unimplemented, default implementations will be used. `message/1` transforms the error-struct to a readable message when called with `raise`. `exception/1` allows additional context to be added to the message when it is called with `raise/2`
|
||||||
|
|
||||||
|
### Defining an exception
|
||||||
|
|
||||||
|
To define an exception from an error module, we use the `defexception` macro:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Defines a minimal error, with the name `MyError`
|
||||||
|
defmodule MyError do
|
||||||
|
defexception message: "error"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Defines an error with a customized exception/1 function
|
||||||
|
defmodule MyCustomizedError do
|
||||||
|
defexception message: "custom error"
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def exception(value) do
|
||||||
|
case value do
|
||||||
|
[] ->
|
||||||
|
%MyCustomizedError{}
|
||||||
|
_ ->
|
||||||
|
%MyCustomizedError{message: "Alert: " <> value}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using exceptions
|
||||||
|
|
||||||
|
Defined errors may be used like a built in error using either `raise/1` or `raise/2`.
|
||||||
|
|
||||||
|
- `raise/1` raises a specific error by its module name, or, if the argument is a string, it will raise a `RuntimeError` with the string as the message.
|
||||||
|
- `raise/2` raises a specific error by its module name, and accepts an attributes argument which is used to obtain the error with the appropriate message.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
While continuing your work at _Instruments of Texas_, there is progress being made on the Elixir implementation of the RPN calculator. Your team would like to be able to raise errors that are more specific than the generic errors provided by the standard library. You are doing some research, but you have decided to implement two new errors which implement the _Exception Behaviour_.
|
||||||
|
|
||||||
|
## 1. Error for Division by Zero
|
||||||
|
|
||||||
|
Dividing a number by zero produces an undefined result, which the team decides is best represented by an error.
|
||||||
|
|
||||||
|
Implement the `DivisionByZeroError` module to have the error message: `"division by zero occurred"`
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
raise DivisionByZeroError
|
||||||
|
# => ** (DivisionByZeroError) division by zero occurred
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Error when encountering stack underflow
|
||||||
|
|
||||||
|
RPN calculators use a _stack_ to keep track of numbers before they are added. The team represents this _stack_ with a list of numbers (integer and floating-point), e.g.: `[3, 4.0]`. Each operation needs a specific number of numbers on the stack in order to perform its calculation. When there are not enough numbers on the stack, this is called a stack underflow error. Implement the `StackUnderflowError` exception which provides a default message, and optional extra context
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
raise StackUnderflowError
|
||||||
|
# => ** (StackUnderflowError) stack underflow occurred
|
||||||
|
|
||||||
|
raise StackUnderflowError, "when dividing"
|
||||||
|
# => ** (StackUnderflowError) stack underflow occurred, context: when dividing
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Write a dividing function
|
||||||
|
|
||||||
|
Implement the `divide/1` function which takes a stack _(list of numbers)_ and:
|
||||||
|
|
||||||
|
- raises _stack underflow_ when the stack does not contain enough numbers
|
||||||
|
- raises _division by zero_ when the divisor is 0 (note the stack of numbers is stored in the reverse order)
|
||||||
|
- performs the division when no errors are raised
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
RPNCalculator.Exception.divide([])
|
||||||
|
# => ** (StackUnderflowError) stack underflow occurred, context: when dividing
|
||||||
|
|
||||||
|
RPNCalculator.Exception.divide([0, 100])
|
||||||
|
# => ** (DivisionByZeroError) division by zero occurred
|
||||||
|
|
||||||
|
RPNCalculator.Exception.divide([4, 16])
|
||||||
|
# => 4
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note the order of the list is reversed!
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
### Created by
|
||||||
|
|
||||||
|
- @neenjaw
|
||||||
|
|
||||||
|
### Contributed to by
|
||||||
|
|
||||||
|
- @angelikatyborska
|
||||||
22
stack-underflow/lib/rpn_calculator/exception.ex
Normal file
22
stack-underflow/lib/rpn_calculator/exception.ex
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
defmodule RPNCalculator.Exception do
|
||||||
|
defmodule DivisionByZeroError do
|
||||||
|
defexception message: "division by zero occurred"
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule StackUnderflowError do
|
||||||
|
defexception message: "stack underflow occurred"
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def exception(context) do
|
||||||
|
case context do
|
||||||
|
[] -> %__MODULE__{}
|
||||||
|
_ -> %__MODULE__{message: "stack underflow occurred, context: #{context}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def divide([]), do: raise(StackUnderflowError, "when dividing")
|
||||||
|
def divide([_]), do: raise(StackUnderflowError, "when dividing")
|
||||||
|
def divide([0 | _]), do: raise(DivisionByZeroError)
|
||||||
|
def divide([denom, num]), do: div(num, denom)
|
||||||
|
end
|
||||||
28
stack-underflow/mix.exs
Normal file
28
stack-underflow/mix.exs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
defmodule RPNCalculator.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :stack_underflow,
|
||||||
|
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
|
||||||
140
stack-underflow/test/rpn_calculator/exception_test.exs
Normal file
140
stack-underflow/test/rpn_calculator/exception_test.exs
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
defmodule RPNCalculator.ExceptionTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
|
||||||
|
config = ExUnit.configuration()
|
||||||
|
|
||||||
|
# When modules used in the test suite don't exist, or don't implement the desired struct, a compile error occurs
|
||||||
|
# so the test suite is never run, so using the cond to conditionally create test cases based on if the module is defined and loaded
|
||||||
|
# allows for meaningful error messages.
|
||||||
|
cond do
|
||||||
|
config[:undefined_division_by_zero_error_module] ->
|
||||||
|
@tag task_id: 1
|
||||||
|
test "DivisionByZeroError defined" do
|
||||||
|
flunk("Implement the DivisionByZeroError inside of the `RPNCalculator.Exception` module")
|
||||||
|
end
|
||||||
|
|
||||||
|
config[:undefined_division_by_zero_error_exception] ->
|
||||||
|
@tag task_id: 1
|
||||||
|
test "DivisionByZeroError defined" do
|
||||||
|
flunk("define DivisionByZeroError exception using `defexception`")
|
||||||
|
end
|
||||||
|
|
||||||
|
true ->
|
||||||
|
@tag task_id: 1
|
||||||
|
test "DivisionByZeroError fields defined by `defexception`" do
|
||||||
|
assert %RPNCalculator.Exception.DivisionByZeroError{}.__exception__ == true
|
||||||
|
|
||||||
|
assert %RPNCalculator.Exception.DivisionByZeroError{}.__struct__ ==
|
||||||
|
RPNCalculator.Exception.DivisionByZeroError
|
||||||
|
|
||||||
|
assert %RPNCalculator.Exception.DivisionByZeroError{}.message ==
|
||||||
|
"division by zero occurred"
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 1
|
||||||
|
test "DivisionByZeroError message when raised with raise/1" do
|
||||||
|
assert_raise(
|
||||||
|
RPNCalculator.Exception.DivisionByZeroError,
|
||||||
|
"division by zero occurred",
|
||||||
|
fn ->
|
||||||
|
raise RPNCalculator.Exception.DivisionByZeroError
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
cond do
|
||||||
|
config[:undefined_stack_underflow_error_module] ->
|
||||||
|
@tag task_id: 2
|
||||||
|
test "StackUnderflowError defined" do
|
||||||
|
flunk("Implement the StackUnderflowError inside of the `RPNCalculator.Exception` module")
|
||||||
|
end
|
||||||
|
|
||||||
|
config[:undefined_stack_underflow_error_exception] ->
|
||||||
|
@tag task_id: 2
|
||||||
|
test "StackUnderflowError defined" do
|
||||||
|
flunk("define StackUnderflowError exception using `defexception`")
|
||||||
|
end
|
||||||
|
|
||||||
|
true ->
|
||||||
|
@tag task_id: 2
|
||||||
|
test "StackUnderflowError fields defined by `defexception`" do
|
||||||
|
assert %RPNCalculator.Exception.StackUnderflowError{}.__exception__ == true
|
||||||
|
|
||||||
|
assert %RPNCalculator.Exception.StackUnderflowError{}.__struct__ ==
|
||||||
|
RPNCalculator.Exception.StackUnderflowError
|
||||||
|
|
||||||
|
assert %RPNCalculator.Exception.StackUnderflowError{}.message ==
|
||||||
|
"stack underflow occurred"
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 2
|
||||||
|
test "StackUnderflowError message when raised with raise/1" do
|
||||||
|
assert_raise(
|
||||||
|
RPNCalculator.Exception.StackUnderflowError,
|
||||||
|
"stack underflow occurred",
|
||||||
|
fn ->
|
||||||
|
raise RPNCalculator.Exception.StackUnderflowError
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 2
|
||||||
|
test "StackUnderflowError message when raised with raise/2" do
|
||||||
|
assert_raise(
|
||||||
|
RPNCalculator.Exception.StackUnderflowError,
|
||||||
|
"stack underflow occurred, context: test",
|
||||||
|
fn ->
|
||||||
|
raise RPNCalculator.Exception.StackUnderflowError, "test"
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "divide/1" do
|
||||||
|
@tag task_id: 3
|
||||||
|
test "when stack doesn't contain any numbers, raise StackUnderflowError" do
|
||||||
|
assert_raise(
|
||||||
|
RPNCalculator.Exception.StackUnderflowError,
|
||||||
|
"stack underflow occurred, context: when dividing",
|
||||||
|
fn ->
|
||||||
|
RPNCalculator.Exception.divide([])
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "when stack contains only one number, raise StackUnderflowError" do
|
||||||
|
assert_raise(
|
||||||
|
RPNCalculator.Exception.StackUnderflowError,
|
||||||
|
"stack underflow occurred, context: when dividing",
|
||||||
|
fn ->
|
||||||
|
RPNCalculator.Exception.divide([3])
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "when stack contains only one number, raise StackUnderflowError, even when it's a zero" do
|
||||||
|
assert_raise(
|
||||||
|
RPNCalculator.Exception.StackUnderflowError,
|
||||||
|
"stack underflow occurred, context: when dividing",
|
||||||
|
fn ->
|
||||||
|
RPNCalculator.Exception.divide([0])
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "when divisor is 0, raise DivisionByZeroError" do
|
||||||
|
assert_raise(RPNCalculator.Exception.DivisionByZeroError, "division by zero occurred", fn ->
|
||||||
|
RPNCalculator.Exception.divide([0, 2])
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "divisor is not 0, don't raise" do
|
||||||
|
assert RPNCalculator.Exception.divide([2, 4]) == 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
13
stack-underflow/test/test_helper.exs
Normal file
13
stack-underflow/test/test_helper.exs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
options = [
|
||||||
|
undefined_division_by_zero_error_module:
|
||||||
|
Code.ensure_compiled(RPNCalculator.Exception.DivisionByZeroError) == {:error, :nofile},
|
||||||
|
undefined_stack_underflow_error_module:
|
||||||
|
Code.ensure_compiled(RPNCalculator.Exception.StackUnderflowError) == {:error, :nofile},
|
||||||
|
undefined_struct_division_by_zero_error_exception:
|
||||||
|
not function_exported?(RPNCalculator.Exception.DivisionByZeroError, :__struct__, 0),
|
||||||
|
undefined_struct_stack_underflow_error_exception:
|
||||||
|
not function_exported?(RPNCalculator.Exception.StackUnderflowError, :__struct__, 0)
|
||||||
|
]
|
||||||
|
|
||||||
|
ExUnit.start(options)
|
||||||
|
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
||||||
Loading…
Reference in a new issue