将时间间隔转换为年份?

187

我需要检查某个日期距离现在过去了多少年。目前我已经从datetime模块得到了一个timedelta,但是我不知道如何将其转换成年份。


5
请看这个答案:https://dev59.com/73E95IYBdhLWcg3wp_kg#9754466。 - Adam
1
如何将年份转换为秒数。相关链接:How to convert years to second - jfs
21个回答

194

你需要不仅仅是一个 timedelta 来计算经过了多少年;你还需要知道开始(或结束)日期。(这与闰年有关。)

你最好使用dateutil.relativedelta 这个第三方模块,如果你想知道某个日期(默认为现在)经过了 n 年后的 datetime ,可以按照以下方式进行操作:

from dateutil.relativedelta import relativedelta

def yearsago(years, from_date=None):
    if from_date is None:
        from_date = datetime.now()
    return from_date - relativedelta(years=years)

如果您更喜欢使用标准库,答案会稍微复杂一些:

from datetime import datetime
def yearsago(years, from_date=None):
    if from_date is None:
        from_date = datetime.now()
    try:
        return from_date.replace(year=from_date.year - years)
    except ValueError:
        # Must be 2/29!
        assert from_date.month == 2 and from_date.day == 29 # can be removed
        return from_date.replace(month=2, day=28,
                                 year=from_date.year-years)
如果今天是2月29日,18年前没有2月29日,这个函数将返回2月28日。如果您想返回3月1日,只需更改最后一个 return 语句为:
    return from_date.replace(month=3, day=1,
                             year=from_date.year-years)

你最初的问题是想知道自某个日期以来过了多少年。假设你想得到一个整数年数,你可以基于每年365.2425天进行猜测,然后使用上面定义的yearsago函数之一进行检查:

def num_years(begin, end=None):
    if end is None:
        end = datetime.now()
    num_years = int((end - begin).days / 365.2425)
    if begin > yearsago(num_years, end):
        return num_years - 1
    else:
        return num_years

37
你可以完全准确地使用365.2425(而不是365.25),这将考虑到公历的400年例外情况。 - brianary
3
由于英国和香港等国家在闰年中“向上取整”至3月1日,因此您的函数在这些国家会出现合法性问题。 - antihero
4
@brianary说,它不会是“完全准确的”,例如,本地时区引入的差异(datetime.now())大于平均朱利安(365.25)和格雷戈里(365.2425)年之间的差异。在@Adam Rosenfield的答案中提供了正确的方法。 - jfs
3
参见这个链接这个链接,它们都列出了有关时间的不正确想法清单,非常优秀。 - gvoysey
dateutil 现在似乎是一个内置的包(至少对于我的来自Miniconda的Python 3.9.6而言)。 - Addison Klinke
只是在思考为什么我不确定为什么timedelta不能做到这一点,因为datetime确实具有闰年的意识... date = datetime.strptime("28.02.1981, 23:00:00","%d.%m.%Y, %H:%M:%S") print(date+timedelta(hours=1)) 会产生3月1日,而 date = datetime.strptime("28.02.1980, 23:00:00","%d.%m.%Y, %H:%M:%S") print(date+timedelta(hours=1)) 会产生2月29日 --> 正确 - EvilSmurf

62

如果您要检查某人是否已满18岁,使用 timedelta 可能在某些边缘情况下不会正确工作,因为闰年的存在。例如,出生于2000年1月1日的人将在6575天后(包括5个闰年)于2018年1月1日刚好年满18岁,但出生于2001年1月1日的人将在6574天后(包括4个闰年)于2019年1月1日刚好年满18岁。因此,如果某人恰好是6574天大,您无法确定他们是否17或18岁,除非您了解有关其出生日期的更多信息。

正确的方法是直接从日期中计算年龄,通过年份相减,然后如果当前日期在出生日期之前,则再减去一岁。


2
这是正确的方式...除非你从事喜欢猜测的行业,如医疗、地质等。 - Braiam
仅减去年份是一种优雅的解决方案,但并不是非常明显。 - xyres
这些日期现在已经过去了。只是觉得很有趣。 - trpt4him

10

首先,在最详细的层面上,这个问题无法得到精确的解决。年份的长度有所不同,而且没有明确的“正确选择”来确定年份长度。

话虽如此,可以用“自然”的单位(可能是秒)获取差异,然后除以该单位与年份之间的比率。例如:

delta_in_days / (365.25)
delta_in_seconds / (365.25*24*60*60)

...或其他什么。避免使用月份,因为它们比年份的定义还要模糊。


4
这不是任何人在谈论服务年限或某人是否达到特定年龄时使用的意思或用法。 - John Machin
5
为考虑公历400年例外情况,你的365.25应改为365.2425。 - brianary
2
问题可以被正确解决 - 你可以提前知道哪些年份有闰日和闰秒等。只是没有一种非常优雅的方法可以在两个格式化日期中进行减法运算,然后再减去年份、月份、天数等。 - Litherum

9
这是一个更新后的DOB函数,它以与人类相同的方式计算生日:
import datetime
import locale


# Source: https://en.wikipedia.org/wiki/February_29
PRE = [
    'US',
    'TW',
]
POST = [
    'GB',
    'HK',
]


def get_country():
    code, _ = locale.getlocale()
    try:
        return code.split('_')[1]
    except IndexError:
        raise Exception('Country cannot be ascertained from locale.')


def get_leap_birthday(year):
    country = get_country()
    if country in PRE:
        return datetime.date(year, 2, 28)
    elif country in POST:
        return datetime.date(year, 3, 1)
    else:
        raise Exception('It is unknown whether your country treats leap year '
                      + 'birthdays as being on the 28th of February or '
                      + 'the 1st of March. Please consult your country\'s '
                      + 'legal code for in order to ascertain an answer.')
def age(dob):
    today = datetime.date.today()
    years = today.year - dob.year

    try:
        birthday = datetime.date(today.year, dob.month, dob.day)
    except ValueError as e:
        if dob.month == 2 and dob.day == 29:
            birthday = get_leap_birthday(today.year)
        else:
            raise e

    if today < birthday:
        years -= 1
    return years

print(age(datetime.date(1988, 2, 29)))

当出生日期是2月29日,而当前年份不是闰年时,此代码会出错。 - Trey Hunner

8

获取天数,然后除以365.2425(平均公历年)得到年份。除以30.436875(平均公历月)得到月份。


6
简单的解决方案!
import datetime as dt
from dateutil.relativedelta import relativedelta

dt1 = dt.datetime(1990,2,1)
dt2 = dt.datetime(2021,5,16)
out = relativedelta(dt2, dt1)

print(f'Complete: {out}')
print(f'years:{out.years}, months:{out.months}, days:{out.days}') `

完成:relativedelta(年= +31,月= +3,日= +15)

年份:31,月份:3,天数:15


5
def age(dob):
    import datetime
    today = datetime.date.today()

    if today.month < dob.month or \
      (today.month == dob.month and today.day < dob.day):
        return today.year - dob.year - 1
    else:
        return today.year - dob.year

>>> import datetime
>>> datetime.date.today()
datetime.date(2009, 12, 1)
>>> age(datetime.date(2008, 11, 30))
1
>>> age(datetime.date(2008, 12, 1))
1
>>> age(datetime.date(2008, 12, 2))
0

一个出生在2月29日的人将被视为在随后的2月28日达到了1岁。 - John Machin
好的。为了适应出生于29日的0.08%人口,将测试从“生日是否在今天之后”改为“生日是否在今天之前”。这样解决了吗? - John Mee
它对你的例子确实有效吗?!如果我将“今天”设置为2009年2月28日,出生日期设置为2008年2月29日,则它对我来说返回零;而不是像您建议的那样返回1(Python 2.5.2)。Machin先生没有必要这么粗鲁。您遇到了哪两个日期的问题? - John Mee
我再试一次:对于大多数法律目的,出生于2月29日的人将被视为在随后的2月28日达到1岁。您的代码在“更正”之前和之后都产生0。实际上,您的两个代码版本对于所有9个输入可能性(月份<==>X天<==>)产生完全相同的输出。顺便说一下,100.0 /(4 * 365 + 1)的结果是0.068,而不是0.08。 - John Machin
哦,好的,你说得对。我不知道法律条款,而你也没有让它们变得明确无误。听起来第一次做的时候更好。不幸的是,我无法从评论中确定你的9种情况是什么。但不要费心解释了;现在是时候与我们分享你的解决方案了;一个能按照你喜欢的方式工作的方案。 - John Mee
2
叹气。(0) 在任何日期计算中,解决2月29日的问题都是必要的;你只是忽略了它。(1) 你的代码第一次并没有变得更好;你不明白“产生完全相同的输出”这句话吗?(2) 比较今天的月份和出生日期的月份有三种可能的原子结果(<,==,>);比较今天的日期和出生日期的日期有三种可能的原子结果;3 * 3 == 9。(3) https://dev59.com/73E95IYBdhLWcg3wp_kg#2218654 - John Machin

5
你需要多准确呢?如果你担心闰年,td.days / 365.25 可以得出相当接近的结果。

我真的很担心闰年。它应该检查一个人是否超过18岁。 - Migol
那么就没有简单的一行代码了,你需要解析这两个日期并确定这个人是否已经过了18岁生日。 - eduffy

2
这种方法是datetime库的一个技巧:
def calculate_age(birthday_date, fmt="%Y-%m-%d"):
    birthday = datetime.datetime.strptime(birthday_date, fmt)
    age = datetime.datetime.now() - birthday
    # first datime valid is (1, 1, 1), I use (1, 12, 31) => (2, 0, 0) to hack the lib
    age = (datetime.datetime(1, 12, 31) + age)
    return age.year - 2

这个解决方案有些奇怪,但是我还是想和你分享。它可能不是最优雅的函数。
now : 2019-09-21

print(calculate_age("2019-09-21")) => 2 (Done) 

-> age + (1,12,31) = 0004-01-01 23:42:17.767031

print(calculate_age("2020-09-21")) => 0 (Undone)

-> age + (1,12,31) = 0002-12-31 23:46:39.144091

差异是一天。我理解这种差异的原因是由于闰年。

为了纠正这种不期望的行为,您需要使用生日年份来添加结果。


2
晚来的宾客,但这将准确而轻松地为您提供年龄(以年为单位):
b = birthday
today = datetime.datetime.today()
age = today.year - b.year + (today.month - b.month > 0 or 
                             (today.month == b.month > 0 and 
                              today.day - b.day > 0))

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