From 5ad6ea1c3846ddd050f09c4acb47525b37162757 Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Tue, 16 Jan 2024 15:42:55 -0500 Subject: [PATCH 1/4] wip --- .../leap/.approaches/boolean-chain/content.md | 47 +++++++++++++ .../.approaches/boolean-chain/snippet.txt | 7 ++ .../practice/leap/.approaches/config.json | 38 +++++++++++ .../.approaches/external-tools/content.md | 61 +++++++++++++++++ .../.approaches/external-tools/snippet.txt | 3 + .../practice/leap/.approaches/introduction.md | 52 ++++++++++++++ .../practice/leap/.approaches/performance | 47 +++++++++++++ .../.approaches/ternary-operator/content.md | 68 +++++++++++++++++++ .../.approaches/ternary-operator/snippet.txt | 6 ++ 9 files changed, 329 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/performance 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..20dac4d7 --- /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..a91d9b70 --- /dev/null +++ b/exercises/practice/leap/.approaches/config.json @@ -0,0 +1,38 @@ +{ + "introduction": { + "authors": [ + "glennj" + ], + "contributors": [ + ] + }, + "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": "clock-command", + "title": "Clock command", + "blurb": "Use the clock command 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..9004c9b0 --- /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..cf3fe902 --- /dev/null +++ b/exercises/practice/leap/.approaches/introduction.md @@ -0,0 +1,52 @@ +# Introduction + +There are various idiomatic approaches to solve Leap. + +## 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 + +```tcl +expr {$year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0)} +``` + +For more information, check the [Boolean chain approach][approach-boolean-chain]. + +## Approach: Arithmetic expression Ternary operator of Boolean expressions + +```tcl +expr {$year % 100 == 0 ? $year % 400 == 0 : $year % 4 == 0} +``` + +For more information, check the [Ternary operator approach][approach-ternary-operator]. + +## Approach: `clock` command + +TODO... + +```tcl +set timestamp [clock scan "$year-02-28" -format {%Y-%m-%d} -timezone :UTC] +set next_day [clock add $timestamp 1 day -timezone :UTC] +set day [clock format $next_day -format {%d} -timezone :UTC] +expr {$day == 29} +``` + +Add a day to February 28th for the year and see if the new day is the 29th. For more information, see the [`clock` command approach][approach-clock-command]. + +## 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 the `clock` command to do datetime arithmetic will be slower than the other approaches, just because Tcl has much more work to do under the hood. + +TODO performance + +[modulo-operator]: https://tcl.tk/man/tcl8.6/TclCmd/expr.htm#M9 +[approach-boolean-chain]: https://exercism.org/tracks/tcl/exercises/leap/approaches/boolean-chain +[approach-ternary-operator]: https://exercism.org/tracks/tcl/exercises/leap/approaches/ternary-operator +[approach-clock-command]: https://exercism.org/tracks/tcl/exercises/leap/approaches/clock-command diff --git a/exercises/practice/leap/.approaches/performance b/exercises/practice/leap/.approaches/performance new file mode 100644 index 00000000..4ac10170 --- /dev/null +++ b/exercises/practice/leap/.approaches/performance @@ -0,0 +1,47 @@ +proc leap_bool {year} { + return [expr {$year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0)}] +} + +proc leap_ternary {year} { + return [expr {$year % 100 == 0 ? $year % 400 == 0 : $year % 4 == 0}] +} + +proc leap_clock {year} { + set day 29 + set t [clock scan "$year-02-$day" -format {%Y-%m-%d} -timezone :UTC] + set next_day [clock format $t -format {%d} -timezone :UTC] + return [expr {$day == $next_day}] +} + +proc time_it {procname} { + foreach year {2023 2024 1900 2000} { + puts [format {%d - %d - %s} \ + $year \ + [$procname $year] \ + [time {$procname $year} 1000]] + } +} + +foreach procname {leap_bool leap_ternary leap_clock} { + puts $procname + time_it $procname + puts "" +} + +# leap_bool +# 2023 - 0 - 0.561253 microseconds per iteration +# 2024 - 1 - 0.6741269999999999 microseconds per iteration +# 1900 - 0 - 0.7831269999999999 microseconds per iteration +# 2000 - 1 - 0.7923790000000001 microseconds per iteration +# +# leap_ternary +# 2023 - 0 - 0.6397630000000001 microseconds per iteration +# 2024 - 1 - 0.674559 microseconds per iteration +# 1900 - 0 - 0.642119 microseconds per iteration +# 2000 - 1 - 0.6421520000000001 microseconds per iteration +# +# leap_clock +# 2023 - 0 - 33.188841000000004 microseconds per iteration +# 2024 - 1 - 30.633122 microseconds per iteration +# 1900 - 0 - 30.4935 microseconds per iteration +# 2000 - 1 - 28.581179 microseconds per iteration 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..fc9405cd --- /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 as a "Boolean condition" in the `if` statement. + +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 From 75a7dd326e39b04bc50d20fd0427f8a60f71f533 Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Tue, 16 Jan 2024 15:42:55 -0500 Subject: [PATCH 2/4] leap approaches and performance article --- .../leap/.approaches/boolean-chain/content.md | 34 ++------ .../.approaches/boolean-chain/snippet.txt | 8 +- .../leap/.approaches/clock-command/content.md | 23 ++++++ .../.approaches/clock-command/snippet.txt | 4 + .../practice/leap/.approaches/config.json | 8 +- .../.approaches/external-tools/content.md | 61 -------------- .../.approaches/external-tools/snippet.txt | 3 - .../practice/leap/.approaches/introduction.md | 27 ++++--- .../practice/leap/.approaches/performance | 47 ----------- .../.approaches/ternary-operator/content.md | 53 ++---------- .../.approaches/ternary-operator/snippet.txt | 7 +- exercises/practice/leap/.articles/config.json | 13 +++ .../leap/.articles/performance/bench.tcl | 49 +++++++++++ .../leap/.articles/performance/content.md | 81 +++++++++++++++++++ .../leap/.articles/performance/snippet.md | 7 ++ 15 files changed, 209 insertions(+), 216 deletions(-) create mode 100644 exercises/practice/leap/.approaches/clock-command/content.md create mode 100644 exercises/practice/leap/.approaches/clock-command/snippet.txt delete mode 100644 exercises/practice/leap/.approaches/external-tools/content.md delete mode 100644 exercises/practice/leap/.approaches/external-tools/snippet.txt delete mode 100644 exercises/practice/leap/.approaches/performance create mode 100644 exercises/practice/leap/.articles/config.json create mode 100644 exercises/practice/leap/.articles/performance/bench.tcl create mode 100644 exercises/practice/leap/.articles/performance/content.md create mode 100644 exercises/practice/leap/.articles/performance/snippet.md diff --git a/exercises/practice/leap/.approaches/boolean-chain/content.md b/exercises/practice/leap/.approaches/boolean-chain/content.md index 20dac4d7..64789d87 100644 --- a/exercises/practice/leap/.approaches/boolean-chain/content.md +++ b/exercises/practice/leap/.approaches/boolean-chain/content.md @@ -1,17 +1,12 @@ # Chaining Boolean expressions -```bash -year=$1 -if (( year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) )); then - echo true -else - echo false -fi +```tcl +expr {$year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0)} ``` -The Boolean expression `year % 4 == 0` checks the remainder from dividing `year` by 4. +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. +All leap years are divisible by 4, and this pattern is then repeated whether a year is not divisible by 100 and whether it is divisible by 400. Parentheses are used to control the [order of precedence][order-of-precedence]: logical AND `&&` has a higher precedence than logical OR `||`. @@ -24,24 +19,5 @@ logical AND `&&` has a higher precedence than logical OR `||`. | 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 +[order-of-precedence]: https://tcl.tk/man/tcl8.6/TclCmd/expr.htm#M6 diff --git a/exercises/practice/leap/.approaches/boolean-chain/snippet.txt b/exercises/practice/leap/.approaches/boolean-chain/snippet.txt index 2fb68df7..a7487c13 100644 --- a/exercises/practice/leap/.approaches/boolean-chain/snippet.txt +++ b/exercises/practice/leap/.approaches/boolean-chain/snippet.txt @@ -1,7 +1 @@ -year=$1 -if (( year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) )); then - echo true -else - echo false -fi - +expr {$year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0)} diff --git a/exercises/practice/leap/.approaches/clock-command/content.md b/exercises/practice/leap/.approaches/clock-command/content.md new file mode 100644 index 00000000..855b6e0b --- /dev/null +++ b/exercises/practice/leap/.approaches/clock-command/content.md @@ -0,0 +1,23 @@ +# Using the `clock` command + +Using [the `clock` command][tcl-clock] approach may be considered a "cheat" for this exercise. + +```tcl +set timestamp [clock scan "$year-02-28" -format {%Y-%m-%d}] +set next_day [clock add $timestamp 1 day] +set day [clock format $next_day -format {%d}] +expr {$day == 29} +``` + +By adding a day to February 28th for the year, you can see if the new day is the 29th or the 1st. +If it is the 29th, then the year is a leap year. + +Reference: [`clock` manual page](https://tcl.tk/man/tcl8.6/TclCmd/clock.htm) + +~~~~exercism/note +[Under the hood][tcl-src-leap], Tcl does have an internal helper function to test for leap years. + +[tcl-src-leap]: https://github.com/tcltk/tcl/blob/37176a333aa886595daaddbdf14ae7cacd1f06b0/generic/tclClock.c#L1561 +~~~~ + +[tcl-clock]: https://tcl.tk/man/tcl8.6/TclCmd/clock.htm diff --git a/exercises/practice/leap/.approaches/clock-command/snippet.txt b/exercises/practice/leap/.approaches/clock-command/snippet.txt new file mode 100644 index 00000000..46965d4c --- /dev/null +++ b/exercises/practice/leap/.approaches/clock-command/snippet.txt @@ -0,0 +1,4 @@ +set timestamp [clock scan "$year-02-28" -format {%Y-%m-%d}] +set next_day [clock add $timestamp 1 day] +set day [clock format $next_day -format {%d}] +expr {$day == 29} diff --git a/exercises/practice/leap/.approaches/config.json b/exercises/practice/leap/.approaches/config.json index a91d9b70..bbe3244a 100644 --- a/exercises/practice/leap/.approaches/config.json +++ b/exercises/practice/leap/.approaches/config.json @@ -2,13 +2,11 @@ "introduction": { "authors": [ "glennj" - ], - "contributors": [ ] }, "approaches": [ { - "uuid": "4e53dfc9-2662-4671-bb00-b2d927569070", + "uuid": "bb72a01e-74f1-4d8e-9df1-625718ced974", "slug": "boolean-chain", "title": "Boolean chain", "blurb": "Use a chain of boolean expressions.", @@ -17,7 +15,7 @@ ] }, { - "uuid": "8a562c42-3c04-4833-8322-bc0323539954", + "uuid": "d4e35a2e-8394-4eee-8f98-8d5c7a680596", "slug": "ternary-operator", "title": "Ternary operator", "blurb": "Use a ternary operator of Boolean expressions.", @@ -26,7 +24,7 @@ ] }, { - "uuid": "c28ae2d8-9f8a-4359-b687-229b42573eef", + "uuid": "c0b494ab-50f7-47dd-9394-a355317be233", "slug": "clock-command", "title": "Clock command", "blurb": "Use the clock command to do date addition.", diff --git a/exercises/practice/leap/.approaches/external-tools/content.md b/exercises/practice/leap/.approaches/external-tools/content.md deleted file mode 100644 index 9004c9b0..00000000 --- a/exercises/practice/leap/.approaches/external-tools/content.md +++ /dev/null @@ -1,61 +0,0 @@ -# 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 deleted file mode 100644 index d8706cbe..00000000 --- a/exercises/practice/leap/.approaches/external-tools/snippet.txt +++ /dev/null @@ -1,3 +0,0 @@ -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 index cf3fe902..51b7678e 100644 --- a/exercises/practice/leap/.approaches/introduction.md +++ b/exercises/practice/leap/.approaches/introduction.md @@ -7,6 +7,11 @@ There are various idiomatic approaches to solve Leap. 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]. +Recall that Tcl commands all look like `cmd arg arg ...`. +You can't just do `set z $x + $y` because `+` would be treated as just an argument, not an operation to add two numbers. +[The `expr` command][expr-command] is needed to perform arithmetic. +It essentially implements arithmetic as a domain-specific language, parsing its arguments as an arithmetic expression. + ## Approach: Arithmetic expression: chain of Boolean expressions ```tcl @@ -23,18 +28,18 @@ expr {$year % 100 == 0 ? $year % 400 == 0 : $year % 4 == 0} For more information, check the [Ternary operator approach][approach-ternary-operator]. -## Approach: `clock` command +## Approach: Using the `clock` command -TODO... +Add a day to February 28th in the given year and see if the new day is the 29th. ```tcl -set timestamp [clock scan "$year-02-28" -format {%Y-%m-%d} -timezone :UTC] -set next_day [clock add $timestamp 1 day -timezone :UTC] -set day [clock format $next_day -format {%d} -timezone :UTC] +set timestamp [clock scan "$year-02-28" -format {%Y-%m-%d}] +set next_day [clock add $timestamp 1 day] +set day [clock format $next_day -format {%d}] expr {$day == 29} ``` -Add a day to February 28th for the year and see if the new day is the 29th. For more information, see the [`clock` command approach][approach-clock-command]. +For more information, see the [`clock` command approach][approach-clock-command]. ## Which approach to use? @@ -44,9 +49,11 @@ It is the most efficient approach when testing a year that is not evenly divisib - The ternary operator has a maximum of only two checks, but it starts from a less likely condition. - Using the `clock` command to do datetime arithmetic will be slower than the other approaches, just because Tcl has much more work to do under the hood. -TODO performance +See [the Performance article][article-perf]. [modulo-operator]: https://tcl.tk/man/tcl8.6/TclCmd/expr.htm#M9 -[approach-boolean-chain]: https://exercism.org/tracks/tcl/exercises/leap/approaches/boolean-chain -[approach-ternary-operator]: https://exercism.org/tracks/tcl/exercises/leap/approaches/ternary-operator -[approach-clock-command]: https://exercism.org/tracks/tcl/exercises/leap/approaches/clock-command +[expr-command]: https://tcl.tk/man/tcl8.6/TclCmd/expr.htm +[approach-boolean-chain]: /tracks/tcl/exercises/leap/approaches/boolean-chain +[approach-ternary-operator]: /tracks/tcl/exercises/leap/approaches/ternary-operator +[approach-clock-command]: /tracks/tcl/exercises/leap/approaches/clock-command +[article-perf]: /tracks/tcl/exercises/leap/articles/performance diff --git a/exercises/practice/leap/.approaches/performance b/exercises/practice/leap/.approaches/performance deleted file mode 100644 index 4ac10170..00000000 --- a/exercises/practice/leap/.approaches/performance +++ /dev/null @@ -1,47 +0,0 @@ -proc leap_bool {year} { - return [expr {$year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0)}] -} - -proc leap_ternary {year} { - return [expr {$year % 100 == 0 ? $year % 400 == 0 : $year % 4 == 0}] -} - -proc leap_clock {year} { - set day 29 - set t [clock scan "$year-02-$day" -format {%Y-%m-%d} -timezone :UTC] - set next_day [clock format $t -format {%d} -timezone :UTC] - return [expr {$day == $next_day}] -} - -proc time_it {procname} { - foreach year {2023 2024 1900 2000} { - puts [format {%d - %d - %s} \ - $year \ - [$procname $year] \ - [time {$procname $year} 1000]] - } -} - -foreach procname {leap_bool leap_ternary leap_clock} { - puts $procname - time_it $procname - puts "" -} - -# leap_bool -# 2023 - 0 - 0.561253 microseconds per iteration -# 2024 - 1 - 0.6741269999999999 microseconds per iteration -# 1900 - 0 - 0.7831269999999999 microseconds per iteration -# 2000 - 1 - 0.7923790000000001 microseconds per iteration -# -# leap_ternary -# 2023 - 0 - 0.6397630000000001 microseconds per iteration -# 2024 - 1 - 0.674559 microseconds per iteration -# 1900 - 0 - 0.642119 microseconds per iteration -# 2000 - 1 - 0.6421520000000001 microseconds per iteration -# -# leap_clock -# 2023 - 0 - 33.188841000000004 microseconds per iteration -# 2024 - 1 - 30.633122 microseconds per iteration -# 1900 - 0 - 30.4935 microseconds per iteration -# 2000 - 1 - 28.581179 microseconds per iteration diff --git a/exercises/practice/leap/.approaches/ternary-operator/content.md b/exercises/practice/leap/.approaches/ternary-operator/content.md index fc9405cd..c7d74e4d 100644 --- a/exercises/practice/leap/.approaches/ternary-operator/content.md +++ b/exercises/practice/leap/.approaches/ternary-operator/content.md @@ -1,15 +1,10 @@ # Ternary operator -```bash -year=$1 -if (( year % 100 == 0 ? year % 400 == 0 : year % 4 == 0 )); then - echo true -else - echo false -fi +```tcl +expr {$year % 100 == 0 ? $year % 400 == 0 : $year % 4 == 0} ``` -A [conditional operator][ternary-operator], also known as a "ternary conditional operator", or just "ternary operator". +A [conditional operator][ternary-operator], also known as a "ternary conditional operator", or just "ternary operator", is a very condensed "if-then-else" 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`. @@ -26,43 +21,5 @@ If the year is _not_ evenly divisible by `100`, then the expression is `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 as a "Boolean condition" in the `if` statement. - -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 +[ternary-operator]: https://tcl.tk/man/tcl8.6/TclCmd/expr.htm#M21 +[remainder-operator]: https://tcl.tk/man/tcl8.6/TclCmd/expr.htm#M9 diff --git a/exercises/practice/leap/.approaches/ternary-operator/snippet.txt b/exercises/practice/leap/.approaches/ternary-operator/snippet.txt index 080ab4e1..42b512d7 100644 --- a/exercises/practice/leap/.approaches/ternary-operator/snippet.txt +++ b/exercises/practice/leap/.approaches/ternary-operator/snippet.txt @@ -1,6 +1 @@ -year=$1 -if (( year % 100 == 0 ? year % 400 == 0 : year % 4 == 0 )); then - echo true -else - echo false -fi +expr {$year % 100 == 0 ? $year % 400 == 0 : $year % 4 == 0} diff --git a/exercises/practice/leap/.articles/config.json b/exercises/practice/leap/.articles/config.json new file mode 100644 index 00000000..f29da0fc --- /dev/null +++ b/exercises/practice/leap/.articles/config.json @@ -0,0 +1,13 @@ +{ + "articles": [ + { + "uuid": "2d4c0b08-4830-4bd9-a985-1c0d3bf71980", + "slug": "performance", + "title": "Performance demonstration", + "blurb": "Compare the performances of the various leap year approaches.", + "authors": [ + "glennj" + ] + } + ] +} diff --git a/exercises/practice/leap/.articles/performance/bench.tcl b/exercises/practice/leap/.articles/performance/bench.tcl new file mode 100644 index 00000000..f42f8e9b --- /dev/null +++ b/exercises/practice/leap/.articles/performance/bench.tcl @@ -0,0 +1,49 @@ +proc leap_bool {year} { + return [expr {$year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0)}] +} + +proc leap_ternary {year} { + return [expr {$year % 100 == 0 ? $year % 400 == 0 : $year % 4 == 0}] +} + +proc leap_clock {year} { + set timestamp [clock scan "$year-02-28" -format {%Y-%m-%d}] + set next_day [clock add $timestamp 1 day] + set day [clock format $next_day -format {%d}] + return [expr {$day == 29}] +} + +proc time_it {procname} { + foreach year {2023 2024 1900 2000} { + puts [format {%d - %d - %s} \ + $year \ + [$procname $year] \ + [time {$procname $year} 1000]] + } +} + +foreach procname {leap_bool leap_ternary leap_clock} { + puts $procname + time_it $procname + puts "" +} + +set output { +leap_bool +2023 - 0 - 0.378 microseconds per iteration +2024 - 1 - 0.472 microseconds per iteration +1900 - 0 - 0.525 microseconds per iteration +2000 - 1 - 0.533 microseconds per iteration + +leap_ternary +2023 - 0 - 0.428 microseconds per iteration +2024 - 1 - 0.425 microseconds per iteration +1900 - 0 - 0.425 microseconds per iteration +2000 - 1 - 0.434 microseconds per iteration + +leap_clock +2023 - 0 - 65.716 microseconds per iteration +2024 - 1 - 70.092 microseconds per iteration +1900 - 0 - 59.396 microseconds per iteration +2000 - 1 - 72.496 microseconds per iteration +} diff --git a/exercises/practice/leap/.articles/performance/content.md b/exercises/practice/leap/.articles/performance/content.md new file mode 100644 index 00000000..14feb2bb --- /dev/null +++ b/exercises/practice/leap/.articles/performance/content.md @@ -0,0 +1,81 @@ +# Performance + +I'm demonstrating the use of [the `time` command][time-command] to compare the various approaches: + +* [Boolean chain][approach-boolean-chain] +* [Ternary operator][approach-ternary-operator] +* [`clock` command][approach-clock-command] + +```tcl +proc leap_bool {year} { + return [expr {$year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0)}] +} + +proc leap_ternary {year} { + return [expr {$year % 100 == 0 ? $year % 400 == 0 : $year % 4 == 0}] +} + +proc leap_clock {year} { + set timestamp [clock scan "$year-02-28" -format {%Y-%m-%d}] + set next_day [clock add $timestamp 1 day] + set day [clock format $next_day -format {%d}] + return [expr {$day == 29}] +} + +proc time_it {procname} { + foreach year {2023 2024 1900 2000} { + puts [format {%d - %d - %s} \ + $year \ + [$procname $year] \ + [time {$procname $year} 1000]] + } +} + +foreach procname {leap_bool leap_ternary leap_clock} { + puts $procname + time_it $procname + puts "" +} +``` + +This outputs: + +```none +leap_bool +2023 - 0 - 0.378 microseconds per iteration +2024 - 1 - 0.472 microseconds per iteration +1900 - 0 - 0.525 microseconds per iteration +2000 - 1 - 0.533 microseconds per iteration + +leap_ternary +2023 - 0 - 0.428 microseconds per iteration +2024 - 1 - 0.425 microseconds per iteration +1900 - 0 - 0.425 microseconds per iteration +2000 - 1 - 0.434 microseconds per iteration + +leap_clock +2023 - 0 - 65.716 microseconds per iteration +2024 - 1 - 70.092 microseconds per iteration +1900 - 0 - 59.396 microseconds per iteration +2000 - 1 - 72.496 microseconds per iteration``` + +## Observations + +1. Boolean chain + + We can see that the non leap year is the quickest to return, only having to execute one comparison. + The 100 and 400 years are the most expensive. + But still, we're talking under one microsecond execution time. + +1. Ternary operator + + All the test years take the same amount of time to run. + That is expected since every year passed in has to perform two comparisons. + +1. `clock` command + + Unsurprisingly, this is much more expensive to run than the purely arithmetic approaches. + +[approach-boolean-chain]: /tracks/tcl/exercises/leap/approaches/boolean-chain +[approach-ternary-operator]: /tracks/tcl/exercises/leap/approaches/ternary-operator +[approach-clock-command]: /tracks/tcl/exercises/leap/approaches/clock-command diff --git a/exercises/practice/leap/.articles/performance/snippet.md b/exercises/practice/leap/.articles/performance/snippet.md new file mode 100644 index 00000000..2b5ffdcc --- /dev/null +++ b/exercises/practice/leap/.articles/performance/snippet.md @@ -0,0 +1,7 @@ +```none +leap_bool +2023 - 0 - 0.378 microseconds per iteration +2024 - 1 - 0.472 microseconds per iteration +1900 - 0 - 0.525 microseconds per iteration +2000 - 1 - 0.533 microseconds per iteration +``` From 26e6b2622638bbb526dc4e418b6b581f5cf984f4 Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Thu, 18 Jan 2024 08:11:46 -0500 Subject: [PATCH 3/4] Update exercises/practice/leap/.articles/performance/content.md Co-authored-by: Isaac Good --- exercises/practice/leap/.articles/performance/content.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/exercises/practice/leap/.articles/performance/content.md b/exercises/practice/leap/.articles/performance/content.md index 14feb2bb..10d12fb0 100644 --- a/exercises/practice/leap/.articles/performance/content.md +++ b/exercises/practice/leap/.articles/performance/content.md @@ -64,8 +64,7 @@ leap_clock 1. Boolean chain We can see that the non leap year is the quickest to return, only having to execute one comparison. - The 100 and 400 years are the most expensive. - But still, we're talking under one microsecond execution time. + Even 100 and 400, the most expensive years to compute, execute in under one microsecond. 1. Ternary operator From 1fe6883056e4f23cd9080cd479b95187e687a08d Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Thu, 18 Jan 2024 13:14:43 +0000 Subject: [PATCH 4/4] not first person --- exercises/practice/leap/.articles/performance/content.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/leap/.articles/performance/content.md b/exercises/practice/leap/.articles/performance/content.md index 10d12fb0..5e15b201 100644 --- a/exercises/practice/leap/.articles/performance/content.md +++ b/exercises/practice/leap/.articles/performance/content.md @@ -1,6 +1,6 @@ # Performance -I'm demonstrating the use of [the `time` command][time-command] to compare the various approaches: +This article demonstrates the use of [the `time` command][time-command] to compare the various approaches: * [Boolean chain][approach-boolean-chain] * [Ternary operator][approach-ternary-operator]