简单实现
计算两个日期差值,通常使用datetime模块即可方便实现。
简单示例如下:
ts1 = datetime.now() + timedelta(days=-2, hours=23)
ts2 = datetime.now()
ts_delta = ts1 - ts2
print(ts_delta.days)
特殊场景
但在某些特殊场景下,我们可能会需要一些特殊的计算日期差值的方法。
例如:
- 日期差值四舍五入,满半天按一天计算,不满半天计为0
- 日期差值向上取整,超出一天的无论多长时间均按一天计算
- 日期差值向下取整,超出一天但不满一天的全部计为0
在如下代码中,简单试下了一下这些功能。
"""
用于计算两个日期时间(datetime对象或字符串)之间的日期差值
"""
from datetime import datetime
from typing import Union
DateTime = Union[str, datetime]
DELTA_ZERO = 0 # seconds 与microseconds均为0
DELTA_UN_ZERO = 1 # seconds 或 microseconds存在不为0的值
DAYS_POSITIVE = 1 # days为自然数
DAYS_NAGATIVE = -1 # days为负整数
def fmt(ts: str, fmt_str: str) -> datetime:
if isinstance(ts, datetime):
return ts
if isinstance(ts, date):
return datetime(year=date.year, month=date.month,day=date.day)
if not fmt_str:
fmt_str = '%Y-%m-%d %H:%M:%S'
return datetime.strptime(ts, fmt_str)
class DateDelta(object):
def __init__(self):
self._ts1 = None
self._ts2 = None
self._ts_delta = None
self._code_days = -2 # days的判断结果
self._code_ts_delta = -2 # seconds 与microseconds的判断结果
def delta(self, ts1: DateTime, ts2: DateTime, fmt_str: str = '', mode=''):
if isinstance(ts1, str) or isinstance(ts2, str):
ts2 = fmt(ts2, fmt_str)
ts1 = fmt(ts1, fmt_str)
if isinstance(ts1, datetime) and isinstance(ts2, datetime):
self._ts1 = ts1
self._ts2 = ts2
self._ts_delta = ts1 - ts2
self._code_days, self._code_ts_delta = self._cmp()
else:
raise ValueError(f'ts1,ts2 must be an instance of datetime or a datetime string:\n{ts1},\n{ts2}\n')
return self
def ceil(self) -> int:
"""
# 向上取整,不足一天按一天计算
# 如 days为自然数:
# seconds或microsecods不为0,向上取整,days+1;
# seconds与microsecods均为0,days取原值
# 如 days为负整数:
# seconds或microsecods不为0,向上取整,days取原值;
# days+1seconds与microsecods均为0,days取原值
:return:
"""
code_days, code_ts_delta = self._code_days, self._code_ts_delta
if code_days == DAYS_POSITIVE:
if code_ts_delta == DELTA_ZERO:
return self._ts_delta.days
else:
return self._ts_delta.days + 1
elif code_days == DAYS_NAGATIVE:
if code_ts_delta == DELTA_ZERO:
return self._ts_delta.days
else:
return self._ts_delta.days
else:
raise ValueError(F'code_days illegal: {code_days}\n')
def floor(self) -> int:
"""
# 向下取整,不足一天计为0
# days为自然数:
# seconds或microsecods不为0(即超出days但不满一天),向下取整,不足1天部分舍去,days取原值;
# seconds与microsecods均为0(刚好相差{days}天),days取原值
# days为负整数:
# seconds或microsecods不为0,向下取整,days+1;
# days+1seconds与microsecods均为0,days取原值
:return:
"""
code_days, code_ts_delta = self._code_days, self._code_ts_delta
if code_days == DAYS_POSITIVE:
if code_ts_delta == DELTA_ZERO:
return self._ts_delta.days
else:
return self._ts_delta.days
elif code_days == DAYS_NAGATIVE:
if code_ts_delta == DELTA_ZERO:
return self._ts_delta.days
else:
return self._ts_delta.days + 1
else:
raise ValueError(F'code_days illegal: {code_days}\n')
def round(self) -> int:
"""
# 四舍五入,超过半天按一天计算,不足半天计为0
# days为自然数:
# seconds(60*60)>=12(即超出days且满半天),四舍五入,days+1;
# seconds(60*60)<12(即超出days但不满半天),四舍五入,days不变;
# days为负整数:
# seconds(60*60)>=12(即超出days且满半天),四舍五入,days+1;
# seconds(60*60)<12(即超出days但不满半天),四舍五入,days不变;
:return:
"""
if self._ts_delta.seconds / (60 * 60) >= 12:
return self._ts_delta.days + 1
elif self._ts_delta.seconds / (60 * 60) < 12:
return self._ts_delta.days
else:
raise ValueError(F'unknown exception happened:\n')
def _cmp(self):
days = self._ts_delta.days
seconds = self._ts_delta.seconds
microseconds = self._ts_delta.microseconds
if days >= 0:
code_days = DAYS_POSITIVE
else:
code_days = DAYS_NAGATIVE
if seconds or microseconds:
# 若 ts2 + timedela(days=days) < ts1
code_ts_delta = DELTA_UN_ZERO
else:
# 若 ts2 + timedela(days=days) == ts1
code_ts_delta = DELTA_ZERO
return code_days, code_ts_delta
最后,为了保证功能的正常可用,必要的单元测试也是必不可少。 这里我使用了pytest来编写单元测试用例。
from date.timedelta import DateDelta
import pytest
from datetime import datetime, timedelta
args = 'ts1,ts2,expect'
ts = datetime.now()
ceil_data = [
(ts, ts, 0),
(ts + timedelta(days=2), ts, 2),
(ts + timedelta(days=-2), ts, -2),
(ts + timedelta(days=2, hours=1), ts, 3),
(ts + timedelta(days=-2, hours=1), ts, -2),
(ts + timedelta(days=2, seconds=1), ts, 3),
(ts + timedelta(days=-2, seconds=1), ts, -2),
(ts + timedelta(days=2, microseconds=1), ts, 3),
(ts + timedelta(days=-2, microseconds=1), ts, -2),
(ts + timedelta(days=-2, microseconds=-1), ts, -3),
]
floor_data = [
(ts, ts, 0),
(ts + timedelta(days=2), ts, 2),
(ts + timedelta(days=-2), ts, -2),
(ts + timedelta(days=2, hours=1), ts, 2),
(ts + timedelta(days=-2, hours=1), ts, -1),
(ts + timedelta(days=2, seconds=1), ts, 2),
(ts + timedelta(days=-2, seconds=1), ts, -1),
(ts + timedelta(days=2, microseconds=1), ts, 2),
(ts + timedelta(days=-2, microseconds=1), ts, -1),
(ts + timedelta(days=-2, microseconds=-1), ts, -2),
]
round_data = [
(ts, ts, 0),
(ts + timedelta(days=2), ts, 2),
(ts + timedelta(days=-2), ts, -2),
(ts + timedelta(days=2, hours=11, minutes=59, seconds=59), ts, 2),
(ts + timedelta(days=-2, hours=11, minutes=59, seconds=59), ts, -2),
(ts + timedelta(days=2, hours=12), ts, 3),
(ts + timedelta(days=-2, hours=12), ts, -1),
]
class TestCase(object):
def setup_class(self):
self.td = DateDelta()
@pytest.mark.parametrize(args, ceil_data)
def test_ceil(self, ts1, ts2, expect):
rst = self.td.delta(ts1, ts2).ceil()
assert rst == expect
@pytest.mark.parametrize(args, floor_data)
def test_floor(self, ts1, ts2, expect):
rst = self.td.delta(ts1, ts2).floor()
assert rst == expect
@pytest.mark.parametrize(args, round_data)
def test_round(self, ts1, ts2, expect):
rst = self.td.delta(ts1, ts2).round()
assert rst == expect