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

Add approaches for Leap #643

Merged
merged 14 commits into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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.
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't match the actual code, since we start by checking 400.
From the description, the code would probably be (untested)

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

    else
      divisibleBy 4

which only has one if statement. If you don't want to change the description, I would consider changing to this code (after double checking that it passes the tests).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It does pass the tests, I'll use it ...

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"
Copy link
Contributor

Choose a reason for hiding this comment

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

That really is esoteric 😆

[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)