Skip to content

Commit 5e65747

Browse files
glennjBNAndrasIsaacG
authored
Add approaches for leap (#653)
Add approaches for leap Co-authored-by: András B Nagy <20251272+BNAndras@users.noreply.github.com> Co-authored-by: Isaac Good <IsaacG@users.noreply.github.com>
1 parent efa875c commit 5e65747

File tree

8 files changed

+295
-0
lines changed

8 files changed

+295
-0
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Chaining Boolean expressions
2+
3+
```bash
4+
year=$1
5+
if (( year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) )); then
6+
echo true
7+
else
8+
echo false
9+
fi
10+
```
11+
12+
The Boolean expression `year % 4 == 0` checks the remainder from dividing `year` by 4.
13+
If a year is evenly divisible by 4, the remainder will be zero.
14+
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.
15+
16+
Parentheses are used to control the [order of precedence][order-of-precedence]:
17+
logical AND `&&` has a higher precedence than logical OR `||`.
18+
19+
| year | divisible by 4 | not divisible by 100 | divisible by 400 | result |
20+
| ---- | -------------- | ------------------- | ---------------- | ------------ |
21+
| 2020 | true | true | not evaluated | true |
22+
| 2019 | false | not evaluated | not evaluated | false |
23+
| 2000 | true | false | true | true |
24+
| 1900 | true | false | false | false |
25+
26+
By situationally skipping some of the tests, we can efficiently calculate the result with fewer operations.
27+
Although in an interpreted language like Bash, that is less crucial than it might be in another language.
28+
29+
~~~~exercism/note
30+
The `if` command takes a _list of commands_ to use as the boolean conditions:
31+
if the command list exits with a zero return status, the "true" branch is followed;
32+
any other return status folls the "false" branch.
33+
34+
The double parentheses is is a builtin construct that can be used as a command.
35+
It is known as the arithmetic conditional construct.
36+
The arithmetic expression is evaluated, and if the result is non-zero the return status is `0` ("true").
37+
If the result is zero, the return status is `1` ("false").
38+
39+
Inside an arithmetic expression, variables can be used without the dollar sign.
40+
41+
See [the Conditional Constructs section][conditional-constructs] in the Bash manual.
42+
43+
[conditional-constructs]: https://www.gnu.org/software/bash/manual/bash.html#Conditional-Constructs
44+
45+
~~~~
46+
47+
[order-of-precedence]: https://www.gnu.org/software/bash/manual/bash.html#Shell-Arithmetic
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
year=$1
2+
if (( year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) )); then
3+
echo true
4+
else
5+
echo false
6+
fi
7+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"introduction": {
3+
"authors": [
4+
"glennj"
5+
],
6+
"contributors": [
7+
"BNAndras",
8+
"IsaacG"
9+
]
10+
},
11+
"approaches": [
12+
{
13+
"uuid": "4e53dfc9-2662-4671-bb00-b2d927569070",
14+
"slug": "boolean-chain",
15+
"title": "Boolean chain",
16+
"blurb": "Use a chain of Boolean expressions.",
17+
"authors": [
18+
"glennj"
19+
]
20+
},
21+
{
22+
"uuid": "8a562c42-3c04-4833-8322-bc0323539954",
23+
"slug": "ternary-operator",
24+
"title": "Ternary operator",
25+
"blurb": "Use a ternary operator of Boolean expressions.",
26+
"authors": [
27+
"glennj"
28+
]
29+
},
30+
{
31+
"uuid": "c28ae2d8-9f8a-4359-b687-229b42573eef",
32+
"slug": "external-tools",
33+
"title": "External tools",
34+
"blurb": "Use external tools to do date addition.",
35+
"authors": [
36+
"glennj"
37+
]
38+
}
39+
]
40+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# External Tools
2+
3+
Calling external tools is a natural way to solve problems in Bash: call out to a specialized tool, capture the output, and process it.
4+
5+
Using GNU `date` to find the date of the day after February 28:
6+
7+
```bash
8+
year=$1
9+
next_day=$(date -d "$year-02-28 + 1 day" '+%d')
10+
if [[ $next_day == 29 ]]; then
11+
echo true
12+
else
13+
echo false
14+
fi
15+
```
16+
17+
Or, more concise but less readable:
18+
19+
```bash
20+
[[ $(date -d "$1-02-28 + 1 day" '+%d') == 29 ]] \
21+
&& echo true \
22+
|| echo false
23+
```
24+
25+
Working with external tools like this is what shells were built to do.
26+
27+
From a performance perspective, it takes more work (than builtin addition) to:
28+
29+
* copy the environment and spawn a child process,
30+
* connect the standard I/O channels,
31+
* wait for the process to complete and capture the exit status.
32+
33+
Particularly inside of a loop, be careful about invoking external tools as the cost can add up.
34+
Over-reliance on external tools can take a job from completing in seconds to completing in minutes (or worse).
35+
36+
~~~~exercism/caution
37+
Take care about using parts of dates in shell arithmetic.
38+
For example, we can get the day of the month:
39+
40+
```bash
41+
day=$(date -d "$some_date" '+%d')
42+
next_day=$((day + 1))
43+
```
44+
45+
That looks innocent, but if `$some_date` is `2024-02-08`, then:
46+
47+
```bash
48+
$ some_date='2024-02-08'
49+
$ day=$(date -d "$some_date" '+%d')
50+
$ next_day=$((day + 1))
51+
bash: 08: value too great for base (error token is "08")
52+
```
53+
54+
Bash treats numbers starting with zero as octal, and `8` is not a valid octal digit.
55+
56+
Workarounds include using `%_d` or `%-d` to avoid the leading zero, or specify base-10 in the arithmetic (the `$` is required in this case).
57+
58+
```bash
59+
next_day=$(( 10#$day + 1 ))
60+
```
61+
~~~~
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
year=$1
2+
next_day=$(date -d "$year-02-28 + 1 day" '+%d')
3+
[[ $next_day == "29" ]] && echo true || echo false
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Introduction
2+
3+
There are various idiomatic approaches to solve Leap.
4+
You can use a chain of Boolean expressions to test the conditions.
5+
6+
## General guidance
7+
8+
The key to solving Leap is to know if the year is evenly divisible by `4`, `100` and `400`.
9+
To determine that, you will use the [modulo operator][modulo-operator].
10+
11+
## Approach: Arithmetic expression: chain of Boolean expressions
12+
13+
```bash
14+
year=$1
15+
if (( year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) )); then
16+
echo true
17+
else
18+
echo false
19+
fi
20+
```
21+
22+
For more information, check the [Boolean chain approach][approach-boolean-chain].
23+
24+
## Approach: Arithmetic expression Ternary operator of Boolean expressions
25+
26+
```bash
27+
year=$1
28+
if (( year % 100 == 0 ? year % 400 == 0 : year % 4 == 0 )); then
29+
echo true
30+
else
31+
echo false
32+
fi
33+
```
34+
35+
For more information, check the [Ternary operator approach][approach-ternary-operator].
36+
37+
## Approach: External tools
38+
39+
Bash is naturally a "glue" language, making external tools easy to use.
40+
Calling out to a tool that can manipulate dates would be another approach to take.
41+
GNU `date` is an appropriate tool for this problem.
42+
43+
```bash
44+
year=$1
45+
next_day=$(date -d "$year-02-28 + 1 day" '+%d')
46+
[[ $next_day == "29" ]] && echo true || echo false
47+
```
48+
49+
Add a day to February 28th for the year and see if the new day is the 29th.
50+
For more information, see the [external tools approach][approach-external-tools].
51+
52+
## Which approach to use?
53+
54+
- The chain of Boolean expressions should be the most efficient, as it proceeds from the most likely to least likely conditions.
55+
It has a maximum of three checks.
56+
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.
57+
- The ternary operator has a maximum of only two checks, but it starts from a less likely condition.
58+
- Using external tools to do `datetime` addition may be considered a "cheat" for the exercise, and it will be slower than the other approaches.
59+
60+
[modulo-operator]: https://www.gnu.org/software/bash/manual/bash.html#Shell-Arithmetic
61+
[approach-boolean-chain]: https://exercism.org/tracks/bash/exercises/leap/approaches/boolean-chain
62+
[approach-ternary-operator]: https://exercism.org/tracks/bash/exercises/leap/approaches/ternary-operator
63+
[approach-external-tools]: https://exercism.org/tracks/bash/exercises/leap/approaches/external-tools
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Ternary operator
2+
3+
```bash
4+
year=$1
5+
if (( year % 100 == 0 ? year % 400 == 0 : year % 4 == 0 )); then
6+
echo true
7+
else
8+
echo false
9+
fi
10+
```
11+
12+
A [conditional operator][ternary-operator], also known as a "ternary conditional operator", or just "ternary operator".
13+
This structure uses a maximum of two checks to determine if a year is a leap year.
14+
15+
It starts by testing the outlier condition of the year being evenly divisible by `100`.
16+
It does this by using the [remainder operator][remainder-operator]: `year % 100 == 0`.
17+
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`.
18+
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`.
19+
20+
| year | divisible by 4 | not divisible by 100 | divisible by 400 | result |
21+
| ---- | -------------- | -------------------- | ---------------- | ------------ |
22+
| 2020 | false | not evaluated | true | true |
23+
| 2019 | false | not evaluated | false | false |
24+
| 2000 | true | true | not evaluated | true |
25+
| 1900 | true | false | not evaluated | false |
26+
27+
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`.
28+
29+
## Refactoring for readability
30+
31+
This is a place where a helper function can result in more elegant code.
32+
33+
```bash
34+
is_leap() {
35+
local year=$1
36+
if (( year % 100 == 0 )); then
37+
return $(( !(year % 400 == 0) ))
38+
else
39+
return $(( !(year % 4 == 0) ))
40+
fi
41+
}
42+
43+
is_leap "$1" && echo true || echo false
44+
```
45+
46+
The result of the arithmetic expression `year % 400 == 0` will be `1` if true and `0` if false.
47+
The value is negated to correspond to the shell's return statuses: `0` is "success" and `1` is "failure.
48+
Then the function can be used to branch between the "true" and "false" output.
49+
50+
The function's `return` statements can be written as
51+
52+
```bash
53+
(( year % 400 != 0 ))
54+
# or even
55+
(( year % 400 ))
56+
```
57+
58+
Without an explicit `return`, the function returns with the status of the last command executed.
59+
The `((` construct will be the last command.
60+
61+
~~~~exercism/note
62+
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).
63+
In the author's opinion, the cognitive dissonance of negating the condition reduces readability, but using `year % 400 != 0`, is worse.
64+
I prefer the more explicit version with the `return` statement and the explicit conversion of the arithmetic result to a return status.
65+
~~~~
66+
67+
[ternary-operator]: https://www.gnu.org/software/bash/manual/bash.html#Shell-Arithmetic
68+
[remainder-operator]: https://www.gnu.org/software/bash/manual/bash.html#Shell-Arithmetic
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
year=$1
2+
if (( year % 100 == 0 ? year % 400 == 0 : year % 4 == 0 )); then
3+
echo true
4+
else
5+
echo false
6+
fi

0 commit comments

Comments
 (0)