'Calculating if a year is a leap year

On HackerRank, I'm attempting to solve a leap year challenge, and when I submit the code, it passes five test cases but fails one: when it tries to check whether 1992 is leap year or not. I'd appreciate it if someone could assist me with this. Here is the question below:

An extra day is added to the calendar almost every four years as February 29, and the day is called a leap day. It corrects the calendar for the fact that our planet takes approximately 365.25 days to orbit the sun. A leap year contains a leap day.

In the Gregorian calendar, three conditions are used to identify leap years:

The year can be evenly divided by 4, is a leap year, unless:

The year can be evenly divided by 100, it is NOT a leap year, unless:

The year is also evenly divisible by 400. Then it is a leap year. This means that in the Gregorian calendar, the years 2000 and 2400 are leap years, while 1800, 1900, 2100, 2200, 2300 and 2500 are NOT leap years. Source

Task

Given a year, determine whether it is a leap year. If it is a leap year, return the Boolean True, otherwise return False.

Note that the code stub provided reads from STDIN and passes arguments to the is_leap function. It is only necessary to complete the is_leap function.

And my answer for this would be :

def is_leap(year):
    leap = False
    if year%4==0 and (year%100==0) and year%400==0:
        leap=True
    elif not(year%4==0) and not(year%100==0) and not(year%400==0):
        leap=False
    else:
        leap=False
        
    return leap

year = int(input())

Link to the question -> https://ibb.co/R0tGvnf



Solution 1:[1]

Solution A

The wording in the specifications is doing this thing where it says check this condition to know if it's a leap year. And then it says I can change my mind if... But then once again I can change my mind if...

We can do the same thing in our code. We can save if it is a leap year, change our mind if it is divisible by 100, and then change our mind again if it is divisible by 400.

def is_leap(year):
    leap = False

    # The year can be evenly divided by 4, is a leap year
    if year%4 == 0:
        leap = True

    # Unless the year can be evenly divided by 100
    if year%4 == 0 and year%100 == 0:
        leap = False

    # Unless the year is also evenly divisible by 400
    if year%4 == 0 and year%100 == 0 and year%400 == 0:
        leap True

    return leap

Solution B

When you see this type of pattern in your code -- where each condition has the previous condition within it, you might wonder if there is a way it can be refactored, and there is. We can remove the extra repetition in two different ways. We can do as @alexpdev did with nested if statements, or we can combine everything into one ginormous if statement which would look something like this:

def is_leap(year):
    if (
        # The year can be evenly divided by 4, is a leap year
        <logic would go here>

        # Unless the year can be evenly divided by 100
        and not <logic would go here>

        # Unless the year is also evenly divisible by 400
        and not <logic would go here>
    ):
        return True
    return False

Here is a discussion on which method is better. Some people don't like combining everything into a single if statement when there are a lot of conditions involved, but I don't mind doing so as long as it's tactfully done (like the way Deestan mentions in this question, often accompanied with comments explaining complex logic). I will get back to using this method later, but first I want to see if we can put everything together like so

Solution C

    # doesn't work
    if year%4 == 0:
        leap = True
    elif year%4 == 0 and year%100 == 0:
        leap = False
    elif year%4 == 0 and year%100 == 0 and year%400 == 0:
        leap True

I feel like we should be able to use an if/elif chain, and it's what you were trying to do too, but we end up in trouble because we would only move on to the next elif if year%4 == 0 was false. That's never going to work, so... maybe we can do the inverse. The specification isn't that straightforward, so let's start invert it and see if we can reword it into something a little bit more procedural:

The year is a leap year unless:

  1. It is not divisible by 4
  2. It is divisible by 100 and not 400

Yes, I think this will work nicely. That's pretty straightforward to code up:

def is_leap(year):
    # It is not divisible by 4
    if year%4 != 0:
        return False

    # It is divisible by 100 and not 400
    elif year%100 == 0 and year%400 != 0:
        return False

    return True

Solution B

Cool. So that is one option for how we could code this up. Now let's finish that ginormous if statement. It seems like it could get gnarly, but this will actually produce a solution that is very elegant.

Let's start with the first condition

The year can be evenly divided by 4, is a leap year

def is_leap(year):
    return year%4 == 0

Then we combine that with

Unless the year can be evenly divided by 100

def is_leap(year):
    return year%4 == 0 and not (year%100 == 0)

As we continue on we will have to be watchful about how we combine. We are going to end up with three conditions combined together, but the third one modifies the second one only, so we will need to make sure we use parenthesis correctly here.

Unless the year is also evenly divisible by 400

def is_leap(year):
    return year%4 == 0  and not (year%100 == 0  and not (year%400 == 0))

And that will calculate if we have a leap year. If we like, we can use the fact that we x != y is the same thing as not(x==y), and we can use De Morgan's Law to convert that function into

def is_leap(year):
    return year%4 == 0 and (year%100 != 0 or year%400 == 0)

Final Solution A

This isn't normally possible, but in this specific question we can use the fact that 400 is a multiple of 100 which is a multiple of 4 to simplify solution A like so:

def is_leap(year):
    leap = False

    # If we can divide they year by 4, it's probably a leap year
    if year%4 == 0:
        leap = True

    # But every 100 years we skip a leap year
    if year%100 == 0:
        # when true it also means the year is divisible by 100
        # so we can skip checking those conditions
        leap = False

    # Unless the year is also divisible by 400
    if year%400 == 0:
        # when true it also means the year is divisible by 100 and by 4
        # so we can skip checking those conditions
        leap True

    return leap

However, we should add some comments to explain the optimization that we've done. Especially because we don't want bugs to be introduced in the future if a new condition needs to be added that can't be optimized in the same way. And with those comments the solution has become a little lengthy.

Final Solution B

Adding some comments

def is_leap(year):
    return (
        # First check if we have a normal leap year
        year%4 == 0
        # However, anything divisible by 100 is not considered a leap year
        # Unless the year is also a multiple of 400
        and (year%100 != 0 or year%400 == 0)
    )

This is a pretty elegant solution and also the fastest to execute.

Final Solution C

It just seems a little unnatural to me to be checking for False conditions and returning True as the default. Usually it's the other way around. We can add a second function called is_not_leap

def is_not_leap(year):
    # It is not divisible by 4
    if year%4 != 0:
        return True

    # It is divisible by 100 and not 400
    elif year%100 == 0 and year%400 != 0:
        return True

    return False

def is_leap(year):
    return not is_not_leap(year)

But now we have when we do year%4 != 0 it's kind of like a double negative. We're checking if it is not divisible by four to know if it is not a leap year. That's still confusing. This solution just seems unnatural to me.

The Winner

Of all three solutions, Solution B is my favorite. It doesn't have any double negatives, doesn't have hidden optimizations, it just works, and it's not that difficult to follow the logic.

Solution 2:[2]

the way your code is set up now, it completely overlooks if the year is divisible by four. It only considers it if it is also divisible by 100 and 400 simultaneously.

def is_leap(year):
    leap = False
    # this line asks if it is divisible by 400, 4, and 100 at the same time
    if year%4==0 and (year%100==0) and year%400==0:
        leap=True
    # this line asks if its not divisible by 400, 4 and 100 at the same time
    elif not(year%4==0) and not(year%100==0) and not(year%400==0):
        leap=False
    
    # you never check the case when it is divisible by 4 and not 
    # divisible by 100, and 400.  Also skipped is if it is divisible by 
    # 4 and 100, but not 400
    else:
        leap=False
        
    return leap

year = int(input())

Try this approach

def is_leap(year):
    if year%4==0:
        if year%100==0:
            if year%400==0:
                return True
            return False
        return True
    return False

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2