Get all months and years in a range - python

I am writing a django application where I have records stored on the basis of datetimefield.
first_record = MyModel.objects.filter().order_by('-added').first()
first_record = (first_record.added.month, first_record.added.year)
last_record = MyModel.objects.filter().order_by('-added').first()
last_record = (last_record.added.month, last_record.added.year)
Now I want to make a list of all months/year between the first record and last record. A rough idea is:
for i in range(first_record, last_record):
# do something
Where the range function is supposed to give me a list to iterate over which looks like this:
[('01','2018'),('02','2018'),('03','2018'),....,('11','2020'),('12','2020')]
Any ideas how do I do that?
Also is (last_record.added.month, last_record.added.year) the right way to get a tuple containing month and year. Note that I want months in the format 01 instead of 1 for first month for example.

I believe Django has a built-in function. You can do:
>>> Entry.objects.dates('pub_date', 'month')
[datetime.date(2005, 2, 1), datetime.date(2005, 3, 1)]
>>> Entry.objects.dates('pub_date', 'week')
[datetime.date(2005, 2, 14), datetime.date(2005, 3, 14)]
Which, translated into your code, will be something like
MyModel.objects.dates('added', 'month')
Documentation

You can do this by using dateutil.relativedelta
Here is the code
from dateutil.relativedelta import relativedelta
import datetime
result = []
today = datetime.date.today()
current = datetime.date(2010, 8, 1)
while current <= today:
result.append(current)
current += relativedelta(months=1)
Know more abou in
https://dateutil.readthedocs.io/en/latest/relativedelta.html

Related

How to get a list of week number with year(202114 in this format) using python or pyspark

How to get a list of week number with year(202114 in this format) using python or pyspark.
considering todays date (202114) the output should be {202113,202112,202111,202110,202109,202108}
You could use datetime's function isocalendar to get the current week number:
from datetime import date
year, week_num, day_num = date.today().isocalendar()
After that, you could use a simple for loop to iterate over the last 6 weeks:
weeks = []
for i in range(1, 7):
weeks.append(str(year) + str(week_num - i))
This should leave you a list that looks like this:
['202113', '202112'..., '20219', '20218']
if that's a problem and you need '20218' to be '202108', use this:
weeks = []
for i in range(1, 7):
curr_week = week_num - i
str_week = str(curr_week) if len(str(curr_week)) == 2 else f'0{curr_week}'
weeks.append(str(year) + str_week)
You could probably write this in a much cleaner way but I hope this helped.

Adding a timedelta to a skyfield Time

The skyfield Almanach documentation
uses this code to define the points in time between which to compute sunrise & sunset:
t0 = ts.utc(2018, 9, 12, 4)
t1 = ts.utc(2018, 9, 13, 4)
What if I just wanted to use one (start) date and set the next date to be exactly one day after? I can't just add one to the day argument since this would not be correct at the end of the month.
Using Python's datetime I could do this using
from datetime import datetime, timedelta
datetime(2019, 1, 31, 12) + timedelta(days=1)
# datetime.datetime(2019, 2, 1, 12, 0)
but I can't find anything like timedelta in the skyfield API documentation.
What if I just wanted to use one (start) date and set the next date to be exactly one day after? I can't just add one to the day argument since this would not be correct at the end of the month.
Happily, you can just add one to the day! As the documentation says:
https://rhodesmill.org/skyfield/time.html
"you are free to provide out-of-range values and leave it to Skyfield to work out the correct result"
>>> from skyfield.api import load
>>> ts = load.timescale()
[#################################] 100% deltat.data
>>> t = ts.utc(2018, 2, 28 + 1)
>>> t.utc_jpl()
'A.D. 2018-Mar-01 00:00:00.0000 UT'
You can use datetime's timedelta and convert back between datetime and skyfield's Time objects like this:
t0 = ts.utc(2019, 1, 31, 12)
t1 = ts.utc(t0.utc_datetime() + timedelta(days=1))
# Print
t1.utc_iso()
# '2019-02-01T12:00:00Z'
While certainly not beautiful, this allows you to use all the features of Python's datetime.

How to Extract Dates in DateRangeField in Django

I'm trying to extract the dates in the DateRange field in django. I'm trying to see if a date is within the range of that DateRange but I'm unable to extract the dates in DateRange. Here's a sample:
'date_range': DateRange(datetime.date(2017, 2, 17), datetime.date(2017, 2, 18), '[)')
So I'm trying to get the first date and the second date and check if a certain date is within that range of DateRange.
How do I do that? Thanks!
You can retrieve the range values through .upper and .lower properties. E.g., my_object.date_range.upper.
if you want to check that a whether a particular date is in a date range or not you can compare the dates
from datetime import date
>>> start = date(2017, 02, 1)
>>> end = date(2017, 02, 28)
>>> curr = date.today()
>>> curr >= start and curr <= end
True

Python: get all months in range?

I want to get all months between now and August 2010, as a list formatted like this:
['2010-08-01', '2010-09-01', .... , '2016-02-01']
Right now this is what I have:
months = []
for y in range(2010, 2016):
for m in range(1, 13):
if (y == 2010) and m < 8:
continue
if (y == 2016) and m > 2:
continue
month = '%s-%s-01' % (y, ('0%s' % (m)) if m < 10 else m)
months.append(month)
What would be a better way to do this?
dateutil.relativedelta is handy here.
I've left the formatting out as an exercise.
from dateutil.relativedelta import relativedelta
import datetime
result = []
today = datetime.date.today()
current = datetime.date(2010, 8, 1)
while current <= today:
result.append(current)
current += relativedelta(months=1)
I had a look at the dateutil documentation. Turns out it provides an even more convenient way than using dateutil.relativedelta: recurrence rules (examples)
For the task at hand, it's as easy as
from dateutil.rrule import *
from datetime import date
months = map(
date.isoformat,
rrule(MONTHLY, dtstart=date(2010, 8, 1), until=date.today())
)
The fine print
Note that we're cheating a little bit, here. The elements dateutil.rrule.rrule produces are of type datetime.datetime, even if we pass dtstart and until of type datetime.date, as we do above. I let map feed them to date's isoformat function, which just turns out to convert them to strings as if it were just dates without any time-of-day information.
Therefore, the seemingly equivalent list comprehension
[day.isoformat()
for day in rrule(MONTHLY, dtstart=date(2010, 8, 1), until=date.today())]
would return a list like
['2010-08-01T00:00:00',
'2010-09-01T00:00:00',
'2010-10-01T00:00:00',
'2010-11-01T00:00:00',
⋮
'2015-12-01T00:00:00',
'2016-01-01T00:00:00',
'2016-02-01T00:00:00']
Thus, if we want to use a list comprehension instead of map, we have to do something like
[dt.date().isoformat()
for dt in rrule(MONTHLY, dtstart=date(2010, 8, 1), until=date.today())]
use datetime and timedelta standard Python's modules - without installing any new libraries
from datetime import datetime, timedelta
now = datetime(datetime.now().year, datetime.now().month, 1)
ctr = datetime(2010, 8, 1)
list = [ctr.strftime('%Y-%m-%d')]
while ctr <= now:
ctr += timedelta(days=32)
list.append( datetime(ctr.year, ctr.month, 1).strftime('%Y-%m-%d') )
I'm adding 32 days to enter new month every time (longest months has 31 days)
It's seems like there's a very simple and clean way to do this by generating a list of dates and subsetting to take only the first day of each month, as shown in the example below.
import datetime
import pandas as pd
start_date = datetime.date(2010,8,1)
end_date = datetime.date(2016,2,1)
date_range = pd.date_range(start_date, end_date)
date_range = date_range[date_range.day==1]
print(date_range)
I got another way using datetime, timedelta and calender:
from calendar import monthrange
from datetime import datetime, timedelta
def monthdelta(d1, d2):
delta = 0
while True:
mdays = monthrange(d1.year, d1.month)[1]
d1 += timedelta(days=mdays)
if d1 <= d2:
delta += 1
else:
break
return delta
start_date = datetime(2016, 1, 1)
end_date = datetime(2016, 12, 1)
num_months = [i-12 if i>12 else i for i in range(start_date.month, monthdelta(start_date, end_date)+start_date.month+1)]
monthly_daterange = [datetime(start_date.year,i, start_date.day, start_date.hour) for i in num_months]
You could reduce the number of if statements to two lines instead of four lines because having a second if statement that does the same thing with the previous if statement is a bit redundant.
if (y == 2010 and m < 8) or (y == 2016 and m > 2):
continue
I don't know whether it's better, but an approach like the following might be considered more 'pythonic':
months = [
'{}-{:0>2}-01'.format(year, month)
for year in xrange(2010, 2016 + 1)
for month in xrange(1, 12 + 1)
if not (year <= 2010 and month < 8 or year >= 2016 and month > 2)
]
The main differences here are:
As we want the iteration(s) to produce a list, use a list comprehension instead of aggregating list elements in a for loop.
Instead of explicitly making a distinction between numbers below 10 and numbers 10 and above, use the capabilities of the format specification mini-language for the .format() method of str to specify
a field width (the 2 in the {:0>2} place holder)
right-alignment within the field (the > in the {:0>2} place holder)
zero-padding (the 0 in the {:0>2} place holder)
xrange instead of range returns a generator instead of a list, so that the iteration values can be produced as they're being consumed and don't have to be held in memory. (Doesn't matter for ranges this small, but it's a good idea to get used to this in Python 2.) Note: In Python 3, there is no xrange and the range function already returns a generator instead of a list.
Make the + 1 for the upper bounds explicit. This makes it easier for human readers of the code to recognize that we want to specify an inclusive bound to a method (range or xrange) that treats the upper bound as exclusive. Otherwise, they might wonder what's the deal with the number 13.
A different approach that doesn't require any additional libraries, nor nested or while loops. Simply convert your dates into an absolute number of months from some reference point (it can be any date really, but for simplicity we can use 1st January 0001). For example
a=datetime.date(2010,2,5)
abs_months = a.year * 12 + a.month
Once you have a number representing the month you are in you can simply use range to loop over the months, and then convert back:
Solution to the generalized problem:
import datetime
def range_of_months(start_date, end_date):
months = []
for i in range(start_date.year * 12 + start_date.month, end_date.year*12+end_date.month + 1)
months.append(datetime.date((i-13) // 12 + 1, (i-1) % 12 + 1, 1))
return months
Additional Notes/explanation:
Here // divides rounding down to the nearest whole number, and % 12 gives the remainder when divided by 12, e.g. 13 % 12 is 1.
(Note also that in the above date.year *12 + date.month does not give the number of months since the 1st of January 0001. For example if date = datetime.datetime(1,1,1), then date.year * 12 + date.month gives 13. If I wanted to do the actual number of months I would need to subtract 1 from the year and month, but that would just make the calculations more complicated. All that matters is that we have a consistent way to convert to and from some integer representation of what month we are in.)
fresh pythonic one-liner from me
from dateutil.relativedelta import relativedelta
import datetime
[(start_date + relativedelta(months=+m)).isoformat()
for m in range(0, relativedelta(start_date, end_date).months+1)]
In case you don't have any months duplicates and they are in correct order you can get what you want with this.
from datetime import date, timedelta
first = date.today()
last = first + timedelta(weeks=20)
date_format = "%Y-%m"
results = []
while last >= first:
results.append(last.strftime(date_format))
last -= timedelta(days=last.day)
Similar to #Mattaf, but simpler...
pandas.date_range() has an option frequency freq='m'...
Here I am adding a day (pd.Timedelta('1d')) in order to reach the beginning of each new month:
import pandas as pd
date_range = pd.date_range('2010-07-01','2016-02-01',freq='M')+pd.Timedelta('1d')
print(list(date_range))

With the parsedatetime library in Python, is it possible to restrict a date to the current year?

Using parsedatetime, I'd like to pass a value like Jan 1 to the calendar parser and have it return Jan 1st of the current year (which, as I post this, would be 2014-01-01).
By default, parsedatetime returns the next occurrence of the date (i.e. 2015-01-01):
>>> import parsedatetime as pdt
>>> from datetime import datetime
>>> from time import mktime
>>> cal = pdt.Calendar()
>>> datetime.now()
datetime.datetime(2014, 8, 1, 15, 41, 7, 486294)
>>> str(datetime.fromtimestamp(mktime(cal.parse('Jan 1')[0])))
'2015-01-01 14:41:13'
>>> str(datetime.fromtimestamp(mktime(cal.parse('Dec 31')[0])))
'2014-12-31 14:41:17'
I've tried inputs like last Jan 1 and Jan 1 this year without success.
Is there a way to tell the parser to return the current year's value?
Editing to add a couple requirements that weren't specified with original question:
Supports natural language processing (that's why I'm using parsedatetime)
Doesn't compromise other parsedatetime parsing functionality (like years other than current and values like yesterday and 6 months before 3/1)
Bear here - have no idea how to get my original SO profile back as I used to use ClaimID...
anywho - you can set a flag to cause parsedatetime to never go forward a year when parsing only month/day values...
import parsedatetime as pdt
ptc = pdt.Constants()
ptc.YearParseStyle = 0
cal = pdt.Calendar(ptc)
print cal.parse('Jan 1')
# ((2014, 1, 1, 15, 57, 32, 5, 214, 1), 1)
The parse function appears to take a sourceTime parameter that you can set to the 1st of the current year.
See https://bear.im/code/parsedatetime/docs/index.html
I would replace the year on your datetime object. For example :
str(datetime.fromtimestamp(mktime(cal.parse('Dec 31')[0])))
would become:
str(datetime.fromtimestamp(mktime(cal.parse('Dec 31')[0])).replace(year=datetime.today().year))
If you aren't tied to using that library (and maybe you are?) you could do it like this:
>>> import datetime
>>> datetime.datetime.now().replace(month=1, day=1).strftime("%Y-%m-%d %H:%M:%S")
'2014-01-01 22:55:56'
>>>
from datetime import date, datetime
d = datetime.strptime('Jan 1', '%b %d')
d = date(datetime.now().year, d.month, d.day)
gives datetime.date(2014, 1, 1) for d, which you can then format with
print d.strftime('%Y-%m-%d')
2014-01-01
An improved implementation based on Bear's answer.
Again this is constrained by the fact that, since this is being implemented within another DSL parser, natural_date can only accept a single string:
import parsedatetime as pdt
from datetime import datetime, date, timedelta
from time import mktime
def natural_date(human_readable):
human_readable = human_readable.lower()
# Flag to cause parsedatetime to never go forward
# https://stackoverflow.com/a/25098991/1093087
ptc = pdt.Constants()
ptc.YearParseStyle = 0
cal = pdt.Calendar(ptc)
result, parsed_as = cal.parse(human_readable)
if not parsed_as:
raise ValueError("Unable to parse %s" % (human_readable))
return date.fromtimestamp(mktime(result))
def test_natural_date():
cases = [
# input, expect
('jan 1', date(date.today().year, 1, 1)),
('dec 31', date(date.today().year, 12, 31)),
('yesterday', date.today() - timedelta(days=1)),
('3 months before 12/31', date(date.today().year, 9, 30))
]
for human_readable, expect in cases:
result = natural_date(human_readable)
print("%s -> %s" % (human_readable, result))
assert result == expect, human_readable
test_natural_date()
Credit also goes to Mark Ransom, who unearthed sourceTime parameter, which provided another way to solve this issue, although that solution was complicated by this issue.

Categories