diff --git a/exercises/practice/leap/.approaches/boolean-chain/content.md b/exercises/practice/leap/.approaches/boolean-chain/content.md index d4c78911e10..4e987b30f37 100644 --- a/exercises/practice/leap/.approaches/boolean-chain/content.md +++ b/exercises/practice/leap/.approaches/boolean-chain/content.md @@ -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 | @@ -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): @@ -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 diff --git a/exercises/practice/leap/.approaches/calendar-isleap/content.md b/exercises/practice/leap/.approaches/calendar-isleap/content.md index 0674eb56a44..e645b554716 100644 --- a/exercises/practice/leap/.approaches/calendar-isleap/content.md +++ b/exercises/practice/leap/.approaches/calendar-isleap/content.md @@ -1,6 +1,6 @@ # The `calendar.isleap()` function -```pythoon +```python from calendar import isleap def leap_year(year): @@ -8,20 +8,20 @@ def leap_year(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): @@ -29,10 +29,10 @@ def isleap(year): 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 diff --git a/exercises/practice/leap/.approaches/config.json b/exercises/practice/leap/.approaches/config.json index 515f702b762..7e3f8f6939a 100644 --- a/exercises/practice/leap/.approaches/config.json +++ b/exercises/practice/leap/.approaches/config.json @@ -1,7 +1,7 @@ { "introduction": { "authors": ["bobahop"], - "contributors": ["colinleach"] + "contributors": ["colinleach", "Yrahcaz7", "Bethanyg"] }, "approaches": [ { @@ -9,29 +9,33 @@ "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"] } ] } + diff --git a/exercises/practice/leap/.approaches/datetime-addition/content.md b/exercises/practice/leap/.approaches/datetime-addition/content.md index 3c28ef480eb..48c58d2c568 100644 --- a/exercises/practice/leap/.approaches/datetime-addition/content.md +++ b/exercises/practice/leap/.approaches/datetime-addition/content.md @@ -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 ``` -~~~~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. -[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 diff --git a/exercises/practice/leap/.approaches/datetime-addition/snippet.txt b/exercises/practice/leap/.approaches/datetime-addition/snippet.txt index b02df891b0f..1045194c360 100644 --- a/exercises/practice/leap/.approaches/datetime-addition/snippet.txt +++ b/exercises/practice/leap/.approaches/datetime-addition/snippet.txt @@ -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 diff --git a/exercises/practice/leap/.approaches/introduction.md b/exercises/practice/leap/.approaches/introduction.md index 4b136dd6b41..ee030bbbcfe 100644 --- a/exercises/practice/leap/.approaches/introduction.md +++ b/exercises/practice/leap/.approaches/introduction.md @@ -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) @@ -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()`][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/ - diff --git a/exercises/practice/leap/.approaches/ternary-operator/content.md b/exercises/practice/leap/.approaches/ternary-operator/content.md index e20142ddc67..9e46cd18f8d 100644 --- a/exercises/practice/leap/.approaches/ternary-operator/content.md +++ b/exercises/practice/leap/.approaches/ternary-operator/content.md @@ -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 | | ---- | --------------- | --------------- | -------------- | ------------ | @@ -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/ diff --git a/exercises/practice/leap/.articles/performance/content.md b/exercises/practice/leap/.articles/performance/content.md index 67d7b57b631..76a33ac2bae 100644 --- a/exercises/practice/leap/.articles/performance/content.md +++ b/exercises/practice/leap/.articles/performance/content.md @@ -4,10 +4,12 @@ In this article, we'll find out how to most efficiently calculate if a year is a The [approaches page][approaches] lists two idiomatic approaches to this exercise: + 1. [Using the boolean chain][approach-boolean-chain] 2. [Using the ternary operator][approach-ternary-operator] For our performance investigation, we will also include a two further approaches: + 3. [datetime addition][approach-datetime-addition] 4. The [`calendar.isleap()`][approach-calendar-isleap] function from the calendar module in the standard library @@ -19,7 +21,7 @@ All methods are "fast", but the difference may be easier to see graphically. **Note**: The y-axis values in the chart have a `1e-7` multiplier. All run times are sub-microsecond. -!["Grouped Bar Chart showing execution timings for 4 leap approaches using the years 1900, 200, 2019, and 202 as input data. Described under the heading Timings for approaches by input year."](https://assets.exercism.org/images/tracks/python/leap/leap_timeit_bar_plot-light.svg) +![Grouped Bar Chart showing execution timings for 4 leap approaches using the years 1900, 2000, 2019, and 2020 as input data. Described under the heading "Timings for approaches by input year."](https://assets.exercism.org/images/tracks/python/leap/leap_timeit_bar_plot-light.svg) ### Timings for approaches by input year @@ -35,16 +37,17 @@ All methods are "fast", but the difference may be easier to see graphically.
-- The `if-statements` (_boolean chain_) is the fastest approach when testing a year that is not evenly divisible by `100` and is not a leap year. +- The `if-statements` (_boolean chain_) approach is the fastest approach when testing a year that is not evenly divisible by `100` and is not a leap year. Since most years fit those conditions, it is overall the most efficient approach. -- The ternary operator is faster in benchmarking when the year is a leap year or is evenly divisible by `100`, -but those are the least likely conditions. -- Adding to the `datetime` may not only be a "cheat", but it is slower than the other approaches. +- The `ternary operator` approach is faster in benchmarking when the year is a leap year or is evenly divisible by `100`, +but those are the _least likely_ conditions. +- Adding to the `datetime` approach may not only be a "cheat", it is also slower than the other approaches. - Comparing `import datatime` and `from datetime import datetime, timedelta` showed little speed difference _(data not shown)_. - Using the built-in `calendar.isleap()` function is terse, convenient and very readable, but not quite as fast as writing your own logic. -This is likely due to the overhead of both loading the `calendar` module and then calling `calendar.isleap()`. +This is likely due to the overhead of both loading the `calendar` module and then calling the `calendar.isleap()` method. + -Often, it is helpful to the programmer to use imported packages, but a large `import` to use a simple function may not give the fastest code. +Often, it is helpful to the programmer to use imported packages, but a large `import` to use a single method may not give the fastest code. Consider the context, and decide which is best for you in each case. [approach-boolean-chain]: https://exercism.org/tracks/python/exercises/leap/approaches/boolean-chain