rpn_calculator

This commit is contained in:
Danil Negrienko 2024-03-07 01:40:53 -05:00
parent 0c15aedc09
commit d1b894006c
11 changed files with 377 additions and 0 deletions

View File

@ -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."
}

View File

@ -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}

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
elixir/rpn-calculator/.gitignore vendored Normal file
View 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

View 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)
- 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.

View 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://hexdocs.pm/elixir/try-catch-and-rescue.html#errors
[docs-try]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#try/1

View File

@ -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

View File

@ -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

View 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

View File

@ -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

View File

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true, seed: 0)