From 841d716586ece3d26ad6ad93396f505d85671277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Por=C4=99ba?= Date: Sat, 13 Jan 2024 15:59:44 +0000 Subject: [PATCH] Leap 48in24 approaches (#1401) * setting up the structure for leap approaches * first draft of the introduction * adding myself to the exercise contributors * fixing formatting * snippets and headers as placeholders * improving the code with divides? * more explicit parameters in divides? * Include approach snippets in formatting * Update exercises/practice/leap/.approaches/introduction.md Co-authored-by: Angelika Tyborska * Update exercises/practice/leap/.approaches/introduction.md Co-authored-by: Angelika Tyborska * Update exercises/practice/leap/.approaches/introduction.md Co-authored-by: Angelika Tyborska * Update exercises/practice/leap/.approaches/introduction.md Co-authored-by: Angelika Tyborska * Update exercises/practice/leap/.approaches/introduction.md Co-authored-by: Angelika Tyborska * Update exercises/practice/leap/.approaches/introduction.md Co-authored-by: Angelika Tyborska * reformatting * boolean operators approach * rem by default, divides? as an option * Update exercises/practice/leap/.approaches/introduction.md Co-authored-by: Angelika Tyborska * functions approach * control flow approaches * Update exercises/practice/leap/.approaches/introduction.md Co-authored-by: Angelika Tyborska * Update exercises/practice/leap/.approaches/flow/content.md Co-authored-by: Angelika Tyborska * Update exercises/practice/leap/.approaches/flow/content.md Co-authored-by: Angelika Tyborska * Update exercises/practice/leap/.approaches/functions/content.md Co-authored-by: Angelika Tyborska * Update exercises/practice/leap/.approaches/introduction.md Co-authored-by: Angelika Tyborska * Update exercises/practice/leap/.approaches/introduction.md Co-authored-by: Angelika Tyborska * Case on the tuple is the most idiomatic I have adjusted the text to make it clear that while there are many ways to use a case statement, the case on the tuple is the most idiomatic one. * typo * clauses not functions * the number is a divisor * Reformat code in code blocks * Run through a grammar checker * Format approach snippets --------- Co-authored-by: Angelika Tyborska --- bin/check_formatting.sh | 18 ++++ bin/format_approach_snippets.sh | 14 +++ .../leap/.approaches/clauses/content.md | 50 ++++++++++ .../leap/.approaches/clauses/snippet.txt | 4 + .../practice/leap/.approaches/config.json | 36 +++++++ .../practice/leap/.approaches/flow/content.md | 98 +++++++++++++++++++ .../leap/.approaches/flow/snippet.txt | 7 ++ .../practice/leap/.approaches/introduction.md | 83 ++++++++++++++++ .../leap/.approaches/operators/content.md | 98 +++++++++++++++++++ .../leap/.approaches/operators/snippet.txt | 5 + exercises/practice/leap/.meta/config.json | 1 + 11 files changed, 414 insertions(+) create mode 100755 bin/format_approach_snippets.sh create mode 100644 exercises/practice/leap/.approaches/clauses/content.md create mode 100644 exercises/practice/leap/.approaches/clauses/snippet.txt create mode 100644 exercises/practice/leap/.approaches/config.json create mode 100644 exercises/practice/leap/.approaches/flow/content.md create mode 100644 exercises/practice/leap/.approaches/flow/snippet.txt create mode 100644 exercises/practice/leap/.approaches/introduction.md create mode 100644 exercises/practice/leap/.approaches/operators/content.md create mode 100644 exercises/practice/leap/.approaches/operators/snippet.txt diff --git a/bin/check_formatting.sh b/bin/check_formatting.sh index 3ab24923e3..c40a830265 100755 --- a/bin/check_formatting.sh +++ b/bin/check_formatting.sh @@ -1,5 +1,14 @@ #!/bin/bash +echo 'Temporarily transforming .txt snippets into .ex snippets' +FILES="exercises/**/**/.approaches/**/snippet.txt" +for file in $FILES +do + txt_file_path=$file + ex_file_path="${file//\.txt/.ex}" + mv $txt_file_path $ex_file_path +done + # ### # check_formatting.sh # ### @@ -10,6 +19,15 @@ echo "Running 'mix format'" mix format --check-formatted FORMAT_EXIT_CODE="$?" +echo 'Transforming snippets back to .txt' +FILES="exercises/**/**/.approaches/**/snippet.ex" +for file in $FILES +do + ex_file_path=$file + txt_file_path="${file//\.ex/.txt}" + mv $ex_file_path $txt_file_path +done + echo "Checking for trailing whitespace" # git grep returns a 0 status if there is a match # so we negate the result for consistency diff --git a/bin/format_approach_snippets.sh b/bin/format_approach_snippets.sh new file mode 100755 index 0000000000..2940bff3ff --- /dev/null +++ b/bin/format_approach_snippets.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# this script is necessary as long as +# the config option approaches.snippet_extension is not supported +# and we're forced to keep snippets in txt files +FILES="exercises/**/**/.approaches/**/snippet.txt" +for file in $FILES +do + txt_file_path=$file + ex_file_path="${file//\.txt/.ex}" + mv $txt_file_path $ex_file_path + mix format $ex_file_path + mv $ex_file_path $txt_file_path +done diff --git a/exercises/practice/leap/.approaches/clauses/content.md b/exercises/practice/leap/.approaches/clauses/content.md new file mode 100644 index 0000000000..f00ff3261d --- /dev/null +++ b/exercises/practice/leap/.approaches/clauses/content.md @@ -0,0 +1,50 @@ +# Multiple clause function + +```elixir +defmodule Year do + @spec leap_year?(non_neg_integer) :: boolean + def leap_year?(year) when rem(year, 400) == 0, do: true + def leap_year?(year) when rem(year, 100) == 0, do: false + def leap_year?(year) when rem(year, 4) == 0, do: true + def leap_year?(_), do: false +end +``` + +In Elixir, functions can have multiple clauses. +Which one will be executed depends on parameter matching and guards. +When a function with multiple clauses is invoked, the parameters are compared to the definitions in the order in which they were defined, and only the first one matching will be invoked. + +While in the [operators approach][operators-approach], it was possible to reorder expressions as long as the suitable boolean operators were used, in this approach, there is only one correct order of definitions. + +In our case, the three guards in the function clauses are as follows: + +```elixir +when rem(year, 400) == 0 +when rem(year, 100) == 0 +when rem(year, 4) == 0 +``` + +But because of the order they are evaluated in, they are equivalent to: + +```elixir +when rem(year, 400) == 0 +when rem(year, 100) == 0 and not rem(year, 400) == 0 +when rem(year, 4) == 0 and not rem(year, 100) == 0 and not rem(year, 400) == 0 +``` + +The final clause, `def leap_year?(_), do: false`, returns false if previous clauses are not a match. + +## Guards + +The [guards][hexdocs-guards] are part of the pattern-matching mechanism. +They allow for more complex checks of values. +However, because of when they are executed to allow the compiler to perform necessary optimization, +only a minimal subset of operations is permitted. +`Kernel.rem/2` is on this limited list, and `Integer.mod/2` is not. +This is why, in this approach, only the first one will work, and the latter will not. + +In this approach, the boolean operators matter too. Only the strict ones, `not`, `and`, `or` are allowed. +The relaxed `!`, `&&`, `||` will fail to compile. + +[operators-approach]: https://exercism.org/tracks/elixir/exercises/leap/approaches/operators +[hexdocs-guards]: https://hexdocs.pm/elixir/main/patterns-and-guards.html#guards diff --git a/exercises/practice/leap/.approaches/clauses/snippet.txt b/exercises/practice/leap/.approaches/clauses/snippet.txt new file mode 100644 index 0000000000..c0d338e3c3 --- /dev/null +++ b/exercises/practice/leap/.approaches/clauses/snippet.txt @@ -0,0 +1,4 @@ +def leap_year?(year) when rem(year, 400) == 0, do: true +def leap_year?(year) when rem(year, 100) == 0, do: false +def leap_year?(year) when rem(year, 4) == 0, do: true +def leap_year?(_), do: false diff --git a/exercises/practice/leap/.approaches/config.json b/exercises/practice/leap/.approaches/config.json new file mode 100644 index 0000000000..07571637a2 --- /dev/null +++ b/exercises/practice/leap/.approaches/config.json @@ -0,0 +1,36 @@ +{ + "introduction": { + "authors": [ + "michalporeba" + ] + }, + "approaches": [ + { + "uuid": "be6d6c6e-8e19-4657-aad5-3382e7ec01db", + "slug": "operators", + "title": "Boolean operators", + "blurb": "Use boolean operators to combine the checks.", + "authors": [ + "michalporeba" + ] + }, + { + "uuid": "0267853e-9607-4b60-b2f9-e4a34f5316db", + "slug": "clauses", + "title": "Multiple clause functions", + "blurb": "Use a multiple clause function to control the order of checks.", + "authors": [ + "michalporeba" + ] + }, + { + "uuid": "428e3cee-309a-4c45-a6d4-3bff4eb41daa", + "slug": "flow", + "title": "Control flow structures", + "blurb": "Use `if, `case` or `cond`, to control order of checks.", + "authors": [ + "michalporeba" + ] + } + ] +} diff --git a/exercises/practice/leap/.approaches/flow/content.md b/exercises/practice/leap/.approaches/flow/content.md new file mode 100644 index 0000000000..af8c7fca04 --- /dev/null +++ b/exercises/practice/leap/.approaches/flow/content.md @@ -0,0 +1,98 @@ +# Control flow structures + +```elixir +defmodule Year do + @spec leap_year?(non_neg_integer) :: boolean + def leap_year?(year) do + if rem(year, 100) == 0 do + rem(year, 400) == 0 + else + rem(year, 4) == 0 + end + end +end +``` + +## If + +Elixir provides four [control flow structures][hexdocs-structures]: `case`, `cond`, `if`, and `unless`. +The `if` and `unless` allow to evaluate only one condition. +Unlike in many other languages, there is no `else if` option in Elixir. + +However, in this case, it is not necessary. We can use `if` once to check if the year is divisible by 100. +If it is, then whether it is a leap year or not depends on if it is divisible by 400. +If it is not, then whether it is a leap year or not depends on if it is divisible by 4. + +```elixir +def leap_year?(year) do + if rem(year, 100) == 0 do + rem(year, 400) == 0 + else + rem(year, 4) == 0 + end +end +``` + +## Cond + +Another option is `cond` which allows for evaluating multiple conditions, similar to `else if` in other languages. + +```elixir +def leap_year?(year) do + cond do + rem(year, 400) == 0 -> true + rem(year, 100) == 0 -> false + rem(year, 4) == 0 -> true + true -> false + end +end +``` + +Similarly to the [multiple clause function approach][clause-approach], the order here matters. +The conditions are evaluated in order, and the first that is not `nil` or `false` leads to the result. + +## Case + +`case` allows to compare a value to multiple patterns, but can also replicate what `if` offers. + +```elixir +def leap_year?(year) do + case rem(year, 100) do + 0 -> rem(year, 400) == 0 + _ -> rem(year, 4) == 0 + end +end +``` + +It also supports [guards][hexdocs-guards], offering another way to solve the problem. + +```elixir +def leap_year?(year) do + case year do + _ when rem(year, 400) == 0 -> true + _ when rem(year, 100) == 0 -> false + _ when rem(year, 4) == 0 -> true + _ -> false + end +end +``` + +The `case` can be very flexible, so many variations are possible. +Using it with pattern matching on a tuple is considered **the most idiomatic**. +In this case, a tuple is created with all the checks. +Then, pattern matching to tuples is performed. + +```elixir +def leap_year?(year) do + case {rem(year, 400), rem(year, 100), rem(year, 4)} do + {0, _, _} -> true + {_, 0, _} -> false + {_, _, 0} -> true + _ -> false + end +end +``` + +[hexdocs-structures]: https://hexdocs.pm/elixir/case-cond-and-if.html +[hexdocs-guards]: https://hexdocs.pm/elixir/main/patterns-and-guards.html#guards +[clause-approach]: https://exercism.org/tracks/elixir/exercises/leap/approaches/clauses diff --git a/exercises/practice/leap/.approaches/flow/snippet.txt b/exercises/practice/leap/.approaches/flow/snippet.txt new file mode 100644 index 0000000000..6e304c5ffc --- /dev/null +++ b/exercises/practice/leap/.approaches/flow/snippet.txt @@ -0,0 +1,7 @@ +def leap_year?(year) do + if rem(year, 100) == 0 do + rem(year, 400) == 0 + else + rem(year, 4) == 0 + end +end diff --git a/exercises/practice/leap/.approaches/introduction.md b/exercises/practice/leap/.approaches/introduction.md new file mode 100644 index 0000000000..1e56f8b458 --- /dev/null +++ b/exercises/practice/leap/.approaches/introduction.md @@ -0,0 +1,83 @@ +# Introduction + +Every fourth year is a leap year (with some exceptions), but let's consider this one condition first. + +To solve the Leap problem, we must determine if a year is evenly divisible by a number or if a reminder of an integer division is zero. +Such operation in computing is called [modulo][modulo]. + +Unlike many languages, Elixir does not have [operators][operators] for either integer division or modulo. +Instead, it provides the [`Kernel.rem/2`][rem] and the [`Integer.mod/2`][mod] functions. + +The two functions differ in how they work with negative divisors, but since, in this exercise, +all the divisors are non-negative, both could work, depending on the approach you choose. + +## General solution + +To check if a year is divisible by `n`, we can do `rem(year, n) == 0`. + +Any approach to the problem will perform this check three times to see if a year is equally divisible by 4, 100 and 400. +What will differ between approaches is what Elixir features we will use to combine the checks. + +## Approach: Boolean operators + +The full rules are as follows: +A year is a leap year if +* it is divisible by 4 +* but not divisible by 100 +* unless it is divisible by 400 + +We can use [boolean operators][boolean-operators] to combine the checks, for example, like so: + +```elixir +rem(year, 5) == 0 and not rem(year, 100) == 0 or rem(year, 400) == 0 +``` +In the [boolean operators approach][operators-approach] we discuss the details of the solution. +It includes variations of the operators and their precedence. + +## Approach: multiple clause function + +Instead of using boolean operators, we can define multiple `leap_year?/1` function clauses with different guards. + +```elixir +def leap_year?(year) when rem(year, 400) == 0, do: true +def leap_year?(year) when rem(year, 100) == 0, do: false +def leap_year?(year) when rem(year, 4) == 0, do: true +def leap_year?(_), do: false +``` + +In the [multiple clause function approach][clause-approach] we discuss why in this approach the `Integer.mod/2` function will not work. + +## Approach: control flow structures + +In addition to the above two approaches, control flow structures offer a number of solutions. +Here are two examples using `if` and `case`. + +```elixir +if rem(year, 100) == 0 do + rem(year, 400) == 0 +else + rem(year, 4) == 0 +end +``` + +```elixir +case {rem(year, 400), rem(year, 100), rem(year, 4)} do + {0, _, _} -> true + {_, 0, _} -> false + {_, _, 0} -> true + _ -> false +end +``` + +We discuss these and other solutions depending on various control flow structures in the [control flow structures approach][flow-approach]. + +[modulo]: https://en.wikipedia.org/wiki/Modulo +[operators]: https://hexdocs.pm/elixir/operators.html +[rem]: https://hexdocs.pm/elixir/Kernel.html#rem/2 +[mod]: https://hexdocs.pm/elixir/Integer.html#mod/2 +[boolean-operators]: https://hexdocs.pm/elixir/operators.html#general-operators +[operators-approach]: https://exercism.org/tracks/elixir/exercises/leap/approaches/operators +[clause-approach]: https://exercism.org/tracks/elixir/exercises/leap/approaches/clauses +[flow-approach]: https://exercism.org/tracks/elixir/exercises/leap/approaches/cond + + diff --git a/exercises/practice/leap/.approaches/operators/content.md b/exercises/practice/leap/.approaches/operators/content.md new file mode 100644 index 0000000000..cef6495433 --- /dev/null +++ b/exercises/practice/leap/.approaches/operators/content.md @@ -0,0 +1,98 @@ +# Boolean operators + +```elixir +defmodule Year do + @spec leap_year?(non_neg_integer) :: boolean + def leap_year?(year) do + rem(year, 4) == 0 and not rem(year, 100) == 0 or rem(year, 400) == 0 + end +end +``` + +## Short-circuiting + +At the core of this approach, three checks are returning three boolean values. +We can use [Boolean logic](https://en.wikipedia.org/wiki/Boolean_algebra) to combine the results. + +When using this approach, it is essential to consider short-circuiting of boolean operators. +The expression `left and right` can be only true if both `left` and `right` are *true*. +If `left` is *false*, `right` will not be evaluated. The result will be *false*. +However, if `left` is *true*, `right` has to be evaluated to determin the outcome. + +The expression `left or right` can be true if either `left` or `right` is *true*. +If `left` is *true*, `right` will not be evaluated. The result will be *true*. +However, if `left` is *false*, `right` has to be evaluated to determine the outcome. + +## Precedence of operators + +Another thing to consider when using Boolean operators is their precedence. +```elixir +true or false and false +``` +The above evaluates to *true* because in Elixir `and` has higher precedence than `or`. +The above expression is equivalent to: +```elixir +true or (false and false) +``` +If `or` should be evaluated first, we must use parenthesis. +```elixir +(true or false) and false +``` +which equals to *false*. + +The `not` operator is evaluated before `and` and `or`. + +## Strict or relaxed? + +Elixir offers two sets of Boolean operators: strict and relaxed. +The strict versions `not`, `and`, `or` require the first (left) argument to be of [boolean type][hexdocs-booleans]. +The relaxed versions `!`, `&&`, `||` require the first argument to be only [truthy or falsy][hexdocs-truthy]. + +In the case of this exercise, both types will work equally well, so the solution could be: +```elixir +def leap_year?(year) do + rem(year, 4) == 0 && !(rem(year, 100) == 0) || rem(year, 400) == 0 +end +``` + +## Being explicit + +The `leap_year?` function could be written like so: +```elixir +def leap_year?(year) do + rem(year, 4) == 0 and not rem(year, 100) == 0 or rem(year, 400) == 0 +end +``` + +Some prefer this form, as it is very direct. We can see what is happening. +We are explicitly checking the reminder, comparing it to zero. + +```elixir +defp divides?(number, divisor), do: rem(number, divisor) == 0 + +def leap_year?(year) do + divides?(year, 4) and not divides?(year, 100) or divides?(year, 400) +end +``` + +Other might prefer the above form, which requires defining the `devides?` function or something similar. +By doing so, we can be explicit about the *intent*. +We want to check if a year can be equally divided into a number. + +Yet another approach might be to use variables to capture the results of individual checks and provided the extra meaning. +This approach also shortens the check so the Boolean operators and relationships between them are more prominent. + +```elixir +def leap_year?(year) do + by4? = divides?(year, 4) + by100? = divides?(year, 100) + by400? = divides?(year, 400) + by4? and not by100? or by400? +end +``` + +All versions of the code will work. Which one to choose is often a personal or sometimes a team preference. What reads best for you? What will make most sense to you when you look at the code again? + +[hexdocs-booleans]: https://hexdocs.pm/elixir/basic-types.html#booleans-and-nil +[hexdocs-truthy]: https://hexdocs.pm/elixir/Kernel.html#module-truthy-and-falsy-values +[exercism-booleans]: https://exercism.org/tracks/elixir/concepts/booleans \ No newline at end of file diff --git a/exercises/practice/leap/.approaches/operators/snippet.txt b/exercises/practice/leap/.approaches/operators/snippet.txt new file mode 100644 index 0000000000..3014fcd85f --- /dev/null +++ b/exercises/practice/leap/.approaches/operators/snippet.txt @@ -0,0 +1,5 @@ +def leap_year?(year) do + (rem(year, 4) == 0 and + not rem(year, 100) == 0) or + rem(year, 400) == 0 +end diff --git a/exercises/practice/leap/.meta/config.json b/exercises/practice/leap/.meta/config.json index 6fd240246a..a914e88c1c 100644 --- a/exercises/practice/leap/.meta/config.json +++ b/exercises/practice/leap/.meta/config.json @@ -12,6 +12,7 @@ "korbin", "kytrinyx", "lpil", + "michalporeba", "neenjaw", "parkerl", "sotojuan",