30

I have the following code below.

I would like to roundup TIME to the nearest 30 minutes in the hour. For example: 12:00PM or 12:30PM and so on.

EASTERN_NOW = timezone.localtime(timezone.now() + timedelta(minutes=30))

TIME = datetime.time(EASTERN_NOW.time().hour, EASTERN_NOW.time().minute).strftime(
    VALID_TIME_FORMATS[2])
Asclepius
  • 57,944
  • 17
  • 167
  • 143
user875139
  • 1,599
  • 4
  • 23
  • 44

14 Answers14

43

To round up to the nearest 30 minutes:

#!/usr/bin/env python3
from datetime import datetime, timedelta

def ceil_dt(dt, delta):
    return dt + (datetime.min - dt) % delta

now = datetime.now()
print(now)    
print(ceil_dt(now, timedelta(minutes=30)))

The formula is suggested by @Mark Dickinson (for a different question).

Output

2015-09-22 19:08:34.839915
2015-09-22 19:30:00

Note: if the input is timezone-aware datetime object such as EASTERN_NOW in your case then you should call timezone.make_aware(rounded_dt.replace(tzinfo=None)) if you want to preserve the rounded local time and to attach the correct tzinfo, otherwise you may get wrong timezone info if the rounding crosses DST boundaries. Or to avoid failing for ambiguous local time, call .localize() manually:

localize = getattr(rounded_dt.tzinfo, 'localize', None)
if localize:
   rounded_dt = localize(rounded_dt.replace(tzinfo=None),
                         is_dst=bool(rounded_dt.dst()))
Community
  • 1
  • 1
jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • I'm getting `TypeError: unsupported operand type(s) for %: 'datetime.timedelta' and 'datetime.timedelta'` – dan-klasson Jul 11 '17 at 19:25
  • 3
    @dan-klasson notice: the python3 shebang in the script – jfs Jul 11 '17 at 19:26
  • 1
    Any Python 2.x equivalent for this? – Rainymood Apr 18 '18 at 08:32
  • @Rainymood: click the link in the answer and look at other answers (the idea is to convert the timedelta to seconds: 1800 in this case). See whether [replacing 900 with 1800 works](https://stackoverflow.com/a/13071613/4279) – jfs Apr 18 '18 at 09:10
  • When passing a timezone-aware dt to ceil_dt(), I get: TypeError: can't subtract offset-naive and offset-aware datetimes – greenie2600 Dec 02 '22 at 19:30
  • @greenie2600 pass naive dt. You could use `replace(tzinfo=None)` – jfs Dec 02 '22 at 21:07
13

to round forward you can use :

#!/usr/bin/env python3
from datetime import datetime, timedelta

def ceil_dt(dt, delta):
    return dt + (datetime.min - dt) % delta

now = datetime.now()
print(now)    
print(ceil_dt(now, timedelta(minutes=30)))

To round back to the nearest 30th minute

def rounded_to_the_last_30th_minute_epoch():
    now = datetime.now()
    rounded = now - (now - datetime.min) % timedelta(minutes=30)
    return rounded
Thomas John
  • 2,138
  • 2
  • 22
  • 38
8

To round down and up you can use these two functions. You don't need to calculate seconds or use math module.

from datetime import timedelta, datetime

def ceil_date(date, **kwargs):
    secs = timedelta(**kwargs).total_seconds()
    return datetime.fromtimestamp(date.timestamp() + secs - date.timestamp() % secs)

def floor_date(date, **kwargs):
    secs = timedelta(**kwargs).total_seconds()
    return datetime.fromtimestamp(date.timestamp() - date.timestamp() % secs)

Usage is similar to timedelta because kwargs is passed

now = datetime.now() #15:12:12

ceil_date(now, minutes=5) #15:15:00
floor_date(now, minutes=5) #15:10:00
wizardzeb
  • 1,546
  • 2
  • 14
  • 30
3

You can divide your minutes by 30, round that and multiply by 30 again to get either 0, 30 or 60 minutes:

date = datetime.datetime(2015, 9, 22, 12, 35)
approx = round(date.minute/30.0) * 30

date = date.replace(minute=0)
date += datetime.timedelta(seconds=approx * 60)
time = date.time()
print(time.strftime('%H:%M'))
# prints '13:30'

I'm using a datetime object because timedelta doesn't work with time objects. In the end you can obtain the time using date.time().

multivac
  • 146
  • 5
  • great..do you have any idea on how to round down to the nearest 10 or 20 minutes.. – Azam Jan 01 '21 at 06:40
  • 1
    @Azam The same way. You divide your minutes by 10, drop the remainder then multiply it by 10. So 36 minutes will be first 3.6, then you drop the fractional part (or use integer divide which only leaves the whole numbers) then you multiply by 10 -> 30. Same for 20 minutes. 36 / 20 -> 1.8, drop the fractional part that leaves 1, multiply by 20 -> 20 minutes – karatedog Jul 16 '21 at 19:54
3

you can just take datetime input apart and ajust time

 def ajustTime():

from datetime import datetime

mytime= datetime.now().strftime("%Y-%m-%d %H-%M")

m = mytime.split()

hours, mints = m[1].split('-')

if 15 <= int(mints) <= 45:
    mints = ':30'
elif int(mints) < 15:
    mints = ':00'
elif int(mints) > 45:
    mints = ':00'
    h = int(hours) + 1
    hours = str(h)

print(m[0] + " " + hours + mints)

ajustTime()

output

2015-09-22 15:42:03.587633

2015-09-22 15:30

2015-09-22 15:46:01.956860

2015-09-22 16:00

LetzerWille
  • 5,355
  • 4
  • 23
  • 26
2
>>> from dateutil.rrule import rrule, MINUTELY
>>> import datetime
>>> import bisect
>>> times = list(rrule(MINUTELY,interval=30,dtstart=datetime.date.today(),count=
48))
>>> print times[bisect.bisect(times,datetime.datetime.now())]
2015-09-22 11:00:00
>>>

Note that this solution uses the 3rd party dateutil library that can be installed with pip install dateutil... Of course, you could solve it without it... but it's easier with it.

Community
  • 1
  • 1
Joran Beasley
  • 110,522
  • 12
  • 160
  • 179
2

To round down:

  • extract the minute from the time that you want to round
  • using floor division, find the whole number intervals there are in the current hour (from the floor division of the current minute and the time interval, i.e. the intervals that you want to round down to).
  • multiply the time interval by that whole number, which gives you the rounded down minutes
  • use .replace() to change the minutes to those rounded down minutes (and seconds and milliseconds to zero).

Or, in words, using the number of whole intervals (x) there are in the current minutes, replace the minutes by that number of intervals (x * interval):

#!/usr/bin/env python3
def floor_dt(dt, interval):
    replace = (dt.minute // interval)*interval
    return dt.replace(minute = replace, second=0, microsecond=0)

print(datetime.now())
# datetime.datetime(2019, 5, 30, 22, 25, 31, 115901)
print(floor_dt(datetime.now(),30))
# datetime.datetime(2019, 5, 30, 22, 0)

print(datetime.now())
# datetime.datetime(2019, 5, 30, 22, 26, 29, 469555)
print(floor_dt(datetime.now(),10))
# datetime.datetime(2019, 5, 30, 22, 20)
myles
  • 175
  • 1
  • 3
  • 9
2

I have written this function that rounds to any unit from fraction of seconds to one day:

from datetime import datetime
from datetime import timedelta

def round_time(dt: datetime, unit=timedelta(seconds=1)):
    seconds = (dt - datetime.min).total_seconds()
    unit_seconds = unit.total_seconds()
    half_over = seconds + unit_seconds / 2
    rounded_seconds = half_over - half_over % unit_seconds
    return datetime.min + timedelta(seconds=rounded_seconds)

Outputs

dt
# datetime.datetime(2022, 2, 3, 17, 44, 56, 659595)

round_time(dt)
# datetime.datetime(2022, 2, 3, 17, 44, 57)

round_time(dt, timedelta(seconds=5))
# datetime.datetime(2022, 2, 3, 17, 44, 55)

round_time(dt, timedelta(seconds=0.25))
# datetime.datetime(2022, 2, 3, 17, 44, 56, 750000)

round_time(dt, timedelta(minutes=1))
# datetime.datetime(2022, 2, 3, 17, 45)

round_time(dt, timedelta(minutes=30))
# datetime.datetime(2022, 2, 3, 17, 30)

round_time(dt, timedelta(hours=1))
# datetime.datetime(2022, 2, 3, 18, 0)

round_time(dt, timedelta(hours=12))
# datetime.datetime(2022, 2, 3, 12, 0)

round_time(dt, timedelta(days=1))
# datetime.datetime(2022, 2, 4, 0, 0)
David
  • 2,942
  • 33
  • 16
1

this should work too, not sure about time zones though

rounded=time.gmtime(30*60*(round(time.time()/(30*60))))
karakfa
  • 66,216
  • 7
  • 41
  • 56
1

Thanks for all the input guys. I solved this with my own approach.

min_time = timezone.localtime(timezone.now())
min_time_est = min_time.minute 
if min_time_est > 30:
    add_mins = 60 - min_time_est
else:
    add_mins = 30 - min_time_est

EASTERN_NOW = timezone.localtime(timezone.now() + timedelta(minutes=add_mins))
TIME = datetime.time(EASTERN_NOW.time().hour, EASTERN_NOW.time().minute).strftime(
    VALID_TIME_FORMATS[2])

In case anyone else has a similar problem. The about 'TIME' outputs every 30 mins e.g '1:00PM' or '1:30PM'.

user875139
  • 1,599
  • 4
  • 23
  • 44
  • use `add_mins = -min_time.minute % 30` (your code skips 30 minutes unnecessarily if `.minute==0`). Your code may fail if changes in the utc offset for the current timezone are not aligned with 30 minutes ([my answer calls `localize()` to keep the time while getting the correct tzinfo](http://stackoverflow.com/a/32724959/4279)) – jfs Sep 22 '15 at 21:28
  • use `eastern_now.strftime(valid_time_format)` (no need to call `eastern_now.time()`, no need to construct `datetime.time()` again). – jfs Sep 22 '15 at 21:32
1

Round up to nearest amount specified in minutes via round_mins without using third-party libraries and just a few lines of code.

import datetime
round_mins = 30
now = datetime.datetime.now()
mins = now.minute - (now.minute % round_mins)
print datetime.datetime(now.year, now.month, now.day, now.hour, mins) + datetime.timedelta(minutes=round_mins)
Matt
  • 3,483
  • 4
  • 36
  • 46
  • 3
    Please provide a bit of explanation. Code-only answers are often not as helpful to people who come here with the same problem in the future. – Engineero Sep 22 '17 at 19:21
0

The function below can round any time string to hours, minutes, seconds by setting parmeter 'unit', default is the smallest format of ts. Parameter 'ts' is the time string you want to round, the fuction accept multiple time formats. With parmeter 'rnd' you can set the number you want to round at, default is 1. With parmeter 'frm' you can set the format of the output string, as default the function takes the format of ts. Just play with the function and you will see how easy it will be. The function even convert time strings between 12 and 24 hours format and vice versa. All this with a minimum on import.

def toNearestTime(ts, unit=None, rnd=1, frm=None):

    ''' round to the Nearest Time
    param ts = auto recognize the most time patterns : not date-time
    param unit = specify unit wich must be rounded 'sec' or 'min' or 'hour', default is the smallest unit of ts :
    param rnd = to which number you will round, the default is 1 :
    param frm = the output (return) format of the time string you want, as default the function take the ts format'''

    from time import strftime, gmtime, strptime
    ustp = ["AM", 'PM']

    if any(e in ts for e in ustp):
        time12_pat = {'%I:%M%p': 'm', '%I:%M %p': 'm', '%I:%M:%S%p': 'se', '%I:%M:%S %p': 'se', '%I%M%p': 'm', '%I%M %p': 'm', '%I%M%S%p': 'se', '%I%M%S %p': 'se'}
        for p, u in time12_pat.items():
            try:
                ts = strftime('%H:%M:%S', strptime(ts, p))
                break
            except:
                continue
        sf = p
        unit = time12_pat[sf] if unit is None else unit
    else:
        time24_pat = {'%H:%M': 'm', '%H:%M:%S': 'se', '%H': 'h', '%H%M': 'm', '%H%M%S': 'se'}
        for p, u in time24_pat.items():
            try:
                ts = strftime('%H:%M:%S', strptime(ts, p))
                break
            except:
                continue
        sf = p
        unit = time24_pat[sf] if unit is None else unit
    if 'se' in unit.lower():
        frm = sf if frm is None else frm
    elif 'm' in unit.lower():
        frm = sf if frm is None else frm
        rnd = rnd * 60
    elif 'h' in unit.lower():
        frm = p if frm is None else frm
        rnd = rnd * 3600
    secs = sum(int(x) * 60 ** i for i, x in enumerate(reversed(ts.split(':'))))
    rtm = int(round(secs / rnd, 0) * rnd)
    nt = strftime(frm, gmtime(rtm))
    return nt

Call function as follow: Round to nearest 5 minutes with ouput format = hh:mm as follow

ts = '02:27:29'
nt = toNearestTime(ts, unit='min', rnd=5, frm='%H:%M')
print(nt)
output: '02:25'

Or round to nearest 30 minutes with ouput 12 hour format hh:mm:ss as follow

ts = '11:14PM'
nt = toNearestTime(ts, rnd=30, frm='%I:%M:%S %p')
print(nt)
output: '11:00:00 PM'
0

here a more generic approach if you need to round time up and down:

from datetime import time, datetime, timedelta

def round_time(source: time, minutes: int, floor: bool = True) -> time:
    source = datetime(year=2021, month=1, day=1, hour=source.hour, minute=source.minute)
    result = source + (datetime.min - source) % timedelta(minutes=minutes)
    if floor and source != result:
        return (result - timedelta(minutes=minutes)).time()
    return result.time()

Test:

import pandas as pd
import pytest

from mpo.model.helper import round_time

time_format = "%d.%m.%Y %H:%M"


@pytest.mark.parametrize(
    "testname, source_time, expected_time, minutes, floor",
    [
        ("no round f on round", "01.01.2021 12:00", "01.01.2021 12:00", 30, True),
        ("no round c on round", "01.01.2021 12:00", "01.01.2021 12:00", 30, False),
        ("round f hh", "01.01.2021 12:45", "01.01.2021 12:30", 30, True),
        ("round c hh", "01.01.2021 12:45", "01.01.2021 13:00", 30, False),
        ("round f 1h", "01.01.2021 12:30", "01.01.2021 12:00", 60, True),
        ("round c 1h", "01.01.2021 12:30", "01.01.2021 13:00", 60, False),
        ("round c epex", "01.01.2021 09:20", "01.01.2021 09:00", 60, True),
        ("round c nord", "01.01.2021 09:50", "01.01.2021 09:00", 60, True),
        ("round c epex", "01.01.2021 09:20", "01.01.2021 10:00", 60, False),
        ("round c nord", "01.01.2021 09:50", "01.01.2021 10:00", 60, False),
    ],
)
def test_round_time(testname, source_time, expected_time, minutes, floor):
    source = pd.to_datetime(source_time, format=time_format).time()
    expect = pd.to_datetime(expected_time, format=time_format).time()
    result = round_time(source=source, minutes=minutes, floor=floor)
    assert result == expect
Macilias
  • 3,279
  • 2
  • 31
  • 43
0

If having a datetime.datetime object, this can be often be done using pandas.Timestamp.ceil.

> import datetime
> import pandas as pd

> dt = datetime.datetime.now()
> dt
atetime.datetime(2022, 10, 18, 12, 55, 2, 260198)

> dt_ceiled = pd.Timestamp(dt).ceil('30T').to_pydatetime()
> dt_ceiled
datetime.datetime(2022, 10, 18, 13, 0)

If you use a time zone, e.g. America/New_York, check to ensure it also works with the time zone.

Asclepius
  • 57,944
  • 17
  • 167
  • 143