Skip to content

Commit

Permalink
leap approaches and performance article
Browse files Browse the repository at this point in the history
  • Loading branch information
glennj committed Jan 18, 2024
1 parent 5ad6ea1 commit e024cbc
Show file tree
Hide file tree
Showing 14 changed files with 209 additions and 167 deletions.
34 changes: 5 additions & 29 deletions exercises/practice/leap/.approaches/boolean-chain/content.md
Original file line number Diff line number Diff line change
@@ -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 `||`.
Expand All @@ -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
Original file line number Diff line number Diff line change
@@ -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)}
23 changes: 23 additions & 0 deletions exercises/practice/leap/.approaches/clock-command/content.md
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions exercises/practice/leap/.approaches/clock-command/snippet.txt
Original file line number Diff line number Diff line change
@@ -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}
6 changes: 3 additions & 3 deletions exercises/practice/leap/.approaches/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand All @@ -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.",
Expand All @@ -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.",
Expand Down
61 changes: 0 additions & 61 deletions exercises/practice/leap/.approaches/external-tools/content.md

This file was deleted.

This file was deleted.

27 changes: 17 additions & 10 deletions exercises/practice/leap/.approaches/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?

Expand All @@ -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
53 changes: 5 additions & 48 deletions exercises/practice/leap/.approaches/ternary-operator/content.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand All @@ -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
Original file line number Diff line number Diff line change
@@ -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}
13 changes: 13 additions & 0 deletions exercises/practice/leap/.articles/config.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
]
}
49 changes: 49 additions & 0 deletions exercises/practice/leap/.articles/performance/bench.tcl
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit e024cbc

Please sign in to comment.