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..64789d87 --- /dev/null +++ b/exercises/practice/leap/.approaches/boolean-chain/content.md @@ -0,0 +1,23 @@ +# Chaining Boolean expressions + +```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. +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 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 `||`. + +| 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. + +[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 new file mode 100644 index 00000000..a7487c13 --- /dev/null +++ b/exercises/practice/leap/.approaches/boolean-chain/snippet.txt @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000..bbe3244a --- /dev/null +++ b/exercises/practice/leap/.approaches/config.json @@ -0,0 +1,36 @@ +{ + "introduction": { + "authors": [ + "glennj" + ] + }, + "approaches": [ + { + "uuid": "bb72a01e-74f1-4d8e-9df1-625718ced974", + "slug": "boolean-chain", + "title": "Boolean chain", + "blurb": "Use a chain of boolean expressions.", + "authors": [ + "glennj" + ] + }, + { + "uuid": "d4e35a2e-8394-4eee-8f98-8d5c7a680596", + "slug": "ternary-operator", + "title": "Ternary operator", + "blurb": "Use a ternary operator of Boolean expressions.", + "authors": [ + "glennj" + ] + }, + { + "uuid": "c0b494ab-50f7-47dd-9394-a355317be233", + "slug": "clock-command", + "title": "Clock command", + "blurb": "Use the clock command to do date addition.", + "authors": [ + "glennj" + ] + } + ] +} diff --git a/exercises/practice/leap/.approaches/introduction.md b/exercises/practice/leap/.approaches/introduction.md new file mode 100644 index 00000000..51b7678e --- /dev/null +++ b/exercises/practice/leap/.approaches/introduction.md @@ -0,0 +1,59 @@ +# 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]. + +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 +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: Using the `clock` command + +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}] +set next_day [clock add $timestamp 1 day] +set day [clock format $next_day -format {%d}] +expr {$day == 29} +``` + +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. + +See [the Performance article][article-perf]. + +[modulo-operator]: https://tcl.tk/man/tcl8.6/TclCmd/expr.htm#M9 +[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 new file mode 100644 index 00000000..c7d74e4d --- /dev/null +++ b/exercises/practice/leap/.approaches/ternary-operator/content.md @@ -0,0 +1,25 @@ +# Ternary operator + +```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", 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`. +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`. + +[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 new file mode 100644 index 00000000..42b512d7 --- /dev/null +++ b/exercises/practice/leap/.approaches/ternary-operator/snippet.txt @@ -0,0 +1 @@ +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..5e15b201 --- /dev/null +++ b/exercises/practice/leap/.articles/performance/content.md @@ -0,0 +1,80 @@ +# Performance + +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] +* [`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. + Even 100 and 400, the most expensive years to compute, execute in under one microsecond. + +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 +```