Introduction
A leap year (in the Gregorian calendar) occurs:
- In every year that is evenly divisible by 4.
- Unless the year is evenly divisible by 100, in which case it’s only a leap year if the year is also evenly divisible by 400.
Some examples:
- 1997 was not a leap year as it’s not divisible by 4.
- 1900 was not a leap year as it’s not divisible by 400.
- 2000 was a leap year!
For a delightful, four-minute explanation of the whole phenomenon of leap years, check out [this YouTube video](https://www.youtube.com/watch?v=xX96xng7sAE).
Instructions
Instructions
Your task is to determine whether a given year is a leap year.
Dig Deeper
Boolean chain
Chaining Boolean expressions
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:
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.
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 taken;
any other return status takes 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
Ternary operator
Ternary operator
year=$1
if (( year % 100 == 0 ? year % 400 == 0 : year % 4 == 0 )); then
echo true
else
echo false
fi
A conditional 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: 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.
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
(( 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.
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.
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:
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:
[[ $(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).
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 ))
```
Source: Exercism bash/leap