Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions exercises/practice/leap/.approaches/boolean-chain/content.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ def leap_year(year):

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.

The first boolean expression uses the [modulo operator][modulo-operator] to check if the year is evenly divided by `4`.
- 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`.
The first boolean expression uses the [modulo operator][modulo-operator] to check if the year is evenly divisible by `4`.
- 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`.
- If the year _is_ evenly divisible by `4`, then the year is checked to _not_ be evenly divisible by `100`.
- 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`).
- 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`.
- If the year is not evenly divisible by `100`, the expression is `True` and the chain will short circuit and return `True`, since the next operator is a [logical OR][logical-or] (`or`).
- If the year _is_ evenly divisible by `100`, then the expression is `False`, and the returned value from the chain will be `True` if the year is evenly divisible by `400`.


| year | year % 4 == 0 | year % 100 != 0 | year % 400 == 0 | is leap year |
Expand All @@ -37,10 +37,10 @@ In Python, `a and b or c` is interpreted as `(a and b) or c`, which would give t
If in doubt, it is always permissible to add extra parentheses for clarity.


## Refactoring
## Variation 1

By using the [falsiness][falsiness] of `0`, the [`not` operator][not-operator] can be used instead of comparing equality to `0`:

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

```python
def leap_year(year):
Expand All @@ -50,10 +50,10 @@ def leap_year(year):

It can be thought of as the expression _not_ having a remainder.

[modulo-operator]: https://realpython.com/python-modulo-operator/
[falsiness]: https://www.pythontutorial.net/python-basics/python-boolean/#falsy-and-truthy-values
[isleap-source]: https://github.com/python/cpython/blob/3.13/Lib/calendar.py#L143-L145
[logical-and]: https://realpython.com/python-and-operator/
[logical-or]: https://realpython.com/python-or-operator/
[falsiness]: https://www.pythontutorial.net/python-basics/python-boolean/
[modulo-operator]: https://realpython.com/python-modulo-operator/
[not-operator]: https://realpython.com/python-not-operator/
[short-ciruiting]: https://mathspp.com/blog/pydonts/boolean-short-circuiting#short-circuiting-in-plain-english
[isleap-source]: https://github.com/python/cpython/blob/main/Lib/calendar.py#L141-L143
16 changes: 8 additions & 8 deletions exercises/practice/leap/.approaches/calendar-isleap/content.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
# The `calendar.isleap()` function

```pythoon
```python
from calendar import isleap

def leap_year(year):
return isleap(year)
```

~~~~exercism/caution
This approach may be considered a "cheat" for this exercise, which is intended to practice Boolean operators and logic.
This approach may be considered a "cheat" for this exercise, which is intended to practice Boolean operators and Boolean logic.
~~~~


The Python standard library includes a [`calendar`][calendar] module for working with many aspects of dates in the [Gregorian calendar][gregorian-calendar].

One of the methods provided is [`isleap()`][isleap], which implements exactly the same functionality as this exercise.

This is not a good way to practice the use of Booleans, as the exercise intends.
However, it may be convenient (_and better tested_) if you are working with calendar functions more broadly.
However, it may be convenient (_and better tested_) if you are working with `calendar` functions more broadly.


## The library function

This is the [implementation][implementation]:
This is the actual [implementation][implementation] in the CPython code:

```python
def isleap(year):
"""Return True for leap years, False for non-leap years."""
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
```

We can see that `calendar.isleap()` is just syntactic sugar for the `boolean-chain` approach.

We can see that `calendar.isleap()` is just syntactic sugar for the [`boolean-chain`][approach-boolean-chain] approach.

[approach-boolean-chain]: https://exercism.org/tracks/python/exercises/leap/approaches/boolean-chain
[calendar]: https://docs.python.org/3/library/calendar.html
[gregorian-calendar]: https://en.wikipedia.org/wiki/Gregorian_calendar
[implementation]: https://github.com/python/cpython/blob/main/Lib/calendar.py
[implementation]: https://github.com/python/cpython/blob/3.13/Lib/calendar.py#L143-L145
[isleap]: https://docs.python.org/3/library/calendar.html
18 changes: 11 additions & 7 deletions exercises/practice/leap/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -1,37 +1,41 @@
{
"introduction": {
"authors": ["bobahop"],
"contributors": ["colinleach"]
"contributors": ["colinleach", "Yrahcaz7", "Bethanyg"]
},
"approaches": [
{
"uuid": "5d42dc83-2473-425a-90bd-bf03f92b8c8b",
"slug": "boolean-chain",
"title": "Boolean chain",
"blurb": "Use a chain of boolean expressions.",
"authors": ["bobahop"]
"authors": ["bobahop"],
"contributors": ["colinleach", "Yrahcaz7", "Bethanyg"]
},
{
"uuid": "9952fef5-9f2f-4575-94fc-bc4e96593cd6",
"uuid": "37193c94-1b5f-4891-a685-11def9204839",
"slug": "ternary-operator",
"title": "Ternary operator",
"blurb": "Use a ternary operator of boolean expressions.",
"authors": ["bobahop"]
"authors": ["bobahop"],
"contributors": ["colinleach", "Yrahcaz7", "Bethanyg"]
},
{
"uuid": "66302791-0770-4f08-beaa-251c49e280a2",
"slug": "datetime-addition",
"title": "datetime addition",
"blurb": "Use datetime addition.",
"authors": ["bobahop"]
"authors": ["bobahop"],
"contributors": ["colinleach", "Yrahcaz7", "Bethanyg"]
},
{
"uuid": "d85be356-211a-4d2f-8af0-fa92e390b0b3",
"slug": "calendar-isleap",
"title": "calendar.isleap() function",
"blurb": "Use the calendar module.",
"authors": ["colinleach",
"BethanyG"]
"authors": ["colinleach", "BethanyG"],
"contributors": ["Yrahcaz7"]
}
]
}

29 changes: 15 additions & 14 deletions exercises/practice/leap/.approaches/datetime-addition/content.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
# `datetime` addition

```python
import datetime
from datetime import datetime, timedelta


def leap_year(year):
return (datetime.datetime(year, 2, 28)
+ datetime.timedelta(days=1)).day == 29

return (datetime(year, 2, 28)
+ timedelta(days=1)).day == 29
Comment thread
BethanyG marked this conversation as resolved.
```

~~~~exercism/caution
This approach may be considered a "cheat" for this exercise, which is intended to practice Boolean operators and logic.
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.
~~~~exercism/note
This approach may be considered a "cheat" for this exercise, which is intended to practice Boolean operators and boolean logic.
It also adds overhead in both performance and memory, as it imports methods from the `datetime` module and requires the instantiation of both a `datetime` object and a `timedelta` object.

For more information, see this exercises performance article.
For more information, see the performance article for this exercise.
~~~~

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.

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.
If it is February 29th, then the function returns `True` for the year being a leap year.
The exact steps are as follows:

- A new [datetime][datetime] object is created for February 28th of the year.
- Then the [timedelta][timedelta] of one day is added to that `datetime`,
and the function returns if the [day][day] property of the resulting `datetime` object is the 29th.
- A new [`datetime`][datetime] object is created for February 28th of the year.
- A [`timedelta`][timedelta] of one day is added to that `datetime`.
- The function returns if the [`day`][day] property of the resulting `datetime` object is the 29th.
Comment thread
BethanyG marked this conversation as resolved.

[timedelta]: https://docs.python.org/3/library/datetime.html#timedelta-objects
[day]: https://docs.python.org/3/library/datetime.html#datetime.datetime.day
[datetime]: https://docs.python.org/3/library/datetime.html#datetime-objects
[day]: https://docs.python.org/3/library/datetime.html#datetime.datetime.day
[timedelta]: https://docs.python.org/3/library/datetime.html#timedelta-objects
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import datetime
from datetime import datetime, timedelta


def leap_year(year):
return (datetime.datetime(year, 2, 28)
+ datetime.timedelta(days=1)).day == 29
return (datetime(year, 2, 28) + timedelta(days=1)).day == 29
58 changes: 41 additions & 17 deletions exercises/practice/leap/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# Introduction

There are multiple idiomatic approaches to solving the Leap exercise.
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.
There are multiple approaches to solving the Leap exercise in Python.
You can use a chain of boolean expressions or a [`ternary operator`][ternary-operator] to test conditions.
You can also utilize built-in methods from the `datetime` or `calendar` modules.


## General Guidance

The key to efficiently solving Leap is to calculate if the year is evenly divisible by `4`, `100` and `400`.
For determining that, you will use the [modulo operator][modulo-operator].
Using the [modulo operator][modulo-operator] is a good way of determining that.


## Approach: Chain of Boolean Expressions


```python
def leap_year(year):
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
Expand All @@ -34,45 +36,67 @@ For more information, see the [Ternary operator approach][approach-ternary-opera

## Other Approaches

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


### Approach: `datetime` Addition
### Approach: Using `datetime` Addition

Add a day to February 28th for the year and see if the new day is the 29th.
However, this approach may trade speed for convenience.

```python
import datetime

def leap_year(year):
return (datetime.datetime(year, 2, 28) + datetime.timedelta(days=1)).day == 29
```

For more information, see the [`datetime` addition approach][approach-datetime-addition].


### Approach: The `calendar` module
### Approach: Using the `calendar` module

It is possible to use `calendar.isleap(year)` from the standard library, which solves this exact problem.
It is possible to use [`calendar.isleap(<year>)`][isleap] from the standard library, which solves this exact problem:

This is self-defeating in an Exercism practice exercise intended to explore ways to use booleans.
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.

```python
from calendar import isleap

def leap_year(year):
return isleap(year)
```

This is self-defeating in the context of Exercism.
This practice exercise was designed to explore ways to use `booleans` and boolean logic.
The point is not _really_ leap year determination.
In a wider context, anyone testing for leap years is likely using `calendar` or related modules (_hand rolling your own test is likely to introduce bugs_), so it is good to know what library functions are available.

For more discussion, see the [`calendar.isleap()` approach][approach-calendar-isleap].


## Which approach to use?

- 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.
- The chain of `boolean expressions` should be the most efficient, as it proceeds from most to least likely conditions and takes advantage of [`short-circuiting`][short-circuting].
It has a maximum of three checks.
It is the fastest approach when testing a year that is not evenly divisible by `100` that is not a leap year.
Since most years fit those conditions, it is overall the most efficient approach.
It also happens to be the approach taken by the maintainers of the Python language in [implementing `calendar.isleap()`][calendar_isleap-code].


- The ternary operator approach has a maximum of only two checks, but it starts from a less likely condition.
The ternary operator was faster in benchmarking when the year was a leap year or was evenly divisible by `100`,
but those are the _least likely_ conditions.
- Using `datetime` addition may be considered a "cheat" for the exercise, and it was slower by far than the other approaches in benchmarking.
- The `ternary operator` approach has a maximum of only two checks, but it starts from a less likely condition.
The `ternary operator` was faster in benchmarking when the year was a leap year or was evenly divisible by `100`,
but those are the _least likely_ conditions!
- Using `datetime` addition may be considered a "cheat" for the exercise, and it was slower by far than the other approaches in benchmarking, due to import and method overhead.

For more information, check out the [Performance article][article-performance].


[approach-boolean-chain]: https://exercism.org/tracks/python/exercises/leap/approaches/boolean-chain
[approach-calendar-isleap]: https://exercism.org/tracks/python/exercises/leap/approaches/calendar-isleap
[approach-datetime-addition]: https://exercism.org/tracks/python/exercises/leap/approaches/datetime-addition
[approach-ternary-operator]: https://exercism.org/tracks/python/exercises/leap/approaches/ternary-operator
[article-performance]: https://exercism.org/tracks/python/exercises/leap/articles/performance
[calendar_isleap-code]: https://github.com/python/cpython/blob/3.9/Lib/calendar.py#L100-L102
[calendar_isleap-code]: https://github.com/python/cpython/blob/3.13/Lib/calendar.py#L143-L145
[isleap]: https://docs.python.org/3/library/calendar.html#calendar.isleap
[modulo-operator]: https://realpython.com/python-modulo-operator/
[short-circuting]: https://www.pythonmorsels.com/short-circuit-evaluation/
[ternary-operator]: https://www.pythontutorial.net/python-basics/python-ternary-operator/

16 changes: 9 additions & 7 deletions exercises/practice/leap/.approaches/ternary-operator/content.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ def leap_year(year):

```

A [ternary operator][ternary-operator] uses a maximum of two checks to determine if a year is a leap year.
A [`ternary operator`][ternary-operator] 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 [modulo operator][modulo-operator].
Also, by using the [falsiness][falsiness] of `0`, the [`not` operator][not-operator] can be used instead of comparing equality to `0`.

- If the year is evenly divisible by `100`, then the expression is `True`, and the ternary operator returns 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 if the year is evenly divisible by `4`.
- If the year is evenly divisible by `100`, then the ternary operator returns `True` if the year is evenly divisible by `400`.
- If the year is _not_ evenly divisible by `100`, the ternary operator returns `True` if the year is evenly divisible by `4`.


| year | year % 100 == 0 | year % 400 == 0 | year % 4 == 0 | is leap year |
| ---- | --------------- | --------------- | -------------- | ------------ |
Expand All @@ -22,13 +23,14 @@ Also, by using the [falsiness][falsiness] of `0`, the [`not` operator][not-opera
| 2000 | True | True | not evaluated | True |
| 1900 | True | False | not evaluated | False |


Although it uses a maximum of only 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`.
making it less efficient than the [boolean chain approach][approach-boolean-chain] that first tests if the year is evenly divisible by `4` (_which is more likely than the year being evenly divisible by `100`_).
The ternary operator was fastest in benchmarking when the year was a leap year or was evenly divisible by `100`,
but those are the least likely conditions.

[ternary-operator]: https://www.pythontutorial.net/python-basics/python-ternary-operator/
[approach-boolean-chain]: https://exercism.org/tracks/python/exercises/leap/approaches/boolean-chain
[falsiness]: https://www.pythontutorial.net/python-basics/python-boolean/#falsy-and-truthy-values
[modulo-operator]: https://realpython.com/python-modulo-operator/
[falsiness]: https://www.pythontutorial.net/python-basics/python-boolean/
[not-operator]: https://realpython.com/python-not-operator/
[ternary-operator]: https://www.pythontutorial.net/python-basics/python-ternary-operator/
Loading
Loading