Visit the leap exercise on Exercism to read the full instructions and download the exercise files.
Dig Deeper
Boolean operations
Boolean operations
(defn- divides? [number divisor]
(zero? (mod number divisor)))
(defn leap-year? [year]
(and (divides? year 4)
(or (not (divides? year 100))
(divides? year 400))))
At the core of this approach, three checks are returning three boolean values.
We can use Boolean logic to combine the results. Multiple variations are possible. One is shown above, but the one below is also possible.
(defn leap-year? [year]
(or (divides? year 400)
(and (not (divides? year 100))
(divides? year 4))))
Being explicit
The above examples use a private function divides? to be more explicit, showing what logical check is done rather than what operation is performed.
This is common in Clojure code, but it is also possible to do the checks directly.
(defn leap-year? [year]
(and (zero? year 4)
(or (pos? year 100)
(zero? year 400))))
In this example, in addition, instead of negating the second check, we check if the reminder from the division is greater than zero.
If the concern is that defining divides? adds confusion because it is not known in the only place where it is used, letfn can be used instead. Here is another example.
(defn leap-year? [year]
(letfn [(is-multiple-of? [n] (zero? (mod year n)))]
(and (is-multiple-of? 4)
(or (not (is-multiple-of? 100))
(is-multiple-of? 400)))))
Flow control expressions
Flow control expressions
(defn- divides? [number divisor]
(zero? (mod number divisor)))
(defn leap-year? [year]
(if (divides? year 100)
(divides? year 400)
(divides? year 4)))
If
Clojure provides if, cond, condp and case expressions to control the flow of the program.
The if doesn’t have an else if option, but it is not necessary here.
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 if it is divisible by 400.
If it is not, then whether it is a leap year or not depends if it is divisible by 4.
(defn leap-year? [year]
(if (divides? year 100)
(divides? year 400)
(divides? year 4)))
Cond
Another option is cond which allows for evaluating multiple conditions, similar to else if in other languages.
(defn leap-year? [year]
(cond
(divides? year 400) true
(divides? year 100) false
(divides? year 4) true
:else false))
A very similar alternative is to use the condp macro, which takes a predicate and applies it to a series of test values and expected result pairs.
(defn leap-year? [year]
(condp #(zero? (mod %2 %1)) year
400 true
100 false
4 true
false))
When using both cond and condp, the order matters, as the first true condition stops evaluating the list and determines the result.
Case
Finally, it is also possible to use case, but this solution is not popular.
(defn leap-year? [year]
(case [(divides? year 400)
(divides? year 100)
(divides? year 4)
]
[true true true] true
[false true true] false
[false false true] true
false))
Data shapes
Data shapes
(def leap-shapes
#{{:by-4 true :by-100 true :by-400 true}
{:by-4 true :by-100 false :by-400 false}})
(defn- to-shape [year]
{:by-4 (zero? (mod year 4))
:by-100 (zero? (mod year 100))
:by-400 (zero? (mod year 400))})
(defn leap-year? [year]
(->> year
to-shape
leap-shapes
some?))
We can define a data structure that describes the properties of a number.
In this case, the number is a year, and the properties are whether it is divisible by 4, 100 and 400. Let’s call any instance of such data structure the shape of a number.
While there are many numbers, there are fewer shapes, so we can define all shapes corresponding to all leap years. There are only two.
(def leap-shapes
#{{:by-4 true :by-100 true :by-400 true}
{:by-4 true :by-100 false :by-400 false}})
We now need a function to convert any number to its shape.
(defn- to-shape [year]
{:by-4 (zero? (mod year 4))
:by-100 (zero? (mod year 100))
:by-400 (zero? (mod year 400))})
With the above, we can now take a year number and check if its shape is one of the leap shapes.
(defn leap-year? [year]
(->> year
to-shape
leap-shapes
some?))
In Clojure, if a value is passed to a set, the set acts like a function checking if the value exists in that set. This is how leap-shapes acts as a predicate when combined with some?.
Source: Exercism clojure/leap