From 5f3aa01e4dda78dc331c6b970b32b305b7505408 Mon Sep 17 00:00:00 2001 From: Danylo Negrienko Date: Mon, 18 Dec 2023 04:03:01 -0500 Subject: [PATCH] city-office & high-score --- elixir/city-office/.exercism/config.json | 22 ++ elixir/city-office/.exercism/metadata.json | 1 + elixir/city-office/.formatter.exs | 4 + elixir/city-office/.gitignore | 24 ++ elixir/city-office/HELP.md | 75 ++++++ elixir/city-office/HINTS.md | 63 +++++ elixir/city-office/README.md | 173 ++++++++++++ elixir/city-office/lib/form.ex | 74 ++++++ elixir/city-office/mix.exs | 28 ++ elixir/city-office/test/form_test.exs | 292 +++++++++++++++++++++ elixir/city-office/test/test_helper.exs | 2 + elixir/high-score/.exercism/config.json | 19 ++ elixir/high-score/.exercism/metadata.json | 1 + elixir/high-score/.formatter.exs | 4 + elixir/high-score/.gitignore | 24 ++ elixir/high-score/HELP.md | 75 ++++++ elixir/high-score/HINTS.md | 42 +++ elixir/high-score/README.md | 161 ++++++++++++ elixir/high-score/lib/high_score.ex | 21 ++ elixir/high-score/mix.exs | 28 ++ elixir/high-score/test/high_score_test.exs | 186 +++++++++++++ elixir/high-score/test/test_helper.exs | 2 + 22 files changed, 1321 insertions(+) create mode 100644 elixir/city-office/.exercism/config.json create mode 100644 elixir/city-office/.exercism/metadata.json create mode 100644 elixir/city-office/.formatter.exs create mode 100644 elixir/city-office/.gitignore create mode 100644 elixir/city-office/HELP.md create mode 100644 elixir/city-office/HINTS.md create mode 100644 elixir/city-office/README.md create mode 100644 elixir/city-office/lib/form.ex create mode 100644 elixir/city-office/mix.exs create mode 100644 elixir/city-office/test/form_test.exs create mode 100644 elixir/city-office/test/test_helper.exs create mode 100644 elixir/high-score/.exercism/config.json create mode 100644 elixir/high-score/.exercism/metadata.json create mode 100644 elixir/high-score/.formatter.exs create mode 100644 elixir/high-score/.gitignore create mode 100644 elixir/high-score/HELP.md create mode 100644 elixir/high-score/HINTS.md create mode 100644 elixir/high-score/README.md create mode 100644 elixir/high-score/lib/high_score.ex create mode 100644 elixir/high-score/mix.exs create mode 100644 elixir/high-score/test/high_score_test.exs create mode 100644 elixir/high-score/test/test_helper.exs diff --git a/elixir/city-office/.exercism/config.json b/elixir/city-office/.exercism/config.json new file mode 100644 index 0000000..eb609d4 --- /dev/null +++ b/elixir/city-office/.exercism/config.json @@ -0,0 +1,22 @@ +{ + "authors": [ + "angelikatyborska" + ], + "contributors": [ + "neenjaw", + "michallepicki" + ], + "files": { + "solution": [ + "lib/form.ex" + ], + "test": [ + "test/form_test.exs" + ], + "exemplar": [ + ".meta/exemplar.ex" + ] + }, + "language_versions": ">=1.10", + "blurb": "Learn about writing documentation and typespecs by getting your code ready for the arrival of a new colleague at the city office." +} diff --git a/elixir/city-office/.exercism/metadata.json b/elixir/city-office/.exercism/metadata.json new file mode 100644 index 0000000..9a540cd --- /dev/null +++ b/elixir/city-office/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"elixir","exercise":"city-office","id":"4affdb892b92418cbb20daf2d4ecce5f","url":"https://exercism.org/tracks/elixir/exercises/city-office","handle":"negrienko","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/elixir/city-office/.formatter.exs b/elixir/city-office/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/elixir/city-office/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/elixir/city-office/.gitignore b/elixir/city-office/.gitignore new file mode 100644 index 0000000..4abee35 --- /dev/null +++ b/elixir/city-office/.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"). +match_binary-*.tar + diff --git a/elixir/city-office/HELP.md b/elixir/city-office/HELP.md new file mode 100644 index 0000000..7f6c379 --- /dev/null +++ b/elixir/city-office/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/form.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/city-office/HINTS.md b/elixir/city-office/HINTS.md new file mode 100644 index 0000000..01a107f --- /dev/null +++ b/elixir/city-office/HINTS.md @@ -0,0 +1,63 @@ +# Hints + +## General + +- Read the official documentation for [typespecs][typespecs]. +- Read the official documentation about [writing documentation][writing-documentation]. +- Read about using module attributes as annotations in the [official Getting Started guide][getting-started-module-attributes]. +- Read about using typespecs in the [official Getting Started guide][getting-started-typespecs]. + +## 1. Document the purpose of the form tools + +- The module attribute `@moduledoc` can be used to write documentation for a module. + +## 2. Document filling out fields with blank values + +- The module attribute `@doc` can be used to write documentation for a function. +- The module attribute `@spec` can be used to write a typespec for a function. +- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe. +- Refer to the [typespecs documentation][typespecs-types] for a list of all available types. +- The correct type for strings is [defined in the `String` module][string-t]. + +## 3. Document splitting values into lists of uppercase letters + +- The module attribute `@doc` can be used to write documentation for a function. +- The module attribute `@spec` can be used to write a typespec for a function. +- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe. +- Refer to the [typespecs documentation][typespecs-types] for a list of all available types. +- The correct type for strings is [defined in the `String` module][string-t]. +- A list is a parametrized type. + +## 4. Document checking if a value fits a field with a max length + +- The module attribute `@doc` can be used to write documentation for a function. +- The module attribute `@spec` can be used to write a typespec for a function. +- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe. +- Refer to the [typespecs documentation][typespecs-types] for a list of all available types. +- The correct type for strings is [defined in the `String` module][string-t]. +- Literal values can be used in a typespec. +- The pipe `|` can be used to represent a union of types. + +## 5. Document different address formats + +- The module attribute `@type` can be use to define a custom public type. +- Types can be compound, e.g. when specifying a type that's a map, you can also specify the types of the values under the specific keys. +- [The type operator `::`][type-operator] can also be used to prepend a variable name to a type. +- Custom types can be used to define other custom types. + +## 6. Document formatting the address + +- The module attribute `@doc` can be used to write documentation for a function. +- The module attribute `@spec` can be used to write a typespec for a function. +- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe. +- Refer to the [typespecs documentation][typespecs-types] for a list of all available types. +- The correct type for strings is [defined in the `String` module][string-t]. +- Custom types can be used in a typespec. + +[writing-documentation]: https://hexdocs.pm/elixir/writing-documentation.html +[typespecs]: https://hexdocs.pm/elixir/typespecs.html +[typespecs-types]: https://hexdocs.pm/elixir/typespecs.html#types-and-their-syntax +[getting-started-module-attributes]: https://elixir-lang.org/getting-started/module-attributes.html#as-annotations +[getting-started-typespecs]: https://elixir-lang.org/getting-started/typespecs-and-behaviours.html#types-and-specs +[string-t]: https://hexdocs.pm/elixir/String.html#t:t/0 +[type-operator]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#::/2 \ No newline at end of file diff --git a/elixir/city-office/README.md b/elixir/city-office/README.md new file mode 100644 index 0000000..81c46f5 --- /dev/null +++ b/elixir/city-office/README.md @@ -0,0 +1,173 @@ +# City Office + +Welcome to City Office 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 + +## Docs + +Documentation in Elixir is a first-class citizen. + +There are two module attributes commonly used to document your code - `@moduledoc` for documenting a module and `@doc` for documenting a function that follows the attribute. The `@moduledoc` attribute usually appears on the first line of the module, and the `@doc` attribute usually appears right before a function definition, or the function's typespec if it has one. The documentation is commonly written in a multiline string using the heredoc syntax. + +Elixir documentation is written in [**Markdown**][markdown]. + +```elixir +defmodule String do + @moduledoc """ + Strings in Elixir are UTF-8 encoded binaries. + """ + + @doc """ + Converts all characters in the given string to uppercase according to `mode`. + + ## Examples + + iex> String.upcase("abcd") + "ABCD" + + iex> String.upcase("olá") + "OLÁ" + """ + def upcase(string, mode \\ :default) +end +``` + +## Typespecs + +Elixir is a dynamically typed language, which means it doesn't provide compile-time type checks. Still, type specifications can be used as a form of documentation. + +A type specification can be added to a function using the `@spec` module attribute right before the function definition. `@spec` is followed by the function name and a list of all of its arguments' types, in parentheses, separated by commas. The type of the return value is separated from the function's arguments with a double colon `::`. + +```elixir +@spec longer_than?(String.t(), non_neg_integer()) :: boolean() +def longer_than?(string, length), do: String.length(string) > length +``` + +### Types + +Most commonly used types include: + +- booleans: `boolean()` +- strings: `String.t()` +- numbers: `integer()`, `non_neg_integer()`, `pos_integer()`, `float()` +- lists: `list()` +- a value of any type: `any()` + +Some types can also be parameterized, for example `list(integer)` is a list of integers. + +Literal values can also be used as types. + +A union of types can be written using the pipe `|`. For example, `integer() | :error` means either an integer or the atom literal `:error`. + +A full list of all types can be found in the ["Typespecs" section in the official documentation][types]. + +### Naming arguments + +Arguments in the typespec could also be named which is useful for distinguishing multiple arguments of the same type. The argument name, followed by a double colon, goes before the argument's type. + +```elixir +@spec to_hex({hue :: integer, saturation :: integer, lightness :: integer}) :: String.t() +``` + +### Custom types + +Typespecs aren't limited to just the built-in types. Custom types can be defined using the `@type` module attribute. A custom type definition starts with the type's name, followed by a double colon and then the type itself. + +```elixir +@type color :: {hue :: integer, saturation :: integer, lightness :: integer} + +@spec to_hex(color()) :: String.t() +``` + +A custom type can be used from the same module where it's defined, or from another module. + +[markdown]: https://docs.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax +[types]: https://hexdocs.pm/elixir/typespecs.html#types-and-their-syntax + +## Instructions + +You have been working in the city office for a while, and you have developed a set of tools that speed up your day-to-day work, for example with filling out forms. + +Now, a new colleague is joining you, and you realized your tools might not be self-explanatory. There are a lot of weird conventions in your office, like always filling out forms with uppercase letters and avoiding leaving fields empty. + +You decide to write some documentation so that it's easier for your new colleague to hop right in and start using your tools. + +## 1. Document the purpose of the form tools + +Add documentation to the `Form` module that describes its purpose. It should read: + +``` +A collection of loosely related functions helpful for filling out various forms at the city office. +``` + +## 2. Document filling out fields with blank values + +Add documentation and a typespec to the `Form.blanks/1` function. The documentation should read: + +``` +Generates a string of a given length. + +This string can be used to fill out a form field that is supposed to have no value. +Such fields cannot be left empty because a malicious third party could fill them out with false data. +``` + +The typespec should explain that the function accepts a single argument, a non-negative integer, and returns a string. + +## 3. Document splitting values into lists of uppercase letters + +Add documentation and a typespec to the `Form.letters/1` function. The documentation should read: + +``` +Splits the string into a list of uppercase letters. + +This is needed for form fields that don't offer a single input for the whole string, +but instead require splitting the string into a predefined number of single-letter inputs. +``` + +The typespec should explain that the function accepts a single argument, a string, and returns a list of strings. + +## 4. Document checking if a value fits a field with a max length + +Add documentation and a typespec to the `Form.check_length/2` function. The documentation should read: + +``` +Checks if the value has no more than the maximum allowed number of letters. + +This is needed to check that the values of fields do not exceed the maximum allowed length. +It also tells you by how much the value exceeds the maximum. +``` + +The typespec should explain that the function accepts two arguments, a string and a non-negative integer, and returns one of two possible values. It returns either the `:ok` atom or a 2-tuple with the first element being the `:error` atom, and the second a positive integer. + +## 5. Document different address formats + +For some unknown to you reason, the city office's internal system uses two different ways of representing addresses - either as a map or as a tuple. + +Document this fact by defining three custom public types: +- `address_map` - a map with the keys `:street`, `:postal_code`, and `:city`. Each key holds a value of type string. +- `address_tuple` - a tuple with three values - `street`, `postal_code`, and `city`. Each value is of type string. Differentiate the values by giving them names in the typespec. +- `address` - can be either an `address_map` or an `address_tuple`. + +## 6. Document formatting the address + +Add documentation and a typespec to the `Form.format_address/1` function. The documentation should read: + +``` +Formats the address as an uppercase multiline string. +``` + +The typespec should explain that the function accepts one argument, an address, and returns a string. + +## Source + +### Created by + +- @angelikatyborska + +### Contributed to by + +- @neenjaw +- @michallepicki \ No newline at end of file diff --git a/elixir/city-office/lib/form.ex b/elixir/city-office/lib/form.ex new file mode 100644 index 0000000..2b4d3ee --- /dev/null +++ b/elixir/city-office/lib/form.ex @@ -0,0 +1,74 @@ +defmodule Form do + @moduledoc """ + A collection of loosely related functions helpful for filling out various forms at the city office. + """ + @type address_map :: %{ + street: String.t(), + postal_code: String.t(), + city: String.t() + } + + @type address_tuple :: { + street :: String.t(), + postal_code :: String.t(), + city :: String.t() + } + + @type address :: address_map | address_tuple + + @doc """ + Generates a string of a given length. + + This string can be used to fill out a form field that is supposed to have no value. + Such fields cannot be left empty because a malicious third party could fill them out with false data. + """ + @spec blanks(non_neg_integer()) :: String.t() + def blanks(n) do + String.duplicate("X", n) + end + + @doc """ + Splits the string into a list of uppercase letters. + + This is needed for form fields that don't offer a single input for the whole string, + but instead require splitting the string into a predefined number of single-letter inputs. + """ + @spec letters(String.t()) :: [String.t()] + def letters(word) do + word + |> String.upcase() + |> String.split("", trim: true) + end + + @doc """ + Checks if the value has no more than the maximum allowed number of letters. + + This is needed to check that the values of fields do not exceed the maximum allowed length. + It also tells you by how much the value exceeds the maximum. + """ + @spec check_length(String.t(), non_neg_integer()) :: :ok | {:error, pos_integer()} + def check_length(word, length) do + diff = String.length(word) - length + + if diff <= 0 do + :ok + else + {:error, diff} + end + end + + @doc """ + Formats the address as an uppercase multiline string. + """ + @spec format_address(address) :: String.t() + def format_address(%{street: street, postal_code: postal_code, city: city}) do + format_address({street, postal_code, city}) + end + + def format_address({street, postal_code, city}) do + """ + #{String.upcase(street)} + #{String.upcase(postal_code)} #{String.upcase(city)} + """ + end +end diff --git a/elixir/city-office/mix.exs b/elixir/city-office/mix.exs new file mode 100644 index 0000000..9e8d828 --- /dev/null +++ b/elixir/city-office/mix.exs @@ -0,0 +1,28 @@ +defmodule Form.MixProject do + use Mix.Project + + def project do + [ + app: :city_office, + 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/city-office/test/form_test.exs b/elixir/city-office/test/form_test.exs new file mode 100644 index 0000000..0f9623a --- /dev/null +++ b/elixir/city-office/test/form_test.exs @@ -0,0 +1,292 @@ +defmodule FormTest do + use ExUnit.Case + + # Dear Elixir learner, + # If you're reading this test suite to gain some insights, + # please be advised that it is somewhat unusual. + # + # Don't worry if you don't understand this test suite at this stage of your learning journey. + # We had to use some advanced features to be able to write assertions about docs and typespecs. + # You wouldn't normally write assertions for that in a typical codebase. + # We're doing it here strictly for educational purposes. + + defmacrop assert_moduledoc(expected_moduledoc) do + quote do + {:docs_v1, _, _, _, module_doc, _, _} = Code.fetch_docs(Form) + + if module_doc == :none do + flunk("expected the module Form to have documentation") + else + actual_moduledoc = module_doc["en"] + assert actual_moduledoc == unquote(expected_moduledoc) + end + end + end + + defmacrop assert_doc({function_name, function_arity}, expected_doc) do + quote do + {:docs_v1, _, _, _, _, _, docs} = Code.fetch_docs(Form) + + {_, _, _, doc_content, _} = + Enum.find(docs, fn {{kind, function_name, arity}, _, _, _, _} -> + {kind, function_name, arity} == + {:function, unquote(function_name), unquote(function_arity)} + end) + + if doc_content == :none do + flunk( + "expected the function Form.#{unquote(function_name)}/#{unquote(function_arity)} to have documentation" + ) + else + actual_doc = doc_content["en"] + assert actual_doc == unquote(expected_doc) + end + end + end + + defmacrop assert_spec({function_name, function_arity}, arguments_specs, return_spec) do + quote do + {:ok, specs} = Code.Typespec.fetch_specs(Form) + + spec = + Enum.find(specs, fn {{function_name, arity}, _} -> + {function_name, arity} == {unquote(function_name), unquote(function_arity)} + end) + + assert spec, + "expected the function Form.#{unquote(function_name)}/#{unquote(function_arity)} to have a typespec" + + {{unquote(function_name), unquote(function_arity)}, [{:type, _, :fun, _} = function_spec]} = + spec + + {:"::", _, [arguments, return]} = + Code.Typespec.spec_to_quoted(unquote(function_name), function_spec) + + accepted_arguments_specs = + Enum.map(unquote(arguments_specs), fn arguments_spec -> + "#{unquote(function_name)}(#{arguments_spec})" + end) + + actual_arguments_spec = Macro.to_string(arguments) + assert actual_arguments_spec in accepted_arguments_specs + + expected_return_spec = unquote(return_spec) + actual_return_spec = Macro.to_string(return) + assert actual_return_spec == expected_return_spec + end + end + + defmacrop assert_type({module_name, type_name}, expected_type_definition) do + quote do + {:ok, types} = Code.Typespec.fetch_types(unquote(module_name)) + + type = + Enum.find(types, fn {declaration, {type_name, _, _}} -> + declaration == :type && type_name == unquote(type_name) + end) + + assert type, + "expected the module #{unquote(module_name)} to have a public type named #{unquote(type_name)}" + + {:type, type} = type + + {:"::", _, [_, type_definition]} = Code.Typespec.type_to_quoted(type) + + actual_type_definition = Macro.to_string(type_definition) + + if is_list(unquote(expected_type_definition)) do + if actual_type_definition in unquote(expected_type_definition) do + assert true + else + # we know this will fail at this point, but we're using it to provide a nice failure message + assert actual_type_definition == hd(unquote(expected_type_definition)) + end + else + assert actual_type_definition == unquote(expected_type_definition) + end + end + end + + describe "the Form module" do + @tag task_id: 1 + test "has documentation" do + expected_moduledoc = """ + A collection of loosely related functions helpful for filling out various forms at the city office. + """ + + assert_moduledoc(expected_moduledoc) + end + end + + describe "blanks/1" do + @tag task_id: 2 + test "returns a string with Xs of a given length" do + assert Form.blanks(5) == "XXXXX" + end + + @tag task_id: 2 + test "returns an empty string when given length is 0" do + assert Form.blanks(0) == "" + end + + @tag task_id: 2 + test "has documentation" do + expected_doc = """ + Generates a string of a given length. + + This string can be used to fill out a form field that is supposed to have no value. + Such fields cannot be left empty because a malicious third party could fill them out with false data. + """ + + assert_doc({:blanks, 1}, expected_doc) + end + + @tag task_id: 2 + test "has a correct spec" do + assert_spec({:blanks, 1}, ["n :: non_neg_integer()", "non_neg_integer()"], "String.t()") + end + end + + describe "letters/1" do + @tag task_id: 3 + test "returns a list of upcase letters" do + assert Form.letters("Sao Paulo") == ["S", "A", "O", " ", "P", "A", "U", "L", "O"] + end + + @tag task_id: 3 + test "returns an empty list when given an empty string" do + assert Form.letters("") == [] + end + + @tag task_id: 3 + test "has documentation" do + expected_doc = """ + Splits the string into a list of uppercase letters. + + This is needed for form fields that don't offer a single input for the whole string, + but instead require splitting the string into a predefined number of single-letter inputs. + """ + + assert_doc({:letters, 1}, expected_doc) + end + + @tag task_id: 3 + test "has a typespec" do + assert_spec({:letters, 1}, ["word :: String.t()", "String.t()"], "[String.t()]") + end + end + + describe "check_length/2" do + @tag task_id: 4 + test "returns :ok is value is below max length" do + assert Form.check_length("Ruiz", 6) == :ok + end + + @tag task_id: 4 + test "returns :ok is value is of exactly max length" do + assert Form.check_length("Martinez-Cooper", 15) == :ok + end + + @tag task_id: 4 + test "returns an error tuple with the difference between max length and actual length" do + assert Form.check_length("Martinez-Campbell", 10) == {:error, 7} + end + + @tag task_id: 4 + test "has documentation" do + expected_doc = """ + Checks if the value has no more than the maximum allowed number of letters. + + This is needed to check that the values of fields do not exceed the maximum allowed length. + It also tells you by how much the value exceeds the maximum. + """ + + assert_doc({:check_length, 2}, expected_doc) + end + + @tag task_id: 4 + test "has a typespec" do + assert_spec( + {:check_length, 2}, + ["word :: String.t(), length :: non_neg_integer()", "String.t(), non_neg_integer()"], + ":ok | {:error, pos_integer()}" + ) + end + end + + describe "custom types in the Form module" do + @tag task_id: 5 + test "has a custom 'address_map' type" do + expected_type_definitions = [ + "%{street: String.t(), postal_code: String.t(), city: String.t()}", + "%{street: String.t(), city: String.t(), postal_code: String.t()}", + "%{postal_code: String.t(), street: String.t(), city: String.t()}", + "%{postal_code: String.t(), city: String.t(), street: String.t()}", + "%{city: String.t(), street: String.t(), postal_code: String.t()}", + "%{city: String.t(), postal_code: String.t(), street: String.t()}" + ] + + assert_type({Form, :address_map}, expected_type_definitions) + end + + @tag task_id: 5 + test "has a custom 'address_tuple' type with named arguments" do + expected_type_definition = + "{street :: String.t(), postal_code :: String.t(), city :: String.t()}" + + assert_type({Form, :address_tuple}, expected_type_definition) + end + + @tag task_id: 5 + test "has a custom 'address' type that is a union of 'address_map' and 'address_tuple'" do + expected_type_definitions = [ + "address_map() | address_tuple()", + "address_tuple() | address_map()" + ] + + assert_type({Form, :address}, expected_type_definitions) + end + end + + describe "format_address/1" do + @tag task_id: 6 + test "accepts a map" do + input = %{ + street: "Wiejska 4/6/8", + postal_code: "00-902", + city: "Warsaw" + } + + result = """ + WIEJSKA 4/6/8 + 00-902 WARSAW + """ + + assert Form.format_address(input) == result + end + + @tag task_id: 6 + test "accepts a 3 string tuple" do + result = """ + PLATZ DER REPUBLIK 1 + 11011 BERLIN + """ + + assert Form.format_address({"Platz der Republik 1", "11011", "Berlin"}) == result + end + + @tag task_id: 6 + test "has documentation" do + expected_doc = """ + Formats the address as an uppercase multiline string. + """ + + assert_doc({:format_address, 1}, expected_doc) + end + + @tag task_id: 6 + test "has a typespec" do + assert_spec({:format_address, 1}, ["address :: address()", "address()"], "String.t()") + end + end +end diff --git a/elixir/city-office/test/test_helper.exs b/elixir/city-office/test/test_helper.exs new file mode 100644 index 0000000..e8677a3 --- /dev/null +++ b/elixir/city-office/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true, seed: 0) diff --git a/elixir/high-score/.exercism/config.json b/elixir/high-score/.exercism/config.json new file mode 100644 index 0000000..947ae73 --- /dev/null +++ b/elixir/high-score/.exercism/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "neenjaw" + ], + "files": { + "solution": [ + "lib/high_score.ex" + ], + "test": [ + "test/high_score_test.exs" + ], + "exemplar": [ + ".meta/exemplar.ex" + ] + }, + "language_versions": ">=1.10", + "icon": "high-scores", + "blurb": "Learn about maps by keeping track of the high scores in your local arcade hall." +} diff --git a/elixir/high-score/.exercism/metadata.json b/elixir/high-score/.exercism/metadata.json new file mode 100644 index 0000000..1445d1d --- /dev/null +++ b/elixir/high-score/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"elixir","exercise":"high-score","id":"7ad834a7e1364fd2b9244ec879f0ee69","url":"https://exercism.org/tracks/elixir/exercises/high-score","handle":"negrienko","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/elixir/high-score/.formatter.exs b/elixir/high-score/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/elixir/high-score/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/elixir/high-score/.gitignore b/elixir/high-score/.gitignore new file mode 100644 index 0000000..a0f694e --- /dev/null +++ b/elixir/high-score/.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"). +maps-*.tar + diff --git a/elixir/high-score/HELP.md b/elixir/high-score/HELP.md new file mode 100644 index 0000000..ff13f89 --- /dev/null +++ b/elixir/high-score/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/high_score.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/high-score/HINTS.md b/elixir/high-score/HINTS.md new file mode 100644 index 0000000..2cafa1b --- /dev/null +++ b/elixir/high-score/HINTS.md @@ -0,0 +1,42 @@ +# Hints + +## General + +- A [map][maps] is an associative data structure of key-value pairs. +- Elixir offers [many useful Map module functions in the standard library][map-module]. + +## 1. Define a new high score map + +- It should return an empty [map][maps]. +- [Map module][map-module] functions or literal forms can be useful. + +## 2. Add players to the high score map + +- The resulting map should be returned. +- [Map module][map-module] contains functions useful for manipulating maps. [One of them][map-put] puts a value in a map under a given key. + +## 3. Remove players from the score map + +- The resulting map should be returned. +- [Map module][map-module] contains functions useful for manipulating maps. [One of them][map-delete] deletes a key from a map. + +## 4. Reset a player's score + +- The resulting map should be returned with the player's score reset to an initial value. +- [Map module][map-module] contains functions useful for manipulating maps. [One of them][map-put] puts a value in a map under a given key. + +## 5. Update a player's score + +- The resulting map should be returned with the player's updated score. +- [Map module][map-module] contains functions useful for manipulating maps. [One of them][map-update] updates a value in a map under a given key. + +## 6. Get a list of players + +- [Map module][map-module] contains functions useful for manipulating maps. [One of them][map-keys] returns a list of all keys in a map. + +[maps]: https://elixir-lang.org/getting-started/keywords-and-maps.html#maps +[map-module]: https://hexdocs.pm/elixir/Map.html +[map-put]: https://hexdocs.pm/elixir/Map.html#put/3 +[map-delete]: https://hexdocs.pm/elixir/Map.html#delete/2 +[map-update]: https://hexdocs.pm/elixir/Map.html#update/4 +[map-keys]: https://hexdocs.pm/elixir/Map.html#keys/1 \ No newline at end of file diff --git a/elixir/high-score/README.md b/elixir/high-score/README.md new file mode 100644 index 0000000..a63a308 --- /dev/null +++ b/elixir/high-score/README.md @@ -0,0 +1,161 @@ +# High Score + +Welcome to High Score 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 + +## Maps + +Maps in Elixir are the data structure for storing information in key-value pairs. In other languages, these might also be known as associative arrays (PHP), hashes (Perl 5, Raku), or dictionaries (Python). + +Keys and values can be of any data type, but if the key is an atom we can use a shorthand syntax. Maps do not guarantee the order of their entries when accessed or returned. + +### Literal forms + +An empty map is simply declared with `%{}`. If we want to add items to a map literal, we can use two forms: + +```elixir +# If the key is an atom: +%{atom_key: 1} + +# If the key is a different type: +%{1 => :atom_value} + +# You can even mix these if the atom form comes second: +%{"first_form" => :a, atom_form: :b} +``` + +While there is no canonical format, choose a consistent way to represent the key-value literal pairs. + +### Map module functions + +Elixir provides many functions for working with maps in the _Map module_. Some _Map module_ functions require an _anonymous_ function to be passed into the function to assist with the map transformation. + +## Module Attributes As Constants + +In Elixir, we can define module attributes which can be used as constants in our functions. + +```elixir +defmodule Example do + + # Defines the attribute as the value 1 + @constant_number 1 + + def example_value() do + # Returns the value 1 + @constant_number + end +end +``` + +When used to define module constants, attributes can be any expression which can be evaluated at compilation time. After compilation, module attributes are not accessible since they are expanded during compilation, similar to defined macros in languages like C. + +## Instructions + +In this exercise, you're implementing a way to keep track of the high scores for the most popular game in your local arcade hall. + +## 1. Define a new high score map + +To make a new high score map, define the `HighScore.new/0` function which doesn't take any arguments and returns a new, empty map of high scores. + +```elixir +HighScore.new() +# => %{} +``` + +## 2. Add players to the high score map + +To add a player to the high score map, define `HighScore.add_player/3`, which is a function which takes 3 arguments: + +- The first argument is the map of scores. +- The second argument is the name of a player as a string. +- The third argument is the score as an integer. The argument is optional, implement the third argument with a default value of 0. + +Store the default initial score in a module attribute. It will be needed again. + +```elixir +score_map = HighScore.new() +# => %{} +score_map = HighScore.add_player(score_map, "Dave Thomas") +# => %{"Dave Thomas" => 0} +score_map = HighScore.add_player(score_map, "José Valim", 486_373) +# => %{"Dave Thomas" => 0, "José Valim"=> 486_373} +``` + +## 3. Remove players from the score map + +To remove a player from the high score map, define `HighScore.remove_player/2`, which takes 2 arguments: + +- The first argument is the map of scores. +- The second argument is the name of the player as a string. + +```elixir +score_map = HighScore.new() +# => %{} +score_map = HighScore.add_player(score_map, "Dave Thomas") +# => %{"Dave Thomas" => 0} +score_map = HighScore.remove_player(score_map, "Dave Thomas") +# => %{} +``` + +## 4. Reset a player's score + +To reset a player's score, define `HighScore.reset_score/2`, which takes 2 arguments: + +- The first argument is the map of scores. +- The second argument is the name of the player as a string, whose score you wish to reset. + +The function should also work if the player doesn't have a score. + +```elixir +score_map = HighScore.new() +# => %{} +score_map = HighScore.add_player(score_map, "José Valim", 486_373) +# => %{"José Valim"=> 486_373} +score_map = HighScore.reset_score(score_map, "José Valim") +# => %{"José Valim"=> 0} +``` + +## 5. Update a player's score + +To update a player's score by adding to the previous score, define `HighScore.update_score/3`, which takes 3 arguments: + +- The first argument is the map of scores. +- The second argument is the name of the player as a string, whose score you wish to update. +- The third argument is the score that you wish to **add** to the stored high score. + +The function should also work if the player doesn't have a previous score - assume the previous score is 0. + +```elixir +score_map = HighScore.new() +# => %{} +score_map = HighScore.add_player(score_map, "José Valim", 486_373) +# => %{"José Valim"=> 486_373} +score_map = HighScore.update_score(score_map, "José Valim", 5) +# => %{"José Valim"=> 486_378} +``` + +## 6. Get a list of players + +To get a list of players, define `HighScore.get_players/1`, which takes 1 argument: + +- The first argument is the map of scores. + +```elixir +score_map = HighScore.new() +# => %{} +score_map = HighScore.add_player(score_map, "Dave Thomas", 2_374) +# => %{"Dave Thomas" => 2_374} +score_map = HighScore.add_player(score_map, "José Valim", 486_373) +# => %{"Dave Thomas" => 2_374, "José Valim"=> 486_373} +HighScore.get_players(score_map) +# => ["Dave Thomas", "José Valim"] +``` + +## Source + +### Created by + +- @neenjaw \ No newline at end of file diff --git a/elixir/high-score/lib/high_score.ex b/elixir/high-score/lib/high_score.ex new file mode 100644 index 0000000..baebb28 --- /dev/null +++ b/elixir/high-score/lib/high_score.ex @@ -0,0 +1,21 @@ +defmodule HighScore do + @default_score 0 + + def new(), + do: %{} + + def add_player(scores, name, score \\ @default_score), + do: Map.put(scores, name, score) + + def remove_player(scores, name), + do: Map.delete(scores, name) + + def reset_score(scores, name), + do: Map.put(scores, name, @default_score) + + def update_score(scores, name, score), + do: Map.update(scores, name, score, &(&1 + score)) + + def get_players(scores), + do: Map.keys(scores) +end diff --git a/elixir/high-score/mix.exs b/elixir/high-score/mix.exs new file mode 100644 index 0000000..896cd64 --- /dev/null +++ b/elixir/high-score/mix.exs @@ -0,0 +1,28 @@ +defmodule HighScore.MixProject do + use Mix.Project + + def project do + [ + app: :high_score, + 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/high-score/test/high_score_test.exs b/elixir/high-score/test/high_score_test.exs new file mode 100644 index 0000000..ff60b16 --- /dev/null +++ b/elixir/high-score/test/high_score_test.exs @@ -0,0 +1,186 @@ +defmodule HighScoreTest do + use ExUnit.Case + + # Trivia: Scores used in this test suite are based on lines of code + # added to the elixir-lang/elixir github repository as of Apr 27, 2020. + + @tag task_id: 1 + test "new/1 result in empty score map" do + assert HighScore.new() == %{} + end + + describe "add_player/2" do + @tag task_id: 2 + test "add player without score to empty score map" do + scores = HighScore.new() + + assert HighScore.add_player(scores, "José Valim") == %{"José Valim" => 0} + end + + @tag task_id: 2 + test "add two players without score to empty map" do + scores = + HighScore.new() + |> HighScore.add_player("José Valim") + |> HighScore.add_player("Chris McCord") + + assert scores == %{"Chris McCord" => 0, "José Valim" => 0} + end + + @tag task_id: 2 + test "add player with score to empty score map" do + scores = + HighScore.new() + |> HighScore.add_player("José Valim", 486_373) + + assert scores == %{"José Valim" => 486_373} + end + + @tag task_id: 2 + test "add players with scores to empty score map" do + scores = + HighScore.new() + |> HighScore.add_player("José Valim", 486_373) + |> HighScore.add_player("Dave Thomas", 2_374) + + assert scores == %{"José Valim" => 486_373, "Dave Thomas" => 2_374} + end + end + + describe "remove_player/2" do + @tag task_id: 3 + test "remove from empty score map results in empty score map" do + scores = + HighScore.new() + |> HighScore.remove_player("José Valim") + + assert scores == %{} + end + + @tag task_id: 3 + test "remove player after adding results in empty score map" do + map = + HighScore.new() + |> HighScore.add_player("José Valim") + |> HighScore.remove_player("José Valim") + + assert map == %{} + end + + @tag task_id: 3 + test "remove first player after adding two results in map with remaining player" do + scores = + HighScore.new() + |> HighScore.add_player("José Valim") + |> HighScore.add_player("Chris McCord") + |> HighScore.remove_player("José Valim") + + assert scores == %{"Chris McCord" => 0} + end + + @tag task_id: 3 + test "remove second player after adding two results in map with remaining player" do + scores = + HighScore.new() + |> HighScore.add_player("José Valim") + |> HighScore.add_player("Chris McCord") + |> HighScore.remove_player("Chris McCord") + + assert scores == %{"José Valim" => 0} + end + end + + describe "reset_score/2" do + @tag task_id: 4 + test "resetting score for non-existent player sets player score to 0" do + scores = + HighScore.new() + |> HighScore.reset_score("José Valim") + + assert scores == %{"José Valim" => 0} + end + + @tag task_id: 4 + test "resetting score for existing player sets previous player score to 0" do + scores = + HighScore.new() + |> HighScore.add_player("José Valim", 486_373) + |> HighScore.reset_score("José Valim") + + assert scores == %{"José Valim" => 0} + end + end + + describe "update_score/3" do + @tag task_id: 5 + test "update score for non existent player initializes value" do + scores = + HighScore.new() + |> HighScore.update_score("José Valim", 486_373) + + assert scores == %{"José Valim" => 486_373} + end + + @tag task_id: 5 + test "update score for existing player adds score to previous" do + scores = + HighScore.new() + |> HighScore.add_player("José Valim") + |> HighScore.update_score("José Valim", 486_373) + + assert scores == %{"José Valim" => 486_373} + end + + @tag task_id: 5 + test "update score for existing player with non-zero score adds score to previous" do + scores = + HighScore.new() + |> HighScore.add_player("José Valim") + |> HighScore.update_score("José Valim", 1) + |> HighScore.update_score("José Valim", 486_373) + + assert scores == %{"José Valim" => 486_374} + end + end + + describe "get_players/1" do + @tag task_id: 6 + test "empty score map gives empty list" do + scores_by_player = + HighScore.new() + |> HighScore.get_players() + + assert scores_by_player == [] + end + + @tag task_id: 6 + test "score map with one entry gives one result" do + players = + HighScore.new() + |> HighScore.add_player("José Valim") + |> HighScore.update_score("José Valim", 486_373) + |> HighScore.get_players() + + assert players == ["José Valim"] + end + + @tag task_id: 6 + test "score map with multiple entries gives results in unknown order" do + players = + HighScore.new() + |> HighScore.add_player("José Valim", 486_373) + |> HighScore.add_player("Dave Thomas", 2_374) + |> HighScore.add_player("Chris McCord", 0) + |> HighScore.add_player("Saša Jurić", 762) + |> HighScore.get_players() + |> Enum.sort() + + assert players == [ + "Chris McCord", + "Dave Thomas", + "José Valim", + "Saša Jurić" + ] + end + end +end diff --git a/elixir/high-score/test/test_helper.exs b/elixir/high-score/test/test_helper.exs new file mode 100644 index 0000000..e8677a3 --- /dev/null +++ b/elixir/high-score/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true, seed: 0)