目录 |
| 目录 |
|
---|---|---|---|
数字日期和时间(一) | 1.数字的四舍五入 2.执行精确的浮点数运算 3.数字的格式化输出 4.二、八、十六进制整数 | 数字日期和时间(三) | 9.大型数组运算 10.矩阵与线性代数运算 11.随机选择 12.基本的日期与时间转换 |
数字日期和时间(二) | 5.字节到大整数的打包与解包 6.复数的数学运算 7.无穷大与 NaN 8.分数运算 | 数字日期和时间(四) | 13.计算上一个周五的日期 14.计算当前月份的日期范围 15.字符串转换为日期 16.结合时区的日期操作 |
13.计算上一个周五的日期
你需要一个通用方法来计算一周中某一天上一次出现的日期,例如上一个周五的日期。
Python 的 datetime
模块中有工具函数和类可以帮助你执行这样的计算。下面是对类似这样的问题的一个通用解决方案:
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
Topic: 最后的周五
Desc :
"""
from datetime import datetime, timedelta
weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
'Friday', 'Saturday', 'Sunday']
def get_previous_byday(dayname, start_date=None):
if start_date is None:
start_date = datetime.today()
day_num = start_date.weekday()
day_num_target = weekdays.index(dayname)
days_ago = (7 + day_num - day_num_target) % 7
if days_ago == 0:
days_ago = 7
target_date = start_date - timedelta(days=days_ago)
return target_date
在交互式解释器中使用如下:
>>> datetime.today() # For reference
datetime.datetime(2012, 8, 28, 22, 4, 30, 263076)
>>> get_previous_byday('Monday')
datetime.datetime(2012, 8, 27, 22, 3, 57, 29045)
>>> get_previous_byday('Tuesday') # Previous week, not today
datetime.datetime(2012, 8, 21, 22, 4, 12, 629771)
>>> get_previous_byday('Friday')
datetime.datetime(2012, 8, 24, 22, 5, 9, 911393)
>>>
可选的 start_date
参数可以由另外一个 datetime
实例来提供。比如:
>>> get_previous_byday('Sunday', datetime(2012, 12, 21))
datetime.datetime(2012, 12, 16, 0, 0)
>>>
上面的算法原理是这样的:先将开始日期和目标日期映射到星期数组的位置上(星期一索引为 0 0 0),然后通过模运算计算出目标日期要经过多少天才能到达开始日期。然后用开始日期减去那个时间差即得到结果日期。
如果你要像这样执行大量的日期计算的话,你最好安装第三方包 python-dateutil
来代替。比如,下面是是使用 dateutil
模块中的 relativedelta()
函数执行同样的计算:
>>> from datetime import datetime
>>> from dateutil.relativedelta import relativedelta
>>> from dateutil.rrule import *
>>> d = datetime.now()
>>> print(d)
2012-12-23 16:31:52.718111
>>> # Next Friday
>>> print(d + relativedelta(weekday=FR))
2012-12-28 16:31:52.718111
>>>
>>> # Last Friday
>>> print(d + relativedelta(weekday=FR(-1)))
2012-12-21 16:31:52.718111
>>>
14.计算当前月份的日期范围
你的代码需要在当前月份中循环每一天,想找到一个计算这个日期范围的高效方法。
在这样的日期上循环并需要事先构造一个包含所有日期的列表。你可以先计算出开始日期和结束日期, 然后在你步进的时候使用 datetime.timedelta
对象递增这个日期变量即可。
下面是一个接受任意 datetime
对象并返回一个由当前月份开始日和下个月开始日组成的元组对象。
from datetime import datetime, date, timedelta
import calendar
def get_month_range(start_date=None):
if start_date is None:
start_date = date.today().replace(day=1)
_, days_in_month = calendar.monthrange(start_date.year, start_date.month)
end_date = start_date + timedelta(days=days_in_month)
return (start_date, end_date)
有了这个就可以很容易的在返回的日期范围上面做循环操作了:
>>> a_day = timedelta(days=1)
>>> first_day, last_day = get_month_range()
>>> while first_day < last_day:
... print(first_day)
... first_day += a_day
...
2012-08-01
2012-08-02
2012-08-03
2012-08-04
2012-08-05
2012-08-06
2012-08-07
2012-08-08
2012-08-09
#... and so on...
上面的代码先计算出一个对应月份第一天的日期。一个快速的方法就是使用 date
或 datetime
对象的 replace()
方法简单的将 days
属性设置成 1
即可。replace()
方法一个好处就是它会创建和你开始传入对象类型相同的对象。所以,如果输入参数是一个 date
实例,那么结果也是一个 date
实例。同样的,如果输入是一个 datetime
实例,那么你得到的就是一个 datetime
实例。
然后,使用 calendar.monthrange()
函数来找出该月的总天数。任何时候只要你想获得日历信息,那么 calendar
模块就非常有用了。monthrange()
函数会返回包含星期和该月天数的元组。
一旦该月的天数已知了,那么结束日期就可以通过在开始日期上面加上这个天数获得。有个需要注意的是结束日期并不包含在这个日期范围内(事实上它是下个月的开始日期)。这个和 Python 的 slice
与 range
操作行为保持一致,同样也不包含结尾。
为了在日期范围上循环,要使用到标准的数学和比较操作。比如,可以利用 timedelta
实例来递增日期,小于号 <
用来检查一个日期是否在结束日期之前。
理想情况下,如果能为日期迭代创建一个同内置的 range()
函数一样的函数就好了。幸运的是,可以使用一个生成器来很容易的实现这个目标:
def date_range(start, stop, step):
while start < stop:
yield start
start += step
下面是使用这个生成器的例子:
>>> for d in date_range(datetime(2012, 9, 1), datetime(2012,10,1),
timedelta(hours=6)):
... print(d)
...
2012-09-01 00:00:00
2012-09-01 06:00:00
2012-09-01 12:00:00
2012-09-01 18:00:00
2012-09-02 00:00:00
2012-09-02 06:00:00
...
>>>
这种实现之所以这么简单,还得归功于 Python 中的日期和时间能够使用标准的数学和比较操作符来进行运算。
15.字符串转换为日期
你的应用程序接受字符串格式的输入,但是你想将它们转换为 datetime
对象以便在上面执行非字符串操作。
使用 Python 的标准模块 datetime
可以很容易的解决这个问题。比如:
>>> from datetime import datetime
>>> text = '2012-09-20'
>>> y = datetime.strptime(text, '%Y-%m-%d')
>>> z = datetime.now()
>>> diff = z - y
>>> diff
datetime.timedelta(3, 77824, 177393)
>>>
datetime.strptime()
方法支持很多的格式化代码, 比如 %Y
代表 4 位数年份,%m
代表两位数月份。还有一点值得注意的是这些格式化占位符也可以反过来使用,将日期输出为指定的格式字符串形式。
比如,假设你的代码中生成了一个 datetime
对象,你想将它格式化为漂亮易读形式后放在自动生成的信件或者报告的顶部:
>>> z
datetime.datetime(2012, 9, 23, 21, 37, 4, 177393)
>>> nice_z = datetime.strftime(z, '%A %B %d, %Y')
>>> nice_z
'Sunday September 23, 2012'
>>>
还有一点需要注意的是,strptime()
的性能要比你想象中的差很多,因为它是使用纯 Python 实现,并且必须处理所有的系统本地设置。如果你要在代码中需要解析大量的日期并且已经知道了日期字符串的确切格式,可以自己实现一套解析方案来获取更好的性能。比如,如果你已经知道所以日期格式是 YYYY-MM-DD
,你可以像下面这样实现一个解析函数:
from datetime import datetime
def parse_ymd(s):
year_s, mon_s, day_s = s.split('-')
return datetime(int(year_s), int(mon_s), int(day_s))
实际测试中,这个函数比 datetime.strptime()
快
7
7
7 倍多。如果你要处理大量的涉及到日期的数据的话,那么最好考虑下这个方案!
16.结合时区的日期操作
你有一个安排在 2012 2012 2012 年 12 12 12 月 21 21 21 日早上 9 : 30 9:30 9:30 的电话会议,地点在芝加哥。而你的朋友在印度的班加罗尔,那么他应该在当地时间几点参加这个会议呢?
对几乎所有涉及到时区的问题,你都应该使用 pytz
模块。这个包提供了 Olson 时区数据库,它是时区信息的事实上的标准,在很多语言和操作系统里面都可以找到。
🚀 Olson 时区数据库(也称为
tz database
或zoneinfo database
)是一个全球时区信息的标准数据库,用于计算机系统和软件中正确表示和转换不同地区的时区。它由 Arthur David Olson 创建并维护,因此通常称为 Olson 时区。
pytz
模块一个主要用途是将 datetime
库创建的简单日期对象本地化。比如,下面如何表示一个芝加哥时间的示例:
>>> from datetime import datetime
>>> from pytz import timezone
>>> d = datetime(2012, 12, 21, 9, 30, 0)
>>> print(d)
2012-12-21 09:30:00
>>>
>>> # Localize the date for Chicago
>>> central = timezone('US/Central')
>>> loc_d = central.localize(d)
>>> print(loc_d)
2012-12-21 09:30:00-06:00
>>>
一旦日期被本地化了, 它就可以转换为其他时区的时间了。为了得到班加罗尔对应的时间,你可以这样做:
>>> # Convert to Bangalore time
>>> bang_d = loc_d.astimezone(timezone('Asia/Kolkata'))
>>> print(bang_d)
2012-12-21 21:00:00+05:30
>>>
如果你打算在本地化日期上执行计算,你需要特别注意夏令时转换和其他细节。比如,在 2013 2013 2013 年,美国标准夏令时时间开始于本地时间 3 3 3 月 10 10 10 日凌晨 2 : 00 2:00 2:00(在那时,时间向前跳过一小时)。如果你正在执行本地计算,你会得到一个错误。比如:
>>> d = datetime(2013, 3, 10, 1, 45)
>>> loc_d = central.localize(d)
>>> print(loc_d)
2013-03-10 01:45:00-06:00
>>> later = loc_d + timedelta(minutes=30)
>>> print(later)
2013-03-10 02:15:00-06:00 # WRONG! WRONG!
>>>
这段代码中的日期计算错误是因为 夏令时(DST)转换 导致的。具体来说,
2013
2013
2013 年
3
3
3 月
10
10
10 日是美国中部时区(Central Time
,如芝加哥)夏令时开始 的日期,时钟会在凌晨
2
2
2 点直接跳到
3
3
3 点(即 1:59:59
→ 3:00:00
)。因此,2013-03-10 02:15:00
这个时间实际上 不存在。
为了修正这个错误,可以使用时区对象 normalize()
方法。比如:
>>> from datetime import timedelta
>>> later = central.normalize(loc_d + timedelta(minutes=30))
>>> print(later)
2013-03-10 03:15:00-05:00
>>>
为了不让你被这些弄的晕头转向,处理本地化日期的通常的策略先将所有日期转换为 UTC 时间,并用它来执行所有的中间存储和操作。比如:
>>> print(loc_d)
2013-03-10 01:45:00-06:00
>>> utc_d = loc_d.astimezone(pytz.utc)
>>> print(utc_d)
2013-03-10 07:45:00+00:00
>>>
一旦转换为 UTC,你就不用去担心跟夏令时相关的问题了。因此,你可以跟之前一样放心的执行常见的日期计算。当你想将输出变为本地时间的时候,使用合适的时区去转换下就行了。比如:
>>> later_utc = utc_d + timedelta(minutes=30)
>>> print(later_utc.astimezone(central))
2013-03-10 03:15:00-05:00
>>>
当涉及到时区操作的时候,有个问题就是我们如何得到时区的名称。比如,在这个例子中,我们如何知道 Asia/Kolkata
就是印度对应的时区名呢?为了查找,可以使用 ISO 3166 国家代码作为关键字去查阅字典 pytz.country_timezones
。比如:
>>> pytz.country_timezones['IN']
['Asia/Kolkata']
>>>
注:当你阅读到这里的时候,有可能 pytz
模块已经不再建议使用了,因为 PEP 431 提出了更先进的时区支持。但是这里谈到的很多问题还是有参考价值的(比如使用 UTC 日期的建议等)。