1
\$\begingroup\$

What is a good way to get rid of these if/elif/else inside the for loop? How you would improve readability of it?

from datetime import datetime
from calendar import monthrange


def iter_completed_months(initial_date, today):
    delta_months_years = (today.year - initial_date.year)*12
    delta_months_current_year = today.month - initial_date.month
    return delta_months_years + delta_months_current_year


initial_date = datetime(2021, 11, 30)
today = datetime.now()

# populate a list that goes from initial_date until today with all the dates
# representing the very end of each month
year = initial_date.year
month = initial_date.month
end_month_dates = []

for _iter_month in range(iter_completed_months(initial_date, today)):
    if month <= 12:
        day = monthrange(year, month)[1]
    elif month > 12:
        year = year + 1
        month = 1
        day = monthrange(year, month)[1]
    else:
        day = monthrange(year, month)[1]

    end_month_dates.append(datetime(year, month, day))
    month = month + 1


print(end_month_dates)

this code populates a list end_month_dates with all "end of months" from initial_date to today.

\$\endgroup\$
3
  • \$\begingroup\$ Please do not edit the question, especially the code, after an answer has been posted. Changing the question may cause answer invalidation. Everyone needs to be able to see what the reviewer was referring to. What to do after the question has been answered. You can ask a follow up question with a link back to this question. \$\endgroup\$
    – pacmaninbw
    Mar 20 at 0:50
  • \$\begingroup\$ Hi, I rolled back your last update. As pacmaninbw pointed out, do not edit your question after answers have been posted. If you want your updated code to be reviewed again, post a new question possibly with a link to this one. \$\endgroup\$
    – slepic
    Mar 20 at 5:29
  • 1
    \$\begingroup\$ I have also updated the title of your question to fit the standard of this site - that is to state the task accomplished by your code. Please get inspired in your next question. \$\endgroup\$
    – slepic
    Mar 20 at 5:34

3 Answers 3

2
\$\begingroup\$

Put your code in functions. The benefits are numerous. Even if you don't appreciate them fully yet, you should do it anyway. Stand on the shoulders of the computing giants, who knew that functions were the way to go.

You are using datetimes, but dates seem more appropriate. Based on the information you have given us, the function should reason with dates.

A simpler approach. Compute the month start and end dates. Yield the latter if it's still within the desired bounds. Then advance to the next month by adding 31 days to the month start. [See the comment from RootTwo for a further simplification, allowing one to drop month_start.]

from calendar import monthrange
from datetime import date, timedelta

def last_dates_of_months(start_date, end_date):
    d = start_date
    while True:
        # Compute first and last dates of the month.
        month_start = d.replace(day = 1)
        n_days = monthrange(d.year, d.month)[1]
        month_end = d.replace(day = n_days)
        # Yield or break.
        if month_end <= end_date:
            yield month_end
        else:
            break
        # Advance to a date in the next month.
        d = month_start + timedelta(days = 31)
\$\endgroup\$
4
  • \$\begingroup\$ this is a great feedback. I adjusted things to be inside functions from this early stage. I'm just wondering if I should use yield for something that is simple and such a short list. will it be more or less readable with yield? \$\endgroup\$
    – Pabluez
    Mar 19 at 22:53
  • 1
    \$\begingroup\$ @Pabluez I implemented in with yield mostly to save myself keystrokes, but switching it to return a list would be perfectly reasonable. That decision depends on how you intend to use the function. \$\endgroup\$
    – FMc
    Mar 19 at 23:04
  • 2
    \$\begingroup\$ Just add 1 day to month_end to advance to the first day of the next month. Python typically excludes the end of a range, but the test if month_end <= end_date causes the code to include the end of the range if it falls on the last day of the month. \$\endgroup\$
    – RootTwo
    Mar 21 at 0:00
  • \$\begingroup\$ @RootTwo Yes, that a better idea -- no need for month_start at all. Regarding end of range, the code behaves the same as the OP's original code, so I assume that's what was desired. \$\endgroup\$
    – FMc
    Mar 21 at 0:40
2
\$\begingroup\$

Fixing your ranges, and making the function iterable

This function name implies it is iterable, when it isn't. To make it iterable, it might be better to yield each month. However, you can just calculate the months themselves using some functions from itertools. islice will allow you to skip portions of an iterator, and chain will allow you to stick multiple iterators together.

While we're here, we can go ahead and make sure that the months number will never be outside of the range 1-12 by simply using range(1, 13). This will pretty much eliminate your if statements.

from itertools import islice, chain


def get_months(start, end=today):
    # calculate the first year by using `islice` to 
    # cut off the starting months that were skipped
    first_year = ((start.year, month) for month in islice(range(1, 13), start.month - 1, None))

    # same with the last year for the months that are past the end
    # date
    last_year = ((end.year, month) for month in islice(range(1, 13), end.month))

    # then use a loop to get the months in between the two
    in_between = ((year, month) for year in range(start.year + 1, end.year) for month in range(1, 13))

    # chain them together
    months = chain(first_year, in_between, last_year)

    # yield all of the values
    yield from months

Now that this function is actually iterable and we have fixed the range issues, you no longer need your if statement:

# Now to calculate, simply iterate over the function call
# and calculate the end days
for year, month in get_months(start):
    _, day = monthrange(year, month)

    dt = datetime(year, month, day)

    end_month_dates.append(dt)
        
\$\endgroup\$
1
  • \$\begingroup\$ thanks for the feedback, I'm just wondering if adding yield, chain and islice will improve the readability. It's not a performance thing. what do you think? \$\endgroup\$
    – Pabluez
    Mar 19 at 22:57
2
\$\begingroup\$

General

iter_completed_months is a poor name for a function that doesn't iterate over the months or return an iterator. The function returns the number of months in the given date range. Perhaps number_of_months would be a better name. Similarly _iter_month is a number, not an iterator.

By convention, starting a name with '_' means the name is not part of the public interface for a class or module. So avoid using a leading _ unless that is what you are doing.

The else clause is never executed in this code, because the if and elif clauses cover all possible values of month

if month <= 12:
    day = monthrange(year, month)[1]
elif month > 12:
    year = year + 1
    month = 1
    day = monthrange(year, month)[1]
else:
    day = monthrange(year, month)[1]
    

Algorithm

Observing that the last day of a month is 1 day before the first day of the next month leads to very simple code:

from datetime import date, timedelta

ONE_DAY = timedelta(days=1)

def month_ends(start, end):
    """Produces a sequence of datetime.date() objects corresponding to the ends of the 
    month between start and end.
    """

    year = start.year
    month = start.month

    # The last day of a month is the first of the next month minus one day
    while True:
        month += 1
        if month > 12:
            month = 1
            year += 1

        month_end = date(year, month, 1) - ONE_DAY

        if month_end >= end:
            break

        yield month_end
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.