Skip to content

Commit

Permalink
Leap 48in24 approaches (#1401)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Update exercises/practice/leap/.approaches/introduction.md

Co-authored-by: Angelika Tyborska <[email protected]>

* Update exercises/practice/leap/.approaches/introduction.md

Co-authored-by: Angelika Tyborska <[email protected]>

* Update exercises/practice/leap/.approaches/introduction.md

Co-authored-by: Angelika Tyborska <[email protected]>

* Update exercises/practice/leap/.approaches/introduction.md

Co-authored-by: Angelika Tyborska <[email protected]>

* Update exercises/practice/leap/.approaches/introduction.md

Co-authored-by: Angelika Tyborska <[email protected]>

* reformatting

* boolean operators approach

* rem by default, divides? as an option

* Update exercises/practice/leap/.approaches/introduction.md

Co-authored-by: Angelika Tyborska <[email protected]>

* functions approach

* control flow approaches

* Update exercises/practice/leap/.approaches/introduction.md

Co-authored-by: Angelika Tyborska <[email protected]>

* Update exercises/practice/leap/.approaches/flow/content.md

Co-authored-by: Angelika Tyborska <[email protected]>

* Update exercises/practice/leap/.approaches/flow/content.md

Co-authored-by: Angelika Tyborska <[email protected]>

* Update exercises/practice/leap/.approaches/functions/content.md

Co-authored-by: Angelika Tyborska <[email protected]>

* Update exercises/practice/leap/.approaches/introduction.md

Co-authored-by: Angelika Tyborska <[email protected]>

* Update exercises/practice/leap/.approaches/introduction.md

Co-authored-by: Angelika Tyborska <[email protected]>

* 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 <[email protected]>
  • Loading branch information
michalporeba and angelikatyborska authored Jan 13, 2024
1 parent d7e80b3 commit 841d716
Show file tree
Hide file tree
Showing 11 changed files with 414 additions and 0 deletions.
18 changes: 18 additions & 0 deletions bin/check_formatting.sh
Original file line number Diff line number Diff line change
@@ -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
# ###
Expand All @@ -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
Expand Down
14 changes: 14 additions & 0 deletions bin/format_approach_snippets.sh
Original file line number Diff line number Diff line change
@@ -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
50 changes: 50 additions & 0 deletions exercises/practice/leap/.approaches/clauses/content.md
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions exercises/practice/leap/.approaches/clauses/snippet.txt
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions exercises/practice/leap/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
]
}
98 changes: 98 additions & 0 deletions exercises/practice/leap/.approaches/flow/content.md
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions exercises/practice/leap/.approaches/flow/snippet.txt
Original file line number Diff line number Diff line change
@@ -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
83 changes: 83 additions & 0 deletions exercises/practice/leap/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -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


Loading

0 comments on commit 841d716

Please sign in to comment.