-
-
Notifications
You must be signed in to change notification settings - Fork 399
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
angelikatyborska
merged 33 commits into
exercism:main
from
michalporeba:leap-48in24-approaches
Jan 13, 2024
Merged
Leap 48in24 approaches #1401
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 630a266
first draft of the introduction
michalporeba d4084e0
adding myself to the exercise contributors
michalporeba 2b8ec47
fixing formatting
michalporeba 616f6ec
snippets and headers as placeholders
michalporeba 0a4fddc
improving the code with divides?
michalporeba 5038fcb
more explicit parameters in divides?
michalporeba 0e16a73
Include approach snippets in formatting
angelikatyborska 0d19aa2
Update exercises/practice/leap/.approaches/introduction.md
michalporeba 54a79e1
Update exercises/practice/leap/.approaches/introduction.md
michalporeba 3c81c2c
Update exercises/practice/leap/.approaches/introduction.md
michalporeba 09d55ca
Update exercises/practice/leap/.approaches/introduction.md
michalporeba 085dfa3
Update exercises/practice/leap/.approaches/introduction.md
michalporeba 6e85e4f
Update exercises/practice/leap/.approaches/introduction.md
michalporeba 5eb7841
reformatting
michalporeba 510c33c
boolean operators approach
michalporeba fc4a44c
rem by default, divides? as an option
michalporeba f98d44f
Update exercises/practice/leap/.approaches/introduction.md
michalporeba b9bd559
functions approach
michalporeba 431da1d
control flow approaches
michalporeba deb494f
Update exercises/practice/leap/.approaches/introduction.md
michalporeba ff5bc09
Update exercises/practice/leap/.approaches/flow/content.md
michalporeba 8db3e1e
Update exercises/practice/leap/.approaches/flow/content.md
michalporeba 0754b52
Update exercises/practice/leap/.approaches/functions/content.md
michalporeba 33e7fc6
Update exercises/practice/leap/.approaches/introduction.md
michalporeba a930e7f
Update exercises/practice/leap/.approaches/introduction.md
michalporeba 06d05d2
Case on the tuple is the most idiomatic
michalporeba 19a3bb6
typo
michalporeba 6e527c7
clauses not functions
michalporeba 16f7034
the number is a divisor
michalporeba 5c8503c
Reformat code in code blocks
angelikatyborska a2172ae
Run through a grammar checker
angelikatyborska 5a8d670
Format approach snippets
angelikatyborska File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 😇