Python中如何计算日期差值

136 阅读4分钟

简单实现

计算两个日期差值,通常使用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