rpn_calculator
This commit is contained in:
parent
0c15aedc09
commit
d1b894006c
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"icon": "instruments-of-texas",
|
||||||
|
"blurb": "Learn about errors and rescuing them by working on an experimental Reverse Polish Notation calculator."
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
{"track":"elixir","exercise":"rpn-calculator","id":"ea46ce98f99e40f18d385997dfdae1de","url":"https://exercism.org/tracks/elixir/exercises/rpn-calculator","handle":"negrienko","is_requester":true,"auto_approve":false}
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
- The [Elixir track's programming category on the forum](https://forum.exercism.org/c/programming/elixir)
|
||||||
|
- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
|
||||||
|
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
|
||||||
|
|
||||||
|
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
|
||||||
|
|
||||||
|
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
|
|
@ -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://hexdocs.pm/elixir/try-catch-and-rescue.html#errors
|
||||||
|
[docs-try]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#try/1
|
|
@ -0,0 +1,121 @@
|
||||||
|
# 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-formatter-disable-next-block)
|
||||||
|
|
||||||
|
```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][exercism-exceptions] you will learn more about error structs, including how to define your own custom error.
|
||||||
|
|
||||||
|
[exercism-exceptions]: https://exercism.org/tracks/elixir/concepts/exceptions
|
||||||
|
|
||||||
|
## 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
|
|
@ -0,0 +1,21 @@
|
||||||
|
defmodule RPNCalculator do
|
||||||
|
def calculate!(stack, operation) do
|
||||||
|
operation.(stack)
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate(stack, operation) do
|
||||||
|
try do
|
||||||
|
{:ok, operation.(stack)}
|
||||||
|
rescue
|
||||||
|
_e -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_verbose(stack, operation) do
|
||||||
|
try do
|
||||||
|
{:ok, operation.(stack)}
|
||||||
|
rescue
|
||||||
|
e -> {:error, e.message}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -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
|
|
@ -0,0 +1,55 @@
|
||||||
|
defmodule RPNCalculatorTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
|
||||||
|
@tag task_id: 1
|
||||||
|
test "calculate! returns what the operation does: :ok atom" do
|
||||||
|
assert RPNCalculator.calculate!([], fn _ -> :ok end) == :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 1
|
||||||
|
test "calculate! returns what the operation does: an ok string" 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 the result of the operation (a string) wrapped in an :ok tuple" do
|
||||||
|
assert RPNCalculator.calculate([], fn _ -> "operation completed" end) ==
|
||||||
|
{:ok, "operation completed"}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 2
|
||||||
|
test "calculate returns the result of the operation (an atom) wrapped in an :ok tuple" do
|
||||||
|
assert RPNCalculator.calculate([], fn _ -> :success end) ==
|
||||||
|
{:ok, :success}
|
||||||
|
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 the result of the operation (a string) wrapped in an :ok tuple" do
|
||||||
|
assert RPNCalculator.calculate_verbose([], fn _ -> "operation completed" end) ==
|
||||||
|
{:ok, "operation completed"}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "calculate_verbose returns the result of the operation (an atom) wrapped in an :ok tuple" do
|
||||||
|
assert RPNCalculator.calculate_verbose([], fn _ -> :success end) ==
|
||||||
|
{:ok, :success}
|
||||||
|
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
|
|
@ -0,0 +1,2 @@
|
||||||
|
ExUnit.start()
|
||||||
|
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
Loading…
Reference in New Issue