-
-
Notifications
You must be signed in to change notification settings - Fork 88
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add approaches for leap Co-authored-by: András B Nagy <[email protected]> Co-authored-by: Isaac Good <[email protected]>
- Loading branch information
1 parent
efa875c
commit 5e65747
Showing
8 changed files
with
295 additions
and
0 deletions.
There are no files selected for viewing
47 changes: 47 additions & 0 deletions
47
exercises/practice/leap/.approaches/boolean-chain/content.md
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,47 @@ | ||
# Chaining Boolean expressions | ||
|
||
```bash | ||
year=$1 | ||
if (( year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) )); then | ||
echo true | ||
else | ||
echo false | ||
fi | ||
``` | ||
|
||
The Boolean expression `year % 4 == 0` checks the remainder from dividing `year` by 4. | ||
If a year is evenly divisible by 4, the remainder will be zero. | ||
All leap years are divisible by 4, and this pattern is then repeated whether a year is not divisible by 100 and whether it's divisible by 400. | ||
|
||
Parentheses are used to control the [order of precedence][order-of-precedence]: | ||
logical AND `&&` has a higher precedence than logical OR `||`. | ||
|
||
| year | divisible by 4 | not divisible by 100 | divisible by 400 | result | | ||
| ---- | -------------- | ------------------- | ---------------- | ------------ | | ||
| 2020 | true | true | not evaluated | true | | ||
| 2019 | false | not evaluated | not evaluated | false | | ||
| 2000 | true | false | true | true | | ||
| 1900 | true | false | false | false | | ||
|
||
By situationally skipping some of the tests, we can efficiently calculate the result with fewer operations. | ||
Although in an interpreted language like Bash, that is less crucial than it might be in another language. | ||
|
||
~~~~exercism/note | ||
The `if` command takes a _list of commands_ to use as the boolean conditions: | ||
if the command list exits with a zero return status, the "true" branch is followed; | ||
any other return status folls the "false" branch. | ||
The double parentheses is is a builtin construct that can be used as a command. | ||
It is known as the arithmetic conditional construct. | ||
The arithmetic expression is evaluated, and if the result is non-zero the return status is `0` ("true"). | ||
If the result is zero, the return status is `1` ("false"). | ||
Inside an arithmetic expression, variables can be used without the dollar sign. | ||
See [the Conditional Constructs section][conditional-constructs] in the Bash manual. | ||
[conditional-constructs]: https://www.gnu.org/software/bash/manual/bash.html#Conditional-Constructs | ||
~~~~ | ||
|
||
[order-of-precedence]: https://www.gnu.org/software/bash/manual/bash.html#Shell-Arithmetic |
7 changes: 7 additions & 0 deletions
7
exercises/practice/leap/.approaches/boolean-chain/snippet.txt
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 @@ | ||
year=$1 | ||
if (( year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) )); then | ||
echo true | ||
else | ||
echo false | ||
fi | ||
|
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,40 @@ | ||
{ | ||
"introduction": { | ||
"authors": [ | ||
"glennj" | ||
], | ||
"contributors": [ | ||
"BNAndras", | ||
"IsaacG" | ||
] | ||
}, | ||
"approaches": [ | ||
{ | ||
"uuid": "4e53dfc9-2662-4671-bb00-b2d927569070", | ||
"slug": "boolean-chain", | ||
"title": "Boolean chain", | ||
"blurb": "Use a chain of Boolean expressions.", | ||
"authors": [ | ||
"glennj" | ||
] | ||
}, | ||
{ | ||
"uuid": "8a562c42-3c04-4833-8322-bc0323539954", | ||
"slug": "ternary-operator", | ||
"title": "Ternary operator", | ||
"blurb": "Use a ternary operator of Boolean expressions.", | ||
"authors": [ | ||
"glennj" | ||
] | ||
}, | ||
{ | ||
"uuid": "c28ae2d8-9f8a-4359-b687-229b42573eef", | ||
"slug": "external-tools", | ||
"title": "External tools", | ||
"blurb": "Use external tools to do date addition.", | ||
"authors": [ | ||
"glennj" | ||
] | ||
} | ||
] | ||
} |
61 changes: 61 additions & 0 deletions
61
exercises/practice/leap/.approaches/external-tools/content.md
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,61 @@ | ||
# External Tools | ||
|
||
Calling external tools is a natural way to solve problems in Bash: call out to a specialized tool, capture the output, and process it. | ||
|
||
Using GNU `date` to find the date of the day after February 28: | ||
|
||
```bash | ||
year=$1 | ||
next_day=$(date -d "$year-02-28 + 1 day" '+%d') | ||
if [[ $next_day == 29 ]]; then | ||
echo true | ||
else | ||
echo false | ||
fi | ||
``` | ||
|
||
Or, more concise but less readable: | ||
|
||
```bash | ||
[[ $(date -d "$1-02-28 + 1 day" '+%d') == 29 ]] \ | ||
&& echo true \ | ||
|| echo false | ||
``` | ||
|
||
Working with external tools like this is what shells were built to do. | ||
|
||
From a performance perspective, it takes more work (than builtin addition) to: | ||
|
||
* copy the environment and spawn a child process, | ||
* connect the standard I/O channels, | ||
* wait for the process to complete and capture the exit status. | ||
|
||
Particularly inside of a loop, be careful about invoking external tools as the cost can add up. | ||
Over-reliance on external tools can take a job from completing in seconds to completing in minutes (or worse). | ||
|
||
~~~~exercism/caution | ||
Take care about using parts of dates in shell arithmetic. | ||
For example, we can get the day of the month: | ||
```bash | ||
day=$(date -d "$some_date" '+%d') | ||
next_day=$((day + 1)) | ||
``` | ||
That looks innocent, but if `$some_date` is `2024-02-08`, then: | ||
```bash | ||
$ some_date='2024-02-08' | ||
$ day=$(date -d "$some_date" '+%d') | ||
$ next_day=$((day + 1)) | ||
bash: 08: value too great for base (error token is "08") | ||
``` | ||
Bash treats numbers starting with zero as octal, and `8` is not a valid octal digit. | ||
Workarounds include using `%_d` or `%-d` to avoid the leading zero, or specify base-10 in the arithmetic (the `$` is required in this case). | ||
```bash | ||
next_day=$(( 10#$day + 1 )) | ||
``` | ||
~~~~ |
3 changes: 3 additions & 0 deletions
3
exercises/practice/leap/.approaches/external-tools/snippet.txt
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,3 @@ | ||
year=$1 | ||
next_day=$(date -d "$year-02-28 + 1 day" '+%d') | ||
[[ $next_day == "29" ]] && echo true || echo 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,63 @@ | ||
# Introduction | ||
|
||
There are various idiomatic approaches to solve Leap. | ||
You can use a chain of Boolean expressions to test the conditions. | ||
|
||
## General guidance | ||
|
||
The key to solving Leap is to know if the year is evenly divisible by `4`, `100` and `400`. | ||
To determine that, you will use the [modulo operator][modulo-operator]. | ||
|
||
## Approach: Arithmetic expression: chain of Boolean expressions | ||
|
||
```bash | ||
year=$1 | ||
if (( year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) )); then | ||
echo true | ||
else | ||
echo false | ||
fi | ||
``` | ||
|
||
For more information, check the [Boolean chain approach][approach-boolean-chain]. | ||
|
||
## Approach: Arithmetic expression Ternary operator of Boolean expressions | ||
|
||
```bash | ||
year=$1 | ||
if (( year % 100 == 0 ? year % 400 == 0 : year % 4 == 0 )); then | ||
echo true | ||
else | ||
echo false | ||
fi | ||
``` | ||
|
||
For more information, check the [Ternary operator approach][approach-ternary-operator]. | ||
|
||
## Approach: External tools | ||
|
||
Bash is naturally a "glue" language, making external tools easy to use. | ||
Calling out to a tool that can manipulate dates would be another approach to take. | ||
GNU `date` is an appropriate tool for this problem. | ||
|
||
```bash | ||
year=$1 | ||
next_day=$(date -d "$year-02-28 + 1 day" '+%d') | ||
[[ $next_day == "29" ]] && echo true || echo false | ||
``` | ||
|
||
Add a day to February 28th for the year and see if the new day is the 29th. | ||
For more information, see the [external tools approach][approach-external-tools]. | ||
|
||
## Which approach to use? | ||
|
||
- The chain of Boolean expressions should be the most efficient, as it proceeds from the most likely to least likely conditions. | ||
It has a maximum of three checks. | ||
It is the most efficient approach when testing a year that is not evenly divisible by `100` and is not a leap year, since the most likely outcome is eliminated first. | ||
- The ternary operator has a maximum of only two checks, but it starts from a less likely condition. | ||
- Using external tools to do `datetime` addition may be considered a "cheat" for the exercise, and it will be slower than the other approaches. | ||
|
||
[modulo-operator]: https://www.gnu.org/software/bash/manual/bash.html#Shell-Arithmetic | ||
[approach-boolean-chain]: https://exercism.org/tracks/bash/exercises/leap/approaches/boolean-chain | ||
[approach-ternary-operator]: https://exercism.org/tracks/bash/exercises/leap/approaches/ternary-operator | ||
[approach-external-tools]: https://exercism.org/tracks/bash/exercises/leap/approaches/external-tools |
68 changes: 68 additions & 0 deletions
68
exercises/practice/leap/.approaches/ternary-operator/content.md
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,68 @@ | ||
# Ternary operator | ||
|
||
```bash | ||
year=$1 | ||
if (( year % 100 == 0 ? year % 400 == 0 : year % 4 == 0 )); then | ||
echo true | ||
else | ||
echo false | ||
fi | ||
``` | ||
|
||
A [conditional operator][ternary-operator], also known as a "ternary conditional operator", or just "ternary operator". | ||
This structure uses a maximum of two checks to determine if a year is a leap year. | ||
|
||
It starts by testing the outlier condition of the year being evenly divisible by `100`. | ||
It does this by using the [remainder operator][remainder-operator]: `year % 100 == 0`. | ||
If the year is evenly divisible by `100`, then the expression is `true`, and the ternary operator returns the result of testing if the year is evenly divisible by `400`. | ||
If the year is _not_ evenly divisible by `100`, then the expression is `false`, and the ternary operator returns the result of testing if the year is evenly divisible by `4`. | ||
|
||
| year | divisible by 4 | not divisible by 100 | divisible by 400 | result | | ||
| ---- | -------------- | -------------------- | ---------------- | ------------ | | ||
| 2020 | false | not evaluated | true | true | | ||
| 2019 | false | not evaluated | false | false | | ||
| 2000 | true | true | not evaluated | true | | ||
| 1900 | true | false | not evaluated | false | | ||
|
||
Although it uses a maximum of two checks, the ternary operator tests an outlier condition first, making it less efficient than another approach that would first test if the year is evenly divisible by `4`, which is more likely than the year being evenly divisible by `100`. | ||
|
||
## Refactoring for readability | ||
|
||
This is a place where a helper function can result in more elegant code. | ||
|
||
```bash | ||
is_leap() { | ||
local year=$1 | ||
if (( year % 100 == 0 )); then | ||
return $(( !(year % 400 == 0) )) | ||
else | ||
return $(( !(year % 4 == 0) )) | ||
fi | ||
} | ||
|
||
is_leap "$1" && echo true || echo false | ||
``` | ||
|
||
The result of the arithmetic expression `year % 400 == 0` will be `1` if true and `0` if false. | ||
The value is negated to correspond to the shell's return statuses: `0` is "success" and `1` is "failure. | ||
Then the function can be used to branch between the "true" and "false" output. | ||
|
||
The function's `return` statements can be written as | ||
|
||
```bash | ||
(( year % 400 != 0 )) | ||
# or even | ||
(( year % 400 )) | ||
``` | ||
|
||
Without an explicit `return`, the function returns with the status of the last command executed. | ||
The `((` construct will be the last command. | ||
|
||
~~~~exercism/note | ||
It is unfortunate that the meaning of the shell's exit status (`0` is success) is opposite to the arithmetic meaning of zero (failure, the condition is not met). | ||
In the author's opinion, the cognitive dissonance of negating the condition reduces readability, but using `year % 400 != 0`, is worse. | ||
I prefer the more explicit version with the `return` statement and the explicit conversion of the arithmetic result to a return status. | ||
~~~~ | ||
|
||
[ternary-operator]: https://www.gnu.org/software/bash/manual/bash.html#Shell-Arithmetic | ||
[remainder-operator]: https://www.gnu.org/software/bash/manual/bash.html#Shell-Arithmetic |
6 changes: 6 additions & 0 deletions
6
exercises/practice/leap/.approaches/ternary-operator/snippet.txt
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,6 @@ | ||
year=$1 | ||
if (( year % 100 == 0 ? year % 400 == 0 : year % 4 == 0 )); then | ||
echo true | ||
else | ||
echo false | ||
fi |