From ed251fd37ddeb0b066c6099de0601db2a48540ac Mon Sep 17 00:00:00 2001 From: Danylo Negrienko Date: Thu, 7 Mar 2024 02:05:57 -0500 Subject: [PATCH] stack-underflow --- elixir/stack-underflow/.exercism/config.json | 22 +++ .../stack-underflow/.exercism/metadata.json | 1 + elixir/stack-underflow/.formatter.exs | 4 + elixir/stack-underflow/.gitignore | 24 +++ elixir/stack-underflow/HELP.md | 75 ++++++++++ elixir/stack-underflow/HINTS.md | 28 ++++ elixir/stack-underflow/README.md | 110 ++++++++++++++ .../lib/rpn_calculator/exception.ex | 25 ++++ elixir/stack-underflow/mix.exs | 28 ++++ .../test/rpn_calculator/exception_test.exs | 140 ++++++++++++++++++ elixir/stack-underflow/test/test_helper.exs | 13 ++ 11 files changed, 470 insertions(+) create mode 100644 elixir/stack-underflow/.exercism/config.json create mode 100644 elixir/stack-underflow/.exercism/metadata.json create mode 100644 elixir/stack-underflow/.formatter.exs create mode 100644 elixir/stack-underflow/.gitignore create mode 100644 elixir/stack-underflow/HELP.md create mode 100644 elixir/stack-underflow/HINTS.md create mode 100644 elixir/stack-underflow/README.md create mode 100644 elixir/stack-underflow/lib/rpn_calculator/exception.ex create mode 100644 elixir/stack-underflow/mix.exs create mode 100644 elixir/stack-underflow/test/rpn_calculator/exception_test.exs create mode 100644 elixir/stack-underflow/test/test_helper.exs diff --git a/elixir/stack-underflow/.exercism/config.json b/elixir/stack-underflow/.exercism/config.json new file mode 100644 index 0000000..8f7f2bc --- /dev/null +++ b/elixir/stack-underflow/.exercism/config.json @@ -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." +} diff --git a/elixir/stack-underflow/.exercism/metadata.json b/elixir/stack-underflow/.exercism/metadata.json new file mode 100644 index 0000000..bdd8854 --- /dev/null +++ b/elixir/stack-underflow/.exercism/metadata.json @@ -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} \ No newline at end of file diff --git a/elixir/stack-underflow/.formatter.exs b/elixir/stack-underflow/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/elixir/stack-underflow/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/elixir/stack-underflow/.gitignore b/elixir/stack-underflow/.gitignore new file mode 100644 index 0000000..d343c0d --- /dev/null +++ b/elixir/stack-underflow/.gitignore @@ -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 + diff --git a/elixir/stack-underflow/HELP.md b/elixir/stack-underflow/HELP.md new file mode 100644 index 0000000..5788e70 --- /dev/null +++ b/elixir/stack-underflow/HELP.md @@ -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/.exs:LINENUM` - runs only a single test, the test from `.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. \ No newline at end of file diff --git a/elixir/stack-underflow/HINTS.md b/elixir/stack-underflow/HINTS.md new file mode 100644 index 0000000..1b40b9c --- /dev/null +++ b/elixir/stack-underflow/HINTS.md @@ -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 \ No newline at end of file diff --git a/elixir/stack-underflow/README.md b/elixir/stack-underflow/README.md new file mode 100644 index 0000000..97c0253 --- /dev/null +++ b/elixir/stack-underflow/README.md @@ -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 \ No newline at end of file diff --git a/elixir/stack-underflow/lib/rpn_calculator/exception.ex b/elixir/stack-underflow/lib/rpn_calculator/exception.ex new file mode 100644 index 0000000..63a53d5 --- /dev/null +++ b/elixir/stack-underflow/lib/rpn_calculator/exception.ex @@ -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 diff --git a/elixir/stack-underflow/mix.exs b/elixir/stack-underflow/mix.exs new file mode 100644 index 0000000..12236d0 --- /dev/null +++ b/elixir/stack-underflow/mix.exs @@ -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 diff --git a/elixir/stack-underflow/test/rpn_calculator/exception_test.exs b/elixir/stack-underflow/test/rpn_calculator/exception_test.exs new file mode 100644 index 0000000..38376d8 --- /dev/null +++ b/elixir/stack-underflow/test/rpn_calculator/exception_test.exs @@ -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 diff --git a/elixir/stack-underflow/test/test_helper.exs b/elixir/stack-underflow/test/test_helper.exs new file mode 100644 index 0000000..e8d2c3c --- /dev/null +++ b/elixir/stack-underflow/test/test_helper.exs @@ -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)