stack-underflow

This commit is contained in:
Danil Negrienko 2024-03-07 02:05:57 -05:00
parent 7ae168f5ea
commit ed251fd37d
11 changed files with 470 additions and 0 deletions

View File

@ -0,0 +1,22 @@
{
"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",
"icon": "error-handling",
"blurb": "Learn about defining custom exceptions by continuing work on your experimental Reverse Polish Notation calculator."
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"stack-underflow","id":"27a7dac3a9d849a49e365ae1576a04e7","url":"https://exercism.org/tracks/elixir/exercises/stack-underflow","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/stack-underflow/.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/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)
- 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,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://hexdocs.pm/elixir/try-catch-and-rescue.html#errors
[defexception]: https://hexdocs.pm/elixir/Kernel.html#defexception/1
[exception-behaviour]: https://hexdocs.pm/elixir/Exception.html

View File

@ -0,0 +1,110 @@
# 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 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 _(a list of two 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

View File

@ -0,0 +1,25 @@
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(value) do
case value do
[] ->
%StackUnderflowError{}
_ ->
%StackUnderflowError{message: "stack underflow occurred, context: " <> value}
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([x, y]), do: div(y, x)
end

View 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

View 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

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