Skip to content

Commit

Permalink
Add Leap approaches (#314)
Browse files Browse the repository at this point in the history
Co-authored-by: Isaac Good <[email protected]>
  • Loading branch information
glennj and IsaacG authored Jan 18, 2024
1 parent eda354d commit b52046c
Show file tree
Hide file tree
Showing 12 changed files with 321 additions and 0 deletions.
23 changes: 23 additions & 0 deletions exercises/practice/leap/.approaches/boolean-chain/content.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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}
36 changes: 36 additions & 0 deletions exercises/practice/leap/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
]
}
59 changes: 59 additions & 0 deletions exercises/practice/leap/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions exercises/practice/leap/.approaches/ternary-operator/content.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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
}
80 changes: 80 additions & 0 deletions exercises/practice/leap/.articles/performance/content.md
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions exercises/practice/leap/.articles/performance/snippet.md
Original file line number Diff line number Diff line change
@@ -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
```

0 comments on commit b52046c

Please sign in to comment.