Itération à travers une plage de dates en Python


369

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_dateet end_datesont des datetime.dateobjets 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 2009-05-30et 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

3
Juste pour souligner: je ne pense pas qu'il y ait de différence entre 'time.strftime ("% Y-% m-% d", single_date.timetuple ())' et le plus court 'single_date.strftime ("% Y-% Maryland")'. La plupart des réponses semblent copier le style le plus long.
Mu Mind

8
Wow, ces réponses sont beaucoup trop compliquées. Essayez ceci: stackoverflow.com/questions/7274267/…
Gringo Suave

@GringoSuave: qu'est-ce qui est compliqué dans la réponse de Sean Cavanagh ?
jfs


1
Dupliquer ou non, vous obtiendrez une réponse plus simple à l'autre page.
Gringo Suave

Réponses:


554

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 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 / abstraire 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 range()fonction intégrée, cette itération s'arrête avant d' atteindre le end_date. Donc, pour une itération inclusive, utilisez le lendemain, comme vous le feriez avec range().


4
-1 ... avoir un calcul préliminaire de day_count et utiliser range n'est pas génial quand une simple boucle while suffira.
John Machin

7
@John Machin: D'accord. Je préfère cependant une itération sur les boucles while avec incrémentation explicite d'un certain compteur ou valeur. Le modèle d'interaction est plus pythonique (au moins à mon avis) et aussi plus général, car il permet d'exprimer une itération tout en cachant les détails de la façon dont cette itération est effectuée.
Ber

10
@Ber: Je n'aime pas ça du tout; c'est DOUBLEMENT mauvais. Vous avez DÉJÀ eu une itération! En enveloppant les constructions incriminées dans un générateur, vous avez ajouté encore plus de temps d'exécution et détourné l'attention de l'utilisateur vers un autre endroit pour lire le code et / ou les documents de votre 3 lignes. -2
John Machin

8
@John Machin: Je ne suis pas d'accord. Il ne s'agit pas de réduire le nombre de lignes au minimum absolu. Après tout, nous ne parlons pas de Perl ici. De plus, mon code ne fait qu'une seule itération (c'est ainsi que fonctionne le générateur, mais je suppose que vous le savez). *** Mon point concerne l'abstraction des concepts pour la réutilisation et le code auto-explicatif. Je maintiens que cela vaut beaucoup plus que d'avoir le code le plus court possible.
Ber

9
Si vous optez pour la concision, vous pouvez utiliser une expression de générateur:(start_date + datetime.timedelta(n) for n in range((end_date - start_date).days))
Mark Ransom

219

Cela pourrait être plus clair:

from datetime import date, timedelta

start_date = date(2019, 1, 1)
end_date = date(2020, 1, 1)
delta = timedelta(days=1)
while start_date <= end_date:
    print (start_date.strftime("%Y-%m-%d"))
    start_date += delta

3
Très clair et court, mais ne fonctionne pas bien si vous voulez continuer
rslite

fonctionne très bien pour mon cas d'utilisation
doomdaam

169

Utilisez la dateutilbibliothèque:

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, certaines très utiles, comme relative deltas — et est implémentée sous la forme d'un fichier unique (module) qui est facilement inclus dans un projet.


3
Notez que la date finale de la boucle est ici pour y compris de untilque la date finale de la daterangeméthode dans la réponse de Ber est exclusive de end_date.
Ninjakannon


77

Pandas est idéal pour les séries chronologiques en général et prend directement en charge les plages de dates.

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

Vous pouvez ensuite parcourir la daterange pour imprimer la date:

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

Il dispose également de nombreuses options pour vous faciliter la vie. Par exemple, si vous ne vouliez que les jours de la semaine, vous échangeriez simplement bdate_range. Voir http://pandas.pydata.org/pandas-docs/stable/timeseries.html#generating-ranges-of-timestamps

La puissance de Pandas est vraiment ses cadres de données, 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 rapides et faciles.

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

print(daterange)

"un peu comme numpy" - Pandas est construit sur numpy: P
Zach Saucier

15
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 vous avez besoin strictement, en soutenant pas de négatif, etc. Tant que vous tenez compte de votre logique de gamme, alors vous n'avez pas besoin séparé day_countet surtout le code devient plus facile à lire que vous appelez la fonction de plusieurs des endroits.


Merci, renommé pour correspondre plus étroitement aux paramètres de la plage, j'ai oublié de changer dans le corps.

+1 ... mais comme vous autorisez l'étape à être un timedelta, vous devez soit (a) l'appeler dateTIMErange () et faire fonctionner par exemple timedelta (heures = 12) et timedelta (heures = 36) ou ( b) intercepter les étapes qui ne sont pas un nombre entier de jours ou (c) sauver l'appelant les tracas et exprimer l'étape comme un nombre de jours au lieu d'un timedelta.
John Machin

Tout timedelta devrait déjà fonctionner, mais j'ai ajouté datetime_range et date_range à ma collection de scrap personnelle après avoir écrit ceci, à cause de (a). Vous n'êtes pas sûr qu'une autre fonction vaille la peine pour (c), le cas le plus courant de jours = 1 est déjà pris en charge et le fait de devoir passer un timedelta explicite évite la confusion. Peut-être que le télécharger quelque part est le meilleur: bitbucket.org/kniht/scraps/src/tip/python/gen_range.py

pour que cela fonctionne sur des incréments autres que des jours, vous devez vérifier contre step.total_seconds (), et non pas step.days
amohr

12

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

11

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

7

La arangefonction 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 astypeest de convertir de numpy.datetime64à un tableau d' datetime.datetimeobjets.


Construction super allégée! La dernière ligne fonctionne pour moi avecdates = np.arange(d0, d1, dt).astype(datetime.datetime)
pyano

+1 pour la publication d'une solution générique à une ligne qui autorise n'importe quel timedelta, au lieu d'une étape arrondie fixe telle que horaire / minutieuse /….
F.Raab

7

Afficher les n derniers jours d'aujourd'hui:

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

Production:

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

Veuillez ajouter des parenthèses rondes, commeprint((datetime.date.today() + datetime.timedelta(i)).isoformat())
TitanFighter

@TitanFighter n'hésitez pas à faire des modifications, je les accepte.
user1767754

2
J'ai essayé. L'édition nécessite au moins 6 caractères, mais dans ce cas, il est nécessaire d'ajouter seulement 2 caractères, "(" et ")"
TitanFighter

print((datetime.date.today() + datetime.timedelta(i)))sans le .isoformat () donne exactement la même sortie. J'ai besoin de mon script pour imprimer YYMMDD. Quelqu'un sait comment faire cela?
mr.zog

Faites-le dans la boucle for au lieu de l'instruction printd = datetime.date.today() + datetime.timedelta(i); d.strftime("%Y%m%d")
user1767754

5
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 __name__ == "__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
for i in range(16):
    print datetime.date.today() + datetime.timedelta(days=i)

4

Pour être complet, Pandas a également une period_rangefonction pour les horodatages qui sont hors limites:

import pandas as pd

pd.period_range(start='1/1/1626', end='1/08/1627', freq='D')

3

J'ai un problème similaire, mais je dois répéter chaque mois plutôt que quotidiennement.

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)

Production

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)

Production

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

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

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)

Production:

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: * Maintenant, je peux le croire - voir Est - ce que Python optimise la récursivité de la queue? . Merci Tim .


3
Pourquoi voudriez-vous remplacer une simple boucle par récursivité? Cela se rompt pour des plages qui dépassent environ deux ans et demi.
Tim-Erwin

@ Tim-Erwin Honnêtement, je n'avais aucune idée que CPython n'optimise pas la récursivité de la queue, votre commentaire est donc précieux.
Pocketsand

2

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 freq sur D, M, Q, Y (quotidien, mensuel, trimestriel, annuel)


Déjà répondu dans ce fil en 2014
Alexey Vazhnov

2
> 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

Cette fonction a quelques fonctionnalités supplémentaires:

  • peut passer une chaîne correspondant au DATE_FORMAT pour le début ou la fin et il est converti en un objet date
  • peut passer un objet date pour le début ou la fin
  • vérification des erreurs au cas où 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))

1

Voici le code d'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

Qu'en est-il de ce qui suit pour faire 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 pris en charge 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

Approche légèrement différente des étapes réversibles en stockant les rangearguments 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
import datetime
from dateutil.rrule import DAILY,rrule

date=datetime.datetime(2019,1,10)

date1=datetime.datetime(2019,2,2)

for i in rrule(DAILY , dtstart=date,until=date1):
     print(i.strftime('%Y%b%d'),sep='\n')

PRODUCTION:

2019Jan10
2019Jan11
2019Jan12
2019Jan13
2019Jan14
2019Jan15
2019Jan16
2019Jan17
2019Jan18
2019Jan19
2019Jan20
2019Jan21
2019Jan22
2019Jan23
2019Jan24
2019Jan25
2019Jan26
2019Jan27
2019Jan28
2019Jan29
2019Jan30
2019Jan31
2019Feb01
2019Feb02

Bienvenue dans Stack Overflow! Bien que ce code puisse résoudre la question, y compris une explication de comment et pourquoi cela résout le problème, en particulier sur les questions avec trop de bonnes réponses, cela aiderait vraiment à améliorer la qualité de votre message et entraînerait probablement plus de votes positifs. N'oubliez pas que vous répondrez à la question des lecteurs à l'avenir, pas seulement à la personne qui pose la question maintenant. Veuillez modifier votre réponse pour ajouter des explications et donner une indication des limitations et hypothèses applicables. De l'avis
double-bip le
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.