1

Hello StackOverflow Python community!

I want to generate Unix timestamps for the start and end of a month. I'm looking for a way to retrieve the last day (or second, or minute...) in an arbitrary month in Python.

Given datetime.date values for two consecutive months, I can get to the last value in one month by subtracting a delta value from the next like this:

import datetime

# dates without time
jan = datetime.date(2019, 1, 1)
feb = datetime.date(2019, 2, 1)

# start/end times
start_time = datetime.time(0, 0, 0, 0, tzinfo=datetime.timezone.utc)
end_time = datetime.time(23, 59, 59, 999, tzinfo=datetime.timezone.utc)

jan_start = datetime.datetime.combine(jan, start_time)
last_day = feb - datetime.timedelta (days = 1)
jan_end = datetime.datetime.combine(last_day, end_time)

do_stuff(jan_start, jan_end)

But my minimally-python-literate self is stumbling trying to find a way to plug arbitrary months in and get the same results. I'd like to end up with something like:

import datetime

# start/end times
start_time = datetime.time(0, 0, 0, 0, tzinfo=datetime.timezone.utc)
end_time = datetime.time(23, 59, 59, 999, tzinfo=datetime.timezone.utc)

dates = {
    "Jan19": datetime.date(2019, 1, 1),
    "Feb19": datetime.date(2019, 2, 1),
    "Mar19": datetime.date(2019, 3, 1),
    "Apr19": datetime.date(2019, 4, 1)
}

for i in dates: 
    start = datetime.datetime.combine(dates[i], start_time)
    end_day = dates[i].next() - datetime.timedelta (days = 1)
    end = datetime.datetime.combine(end_day, end_time) 
    do_stuff(start, end)

Only, whoops - Python dictionaries are unordered, so there isn't a next() method I can call on i!

It's possible the conventional way to do this is to use midnight on February 1st as equivalent to the last second in January 31st, but I'm curious as to how one can retrieve the last day (or second, or minute...) in an arbitrary month in Python.

Would I need to use an OrderedList, or is there some module out there immune to my googling that has a month.max() property I could use instead of working it out dynamically?

nikobelia
  • 4,595
  • 1
  • 19
  • 27
  • 1
    Have you tried getting the first day of the following month and then subtracting one second using time delta? Getting the first date of the following month is just adding one to the month. – Rashid 'Lee' Ibrahim Aug 30 '19 at 14:38
  • What do you plan to do with the timestamps? Depending on your use case, it may be simpler to operate on a calendar with 28-day "months" that is not necessarily synchronized with any particular solar calendar. – chepner Aug 30 '19 at 14:42
  • 1
    I guess not an exact duplicate, but the discussion of getting the last day of a month is in [this question](https://stackoverflow.com/questions/42950/get-last-day-of-the-month-in-python). – glibdud Aug 30 '19 at 14:43
  • @chepner, that would be easier, but unfortunately I need calendar months! – nikobelia Aug 30 '19 at 16:51
  • @glibdud that calendar module monthrange method looks perfect for what I was after, thank you! happy to close this as a dupe of that, it solves the core problem I was hitting. – nikobelia Aug 30 '19 at 16:53

3 Answers3

4

To get the first day of the next month, one option (there are others) could be:

>>> import datetime
>>> my_date = datetime.datetime(2019, 4, 5)
>>> my_date
datetime.datetime(2019, 4, 5, 0, 0)
>>> first_day_this_month = datetime.datetime(my_date.year, my_date.month, 1)
>>> first_day_this_month
datetime.datetime(2019, 4, 1, 0, 0)

>>> some_day_next_month = first_day_this_month + datetime.timedelta(days=32)
>>> some_day_next_month
datetime.datetime(2019, 5, 3, 0, 0)
>>> first_day_next_month = datetime.datetime(some_day_next_month.year, some_day_next_month.month, 1)
>>> first_day_next_month
datetime.datetime(2019, 5, 1, 0, 0)

Now to get the last second of the current month, one could do:

>>> last_second_this_month = first_day_next_month - datetime.timedelta(seconds=1)
>>> last_second_this_month
datetime.datetime(2019, 4, 30, 23, 59, 59)

>>> import time
>>> time.mktime(last_second_this_month.timetuple())
1556683199.0
Ralf
  • 16,086
  • 4
  • 44
  • 68
0

Rather than trying to find "the last second of the month", you'd be better off just getting "the first moment of the next month" (which is much easier to find), and then modify your do_stuff() function to simply exclude the second argument.

You can do this in one of a couple ways. First, using < instead of <=. Or second, by simply subtracting an arbitrarily small timedelta from the first moment of the next month:

# microseconds is the smallest granularity that python datetime supports
end = datetime.datetime.combine(end_day, start_time) - datetime.timedelta(microseconds=1)

Depending on what you actually want to do here, I'm personally working on a module called ranges which would provide a continuous Range class (so you could go from January 1st inclusive to February 1st exclusive), and will probably be released by mid-september 2019. This might be an easier way to cover your use case once it's finished, I dunno.

Green Cloak Guy
  • 23,793
  • 4
  • 33
  • 53
  • Thanks, but the problem I'm having is that I don't know how to get that 'end_day' value! The suggestion from @Ralf (add 32 days to my start value, then grab the start of that month and take an arbitrarily small number away) solves this - I'm just kinda surprised there's not a cleaner way to do it! – nikobelia Aug 30 '19 at 16:50
0

Because years always have 12 months, you can increment the month and avoid making assumptions about the number of days in a month altogether. Then use timestamp from Python 3.3+ to get the Unix time:

import datetime

def add_month(month: int, year: int) -> tuple[int, int]:
    if month < 12:
        return (month + 1, year)
    return (1, year + 1)

# ...

feb = datetime.date(2019, 2, 1)
end_month, end_year = add_month(feb.month, feb.year)
end_of_month = (datetime.datetime(end_year, end_month, 1, tzinfo=datetime.timezone.utc) - datetime.timedelta(seconds=1)).timestamp() # 1551398399.0
start_of_month = datetime.datetime(feb.year, feb.month, 1, tzinfo=datetime.timezone.utc).timestamp() # 1548979200.0
Evan Byrne
  • 1,105
  • 1
  • 12
  • 17