Skip to content

Commit 4dd9e79

Browse files
[Leap]: Add calendar-isleap approach and improved benchmarks (exercism#3583)
* [Leap]: Add `calendar-isleap` approach and improved benchmarks * added comments on `isleap()` implementation and operator precedence * Approach and Chart Edits Various language edits and touchups. Added chart reference and alt-textm as well as link to long description. Probably unnecessary for this particular article but wanted to establish an example of what to do when alt-texting a chart. See https://www.w3.org/WAI/tutorials/images/complex/ for more information and examples on how to alt-text complex images. * Fixed Awkward Chart Intro * Another try on alt-text * Trying Yet Again * Attempt to align figcaption * Giving up and going for description location in alt --------- Co-authored-by: BethanyG <[email protected]>
1 parent 7e5a83d commit 4dd9e79

File tree

9 files changed

+222
-110
lines changed

9 files changed

+222
-110
lines changed

‎exercises/practice/leap/.approaches/boolean-chain/content.md‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ def leap_year(year):
66

77
```
88

9+
This might be considered the "most idiomatic" or "most Pythonic" solution, as it is exactly the same as the code implemented by the maintainers of the Python language for the [`calendar.isleap()`][isleap-source] method.
10+
911
The first boolean expression uses the [modulo operator][modulo-operator] to check if the year is evenly divided by `4`.
10-
- If the year is not evenly divisible by `4`, then the chain will "short circuit" due to the next operator being a [logical AND][logical-and]{`and`), and will return `False`.
12+
- If the year is _not_ evenly divisible by `4`, then the chain will [short circuit][short-ciruiting] due to the next operator being a [logical AND][logical-and]{`and`), and will return `False`.
1113
- If the year _is_ evenly divisible by `4`, then the year is checked to _not_ be evenly divisible by `100`.
12-
- If the year is not evenly divisible by `100`, then the expression is `True` and the chain will "short-circuit" to return `True`,
13-
since the next operator is a [logical OR][logical-or] (`or`).
14+
- If the year is not evenly divisible by `100`, then the expression is `True` and the interpreter will stop the evaluation to return `True`, since the next operator is a [logical OR][logical-or] (`or`).
1415
- If the year _is_ evenly divisible by `100`, then the expression is `False`, and the returned value from the chain will be if the year is evenly divisible by `400`.
1516

17+
1618
| year | year % 4 == 0 | year % 100 != 0 | year % 400 == 0 | is leap year |
1719
| ---- | ------------- | --------------- | --------------- | ------------ |
1820
| 2020 | True | True | not evaluated | True |
@@ -21,13 +23,24 @@ since the next operator is a [logical OR][logical-or] (`or`).
2123
| 1900 | True | False | False | False |
2224

2325

24-
The chain of boolean expressions is efficient, as it proceeds from testing the most likely to least likely conditions.
26+
The chain of boolean expressions is efficient, as it proceeds from testing the most to least likely conditions.
2527
It is the fastest approach when testing a year that is not evenly divisible by `100` and is not a leap year.
2628

29+
30+
## Operator precedence
31+
32+
The implementation contains one set of parentheses, around the `or` clause:
33+
- One set is enough, because the `%` operator is highest priority, then the `==` and `!=` relational operators.
34+
- Those parentheses are required, because `and` is higher priority than `or`.
35+
In Python, `a and b or c` is interpreted as `(a and b) or c`, which would give the wrong answer for this exercise.
36+
37+
If in doubt, it is always permissible to add extra parentheses for clarity.
38+
39+
2740
## Refactoring
2841

2942
By using the [falsiness][falsiness] of `0`, the [`not` operator][not-operator] can be used instead of comparing equality to `0`.
30-
For example
43+
For example:
3144

3245
```python
3346
defleap_year(year):
@@ -42,3 +55,5 @@ It can be thought of as the expression _not_ having a remainder.
4255
[logical-or]: https://realpython.com/python-or-operator/
4356
[falsiness]: https://www.pythontutorial.net/python-basics/python-boolean/
4457
[not-operator]: https://realpython.com/python-not-operator/
58+
[short-ciruiting]: https://mathspp.com/blog/pydonts/boolean-short-circuiting#short-circuiting-in-plain-english
59+
[isleap-source]: https://github.com/python/cpython/blob/main/Lib/calendar.py#L141-L143
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# The `calendar.isleap()` function
2+
3+
```pythoon
4+
from calendar import isleap
5+
6+
def leap_year(year):
7+
return isleap(year)
8+
```
9+
10+
~~~~exercism/caution
11+
This approach may be considered a "cheat" for this exercise, which is intended to practice Boolean operators and logic.
12+
~~~~
13+
14+
15+
The Python standard library includes a [`calendar`][calendar] module for working with many aspects of dates in the [Gregorian calendar][gregorian-calendar].
16+
17+
One of the methods provided is [`isleap()`][isleap], which implements exactly the same functionality as this exercise.
18+
19+
This is not a good way to practice the use of Booleans, as the exercise intends.
20+
However, it may be convenient (_and better tested_) if you are working with calendar functions more broadly.
21+
22+
## The library function
23+
24+
This is the [implementation][implementation]:
25+
26+
```python
27+
defisleap(year):
28+
"""Return True for leap years, False for non-leap years."""
29+
return year %4==0and (year %100!=0or year %400==0)
30+
```
31+
32+
We can see that `calendar.isleap()` is just syntactic sugar for the `boolean-chain` approach.
33+
34+
35+
[calendar]: https://docs.python.org/3/library/calendar.html
36+
[gregorian-calendar]: https://en.wikipedia.org/wiki/Gregorian_calendar
37+
[implementation]: https://github.com/python/cpython/blob/main/Lib/calendar.py
38+
[isleap]: https://docs.python.org/3/library/calendar.html
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from calendar import isleap
2+
3+
def leap_year(year):
4+
return isleap(year)

‎exercises/practice/leap/.approaches/config.json‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"introduction":{
33
"authors": ["bobahop"],
4-
"contributors": []
4+
"contributors": ["colinleach"]
55
},
66
"approaches": [
77
{
@@ -24,6 +24,14 @@
2424
"title": "datetime addition",
2525
"blurb": "Use datetime addition.",
2626
"authors": ["bobahop"]
27+
},
28+
{
29+
"uuid": "d85be356-211a-4d2f-8af0-fa92e390b0b3",
30+
"slug": "calendar-isleap",
31+
"title": "calendar.isleap() function",
32+
"blurb": "Use the calendar module.",
33+
"authors": ["colinleach",
34+
"BethanyG"]
2735
}
2836
]
2937
}

‎exercises/practice/leap/.approaches/datetime-addition/content.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ def leap_year(year):
1111
```
1212

1313
~~~~exercism/caution
14-
This approach may be considered a "cheat" for this exercise.
14+
This approach may be considered a "cheat" for this exercise, which is intended to practice Boolean operators and logic.
15+
It also adds a tremendous amount of overhead in both performance and memory, as it imports all of the `datetime` module and requires the instantiation of both a `datetime` object and a `datetime.timedelta` object.
16+
17+
For more information, see this exercises performance article.
1518
~~~~
1619

17-
By adding a day to February 28th for the year, you can see if the new day is the 29th or the 1st.
18-
If it is the 29th, then the function returns `True` for the year being a leap year.
20+
By adding a day to February 28th for a given year, you can see if the new day falls on the 29th of February, or the 1st of March.
21+
If it is February 29th, then the function returns `True` for the year being a leap year.
1922

2023
- A new [datetime][datetime] object is created for February 28th of the year.
2124
- Then the [timedelta][timedelta] of one day is added to that `datetime`,
Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,78 @@
11
# Introduction
22

3-
There are various idiomatic approaches to solve Leap.
4-
You can use a chain of boolean expressions to test the conditions.
5-
Or you can use a [ternary operator][ternary-operator].
3+
There are multiple idiomatic approaches to solving the Leap exercise.
4+
You can use a chain of boolean expressions to test the conditions, a [ternary operator][ternary-operator], or built-in methods from the `datetime` or `calendar` modules.
65

7-
## General guidance
86

9-
The key to solving Leap is to know if the year is evenly divisible by `4`, `100` and `400`.
7+
## General Guidance
8+
9+
The key to efficiently solving Leap is to calculate if the year is evenly divisible by `4`, `100` and `400`.
1010
For determining that, you will use the [modulo operator][modulo-operator].
1111

12-
## Approach: Chain of Boolean expressions
12+
13+
## Approach: Chain of Boolean Expressions
1314

1415
```python
1516
defleap_year(year):
1617
return year %4==0and (year %100!=0or year %400==0)
1718

1819
```
1920

20-
For more information, check the [Boolean chain approach][approach-boolean-chain].
21+
For more information, see the [Boolean chain approach][approach-boolean-chain].
22+
2123

22-
## Approach: Ternary operator of Boolean expressions
24+
## Approach: Ternary Operator of Boolean Expressions
2325

2426
```python
2527
defleap_year(year):
2628
return (not year %400ifnot year %100elsenot year %4)
2729

2830
```
2931

30-
For more information, check the [Ternary operator approach][approach-ternary-operator].
32+
For more information, see the [Ternary operator approach][approach-ternary-operator].
3133

32-
## Other approaches
34+
35+
## Other Approaches
3336

3437
Besides the aforementioned, idiomatic approaches, you could also approach the exercise as follows:
3538

36-
### Approach: datetime addition
3739

38-
Add a day to February 28th for the year and see if the new day is the 29th. For more information, see the [`datetime` addition approach][approach-datetime-addition].
40+
### Approach: `datetime` Addition
41+
42+
Add a day to February 28th for the year and see if the new day is the 29th.
43+
However, this approach may trade speed for convenience.
44+
For more information, see the [`datetime` addition approach][approach-datetime-addition].
45+
46+
47+
### Approach: The `calendar` module
48+
49+
It is possible to use `calendar.isleap(year)` from the standard library, which solves this exact problem.
50+
51+
This is self-defeating in an Exercism practice exercise intended to explore ways to use booleans.
52+
In a wider context, anyone testing for leap years may already be using `calendar` or related modules, and it is good to know what library functions are available.
53+
3954

4055
## Which approach to use?
4156

42-
- The chain of boolean expressions should be the most efficient, as it proceeds from the most likely to least likely conditions.
57+
- The chain of boolean expressions should be the most efficient, as it proceeds from the most to least likely conditions and takes advantage of short-circuiting.
4358
It has a maximum of three checks.
44-
It is the fastest approach when testing a year that is not evenly divisible by `100`and is not a leap year.
59+
It is the fastest approach when testing a year that is not evenly divisible by `100`that is not a leap year.
4560
Since most years fit those conditions, it is overall the most efficient approach.
46-
- The ternary operator has a maximum of only two checks, but it starts from a less likely condition.
61+
It also happens to be the approach taken by the maintainers of the Python language in [implementing `calendar.isleap()`][calendar_isleap-code].
62+
63+
64+
- The ternary operator approach has a maximum of only two checks, but it starts from a less likely condition.
4765
The ternary operator was faster in benchmarking when the year was a leap year or was evenly divisible by `100`,
48-
but those are the least likely conditions.
49-
- Using `datetime` addition may be considered a "cheat" for the exercise, and it was slower than the other approaches in benchmarking.
66+
but those are the _least likely_ conditions.
67+
- Using `datetime` addition may be considered a "cheat" for the exercise, and it was slower by far than the other approaches in benchmarking.
5068

51-
For more information, check the [Performance article][article-performance].
69+
For more information, check out the [Performance article][article-performance].
5270

53-
[modulo-operator]: https://realpython.com/python-modulo-operator/
54-
[ternary-operator]: https://www.pythontutorial.net/python-basics/python-ternary-operator/
5571
[approach-boolean-chain]: https://exercism.org/tracks/python/exercises/leap/approaches/boolean-chain
56-
[approach-ternary-operator]: https://exercism.org/tracks/python/exercises/leap/approaches/ternary-operator
5772
[approach-datetime-addition]: https://exercism.org/tracks/python/exercises/leap/approaches/datetime-addition
73+
[approach-ternary-operator]: https://exercism.org/tracks/python/exercises/leap/approaches/ternary-operator
5874
[article-performance]: https://exercism.org/tracks/python/exercises/leap/articles/performance
75+
[calendar_isleap-code]: https://github.com/python/cpython/blob/3.9/Lib/calendar.py#L100-L102
76+
[modulo-operator]: https://realpython.com/python-modulo-operator/
77+
[ternary-operator]: https://www.pythontutorial.net/python-basics/python-ternary-operator/
78+

‎exercises/practice/leap/.articles/config.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"slug": "performance",
66
"title": "Performance deep dive",
77
"blurb": "Deep dive to find out the most performant approach for determining a leap year.",
8-
"authors": ["bobahop"]
8+
"authors": ["bobahop",
9+
"colinleach"]
910
}
1011
]
1112
}
Lines changed: 63 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,91 @@
11
importtimeit
2-
2+
importpandasaspd
3+
importnumpyasnp
4+
importmatplotlibasmpl
5+
importmatplotlib.pyplotasplt
6+
importseabornassns
7+
frommatplotlib.colorsimportListedColormap
8+
9+
# Setting up the Data
310
loops=1_000_000
411

5-
val=timeit.timeit("""leap_year(1900)""",
6-
"""
7-
def leap_year(year):
8-
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
12+
row_headers= ["if-statements", "ternary", "datetime-add", "calendar-isleap"]
13+
col_headers= ["1900", "2000", "2019", "2020"]
914

10-
""", number=loops) /loops
15+
# empty dataframe will be filled in one cell at a time later
16+
df=pd.DataFrame(np.nan, index=row_headers, columns=col_headers)
1117

12-
print(f"if statements 1900: {val}")
18+
setups={}
1319

14-
val=timeit.timeit("""leap_year(2000)""",
15-
"""
20+
setups["if-statements"] ="""
1621
def leap_year(year):
1722
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
23+
"""
1824

19-
""", number=loops) /loops
20-
21-
print(f"if statements 2000: {val}")
22-
23-
24-
val=timeit.timeit("""leap_year(2019)""",
25-
"""
26-
def leap_year(year):
27-
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
28-
29-
""", number=loops) /loops
30-
31-
print(f"if statements 2019: {val}")
32-
33-
val=timeit.timeit("""leap_year(2020)""",
34-
"""
25+
setups["ternary"] ="""
3526
def leap_year(year):
36-
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
37-
38-
""", number=loops) /loops
27+
return not year % 400 if not year % 100 else not year % 4
28+
"""
3929

40-
print(f"if statements 2020: {val}")
30+
setups["datetime-add"] ="""
31+
from datetime import datetime, timedelta
4132
42-
val=timeit.timeit("""leap_year(1900)""",
43-
"""
4433
def leap_year(year):
45-
return not year % 400 if not year % 100 else not year % 4
46-
47-
""", number=loops) /loops
34+
return (datetime(year, 2, 28) + timedelta(days=1)).day == 29
35+
"""
4836

49-
print(f"ternary 1900: {val}")
37+
setups["calendar-isleap"] ="""
38+
from calendar import isleap
5039
51-
val=timeit.timeit("""leap_year(2000)""",
52-
"""
5340
def leap_year(year):
54-
return not year % 400 if not year % 100 else not year % 4
41+
return isleap(year)
42+
"""
5543

56-
""", number=loops) /loops
5744

58-
print(f"ternary 2000: {val}")
45+
# Conducting ghe timings
46+
fordescriptorinrow_headers:
47+
val=timeit.timeit("""leap_year(1900)""", setups[descriptor], number=loops) /loops
48+
year='1900'
49+
print(f"{descriptor}{year}: {val}")
50+
df.loc[descriptor, year] =val
5951

60-
val=timeit.timeit("""leap_year(2019)""",
61-
"""
62-
def leap_year(year):
63-
return not year % 400 if not year % 100 else not year % 4
52+
val=timeit.timeit("""leap_year(2000)""",setups[descriptor], number=loops) /loops
53+
year='2000'
54+
print(f"{descriptor}{year}: {val}")
55+
df.loc[descriptor, year] =val
6456

65-
""", number=loops) /loops
57+
val=timeit.timeit("""leap_year(2019)""", setups[descriptor], number=loops) /loops
58+
year='2019'
59+
print(f"{descriptor}{year}: {val}")
60+
df.loc[descriptor, year] =val
6661

67-
print(f"ternary 2019: {val}")
62+
val=timeit.timeit("""leap_year(2020)""", setups[descriptor], number=loops) /loops
63+
year='2020'
64+
print(f"{descriptor}{year}: {val}")
65+
df.loc[descriptor, year] =val
6866

69-
val=timeit.timeit("""leap_year(2020)""",
70-
"""
71-
def leap_year(year):
72-
return not year % 400 if not year % 100 else not year % 4
7367

74-
""", number=loops) /loops
68+
# Settng up chart details and colors
69+
mpl.rcParams['axes.labelsize'] =18
70+
bar_colors= ["#AFAD6A", "#B1C9FD", "#CDC6FD",
71+
"#FABD19", "#3B76F2", "#7467D1",
72+
"#FA9A19", "#85832F", "#1A54CE","#4536B0"]
7573

76-
print(f"ternary 2020: {val}")
74+
my_cmap=ListedColormap(sns.color_palette(bar_colors, as_cmap=True))
7775

78-
val=timeit.timeit("""leap_year(2019)""",
79-
"""
80-
import datetime
76+
# bar plot of actual run times
77+
ax=df.plot.bar(figsize=(10, 7),
78+
ylabel="time (s)",
79+
fontsize=14,
80+
width=0.8,
81+
rot=0,
82+
colormap=my_cmap)
8183

82-
def leap_year(year):
83-
return (datetime.datetime(year, 2, 28) +
84-
datetime.timedelta(days=1)).day == 29
84+
# Saving the graph for later use
85+
plt.savefig('../timeit_bar_plot.svg')
8586

86-
""", number=loops) /loops
8787

88-
print(f"datetime add 2019: {val}")
88+
# The next bit will be useful for `introduction.md`
89+
# pd.options.display.float_format = '{:,.2e}'.format
90+
print('\nDataframe in Markdown format:\n')
91+
print(df.to_markdown(floatfmt=".1e"))

0 commit comments

Comments
(0)