Tracking leap.py years for Gregorian calendar (Exercism org)

This is not for a course credit. It’s just online courseware. Although I’d prefer that you people not provide a full solution. Instead just provide guidance, hints, and support in a general direction. Thanks.

Here are the Instructions for this task/challenge course module:

Given a year, report if it is a leap year.
The tricky thing here is that a leap year in the Gregorian calendar occurs:

on every year that is evenly divisible by 4
except every year that is evenly divisible by 100
unless the year is also evenly divisible by 400

For example, 1997 is not a leap year, but 1996 is. 1900 is not a leap year, but 2000 is.
For a delightful, four minute explanation of the whole leap year phenomenon, go watch this youtube video.

There is only one variable - - a given year - - but so many different possible cases and conditions that the unit test tries to verify.

The problem involves basic predicate logic and bivalence. I’ve tried many different combinations of operators, like swapping the three instances of disjunctions and conjunctions, back and forth, one for the other. Every time I change one of the terms, I run the test script, and as a result, a different set of Assertions pass while others fail. I’ve also tried different combinations of equality and inequality operators which will also trigger different Assertions to pass and fail. After each change, I am no closer to achieving the end goal which is to have all of them pass.

For the return line, if I change True for False, the Assertions that previously failed, are now passing, which kind of makes sense.

I’ve done my very best to use the exercise’s description as pseudo code and guide:

on every year that is evenly divisible by 4
  except every year that is evenly divisible by 100
    unless the year is also evenly divisible by 400

But I am missing something and not sure what.

Here is my script:

def leap_year(year):
   if (year % 4 == 0 and year % 2 == 0) and (year % 100 == 0) or (year % 400 != 0): # or (year % 100 != 3):
       return True

Using that script, here is my unit test trace back passing 4 tests and failing 5 tests:

============ short test summary info ============
FAILED python/leap/leap_test.py::LeapTest::test_year_divisible_by_100_but_not_by_3_is_still_not_a_leap_year - AssertionError: True is not False
FAILED python/leap/leap_test.py::LeapTest::test_year_divisible_by_100_not_divisible_by_400_in_common_year - AssertionError: True is not False
FAILED python/leap/leap_test.py::LeapTest::test_year_divisible_by_200_not_divisible_by_400_in_common_year - AssertionError: True is not False
FAILED python/leap/leap_test.py::LeapTest::test_year_divisible_by_2_not_divisible_by_4_in_common_year - AssertionError: True is not False
FAILED python/leap/leap_test.py::LeapTest::test_year_not_divisible_by_4_in_common_year - AssertionError: True is not False
========== 5 failed, 4 passed in 0.07s ==========

Here is the unit test class revealing the test conditions:

class LeapTest(unittest.TestCase):
   def test_year_not_divisible_by_4_in_common_year(self):
       self.assertIs(leap_year(2015), False)
 
   def test_year_divisible_by_2_not_divisible_by_4_in_common_year(self):
       self.assertIs(leap_year(1970), False)
 
   def test_year_divisible_by_4_not_divisible_by_100_in_leap_year(self):
       self.assertIs(leap_year(1996), True)
 
   def test_year_divisible_by_4_and_5_is_still_a_leap_year(self):
       self.assertIs(leap_year(1960), True)
 
   def test_year_divisible_by_100_not_divisible_by_400_in_common_year(self):
       self.assertIs(leap_year(2100), False)
 
   def test_year_divisible_by_100_but_not_by_3_is_still_not_a_leap_year(self):
       self.assertIs(leap_year(1900), False)
 
   def test_year_divisible_by_400_is_leap_year(self):
       self.assertIs(leap_year(2000), True)
 
   def test_year_divisible_by_400_but_not_by_125_is_still_a_leap_year(self):
       self.assertIs(leap_year(2400), True)
 
   def test_year_divisible_by_200_not_divisible_by_400_in_common_year(self):
       self.assertIs(leap_year(1800), False)

Hello, @enoren5. I will try not to give you the exact code :slight_smile:.
The thing I understood from the “online courseware” challenge is something like this:

  1. If year is evenly divisible by 400, it DIRECTLY is a leap year.
  2. If not, you must check these two conditions:
    • It MUSTN’T be evenly divisible by 100
    • It MUST be evenly divisible by 4
  3. If your year isn’t obeying at least one of these conditions, you must return False.

Note: You can bring together the first two conditions in just one if statement and return true inside it. And outside the if block, it will be enough to simply return false.

I hope, this will help you.

Here’s one hint: don’t try to squash everything into one line. The text you’re following has this structure:

if year % 4 == 0:
    # now do the
    #     except every year that is evenly divisible by 100
    #     unless the year is also evenly divisible by 400
    # part here
else:
    return False

Keep it as simple and straightforward and as obvious as possible until “it works”. Then, if you think you must, proceed to making it an obscure one-liner :smiley:.

Start with the simplest version of the test, that you know is wrong:

if the year is divisible by four:

    a leap year

otherwise:

    definitely not a leap year

Write that as Python. Run your unit tests and see which cases pass, and which fail.

Now that you know the tests which fail, identify which branch of the code is wrong, and make the next simplest version, which will still be wrong:

if the year is divisible by four:

    if the year is divisible by 100:

        not a leap year

    otherwise:

        a leap year

otherwise:

    definitely not a leap year

That will still be wrong, but more tests will pass. Again concentrate on the failing tests, and see how to fix them.

The important lessons here are:

  • Having good unit tests to check your results is fantastic.

  • Often, it is easier to start with code that is almost correct and gradually fix the bugs, than to try to write the correct code perfectly in one go.

Happy coding!

1 Like

This is my general approach I take to these online Python challenges. Usually the final code I come up with which passes the unit tests isn’t very Pythonic and when I share on message boards, the other veteran Python developers and professional programmers jump into the discussion showing off their prowess with list comprehension and other one liners. As impressive as those alternate solutions may seem, as a newbie I feel a sense of accomplishment and relief just finally seeing my unit tests all light up in bright green in my terminal, even if it isn’t perfect. :innocent:

Although I am always curious to learn from the varied and bizarre ‘next level’ solutions other people come up with.

I scrapped my original approach and started over basing the next iteration of my script using the above as my pseudo code. Here is the solution I arrived at:

def leap_year(year):
    if (year % 400 == 0) and (year % 4==0):
        return True
    if (year % 4 == 0) and (year % 100 != 0):
        return True
    elif (year % 100!=0) and (year % 4 != 0):
        return False  
    else:
        return False

Eureka! It worked!

Now that I have completed this exercise, I would like to hear from as many of your as possible to see your advanced Python skills showing off your concise one-liners! Let’s see what you all have to share.

Even though I have a working solution, I started over and made an additional attempt this time basing my script on @tim.one’s pseudo code above. Here is what I came up with:

def leap_year(year):
    if year % 4 == 0:
        if year % 100==0 and year % 400==0:
            return True 
    else:
        return False

Although this only passes 4 of 5 unit tests.

With the code I brought to the table in my original post, I was already doing exactly as you’ve suggested. Only half the unit tests were passing. Looking at the traceback in my shell, I would then make a slight change to correct one bug to account for the year 1900 not being a leap year (for example, like by switching a dysjunction to a conjunction or by switching an equality operator to a negation). I’d run the test again, and that Assertion would be solved, but then 2 new Assertions would fail. I’d make a few more changes intending to resolve those 2 Assertions, but then the original Assertion would re-appear. This particular exercise was finnicky because there were so many different variables in the unit tests, that my Assertions were tossing me back and forth like a tennis game.

So I just ‘nuked it from orbit’ and started over.

@sandraC’s assistance by rephasing the requirements of the exercise in different language helped tremendously. I still didn’t get it on my first try. So I persevered using @steven.daprano’s suggestion and resolved the 2 outstanding Assertions, carefully, one at a time.

Thanks, @sandraC and @tim.one and @steven.daprano!

1 Like

If you look in Lib/datetime.py in your Python distribution, you’ll find this function, which I wrote decades ago :wink:.

def _is_leap(year):
    "year -> 1 if leap year, else 0."
    return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

Note that and binds more tightly than or: the parentheses are vital there, although I’d use them (for clarity) even if they weren’t necessary.

2 Likes