Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Leap 48in24 approaches #1401

Merged
merged 33 commits into from
Jan 13, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c4108ab
setting up the structure for leap approaches
michalporeba Jan 6, 2024
630a266
first draft of the introduction
michalporeba Jan 6, 2024
d4084e0
adding myself to the exercise contributors
michalporeba Jan 6, 2024
2b8ec47
fixing formatting
michalporeba Jan 6, 2024
616f6ec
snippets and headers as placeholders
michalporeba Jan 6, 2024
0a4fddc
improving the code with divides?
michalporeba Jan 6, 2024
5038fcb
more explicit parameters in divides?
michalporeba Jan 7, 2024
0e16a73
Include approach snippets in formatting
angelikatyborska Jan 8, 2024
0d19aa2
Update exercises/practice/leap/.approaches/introduction.md
michalporeba Jan 10, 2024
54a79e1
Update exercises/practice/leap/.approaches/introduction.md
michalporeba Jan 10, 2024
3c81c2c
Update exercises/practice/leap/.approaches/introduction.md
michalporeba Jan 10, 2024
09d55ca
Update exercises/practice/leap/.approaches/introduction.md
michalporeba Jan 10, 2024
085dfa3
Update exercises/practice/leap/.approaches/introduction.md
michalporeba Jan 10, 2024
6e85e4f
Update exercises/practice/leap/.approaches/introduction.md
michalporeba Jan 10, 2024
5eb7841
reformatting
michalporeba Jan 8, 2024
510c33c
boolean operators approach
michalporeba Jan 11, 2024
fc4a44c
rem by default, divides? as an option
michalporeba Jan 11, 2024
f98d44f
Update exercises/practice/leap/.approaches/introduction.md
michalporeba Jan 11, 2024
b9bd559
functions approach
michalporeba Jan 12, 2024
431da1d
control flow approaches
michalporeba Jan 13, 2024
deb494f
Update exercises/practice/leap/.approaches/introduction.md
michalporeba Jan 13, 2024
ff5bc09
Update exercises/practice/leap/.approaches/flow/content.md
michalporeba Jan 13, 2024
8db3e1e
Update exercises/practice/leap/.approaches/flow/content.md
michalporeba Jan 13, 2024
0754b52
Update exercises/practice/leap/.approaches/functions/content.md
michalporeba Jan 13, 2024
33e7fc6
Update exercises/practice/leap/.approaches/introduction.md
michalporeba Jan 13, 2024
a930e7f
Update exercises/practice/leap/.approaches/introduction.md
michalporeba Jan 13, 2024
06d05d2
Case on the tuple is the most idiomatic
michalporeba Jan 13, 2024
19a3bb6
typo
michalporeba Jan 13, 2024
6e527c7
clauses not functions
michalporeba Jan 13, 2024
16f7034
the number is a divisor
michalporeba Jan 13, 2024
5c8503c
Reformat code in code blocks
angelikatyborska Jan 13, 2024
a2172ae
Run through a grammar checker
angelikatyborska Jan 13, 2024
5a8d670
Format approach snippets
angelikatyborska Jan 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one broken link snuck in to main. Could you open a PR with a fix? If you do it, I can just merge, but if I do it, I will need to wait for another person with write rights to review because I can't approve my own PRs 😇



Loading
Loading