# Support rounding of datetime / a nicer way of rounding

Dear community,

Rounding of datetime has been a recurring issue for many (400,000+) users [ 1, 2, 3, 4 ] and the recommended solutions have been anything but pretty.

I would like to propose the following simple method for rounding to be added/merged into the stdlibs (where?).

``````from datetime import datetime, timedelta, timezone

epoch = datetime(2000,1,1,0,0,0,0,timezone.utc)
epoch_no_tz = datetime(2000,1,1,0,0,0,0)

def round(value, multiple, up=None):
""" a nicer way to round numbers.

:param value: float/integer
:param multiple: base of the rounding.
:param up: None (default) or boolean rounds half, up or down.
round(1.6, 1) rounds to 2.
round(1.4, 1) rounds to 1.
round(1.5, 1, up=True) rounds to 2.
round(1.5, 1, up=False) rounds to 1.
:return: value

Examples:
multiple = 1 is the same as rounding to whole integers.
multiple = 0.001 is the same as rounding to 3 digits precision.
mulitple = 3.1415 is rounding to nearest multiplier of 3.1415

dt = datetime(2022,8,18,11,14,53,440)
td = timedelta(hours=0.5)
round(dt,td) is datetime(2022,8,18,11,0)
"""
epoch = 0
if isinstance(value, (datetime)) and isinstance(multiple, timedelta):
if value.tzinfo is None:
epoch = DataTypes.epoch_no_tz
else:
epoch = DataTypes.epoch

low = ((value - epoch) // multiple) * multiple
high = low + multiple
if up is True:
return high + epoch
elif up is False:
return low + epoch
else:
if abs((high + epoch) - value) < abs(value-(low + epoch)):
return high + epoch
else:
return low + epoch
``````

Here is the test suite:

``````import datatypes   # the module above
import math

def test_round():
xround = datatypes.round  # xround is merely used to distinguish from common `round` method.
# round up
assert xround(1.6, 1, True) == 2
assert xround(1.4, 1, True) == 2
# round down
assert xround(1.6, 1, False) == 1
assert xround(1.4, 1, False) == 1
# round half
assert xround(1.6, 1) == 2
assert xround(1.4, 1) == 1

# round half
assert xround(16, 10) == 20
assert xround(14, 10) == 10

# round half
assert xround(-16, 10) == -20
assert xround(-14, 10) == -10

# round to odd multiples
assert xround(6, 3.1415, 1) == 2 * 3.1415

assert xround(1.2345, 0.001, True) == 1.2349999999999999 and math.isclose(1.2349999999999999, 1.235)
assert xround(1.2345, 0.001, False) == 1.234

assert xround(123, 100, False) == 100
assert xround(123, 100, True) == 200

assert xround(123, 5.07, False) == 24 * 5.07

dt = datetime(2022,8,18,11,14,53,440)

td = timedelta(hours=0.5)
assert xround(dt,td, up=False) == datetime(2022,8,18,11,0)
assert xround(dt,td, up=None) == datetime(2022,8,18,11,0)
assert xround(dt,td, up=True) == datetime(2022,8,18,11,30)

td = timedelta(hours=24)
assert xround(dt,td, up=False) == datetime(2022,8,18)
assert xround(dt,td, up=None) == datetime(2022,8,18)
assert xround(dt,td, up=True) == datetime(2022,8,19)

td = timedelta(days=0.5)
assert xround(dt,td, up=False) == datetime(2022,8,18)
assert xround(dt,td, up=None) == datetime(2022,8,18,12)
assert xround(dt,td, up=True) == datetime(2022,8,18,12)

td = timedelta(days=1.5)
assert xround(dt,td, up=False) == datetime(2022,8,18)
assert xround(dt,td, up=None) == datetime(2022,8,18)
assert xround(dt,td, up=True) == datetime(2022,8,19,12)

td = timedelta(seconds=0.5)
assert xround(dt,td, up=False) == datetime(2022,8,18,11,14,53,0)
assert xround(dt,td, up=None) == datetime(2022,8,18,11,14,53,0)
assert xround(dt,td, up=True) == datetime(2022,8,18,11,14,53,500000)

td = timedelta(seconds=40000)
assert xround(dt,td, up=False) == datetime(2022,8,18,6,40)
assert xround(dt,td, up=None) == datetime(2022,8,18,6,40)
assert xround(dt,td, up=True) == datetime(2022,8,18,17,46,40)
``````

Kind regards

1 Like

Python has a builtin function called `round` which rounds numeric types by calling their `__round__` method (which should only be implemented if rounding makes sense for that type), see Built-in Functions — Python 3.10.6 documentation and 3. Data model — Python 3.10.6 documentation.

I am not sure your function fits well with the numeric protocol, a `datetime` object is very different and I don’t think that class should implement `__round__`. It makes more sense to me to add this functionality as a `dateround`/`datetimeround` function in the `datetime` module.

2 Likes

@ajoino

a `datetime` object is very different

How come? All the required mathematical functions are already implemented.

I’m suggesting that we give this to the users, rather than letting them implement it on their own.
I believe this was the argument for the `statistics` module?

This would be the patch to the datetime class near line 2113:

``````epoch = datetime(2000,1,1,0,0,0,0,timezone.utc)  # any epoch will do for the code below.
epoch_no_tz = datetime(2000,1,1,0,0,0,0)

def __round__(self, value, multiple, up=None):  # self is the datetime class.
if not isinstance(value, datetime):raise TypeError()
if not isinstance(multiple, timedelta):raise TypeError()

if value.tzinfo is None:
epoch = epoch_no_tz
else:
epoch = epoch

low = ((value - epoch) // multiple) * multiple
high = low + multiple
if up is True:
return high + epoch
elif up is False:
return low + epoch
elif abs((high + epoch) - value) < abs(value-(low + epoch)):
return high + epoch
else:
return low + epoch
``````
2 Likes

The `complex` builtin also implements (most) of those methods yet does not implement `__round__`. A `datetime` is a representation of an instance in time, which can be represented as an integer, but the `datetime` representation consists of 6+ parts: years, months, days, hours, minutes, seconds, timezone, and maybe some more. This makes rounding, in the sense of the `round/__round__` protocol, which can only take a single `int` (or integer-like object) as input, not really possible imo.

Furthermore, your implementation of `__round__` does not comply with the protocol. For `datetime` objects to be compliant with the `round/__round__` protocol, it can only take one input which is integer-like (I believe this means that the object has a `__int__` method). For your implementation to work, the `round` builtin would need to be redesigned.

I cannot think of any implementation of `__round__` for a datetime object which correctly rounds all of the parts and takes only an integer-like object as input. Thus, I think it’s better to provide this functionality in the `datetime` module.

your implementation of `__round__` does not comply with the protocol .

Accepted.

``````from datetime import datetime, timedelta, dateround
``````

So what is the next step? Does this require a PEP?

2 Likes

Sorry if I was a bit curt, I do like the idea of a rounding for datetime I think the first step is to consult the maintainer of the `datetime` module, as this would increase the maintenance burden. Who that is however I do not know.

1 Like

No worries. Perhaps @ambv knows who the maintainer of the `datetime` module is?

2 Likes

It’s @pganssle .

2 Likes

Thanks @brettcannon . I look forward to hear what @pganssle thinks.

I’ll have to re-read the thread when I have some time, but I opened a similar bug some years ago, before I was very actively involved with CPython development: Add precision argument to datetime.now · Issue #76703 · python/cpython · GitHub

Of course, the last entry in the thread says that I’ll put together a summary and start a discussion topic “in the next week or so”… in January 2018, and I’m noticing this thread about 5 days before moving to a different city, so maybe the next time I stumble on this I’ll be using my robot arm to type out a message explaining how someone had a similar idea on Discourse way back in 2022 .

Hi @pganssle - time is drifting. May we count on you to add this feature to datetime?

Hi @pganssle - I don’t know if it’s inspirational, but I’ve written a business calendar that you may (?) find useful? Here you go: link

The module / notebook allows a user to set up a business calendar and ask questions like:

• is the business open ?
• when is the business open next?

I need it for solving scheduling problems.

Hello! I’m still hoping for this, or would be happy to implement it for you @pganssle .

1 Like

Hi @TheMellyBee & @pganssle Depending on which implementation you pick, be wary of testing for zero as well:

This implementation (link) has the whole test suite.

Kind regards
Bjorn

I find a ‘floor’ function more helpful than a ‘round’ function. Plus, pandas already supports ‘floor’ on datetime64 series (pandas.Series.dt.floor — pandas 1.5.2 documentation). it would be nice to add similar support to datetime.date and datetime.