From 5e657478b2cfc6486d280576db3f4d03de27529e Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Tue, 16 Jan 2024 16:20:19 -0500 Subject: [PATCH] Add approaches for leap (#653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add approaches for leap Co-authored-by: AndrĂ¡s B Nagy <20251272+BNAndras@users.noreply.github.com> Co-authored-by: Isaac Good --- .../leap/.approaches/boolean-chain/content.md | 47 +++++++++++++ .../.approaches/boolean-chain/snippet.txt | 7 ++ .../practice/leap/.approaches/config.json | 40 +++++++++++ .../.approaches/external-tools/content.md | 61 +++++++++++++++++ .../.approaches/external-tools/snippet.txt | 3 + .../practice/leap/.approaches/introduction.md | 63 +++++++++++++++++ .../.approaches/ternary-operator/content.md | 68 +++++++++++++++++++ .../.approaches/ternary-operator/snippet.txt | 6 ++ 8 files changed, 295 insertions(+) create mode 100644 exercises/practice/leap/.approaches/boolean-chain/content.md create mode 100644 exercises/practice/leap/.approaches/boolean-chain/snippet.txt create mode 100644 exercises/practice/leap/.approaches/config.json create mode 100644 exercises/practice/leap/.approaches/external-tools/content.md create mode 100644 exercises/practice/leap/.approaches/external-tools/snippet.txt create mode 100644 exercises/practice/leap/.approaches/introduction.md create mode 100644 exercises/practice/leap/.approaches/ternary-operator/content.md create mode 100644 exercises/practice/leap/.approaches/ternary-operator/snippet.txt diff --git a/exercises/practice/leap/.approaches/boolean-chain/content.md b/exercises/practice/leap/.approaches/boolean-chain/content.md new file mode 100644 index 00000000..9851ba95 --- /dev/null +++ b/exercises/practice/leap/.approaches/boolean-chain/content.md @@ -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 diff --git a/exercises/practice/leap/.approaches/boolean-chain/snippet.txt b/exercises/practice/leap/.approaches/boolean-chain/snippet.txt new file mode 100644 index 00000000..2fb68df7 --- /dev/null +++ b/exercises/practice/leap/.approaches/boolean-chain/snippet.txt @@ -0,0 +1,7 @@ +year=$1 +if (( year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) )); then + echo true +else + echo false +fi + diff --git a/exercises/practice/leap/.approaches/config.json b/exercises/practice/leap/.approaches/config.json new file mode 100644 index 00000000..1b76b12b --- /dev/null +++ b/exercises/practice/leap/.approaches/config.json @@ -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" + ] + } + ] +} diff --git a/exercises/practice/leap/.approaches/external-tools/content.md b/exercises/practice/leap/.approaches/external-tools/content.md new file mode 100644 index 00000000..eb40a8e9 --- /dev/null +++ b/exercises/practice/leap/.approaches/external-tools/content.md @@ -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 )) +``` +~~~~ diff --git a/exercises/practice/leap/.approaches/external-tools/snippet.txt b/exercises/practice/leap/.approaches/external-tools/snippet.txt new file mode 100644 index 00000000..d8706cbe --- /dev/null +++ b/exercises/practice/leap/.approaches/external-tools/snippet.txt @@ -0,0 +1,3 @@ +year=$1 +next_day=$(date -d "$year-02-28 + 1 day" '+%d') +[[ $next_day == "29" ]] && echo true || echo false diff --git a/exercises/practice/leap/.approaches/introduction.md b/exercises/practice/leap/.approaches/introduction.md new file mode 100644 index 00000000..e7f2963d --- /dev/null +++ b/exercises/practice/leap/.approaches/introduction.md @@ -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 diff --git a/exercises/practice/leap/.approaches/ternary-operator/content.md b/exercises/practice/leap/.approaches/ternary-operator/content.md new file mode 100644 index 00000000..37698551 --- /dev/null +++ b/exercises/practice/leap/.approaches/ternary-operator/content.md @@ -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 diff --git a/exercises/practice/leap/.approaches/ternary-operator/snippet.txt b/exercises/practice/leap/.approaches/ternary-operator/snippet.txt new file mode 100644 index 00000000..080ab4e1 --- /dev/null +++ b/exercises/practice/leap/.approaches/ternary-operator/snippet.txt @@ -0,0 +1,6 @@ +year=$1 +if (( year % 100 == 0 ? year % 400 == 0 : year % 4 == 0 )); then + echo true +else + echo false +fi