如何将datetime对象的分钟数四舍五入

133

我有一个使用strptime()函数生成的datetime对象。

>>> tm
datetime.datetime(2010, 6, 10, 3, 56, 23)

我需要做的是将分钟数四舍五入到最接近的十分钟。到目前为止,我一直在使用round()函数来对分钟数进行处理。

min = round(tm.minute, -1)
然而,与上述示例相同,当分钟值大于56时,它会给出一个无效的时间。例如:3:60。有更好的方法吗?datetime支持这个吗?

3
今天,时间戳有一个名为 floor(...) 的方法。 - Jus
22个回答

168

这将获取存储在tm中的datetime对象的“floor”,并将其舍入到tm之前的10分钟标记。

tm = tm - datetime.timedelta(minutes=tm.minute % 10,
                             seconds=tm.second,
                             microseconds=tm.microsecond)

如果你想要对最接近的10分钟做经典舍入,可以这样做:

discard = datetime.timedelta(minutes=tm.minute % 10,
                             seconds=tm.second,
                             microseconds=tm.microsecond)
tm -= discard
if discard >= datetime.timedelta(minutes=5):
    tm += datetime.timedelta(minutes=10)

或者这样:

tm += datetime.timedelta(minutes=5)
tm -= datetime.timedelta(minutes=tm.minute % 10,
                         seconds=tm.second,
                         microseconds=tm.microsecond)

115

通用函数来将日期时间四舍五入到任意秒的时间间隔:

def roundTime(dt=None, roundTo=60):
   """Round a datetime object to any time lapse in seconds
   dt : datetime.datetime object, default now.
   roundTo : Closest number of seconds to round to, default 1 minute.
   Author: Thierry Husson 2012 - Use it as you want but don't blame me.
   """
   if dt == None : dt = datetime.datetime.now()
   seconds = (dt.replace(tzinfo=None) - dt.min).seconds
   rounding = (seconds+roundTo/2) // roundTo * roundTo
   return dt + datetime.timedelta(0,rounding-seconds,-dt.microsecond)

1小时舍入和30分钟舍入的样本:

print roundTime(datetime.datetime(2012,12,31,23,44,59,1234),roundTo=60*60)
2013-01-01 00:00:00

print roundTime(datetime.datetime(2012,12,31,23,44,59,1234),roundTo=30*60)
2012-12-31 23:30:00

16
很遗憾,这种方法无法处理带有时区信息的日期时间。应该使用dt.replace(hour=0, minute=0, second=0)代替dt.min。请注意,这样可以得到相同的效果,但是会保留时区信息。 - skoval00
4
根据您的建议,我和@skoval00修改了代码以支持带有时区信息的日期时间。感谢您的帮助! - Le Droid
谢谢@skoval00 - 我花了一些时间才弄清楚为什么这个函数不能处理我的数据。 - Michel Mesquita
1
这对我来说长时间完全不起作用。例如,roundTime(datetime.datetime(2012,12,31,23,44,59,1234),roundTo=60*60*24*7)roundTime(datetime.datetime(2012,12,30,23,44,59,1234),roundTo=60*60*24*7) - CPBL
@LeDroid:谢谢,非常正确。但是你的代码说“任何秒的时间间隔”。我不确定3天比7天更难想象:我想在任何情况下,你都需要选择一个任意的开始日期。无论如何,我同意这应该是一个新问题,但你的代码可以澄清(或强制执行)其限制。 - CPBL
显示剩余3条评论

21

我使用了Stijn Nevens的代码(感谢Stijn),并有一个小的附加功能要分享:四舍五入、向下取整和向上取整。

更新于2019-03-09 = Spinxz的评论已被纳入,谢谢。

更新于2019-12-27 = Bart的评论已被纳入,谢谢。

已测试日期差为“X小时”、“X分钟”或“X秒”。

import datetime

def round_time(dt=None, date_delta=datetime.timedelta(minutes=1), to='average'):
    """
    Round a datetime object to a multiple of a timedelta
    dt : datetime.datetime object, default now.
    dateDelta : timedelta object, we round to a multiple of this, default 1 minute.
    from:  https://dev59.com/tnA75IYBdhLWcg3wH1NB
    """
    round_to = date_delta.total_seconds()
    if dt is None:
        dt = datetime.now()
    seconds = (dt - dt.min).seconds

    if seconds % round_to == 0 and dt.microsecond == 0:
        rounding = (seconds + round_to / 2) // round_to * round_to
    else:
        if to == 'up':
            # // is a floor division, not a comment on following line (like in javascript):
            rounding = (seconds + dt.microsecond/1000000 + round_to) // round_to * round_to
        elif to == 'down':
            rounding = seconds // round_to * round_to
        else:
            rounding = (seconds + round_to / 2) // round_to * round_to

    return dt + datetime.timedelta(0, rounding - seconds, - dt.microsecond)

# test data
print(round_time(datetime.datetime(2019,11,1,14,39,00), date_delta=datetime.timedelta(seconds=30), to='up'))
print(round_time(datetime.datetime(2019,11,2,14,39,00,1), date_delta=datetime.timedelta(seconds=30), to='up'))
print(round_time(datetime.datetime(2019,11,3,14,39,00,776980), date_delta=datetime.timedelta(seconds=30), to='up'))
print(round_time(datetime.datetime(2019,11,4,14,39,29,776980), date_delta=datetime.timedelta(seconds=30), to='up'))
print(round_time(datetime.datetime(2018,11,5,14,39,00,776980), date_delta=datetime.timedelta(seconds=30), to='down'))
print(round_time(datetime.datetime(2018,11,6,14,38,59,776980), date_delta=datetime.timedelta(seconds=30), to='down'))
print(round_time(datetime.datetime(2017,11,7,14,39,15), date_delta=datetime.timedelta(seconds=30), to='average'))
print(round_time(datetime.datetime(2017,11,8,14,39,14,999999), date_delta=datetime.timedelta(seconds=30), to='average'))
print(round_time(datetime.datetime(2019,11,9,14,39,14,999999), date_delta=datetime.timedelta(seconds=30), to='up'))
print(round_time(datetime.datetime(2012,12,10,23,44,59,7769),to='average'))
print(round_time(datetime.datetime(2012,12,11,23,44,59,7769),to='up'))
print(round_time(datetime.datetime(2010,12,12,23,44,59,7769),to='down',date_delta=datetime.timedelta(seconds=1)))
print(round_time(datetime.datetime(2011,12,13,23,44,59,7769),to='up',date_delta=datetime.timedelta(seconds=1)))
print(round_time(datetime.datetime(2012,12,14,23,44,59),date_delta=datetime.timedelta(hours=1),to='down'))
print(round_time(datetime.datetime(2012,12,15,23,44,59),date_delta=datetime.timedelta(hours=1),to='up'))
print(round_time(datetime.datetime(2012,12,16,23,44,59),date_delta=datetime.timedelta(hours=1)))
print(round_time(datetime.datetime(2012,12,17,23,00,00),date_delta=datetime.timedelta(hours=1),to='down'))
print(round_time(datetime.datetime(2012,12,18,23,00,00),date_delta=datetime.timedelta(hours=1),to='up'))
print(round_time(datetime.datetime(2012,12,19,23,00,00),date_delta=datetime.timedelta(hours=1)))

这对我很有帮助。我想补充一点,如果在 PySpark 中使用它,请将日期时间解析为字符串而不是日期时间对象。 - Max
5
“向上”取整可能不符合大多数人的预期。即使 dt 不需要四舍五入,您也会将其向上舍入到下一个 date_delta:例如,当 round_to = 60 时,15:30:00.000 将变为 15:31:00.000。 - spinxz
这个函数的“向上”舍入方式在任何情况下都不准确;当date_delta等于30秒且to='up'时,2019-11-07 14:39:00.776980的结果是2019-11-07 14:39:00 - Bart
1
非常感谢!虽然“向上”取整可能不是常见的用例,但在处理以分钟为边界开始的应用程序时是必需的。 - Rahul Bharadwaj
非常好的答案,提供了四舍五入和向下取整的选项,对我非常有用! - Cairan Van Rooyen

20

我从最佳答案进行了修改,使用了仅限于datetime对象的适应版本,这避免了将时间转换为秒并使调用代码更易读:

def roundTime(dt=None, dateDelta=datetime.timedelta(minutes=1)):
    """Round a datetime object to a multiple of a timedelta
    dt : datetime.datetime object, default now.
    dateDelta : timedelta object, we round to a multiple of this, default 1 minute.
    Author: Thierry Husson 2012 - Use it as you want but don't blame me.
            Stijn Nevens 2014 - Changed to use only datetime objects as variables
    """
    roundTo = dateDelta.total_seconds()

    if dt == None : dt = datetime.datetime.now()
    seconds = (dt - dt.min).seconds
    # // is a floor division, not a comment on following line:
    rounding = (seconds+roundTo/2) // roundTo * roundTo
    return dt + datetime.timedelta(0,rounding-seconds,-dt.microsecond)

1小时和15分钟取整的样例:

print roundTime(datetime.datetime(2012,12,31,23,44,59),datetime.timedelta(hour=1))
2013-01-01 00:00:00

print roundTime(datetime.datetime(2012,12,31,23,44,49),datetime.timedelta(minutes=15))
2012-12-31 23:30:00

1
以下代码不可用:print roundTime(datetime.datetime(2012,12,20,23,44,49),datetime.timedelta(days=15)) 输出结果是 2012-12-20 00:00:00。相反,使用以下代码: print roundTime(datetime.datetime(2012,12,21,23,44,49),datetime.timedelta(days=15)) 输出结果为 2012-12-21 00:00:00 - CPBL
4
回复以上内容:只是指出它不能适用于任意的时间间隔,例如超过1天的时间间隔。这个问题是关于舍入分钟的,所以这是一个适当的限制,但代码的编写方式可能需要更清晰明确。 - CPBL

17

这里有一个更简单的通用解决方案,不会出现浮点精度问题,也不需要外部库依赖:

import datetime

def time_mod(time, delta, epoch=None):
    if epoch is None:
        epoch = datetime.datetime(1970, 1, 1, tzinfo=time.tzinfo)
    return (time - epoch) % delta

def time_round(time, delta, epoch=None):
    mod = time_mod(time, delta, epoch)
    if mod < delta / 2:
       return time - mod
    return time + (delta - mod)

def time_floor(time, delta, epoch=None):
    mod = time_mod(time, delta, epoch)
    return time - mod

def time_ceil(time, delta, epoch=None):
    mod = time_mod(time, delta, epoch)
    if mod:
        return time + (delta - mod)
    return time

就您的情况而言:

>>> tm = datetime.datetime(2010, 6, 10, 3, 56, 23)
>>> time_round(tm, datetime.timedelta(minutes=10))
datetime.datetime(2010, 6, 10, 4, 0)
>>> time_floor(tm, datetime.timedelta(minutes=10))
datetime.datetime(2010, 6, 10, 3, 50)
>>> time_ceil(tm, datetime.timedelta(minutes=10))
datetime.datetime(2010, 6, 10, 4, 0)

这是 time round to 函数。如何制作“时间 floor to”函数?我的意思是,例如如果时间在00:00和00:10之间,则将其放置在00:00。如果它在00:10和00:20之间,则将其放置在00:10等。 - s.paszko
1
@s.paszko在time_round函数中,将以if mod < (delta / 2):开头的三行替换为一行return time - mod。相应地更新了答案。 - ofo
非常感谢您更新的答案! - s.paszko
time_ceil 函数存在错误。如果模数为 0,则应该保留原始时间。就像在数学中 Ceil(1) = 1,但是 Ceil(1.000001) 等于 2。 - s.paszko
@s.paszko 已经纠正了它。 - ofo

16

Pandas有一个日期时间舍入功能,但与Pandas中的大多数功能一样,它需要以Series格式呈现。

>>> ts = pd.Series(pd.date_range(Dt(2019,1,1,1,1),Dt(2019,1,1,1,4),periods=8))
>>> print(ts)
0   2019-01-01 01:01:00.000000000
1   2019-01-01 01:01:25.714285714
2   2019-01-01 01:01:51.428571428
3   2019-01-01 01:02:17.142857142
4   2019-01-01 01:02:42.857142857
5   2019-01-01 01:03:08.571428571
6   2019-01-01 01:03:34.285714285
7   2019-01-01 01:04:00.000000000
dtype: datetime64[ns]

>>> ts.dt.round('1min')
0   2019-01-01 01:01:00
1   2019-01-01 01:01:00
2   2019-01-01 01:02:00
3   2019-01-01 01:02:00
4   2019-01-01 01:03:00
5   2019-01-01 01:03:00
6   2019-01-01 01:04:00
7   2019-01-01 01:04:00
dtype: datetime64[ns]

文档 - 根据需要更改频率字符串。


参考资料,Timestamp 包含 floorceil - poulter7
@poulter7 floor函数只能作用于单个值,而不能作用于Datetime索引。 - cona

3

我认为这样做可以解决问题,它使用了round函数的一种非常有用的应用。

from typing import Literal
import math

def round_datetime(dt: datetime.datetime, step: datetime.timedelta, d: Literal['no', 'up', 'down'] = 'no'):
    step = step.seconds
    round_f = {'no': round, 'up': math.ceil, 'down': math.floor}
    return datetime.datetime.fromtimestamp(step * round_f[d](dt.timestamp() / step))

date = datetime.datetime(year=2022, month=11, day=16, hour=10, minute=2, second=30, microsecond=424242)#
print('Original:', date)
print('Standard:', round_datetime(date, datetime.timedelta(minutes=5)))
print('Down:    ', round_datetime(date, datetime.timedelta(minutes=5), d='down'))
print('Up:      ', round_datetime(date, datetime.timedelta(minutes=5), d='up'))

结果:

Original: 2022-11-16 10:02:30.424242
Standard: 2022-11-16 10:05:00
Down:     2022-11-16 10:00:00
Up:       2022-11-16 10:05:00

3

用于向下舍入分钟时间的通用函数:

from datetime import datetime
def round_minute(date: datetime = None, round_to: int = 1):
    """
    round datetime object to minutes
    """
    if not date:
        date = datetime.now()
    date = date.replace(second=0, microsecond=0)
    delta = date.minute % round_to
    return date.replace(minute=date.minute - delta)

1
使用replace对我来说是最直接的答案。谢谢Yaniv! - John
我希望推动你走向巅峰。在2023年,dt.replace是最直接的四舍五入方法。 - valentinmk

3

如果您不想使用条件语句,可以使用模运算操作符:

minutes = int(round(tm.minute, -1)) % 60

更新

你是否想要类似这样的东西?

def timeround10(dt):
    a, b = divmod(round(dt.minute, -1), 60)
    return '%i:%02i' % ((dt.hour + a) % 24, b)

timeround10(datetime.datetime(2010, 1, 1, 0, 56, 0)) # 0:56
# -> 1:00

timeround10(datetime.datetime(2010, 1, 1, 23, 56, 0)) # 23:56
# -> 0:00

如果您想要字符串结果,那么使用timedelta获取日期时间结果会更好-请参考其他回答 ;)


啊,但这里的问题是小时也必须增加。 - Lucas Manco
1
@Lucas Manco - 我的解决方案也很好,我认为更有意义。 - Omnifarious

3

一个简单的方法:

def round_time(dt, round_to_seconds=60):
    """Round a datetime object to any number of seconds
    dt: datetime.datetime object
    round_to_seconds: closest number of seconds for rounding, Default 1 minute.
    """
    rounded_epoch = round(dt.timestamp() / round_to_seconds) * round_to_seconds
    rounded_dt = datetime.datetime.fromtimestamp(rounded_epoch).astimezone(dt.tzinfo)
    return rounded_dt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接