web-dev-qa-db-fra.com

Itérer sur une plage de dates en Python

J'ai le code suivant pour le faire, mais comment puis-je le faire mieux? À l'heure actuelle, je pense que c'est mieux que les boucles imbriquées, mais cela commence à devenir Perl-one-linerish lorsque vous avez un générateur dans une liste de compréhension. 

day_count = (end_date - start_date).days + 1
for single_date in [d for d in (start_date + timedelta(n) for n in range(day_count)) if d <= end_date]:
    print strftime("%Y-%m-%d", single_date.timetuple())

Remarques

  • Je ne l'utilise pas réellement pour imprimer. C'est juste à des fins de démonstration. 
  • Les variables start_date et end_date sont des objets datetime.date car je n'ai pas besoin des horodatages. (Ils vont être utilisés pour générer un rapport).

Exemple de sortie

Pour une date de début de 2009-05-30 et une date de fin de 2009-06-09:

2009-05-30
2009-05-31
2009-06-01
2009-06-02
2009-06-03
2009-06-04
2009-06-05
2009-06-06
2009-06-07
2009-06-08
2009-06-09
278
ShawnMilo

Pourquoi y a-t-il deux itérations imbriquées? Pour moi, il produit la même liste de données avec une seule itération:

for single_date in (start_date + timedelta(n) for n in range(day_count)):
    print ...

Et aucune liste n'est stockée, un seul générateur est itéré. De plus, le "si" dans le générateur semble être inutile.

Après tout, une séquence linéaire ne devrait nécessiter qu'un seul itérateur, pas deux.

Mise à jour après discussion avec John Machin:

La solution la plus élégante est peut-être d'utiliser une fonction de générateur pour masquer/résumer complètement l'itération sur la plage de dates:

from datetime import timedelta, date

def daterange(start_date, end_date):
    for n in range(int ((end_date - start_date).days)):
        yield start_date + timedelta(n)

start_date = date(2013, 1, 1)
end_date = date(2015, 6, 2)
for single_date in daterange(start_date, end_date):
    print single_date.strftime("%Y-%m-%d")

NB: Par souci de cohérence avec la fonction range() intégrée, cette itération est arrêtée avant pour atteindre le end_date. Donc, pour une itération inclusive, utilisez le lendemain, comme vous le feriez avec range().

434
Ber

Cela pourrait être plus clair:

d = start_date
delta = datetime.timedelta(days=1)
while d <= end_date:
    print d.strftime("%Y-%m-%d")
    d += delta
155
Sean Cavanagh

Utilisez la bibliothèque dateutil :

from datetime import date
from dateutil.rrule import rrule, DAILY

a = date(2009, 5, 30)
b = date(2009, 6, 9)

for dt in rrule(DAILY, dtstart=a, until=b):
    print dt.strftime("%Y-%m-%d")

Cette bibliothèque python possède de nombreuses fonctionnalités plus avancées, dont certaines très utiles, telles que relative deltas - et est implémentée sous la forme d'un fichier unique (module) facilement incluse dans un projet.

139
nosklo

Les pandas sont parfaits pour les séries chronologiques en général et prennent en charge directement les plages de dates.

import pandas as pd
daterange = pd.date_range(start_date, end_date)

Vous pouvez ensuite faire une boucle sur la plage pour imprimer la date:

for single_date in daterange:
    print (single_date.strftime("%Y-%m-%d"))

Il a également beaucoup d'options pour vous rendre la vie plus facile. Par exemple, si vous ne voulez que les jours de semaine, vous devez simplement échanger bdate_range. Voir http://pandas.pydata.org/pandas-docs/stable/timeseries.html#generating-ranges-of-timestamps

La puissance des pandas réside en réalité dans ses images, qui prennent en charge les opérations vectorisées (un peu comme Numpy), qui rendent les opérations sur de grandes quantités de données très simples et rapides.

EDIT: Vous pouvez également ignorer complètement la boucle for et l’imprimer directement, ce qui est plus simple et plus efficace:

print(daterange)
47
fantabolous
import datetime

def daterange(start, stop, step=datetime.timedelta(days=1), inclusive=False):
  # inclusive=False to behave like range by default
  if step.days > 0:
    while start < stop:
      yield start
      start = start + step
      # not +=! don't modify object passed in if it's mutable
      # since this function is not restricted to
      # only types from datetime module
  Elif step.days < 0:
    while start > stop:
      yield start
      start = start + step
  if inclusive and start == stop:
    yield start

# ...

for date in daterange(start_date, end_date, inclusive=True):
  print strftime("%Y-%m-%d", date.timetuple())

Cette fonction fait plus que ce dont vous avez strictement besoin, en prenant en charge les pas négatifs, etc. Tant que vous factorisez votre logique de distance, vous n'avez pas besoin du day_count séparé et, surtout, le code devient plus facile à lire lorsque vous appelez la fonction plusieurs endroits.

13
Roger Pate

Pourquoi ne pas essayer:

import datetime as dt

start_date = dt.datetime(2012, 12,1)
end_date = dt.datetime(2012, 12,5)

total_days = (end_date - start_date).days + 1 #inclusive 5 days

for day_number in range(total_days):
    current_date = (start_date + dt.timedelta(days = day_number)).date()
    print current_date
11
john

C’est la solution la plus lisible par l’homme à laquelle je puisse penser.

import datetime

def daterange(start, end, step=datetime.timedelta(1)):
    curr = start
    while curr < end:
        yield curr
        curr += step
9
Patrick

Afficher les n derniers jours à compter de ce jour:

import datetime
for i in range(0, 100):
    print((datetime.date.today() + datetime.timedelta(i)).isoformat())

Sortie:  

2016-06-29
2016-06-30
2016-07-01
2016-07-02
2016-07-03
2016-07-04
6
user1767754

La fonction arange de Numpy peut être appliquée aux dates:

import numpy as np
from datetime import datetime, timedelta
d0 = datetime(2009, 1,1)
d1 = datetime(2010, 1,1)
dt = timedelta(days = 1)
dates = np.arange(d0, d1, dt).astype(datetime)

L'utilisation de astype consiste à convertir de numpy.datetime64 en un tableau d'objets datetime.datetime.

5
Tor
import datetime

def daterange(start, stop, step_days=1):
    current = start
    step = datetime.timedelta(step_days)
    if step_days > 0:
        while current < stop:
            yield current
            current += step
    Elif step_days < 0:
        while current > stop:
            yield current
            current += step
    else:
        raise ValueError("daterange() step_days argument must not be zero")

if __== "__main__":
    from pprint import pprint as pp
    lo = datetime.date(2008, 12, 27)
    hi = datetime.date(2009, 1, 5)
    pp(list(daterange(lo, hi)))
    pp(list(daterange(hi, lo, -1)))
    pp(list(daterange(lo, hi, 7)))
    pp(list(daterange(hi, lo, -7))) 
    assert not list(daterange(lo, hi, -1))
    assert not list(daterange(hi, lo))
    assert not list(daterange(lo, hi, -7))
    assert not list(daterange(hi, lo, 7)) 
4
John Machin

J'ai un problème similaire, mais je dois effectuer une itération mensuelle plutôt que quotidienne.

C'est ma solution

import calendar
from datetime import datetime, timedelta

def days_in_month(dt):
    return calendar.monthrange(dt.year, dt.month)[1]

def monthly_range(dt_start, dt_end):
    forward = dt_end >= dt_start
    finish = False
    dt = dt_start

    while not finish:
        yield dt.date()
        if forward:
            days = days_in_month(dt)
            dt = dt + timedelta(days=days)            
            finish = dt > dt_end
        else:
            _tmp_dt = dt.replace(day=1) - timedelta(days=1)
            dt = (_tmp_dt.replace(day=dt.day))
            finish = dt < dt_end

Exemple 1

date_start = datetime(2016, 6, 1)
date_end = datetime(2017, 1, 1)

for p in monthly_range(date_start, date_end):
    print(p)

Sortie

2016-06-01
2016-07-01
2016-08-01
2016-09-01
2016-10-01
2016-11-01
2016-12-01
2017-01-01

Exemple # 2

date_start = datetime(2017, 1, 1)
date_end = datetime(2016, 6, 1)

for p in monthly_range(date_start, date_end):
    print(p)

Sortie

2017-01-01
2016-12-01
2016-11-01
2016-10-01
2016-09-01
2016-08-01
2016-07-01
2016-06-01
3
juanmhidalgo

Je ne peux pas croire * que cette question existe depuis 9 ans sans que personne ne suggère une fonction récursive simple:

from datetime import datetime, timedelta

def walk_days(start_date, end_date):
    if start_date <= end_date:
        print(start_date.strftime("%Y-%m-%d"))
        next_date = start_date + timedelta(days=1)
        walk_days(next_date, end_date)

#demo
start_date = datetime(2009, 5, 30)
end_date   = datetime(2009, 6, 9)

walk_days(start_date, end_date)

Sortie:

2009-05-30
2009-05-31
2009-06-01
2009-06-02
2009-06-03
2009-06-04
2009-06-05
2009-06-06
2009-06-07
2009-06-08
2009-06-09

Edit: * Je peux maintenant le croire - voir Python optimise-t-il la récursion des queues? . Merci Tim .

2
Pocketsand
for i in range(16):
    print datetime.date.today() + datetime.timedelta(days=i)
2
user368996

Vous pouvez générer une série de dates entre deux dates en utilisant la bibliothèque pandas simplement et en toute confiance.

import pandas as pd

print pd.date_range(start='1/1/2010', end='1/08/2018', freq='M')

Vous pouvez modifier la fréquence de génération des dates en définissant fréq sur D, M, Q, Y (Quotidien, mensuel, trimestriel, annuel )

1
Shinto Joseph
> pip install DateTimeRange

from datetimerange import DateTimeRange

def dateRange(start, end, step):
        rangeList = []
        time_range = DateTimeRange(start, end)
        for value in time_range.range(datetime.timedelta(days=step)):
            rangeList.append(value.strftime('%m/%d/%Y'))
        return rangeList

    dateRange("2018-09-07", "2018-12-25", 7)  

    Out[92]: 
    ['09/07/2018',
     '09/14/2018',
     '09/21/2018',
     '09/28/2018',
     '10/05/2018',
     '10/12/2018',
     '10/19/2018',
     '10/26/2018',
     '11/02/2018',
     '11/09/2018',
     '11/16/2018',
     '11/23/2018',
     '11/30/2018',
     '12/07/2018',
     '12/14/2018',
     '12/21/2018']
1
LetzerWille

Approche légèrement différente des étapes réversibles en stockant des arguments range dans un tuple. 

def date_range(start, stop, step=1, inclusive=False):
    day_count = (stop - start).days
    if inclusive:
        day_count += 1

    if step > 0:
        range_args = (0, day_count, step)
    Elif step < 0:
        range_args = (day_count - 1, -1, step)
    else:
        raise ValueError("date_range(): step arg must be non-zero")

    for i in range(*range_args):
        yield start + timedelta(days=i)
0
GollyJer

Qu'en est-il des éléments suivants pour effectuer une plage incrémentée de jours:

for d in map( lambda x: startDate+datetime.timedelta(days=x), xrange( (stopDate-startDate).days ) ):
  # Do stuff here
  • startDate et stopDate sont des objets datetime.date

Pour une version générique:

for d in map( lambda x: startTime+x*stepTime, xrange( (stopTime-startTime).total_seconds() / stepTime.total_seconds() ) ):
  # Do stuff here
  • startTime et stopTime sont des objets datetime.date ou datetime.datetime (les deux doivent être du même type)
  • stepTime est un objet timedelta

Notez que .total_seconds () n'est supporté qu'après python 2.7. Si vous êtes bloqué avec une version antérieure, vous pouvez écrire votre propre fonction

def total_seconds( td ):
  return float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
0
teambob

Cette fonction a quelques fonctionnalités supplémentaires:

  • peut transmettre une chaîne correspondant à DATE_FORMAT pour le début ou la fin et la convertir en un objet de date
  • peut transmettre un objet de date pour le début ou la fin
  • vérification d'erreur si la fin est plus ancienne que le début

    import datetime
    from datetime import timedelta
    
    
    DATE_FORMAT = '%Y/%m/%d'
    
    def daterange(start, end):
          def convert(date):
                try:
                      date = datetime.datetime.strptime(date, DATE_FORMAT)
                      return date.date()
                except TypeError:
                      return date
    
          def get_date(n):
                return datetime.datetime.strftime(convert(start) + timedelta(days=n), DATE_FORMAT)
    
          days = (convert(end) - convert(start)).days
          if days <= 0:
                raise ValueError('The start date must be before the end date.')
          for n in range(0, days):
                yield get_date(n)
    
    
    start = '2014/12/1'
    end = '2014/12/31'
    print list(daterange(start, end))
    
    start_ = datetime.date.today()
    end = '2015/12/1'
    print list(daterange(start, end))
    
0
DMfll

Voici le code pour une fonction de plage de dates générale, similaire à la réponse de Ber, mais plus flexible:

def count_timedelta(delta, step, seconds_in_interval):
    """Helper function for iterate.  Finds the number of intervals in the timedelta."""
    return int(delta.total_seconds() / (seconds_in_interval * step))


def range_dt(start, end, step=1, interval='day'):
    """Iterate over datetimes or dates, similar to builtin range."""
    intervals = functools.partial(count_timedelta, (end - start), step)

    if interval == 'week':
        for i in range(intervals(3600 * 24 * 7)):
            yield start + datetime.timedelta(weeks=i) * step

    Elif interval == 'day':
        for i in range(intervals(3600 * 24)):
            yield start + datetime.timedelta(days=i) * step

    Elif interval == 'hour':
        for i in range(intervals(3600)):
            yield start + datetime.timedelta(hours=i) * step

    Elif interval == 'minute':
        for i in range(intervals(60)):
            yield start + datetime.timedelta(minutes=i) * step

    Elif interval == 'second':
        for i in range(intervals(1)):
            yield start + datetime.timedelta(seconds=i) * step

    Elif interval == 'millisecond':
        for i in range(intervals(1 / 1000)):
            yield start + datetime.timedelta(milliseconds=i) * step

    Elif interval == 'microsecond':
        for i in range(intervals(1e-6)):
            yield start + datetime.timedelta(microseconds=i) * step

    else:
        raise AttributeError("Interval must be 'week', 'day', 'hour' 'second', \
            'microsecond' or 'millisecond'.")
0
Turtles Are Cute