Introduction
A leap year (in the Gregorian calendar) occurs:
- In every year that is evenly divisible by 4.
- Unless the year is evenly divisible by 100, in which case it’s only a leap year if the year is also evenly divisible by 400.
Some examples:
- 1997 was not a leap year as it’s not divisible by 4.
- 1900 was not a leap year as it’s not divisible by 400.
- 2000 was a leap year!
For a delightful, four-minute explanation of the whole phenomenon of leap years, check out [this YouTube video](https://www.youtube.com/watch?v=xX96xng7sAE).
Instructions
Instructions
Your task is to determine whether a given year is a leap year.
Dig Deeper
Boolean operators
Boolean operators
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 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.
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:
true or (false and false)
If or should be evaluated first, we must use parenthesis.
(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.
The relaxed versions !, &&, || require the first argument to be only truthy or falsy.
In the case of this exercise, both types will work equally well, so the solution could be:
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:
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.
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
Others 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 provide the extra meaning.
This approach also shortens the check so the Boolean operators and relationships between them are more prominent.
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?
Multiple clause functions
Multiple clause function
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, 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:
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:
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 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.
Control flow structures
Control flow structures
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 three control flow structures: case, cond, if.
The if allows 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.
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.
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, 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.
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, offering another way to solve the problem.
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.
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
Source: Exercism elixir/leap