From e024cbc2bdc8721d2e2fd8aebfbaea825906a08a Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Tue, 16 Jan 2024 15:42:55 -0500 Subject: [PATCH] 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 | 6 +- .../.approaches/external-tools/content.md | 61 -------------- .../.approaches/external-tools/snippet.txt | 3 - .../practice/leap/.approaches/introduction.md | 27 ++++--- .../.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 ++ 14 files changed, 209 insertions(+), 167 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 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..167ae140 100644 --- a/exercises/practice/leap/.approaches/config.json +++ b/exercises/practice/leap/.approaches/config.json @@ -8,7 +8,7 @@ }, "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 +17,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 +26,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/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 +```