【问题标题】:How can I interpret a year-naive, RFC 3339 datetime string in Python?如何在 Python 中解释一年前的 RFC 3339 日期时间字符串?
【发布时间】:2021-03-13 17:01:32
【问题描述】:

我正在与一个 API 交互,该 API 提供原始 RFC 3339 日期时间字符串来表示用户生日。自然,我想将此解释为某种 datetime 对象 - 但是,python datetime 库不支持值小于 1 的日期时间字符串。

以下是 API 给出的示例日期时间字符串:0000-09-01T00:00:00-00:00(注意年份设置为 0000)。如果我只是把它扔进datetime.fromisoformat,不出所料会引发错误:

In [1]: from datetime import datetime

In [2]: datetime.fromisoformat("0000-09-01T00:00:00-00:00")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-2-e1d8a5624d92> in <module>
----> 1 datetime.fromisoformat("0000-09-01T00:00:00-00:00")

ValueError: year 0 is out of range

如果我要完全删除字符串的年份部分,它会给出以下内容:

In [1]: from datetime import datetime

In [2]: datetime.fromisoformat("09-01T00:00:00-00:00")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-2-a027335f00c1> in <module>
----> 1 datetime.fromisoformat("09-01T00:00:00-00:00")

ValueError: Invalid isoformat string: '09-01T00:00:00-00:00'

起初,我认为这是一个错误或限制。但经过一番研究,我发现RFC3339 Standard 在其简介中声明了以下内容:

所有日期和时间都假定为“当前时代”,介于公元 0000 年和公元 9999 年之间。

假设这个范围是包容性的(这是基于标准中“介于”一词的其他用法,尽管从未严格指定),暗示 datetime 模块不符合 RFC3339 标准它硬编码最小和最大年份值,并使其成为必需值。但是,它从未声称它确实符合标准。所以新的问题是,如果包含的库不支持 RFC3339,那是什么?

我的问题是:有没有办法将此字符串解释为某种日期时间对象或使用第三方库?

【问题讨论】:

  • 没有第 0 年。公元 1 年紧随公元前 1 年。
  • 是的,我知道。但是,我无法控制 API 提供的数据。
  • 您链接到的文档用于 API,而不是 Python datetime 库。
  • 为什么不用“缺失值标识符”替换年份,例如replace('0000', '2996')?希望这个代码在几百年后不会继续使用......
  • 无论如何,“API 不符合 RFC 3339”的说法是错误的。 API 合规,datetime.fromisoformat() 不合规。但它并没有声称是。

标签: python datetime rfc3339


【解决方案1】:

Anno Domini 日期表示系统中没有year 0

快速查看常见的日期时间替代项(PendulumArrow)表明,使用0000- 解析 ISO 格式字符串时出现ValueError 错误,因为年份是通用的。那不是一个有效的年份,错误在于数据源。

只有一个月和一天的日期并不是真正的日期 - 它是模棱两可的。日期2/23 是在3/1 之前还是之后? 2/23 + 6 天是二月底还是三月初?在这两种情况下,都完全取决于年份。

似乎Square API 使用0000- 作为可选年份的标志,因为有些人不想透露他们的年龄。

如果您的数据标准化为0000 年,您可能只需在第 1 年进行字符串替换即可标准化:

from datetime import datetime

s="0000-09-01T00:00:00-00:00"

>>> datetime.fromisoformat(s.replace("0000-","0001-"))
datetime.datetime(1, 9, 1, 0, 0, tzinfo=datetime.timezone.utc)

或者,如 cmets 所述,也许使用 0004 来容纳 2/29 作为生日:

s="0000-02-29T00:00:00-00:00"

>>> datetime.fromisoformat(s.replace("0000-","0004-"))
datetime.datetime(4, 2, 29, 0, 0, tzinfo=datetime.timezone.utc)

这充其量只是部分解决方案。同样,没有年份的日期不是日期,您需要编写和验证大量代码来尝试解决排序、比较、日期偏移、表示等方面的歧义。

【讨论】:

  • 0004 如果是闰日会更好。
  • 这是我首先选择做的一种避免问题的方法。然而,它有两个自己的问题。首先是用户可能无意中假设年份是有效的,而不仅仅是虚拟数据。另一个是如果我想以另一种方式来做(向 API 发送一个原始日期时间),我需要捕获所有日期时间对象并确保它被正确地序列化回字符串。这在技术上是可行的,但非常老套,并不能解决这两个问题中的第一个。
  • 没有年份的月份和日期并不是真正的有效日期。根据定义,您需要做一些“hacky”来使用日期时间对象,或者您需要拥有自己的日期类型对象。也许专注于 to/from API 组件,以确保所有必需的 to/from 转换都一致地完成。您还可以使用一致的显示代码,以便日期仅显示为 M/D 生日,这样用户就不会看到年份。
  • 考虑到选项,我认为这是唯一合适的解决方案,而不是创建我自己的实现或要求开发人员更改他们的 API。因此,我会接受答案。
  • Nit-pick:为什么第 0 年无效,仅仅因为 Python 实现从第 1 年开始?其他语言对此没有任何问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-08-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多