Iterasi melalui berbagai tanggal dalam Python


369

Saya memiliki kode berikut untuk melakukan ini, tetapi bagaimana saya bisa melakukannya dengan lebih baik? Saat ini saya pikir ini lebih baik daripada loop bersarang, tetapi mulai mendapatkan Perl-one-linerish ketika Anda memiliki generator dalam daftar pemahaman.

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())

Catatan

  • Saya sebenarnya tidak menggunakan ini untuk mencetak. Itu hanya untuk keperluan demo.
  • The start_datedan end_datevariabel datetime.datebenda karena saya tidak perlu cap waktu. (Mereka akan digunakan untuk menghasilkan laporan).

Output Sampel

Untuk tanggal mulai 2009-05-30dan tanggal akhir 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
Hanya untuk menunjukkan: Saya tidak berpikir ada perbedaan antara 'time.strftime ("% Y-% m-% d", single_date.timetuple ())' dan yang lebih pendek 'single_date.strftime ("% Y-% m-% d ") '. Sebagian besar jawaban sepertinya meniru gaya yang lebih panjang.
Mu Mind

8
Wow, jawaban ini terlalu rumit. Coba ini: stackoverflow.com/questions/7274267/...
Gringo Suave

@GringoSuave: apa yang rumit tentang jawaban Sean Cavanagh ?
jfs


1
Duplikat atau tidak, Anda akan mendapatkan jawaban yang lebih sederhana di halaman lain.
Gringo Suave

Jawaban:


553

Mengapa ada dua iterasi bersarang? Bagi saya itu menghasilkan daftar data yang sama dengan hanya satu iterasi:

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

Dan tidak ada daftar yang disimpan, hanya satu generator yang diulang. Juga "jika" di generator tampaknya tidak perlu.

Lagipula, urutan linear seharusnya hanya membutuhkan satu iterator, bukan dua.

Pembaruan setelah diskusi dengan John Machin:

Mungkin solusi yang paling elegan adalah menggunakan fungsi generator untuk sepenuhnya menyembunyikan / mengabstraksi iterasi pada rentang tanggal:

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: Untuk konsistensi dengan range()fungsi bawaan iterasi ini berhenti sebelum mencapai end_date. Jadi untuk iterasi inklusif gunakan hari berikutnya, seperti yang Anda lakukan dengan range().


4
-1 ... memiliki perhitungan awal day_count dan menggunakan rentang tidak dahsyat ketika perulangan sementara yang sederhana sudah cukup.
John Machin

7
@ John Machin: Oke. Namun saya lebih suka iterasi sementara loop dengan kenaikan eksplisit dari beberapa counter atau nilai. Pola interasi lebih pythonic (setidaknya dalam pandangan pribadi saya) dan juga lebih umum, karena memungkinkan untuk mengekspresikan iterasi sambil menyembunyikan rincian bagaimana iterasi dilakukan.
Ber

10
@ Bir: Saya tidak suka sama sekali; itu sangat buruk. Anda SUDAH memiliki iterasi! Dengan membungkus konstruksi yang dikeluhkan dalam generator, Anda telah menambahkan lebih banyak overhead eksekusi dan mengalihkan perhatian pengguna ke tempat lain untuk membaca kode dan / atau dokumen 3-liner Anda. -2
John Machin

8
@ John Machin: Saya tidak setuju. Intinya bukan tentang mengurangi jumlah garis ke minimum absolut. Bagaimanapun, kita tidak berbicara Perl di sini. Juga, kode saya hanya melakukan satu iterasi (begitulah generator bekerja, tapi saya kira Anda tahu itu). *** Maksud saya adalah tentang abstrak konsep untuk digunakan kembali dan kode penjelasan sendiri. Saya berpendapat bahwa ini jauh lebih bermanfaat daripada memiliki kode sesingkat mungkin.
Ber

9
Jika Anda ingin kesederhanaan, Anda dapat menggunakan ekspresi generator:(start_date + datetime.timedelta(n) for n in range((end_date - start_date).days))
Mark Ransom

219

Ini mungkin lebih jelas:

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
Sangat jelas dan singkat, tetapi tidak berfungsi dengan baik jika Anda ingin menggunakan terus
rslite

berfungsi dengan baik untuk kasus penggunaan saya
doomdaam

169

Gunakan dateutilperpustakaan:

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")

Pustaka python ini memiliki banyak fitur canggih, beberapa sangat berguna, seperti relative deltas — dan diimplementasikan sebagai satu file (modul) yang mudah dimasukkan ke dalam proyek.


3
Perhatikan bahwa tanggal akhir dalam untuk loop di sini adalah termasuk dari untilsedangkan tanggal akhir dari daterangemetode dalam jawaban Ber ini adalah eksklusif dari end_date.
Ninjakannon


77

Panda bagus untuk deret waktu secara umum, dan memiliki dukungan langsung untuk rentang tanggal.

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

Anda kemudian dapat mengulangi daterange untuk mencetak tanggal:

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

Ini juga memiliki banyak pilihan untuk membuat hidup lebih mudah. Misalnya, jika Anda hanya menginginkan hari kerja, Anda cukup menukar di bdate_range. Lihat http://pandas.pydata.org/pandas-docs/stable/timeseries.html#generating-ranges-of-timestamps

Kekuatan Pandas adalah benar-benar kerangka datanya, yang mendukung operasi vektor (seperti numpy) yang membuat operasi melintasi sejumlah besar data sangat cepat dan mudah.

EDIT: Anda juga bisa melewatkan loop untuk dan hanya mencetaknya secara langsung, yang lebih mudah dan lebih efisien:

print(daterange)

"seperti numpy" - Panda dibangun di atas 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())

Fungsi ini melakukan lebih dari yang Anda perlukan, dengan mendukung langkah negatif, dll. Selama Anda memfaktorkan logika jangkauan Anda, maka Anda tidak perlu terpisah day_countdan yang paling penting kode menjadi lebih mudah dibaca saat Anda memanggil fungsi dari banyak tempat


Terima kasih, diganti namanya menjadi parameter yang lebih cocok dengan rentang, lupa diubah di badan.

+1 ... tetapi karena Anda mengizinkan langkah menjadi timedelta, Anda harus (a) menyebutnya dateTIMErange () dan membuat langkah-langkah mis. Timedelta (jam = 12) dan timedelta (jam = 36) berfungsi dengan baik atau ( b) menjebak langkah-langkah yang bukan merupakan jumlah hari yang tidak terpisahkan atau (c) menyimpan kerumitan si penelepon dan menyatakan langkah itu sebagai jumlah hari alih-alih timedelta.
John Machin

Setiap timedelta seharusnya sudah berfungsi, tetapi saya menambahkan datetime_range dan date_range ke koleksi memo pribadi saya setelah menulis ini, karena (a). Tidak yakin fungsi lain bermanfaat untuk (c), kasus hari paling umum = 1 sudah diurus, dan harus melewati timedelta eksplisit untuk menghindari kebingungan. Mungkin mengunggahnya di suatu tempat adalah yang terbaik: bitbucket.org/kniht/scraps/src/tip/python/gen_range.py

untuk membuat ini berfungsi dengan selisih selain hari Anda harus memeriksa terhadap step.total_seconds (), dan bukan step.days
amohr

12

Ini adalah solusi yang paling bisa dibaca manusia yang bisa saya pikirkan.

import datetime

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

11

Kenapa tidak mencoba:

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

Fungsi Numpy arangedapat diterapkan ke tanggal:

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)

Penggunaan astypeis untuk mengkonversi dari numpy.datetime64ke array datetime.datetimeobjek.


Konstruksi super ramping! Baris terakhir cocok untuk saya dengandates = np.arange(d0, d1, dt).astype(datetime.datetime)
pyano

+1 untuk memposting solusi satu liner umum yang memungkinkan timedelta apa pun, alih-alih langkah bulat tetap seperti per jam / menit / /.
F.Raab

7

Perlihatkan n hari terakhir mulai hari ini:

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

Keluaran:

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

Silakan tambahkan kurung bulat, sepertiprint((datetime.date.today() + datetime.timedelta(i)).isoformat())
TitanFighter

@TitanFighter jangan ragu untuk melakukan pengeditan, saya akan menerimanya.
user1767754

2
Saya mencoba. Pengeditan membutuhkan minimum 6 karakter, tetapi dalam hal ini perlu menambahkan hanya 2 karakter, "(" dan ")"
TitanFighter

print((datetime.date.today() + datetime.timedelta(i)))tanpa .isoformat () memberikan output yang persis sama. Saya perlu skrip saya untuk mencetak YYMMDD. Ada yang tahu bagaimana melakukan hal itu?
mr.zog

Lakukan saja ini di for for alih-alih pernyataan cetakd = 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

Untuk kelengkapan, Pandas juga memiliki period_rangefungsi untuk cap waktu yang di luar batas:

import pandas as pd

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

3

Saya memiliki masalah yang sama, tetapi saya harus mengulanginya setiap bulan, bukan setiap hari.

Ini solusi saya

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

Contoh 1

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

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

Keluaran

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

Contoh # 2

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

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

Keluaran

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

Dapat 't * percaya pertanyaan ini telah ada selama 9 tahun tanpa ada yang menyarankan fungsi rekursif sederhana:

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)

Keluaran:

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

Sunting: * Sekarang saya dapat mempercayainya - lihat Apakah Python mengoptimalkan rekursi ekor? . Terima kasih, Tim .


3
Mengapa Anda mengganti loop sederhana dengan rekursi? Ini memecah untuk rentang yang lebih lama dari kira-kira dua setengah tahun.
Tim-Erwin

@ Tim-Erwin Jujur saya tidak tahu CPython tidak mengoptimalkan rekursi ekor sehingga komentar Anda sangat berharga.
Pocketsand

2

Anda dapat menghasilkan serangkaian tanggal antara dua tanggal menggunakan perpustakaan panda secara sederhana dan penuh kepercayaan

import pandas as pd

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

Anda dapat mengubah frekuensi menghasilkan tanggal dengan menetapkan freq sebagai D, M, Q, Y (harian, bulanan, triwulanan, tahunan)


Sudah dijawab di utas ini pada tahun 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

Fungsi ini memiliki beberapa fitur tambahan:

  • dapat meneruskan string yang cocok dengan DATE_FORMAT untuk awal atau akhir dan itu dikonversi ke objek tanggal
  • dapat melewati objek tanggal untuk memulai atau mengakhiri
  • memeriksa kesalahan jika akhirnya lebih tua dari awal

    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

Berikut kode untuk fungsi rentang tanggal umum, mirip dengan jawaban Ber, tetapi lebih fleksibel:

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

Bagaimana dengan yang berikut untuk melakukan rentang yang bertambah berdasarkan hari:

for d in map( lambda x: startDate+datetime.timedelta(days=x), xrange( (stopDate-startDate).days ) ):
  # Do stuff here
  • startDate dan stopDate adalah objek datetime.date

Untuk versi generik:

for d in map( lambda x: startTime+x*stepTime, xrange( (stopTime-startTime).total_seconds() / stepTime.total_seconds() ) ):
  # Do stuff here
  • startTime dan stopTime adalah objek datetime.date atau datetime.datetime (keduanya harus bertipe sama)
  • stepTime adalah objek timedelta

Perhatikan bahwa .total_seconds () hanya didukung setelah python 2.7. Jika Anda terjebak dengan versi sebelumnya, Anda dapat menulis fungsi Anda sendiri:

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

0

Pendekatan yang sedikit berbeda untuk langkah reversibel dengan menyimpan rangeargs dalam 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')

KELUARAN:

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

Selamat Datang di Stack Overflow! Meskipun kode ini dapat menyelesaikan pertanyaan, termasuk penjelasan tentang bagaimana dan mengapa ini menyelesaikan masalah, terutama pada pertanyaan dengan terlalu banyak jawaban yang baik, akan sangat membantu untuk meningkatkan kualitas posting Anda, dan mungkin menghasilkan lebih banyak upvotes. Ingatlah bahwa Anda menjawab pertanyaan untuk pembaca di masa depan, bukan hanya orang yang bertanya sekarang. Harap edit jawaban Anda untuk menambahkan penjelasan dan berikan indikasi tentang batasan dan asumsi apa yang berlaku. Dari Ulasan
double-beep
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.