【问题标题】:Split a list of dates into subsets of consecutive dates将日期列表拆分为连续日期的子集
【发布时间】:2020-05-03 14:02:47
【问题描述】:

我有一个日期数组,其中可以包含多个日期范围。

dates = [
 '2020-01-01',
 '2020-01-02',
 '2020-01-03',
 '2020-01-06',
 '2020-01-07',
 '2020-01-08'
]

在此示例中,列表包含 2 个单独的连续日期范围(2020-01-01 到 2020-01-03 和 2020-01-06 到 2020-01-08)

我试图弄清楚如何循环遍历这个列表并找到所有连续的日期范围。

我正在查看的一篇文章 (How to detect if dates are consecutive in Python?) 似乎有一个很好的方法,但是,我正在努力在我的用例中实现这个逻辑。

【问题讨论】:

  • 请向我们展示您的尝试:)
  • 能分享一下你目前写的代码吗?
  • 我已将我的解决方案放在下面,有时您需要输入问题才能点击:)

标签: python python-3.x date datetime python-datetime


【解决方案1】:

更多 itertools 有一个名为 consecutive_groups 的函数可以为您执行此操作:

或者您可以查看源代码并复制它的方法:

from datetime import datetime
from itertools import groupby
from operator import itemgetter

def consecutive_groups(iterable, ordering=lambda x: x):
    for k, g in groupby(enumerate(iterable), key=lambda x: x[0] - ordering(x[1])):
        yield map(itemgetter(1), g)

for g in consecutive_groups(dates, lambda x: datetime.strptime(x, '%Y-%m-%d').toordinal()):
    print(list(g))

['2020-01-01', '2020-01-02', '2020-01-03']
['2020-01-06', '2020-01-07', '2020-01-08']

【讨论】:

  • 优秀的解决方案。也许你可以解释它所基于的关键概念,因为它看起来有点神秘:即 enumerate 提供了一个连续的序列。每个日期与它的差异保持不变,直到出现差距。该更改触发了分组。简单高效,非常优雅
  • 比我的解决方案更干净,更具可读性。并了解了更多-itertools :) 谢谢@jab
【解决方案2】:

这假设单日期“范围”仍由 2 个日期表示:

def makedate(s):
    return datetime.strptime( s, "%Y-%m-%d" )
def splitIntoRanges( dates ):
    ranges = []
    start_s = last_s = dates[0]
    last = makedate(start_s)
    for curr_s in dates[1:]:
        curr = makedate(curr_s)
        if (curr - last).days > 1:
            ranges.append((start_s,last_s))
            start_s = curr_s
        last_s = curr_s
        last = curr
    return ranges + [(start_s,last_s)]

【讨论】:

    【解决方案3】:

    我采用了类似的方法,但绝对不如@Scott 优雅:

    ranges = []
    
    dates = [datetime.strptime(date, '%Y-%m-%d') for date in dates]
    start = dates[0]
    
    for i in range(1, len(dates)):
        if (dates[i] - dates[i-1]).days == 1 and i==len(dates)-1:
            end = dates[i]
            ranges.append(f'{start} to {end}')
            start = dates[i]
        elif (dates[i] - dates[i - 1]).days > 1:
            end = dates[i - 1]
            ranges.append(f'{start} to {end}')
            start = dates[i]
        else:
            continue
    

    【讨论】:

      【解决方案4】:

      我在第二篇文章中找到了解决方案的关键并将其拼凑在一起。

      我的问题有两个部分:

      1. 如何有效地表示日期列表

      回答:https://stackoverflow.com/a/9589929/2150673

      pto = [
          '2020-01-03',
          '2020-01-08',
          '2020-01-02',
          '2020-01-07',
          '2020-01-01',
          '2020-01-06'
      ]
      
      ordinal_dates = [datetime.datetime.strptime(i, '%Y-%m-%d').toordinal() for i in pto]
      
      1. 获得整数表示的日期列表后,您可以简单地查找连续整数并获取每个范围的上限和下限,然后转换回 yyyy-mm-dd 格式。

      回答:https://stackoverflow.com/a/48106843

      def ranges(nums):
          nums = sorted(set(nums))
          gaps = [[s, e] for s, e in zip(nums, nums[1:]) if s+1 < e]
          edges = iter(nums[:1] + sum(gaps, []) + nums[-1:])
          return list(zip(edges, edges))
      

      我的完整功能:

      def get_date_ranges(pto_list: list) -> list:
          pto_dates = [datetime.datetime.strptime(i, '%Y-%m-%d').toordinal() for i in pto_list]
          nums = sorted(set(pto_dates))
          gaps = [[s, e] for s, e in zip(nums, nums[1:]) if s + 1 < e]
          edges = iter(nums[:1] + sum(gaps, []) + nums[-1:])
          ordinal_ranges = list(zip(edges, edges))
          date_bounds = []
          for start, end in ordinal_ranges:
              date_bounds.append((
                  datetime.datetime.fromordinal(start).strftime('%Y-%m-%d'),
                  datetime.datetime.fromordinal(end).strftime('%Y-%m-%d')
              ))
          return date_bounds
      

      【讨论】:

      • 虽然这确实回答了我的问题,但我有兴趣为此寻找优化。惭愧地说,这个函数的每个部分我都没有完全理解,需要花一些时间来学习这些基本的python函数。
      • @Jab 的解决方案是唯一值得选择的解决方案。优雅高效
      【解决方案5】:

      您可以找到所有连续的日期范围并将它们附加到列表列表中并根据索引访问您的范围,但我更喜欢使用字典中的键以提高可读性。

      方法如下:(注意:请阅读 cmets)

      dates = [datetime.strptime(d, "%Y-%m-%d") for d in dates] # new datetime parsed from a string
      date_ints = [d.toordinal() for d in dates]  # toordinal() returns the day count from the date 01/01/01 in integers
      ranges = {}; arange = []; prev=0; index=0; j=1
      for i in date_ints: # iterate through date integers
          if i+1 == date_ints[index] + 1 and i - 1 == prev: # check and compare if integers are in sequence
              arange.append(dates[index].strftime("%Y-%m-%d"))
          elif prev == 0: # append first date to 'arange' list since 'prev' has not been updated
              arange.append(dates[index].strftime("%Y-%m-%d"))
          else:
              ranges.update({f'Range{j}': tuple(arange)}) # integer are no longer in sequence, update dictionary with new range  
              arange = []; j += 1                                   # clear 'arange' and start appending to new range  
              arange.append(dates[index].strftime("%Y-%m-%d"))
          index += 1; prev = i
      ranges.update({f'Range{j}': tuple(arange)})
      print(ranges)  
      print(ranges['Range1'])  # access a range based on the associated key
      print(ranges['Range2']) 
      

      输出:

      {'Range1': ('2020-01-01', '2020-01-02', '2020-01-03'), 'Range2': ('2020-01-06', '2020-01-07', '2020-01-08')}
      ('2020-01-01', '2020-01-02', '2020-01-03')
      ('2020-01-06', '2020-01-07', '2020-01-08')
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-06-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多