Skip to content

Commit

Permalink
Add approaches for Leap (#643)
Browse files Browse the repository at this point in the history
* Add approaches for Leap

* Update config.json for leap approaches

* Add links and backticks

* Format and update uuids

* Reduce snippet lengths as per linting rules

* Fix links and copy

* Use "expression" instead of "statement"

* Update exercises/practice/leap/.approaches/case-expression/content.md

Co-authored-by: Jie <jie.gillet@gmail.com>

* Update exercises/practice/leap/.approaches/if-expression/content.md

Co-authored-by: Jie <jie.gillet@gmail.com>

* Update exercises/practice/leap/.approaches/if-expression/content.md

Co-authored-by: Jie <jie.gillet@gmail.com>

* Address code review comments

* Address code review comments

* Update exercises/practice/leap/.approaches/case-expression/content.md

Co-authored-by: Jie <jie.gillet@gmail.com>

* Update code for if-expression approach

---------

Co-authored-by: Jie <jie.gillet@gmail.com>
  • Loading branch information
ceddlyburge and jiegillet authored Jan 14, 2024
1 parent 5949b5e commit 639e59a
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 0 deletions.
38 changes: 38 additions & 0 deletions exercises/practice/leap/.approaches/case-expression/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Case expression

```elm
isLeapYear : Int -> Bool
isLeapYear year =
let
isDivisibleByFour = (modBy 4 year) == 0
isDivisibleBy100 = (modBy 100 year) == 0
isDivisibleBy400 = (modBy 400 year) == 0
in
case (isDivisibleByFour, isDivisibleBy100, isDivisibleBy400) of
(True, _, True) -> True
(True, False, _) -> True
_ -> False
```

## In this approach

In this approach we use a [`case` expression][case-expression], and match on a [`Tuple`][tuple].
It turns out that, if we are careful to ask questions in the right order, we can always potentially attain certainty about the answer by asking one more question.
This is a [truth-table][truth-table] like approach, which can be easier to read for complicated logic.

## When to use a case expression?

Using a case expression with a `Tuple` is idiomatic in Elm, but tuples have a maximum of 3 values, so can't be used for anything larger than this.

Strings and lists can hold more values and can also be used with case expressions, which are useful in many circumstances.
Using a list with a case expression and recursion is especially common.

[case-expression]:
https://elmprogramming.com/case-expression.html
"case expressions in Elm"
[tuple]
https://elmprogramming.com/tuple.html
"Tuples in Elm"
[truth-table]:
https://brilliant.org/wiki/truth-tables/
"Truth tables"
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
case (isDivisibleByFour, isDivisibleBy100, isDivisibleBy400) of
(True, _, True) -> True
(True, False, _) -> True
_ -> 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": [
"ceddlyburge"
]
},
"approaches": [
{
"uuid": "243cf336-7c0b-430b-a064-c976adea8d70",
"slug": "logical-expression",
"title": "Logical expression",
"blurb": "Use logical operators to combine several tests into one.",
"authors": [
"ceddlyburge"
]
},
{
"uuid": "31ad676e-299b-4b7a-912c-3e4f68b5d873",
"slug": "case-expression",
"title": "Case expression",
"blurb": "Use a case expression.",
"authors": [
"ceddlyburge"
]
},
{
"uuid": "9546e09d-9cde-4da1-b78a-0ef33d7d6db3",
"slug": "if-expression",
"title": "If expression",
"blurb": "Use an if expression.",
"authors": [
"ceddlyburge"
]
}
]
}
50 changes: 50 additions & 0 deletions exercises/practice/leap/.approaches/if-expression/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# If expression

```elm
isLeapYear : Int -> Bool
isLeapYear year =
let
divisibleBy number =
modBy number year == 0
in
if divisibleBy 100 then
divisibleBy 400

else
divisibleBy 4
```

An [`if` expression][if-expression] (`if … then … else …`) is a compound expression that uses a test to determine which of two alternatives to evaluate to.
Many other languages feature a similar construct, often termed 'ternary operator'.

## In this approach

This approach uses exactly two tests to determine whether a year is a leap year.

The first test is for divisibility by 100.
Once we know if the year is a multiple of 100, we know which further test to perform.

- If the year is evenly divisible by 100, then `divisibleBy 100` will evaluate to `True` and the entire `if` expression will evaluate to whatever `divisibleBy 400` evaluates to.
- If the year is _not_ evenly divisible by 100, then `divisibleBy 100` is `False` and so the `if` expression evaluates to `divisibleBy 4`.

This approach is not as concise as the [logical-expression][logical-expression], but it is [easier to describe][describable-code], which makes it easier to comunicate the problem domain.

## When to use `if`?

[`if` expressions][if-expression] might be a good fit when you

- need an expression that
- chooses between exactly two options
- depending on a single `Bool`.

When you have something other than a `Bool`, use `case` instead.

[if-expression]:
https://elm-lang.org/docs/syntax#conditionals
"if expressions in Elm"
[logical-expression]:
https://exercism.org/tracks/elm/exercises/leap/approaches/conditional-expression
"Approach: a conditional expression"
[describable-code]:
https://www.freecodecamp.org/news/writing-describable-code/
"Writing easily describable code"
5 changes: 5 additions & 0 deletions exercises/practice/leap/.approaches/if-expression/snippet.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if divisibleBy 100 then
divisibleBy 400

else
divisibleBy 4
101 changes: 101 additions & 0 deletions exercises/practice/leap/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Introduction

There are various idiomatic approaches to solve Leap.
All approaches listed below check for divisibility by 4, 100, and 400.
However, they differ in the ways in which they combine these checks.

## Approach: a logical expression

```elm
isLeapYear : Int -> Bool
isLeapYear year =
let
divisibleBy number =
modBy number year == 0
in
divisibleBy 4 && (not (divisibleBy 100) || divisibleBy 400)
```

A logicial expression is the most concise approach.
[Read more about this approach][logical-expression].

## Approach: an if expression

```elm
isLeapYear : Int -> Bool
isLeapYear year =
let
divisibleBy number =
modBy number year == 0
in
if divisibleBy 100 then
divisibleBy 400

else
divisibleBy 4
```

An if expression is not as concise, but is [easier to describe][describable-code].
[Read more about this approach][if-expression].

## Approach: a case expression

```elm
isLeapYear : Int -> Bool
isLeapYear year =
let
isDivisibleByFour = (modBy 4 year) == 0
isDivisibleBy100 = (modBy 100 year) == 0
isDivisibleBy400 = (modBy 400 year) == 0
in
case (isDivisibleByFour, isDivisibleBy100, isDivisibleBy400) of
(True, _, True) -> True
(True, False, _) -> True
_ -> False
```

This takes a [truth-table][truth-table] like approach, which can be easier to read for complicated logic.
[Read more about this approach][case-expression].

## Other approaches

There are also more esoteric approaches, [such as this one using recursion, a list and a case expression][esoteric-approach], that are not idiomatic, but that can be interesting to look at, and probably come about by trying to solve the problem using some artificial constraints, as you would often do in a code kata.

## General guidance

The key to determining whether a given year is a leap year is to know whether the year is evenly divisible by `4`, `100`, and `400`.
For determining that, you can use the [`modBy` function][modby-function], which yields the remainder after division.

You don't need to use a ['let' expression][let-expression] to define an `isDivisibleBy` function, although all the examples here do to aid readability.

## Which approach to use?

Code exists primarily for humans to read and reason about.
Therefore, in general, go with the approach that _makes the most sense_.

All approaches listed here are valid choices unless marked otherwise.

[logical-expression]:
https://exercism.org/tracks/elm/exercises/leap/approaches/logical-expression
"Approach: a conditional expression"
[if-expression]:
https://exercism.org/tracks/elm/exercises/leap/approaches/if-expression
"Approach: an if expression"
[case-expression]:
https://exercism.org/tracks/elm/exercises/leap/approaches/case-expression
"Approach: a case expression"
[describable-code]:
https://www.freecodecamp.org/news/writing-describable-code/
"Writing easily describable code"
[truth-table]:
https://brilliant.org/wiki/truth-tables/
"Truth tables"
[esoteric-approach]
https://exercism.org/tracks/elm/exercises/leap/solutions/edgerunner
"An esoteric solution to leap, using recursion, a list and a case expression"
[modby-function]
https://package.elm-lang.org/packages/elm/core/latest/Basics#modBy
"modBy function in Elm"
[let-expression]
https://elm-lang.org/docs/syntax#let-expressions
"let expressions in Elm"
34 changes: 34 additions & 0 deletions exercises/practice/leap/.approaches/logical-expression/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Logical expression

```elm
isLeapYear : Int -> Bool
isLeapYear year =
let
divisibleBy number =
modBy number year == 0
in
divisibleBy 4 && (not (divisibleBy 100) || divisibleBy 400)
```

We can combine smaller logical expressions into larger ones using the logical operators `&&` (and), `||` (or), and `not` (negation).

A logicial expression is the most concise approach.

## Precedence

In school they teach you that `2 + 3 * 4` is to be read as meaning `2 + (3 * 4)`.
This is a convention, chosen for its convenience.
We say that the `*` operator has _higher [precedence][precedence]_ than `+`.

In logic similar ambiguities exist, and these are similarly resolved.

- _and_ has higher precedence than _or_, and
- _not_ has higher precedence than both _and_ and _or_.

For example, `p || q && r` means the same as `p || (q && r)`.

If you don't wish to remember the precendence of all the things, or force readers of your code to do so, you can use parentheses to make it explicit.

[precedence]:
https://en.wikipedia.org/wiki/Order_of_operations
"Wikipedia: Order of operations"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
isLeapYear : Int -> Bool
isLeapYear year =
let
divisibleBy number =
modBy number year == 0
in
divisibleBy 4 && (not (divisibleBy 100) || divisibleBy 400)

0 comments on commit 639e59a

Please sign in to comment.