Find the closest date from today in a list - python

My objective is to get the next closest date (in the future, not past) to today's date from a list. For simplicity's sake, the list (in the format of e.g. 2017-01-31; YYYY-MM-DD) is each football game in the season and I am trying to create a script that in part finds the "next" football game.
I have searched the internet and Stack Overflow for answers and found a promising post, however the solutions provided are using a different format and when I try to tailor it to mine, it trips exceptions.
My logic includes parsing an RSS feed, so I am just going to provide the raw list instead. With this in mind, my simplified code is as follows:
today = str(datetime.date.today())
print(today)
scheduledatelist = ['2017-09-01', '2017-09-09', '2017-09-16', '2017-09-23', '2017-09-30', '2017-10-07', '2017-10-14', '2017-10-21', '2017-10-27', '2017-11-11', '2017-11-18', '2017-11-25']
scheduledatelist = list(reversed(scheduledatelist)) #purpose: to have earliest dates first
This is my attempt at adapting the previous post's solution (I am not well versed in functional programming, so I may not be adapting it right):
get_datetime = lambda s: datetime.datetime.strptime(s, "%Y-%m-%d")
base = get_datetime(today)
later = filter(lambda d: today(d[0]) > today, scheduledatelist)
closest_date = min(later, key = lambda d: today(d[0]))
print(closest_date)
Regardless of my attempt (which may not be the best in my situation as it changes the format and I need the end value to still be YYYY-MM-DD), is there an easier way of doing this? I need that next game (closest to today) value as that will continue on to be used in my logic. So to recap, how can I find the closest date in my list, looking toward the future, from today. Thank you for your help!

You can do:
min(scheduledatelist, key=lambda s:
datetime.datetime.strptime(s, "%Y-%m-%d").date()-datetime.date.today())
For the single closest date to today.
You can use the same function to sort by distance from today:
sorted(scheduledatelist, key=lambda s:
datetime.datetime.strptime(s, "%Y-%m-%d").date()-datetime.date.today())
And the returned list will be in increasing distance in days from today. Works if the dates are before or after today.
If you want only dates in the future, filter out the dates in the past. Since the date strings are in ISO 8601 format, you can compare lexically:
min([d for d in scheduledatelist if d>str(datetime.date.today())], key=lambda s:
datetime.datetime.strptime(s, "%Y-%m-%d").date()-datetime.date.today())

first of all let's create datetime.date objects from strings using datetime.datetime.strptime and datetime.datetime.date methods since datetime.date objects are ordered and easier to work with:
date_format = '%Y-%m-%d'
dates = [datetime.datetime.strptime(date_string,
date_format).date()
then let's filter out dates that take place in future (after today)
today = datetime.date.today()
future_dates = [date
for date in dates
if date >= today]
then we can simply find next closest date using min
next_closest_date = min(future_dates)
which gives us
>>>next_closest_date
2017-09-01
for given example
WARNING
If there is no dates going after today this will cause error like
ValueError: min() arg is an empty sequence
if it's ok then we can leave it, but if we don't want to get errors – we can specify default value for min in case of empty sequence like
next_closest_date = min(future_dates, default=None)
Finally we can write a function as follows
import datetime
# `default` value is returned when there is no future date strings found
def get_next_closest_date(date_strings, date_format, default=None):
today = datetime.date.today()
dates = [datetime.datetime.strptime(date_string,
date_format).date()
for date_string in date_strings]
future_dates = [date
for date in dates
if date >= today]
return min(future_dates, default)
and use it like
scheduledatelist = ['2017-09-01', '2017-09-09', '2017-09-16', '2017-09-23',
'2017-09-30', '2017-10-07', '2017-10-14', '2017-10-21',
'2017-10-27', '2017-11-11', '2017-11-18', '2017-11-25']
next_closest_date = get_next_closest_date(date_strings=scheduledatelist,
date_format='%Y-%m-%d')
print(next_closest_date)

Related

Get previous day's date from date passed as a string in "YYYY-MM-DD" format

I'm trying to create a helper function getPreviousDay to be used in the backend (Flask).
From the front end, I'm receiving the date in "YYYY-MM-DD" format as a string.
From this, I want to get the date of the previous day in the same format as a string.
Here's a sample code of what I want to achieve.
def getPreviousDay(date):
'''
todo: previousDay should also be a string in "YYYY-MM-DD" format
'''
return previousDay
current_day = "2022-09-29" #YYYY-MM-DD
yesterday = getPreviousDay(current_day)
The datetime module provides date and timedelta types that can be used for this kind of thing. A "time delta" is a difference between two dates or times, in this case, 1 day. Subtracting one day from today's date gives yesterday's date.
import datetime
def getPreviousDay(date):
today = datetime.date.fromisoformat(date)
yesterday = today - datetime.timedelta(days=1)
return yesterday.isoformat()
This returns:
>>> getPreviousDay('2022-09-29')
'2022-09-28'
The reference documentation for the datetime module has more details.

How do I establish the Academic Year, when I only have a string date

I have a list of dates, in string format, ranging from "01/01/2000" to "31/12/2022". If I assume the academic year runs from 1st September to 31st August, how can I return the academic year, in the format "2018-19"?
Presently I'm going down the route of converting the string to an integer :-
import datetime
def give_academic_year(theDate):
utc_time = datetime.datetime.strptime(theDate, '%d/%m/%Y')
a = datetime.datetime.timestamp(utc_time)
# Now I was going to do some cluncky comparison for each academic year
if a>946684800 and a<978307200:
return "2000-01"
elif a>978307200 and a<1009756800:
return "2001-02"
etc.
But there is probably a much more elegant way of doing this?
you could do it in the following way just playing with the year and the formats from the datetime function of the same library.
from datetime import datetime
def give_academic_year(theDate):
if theDate.month > 8:
return '{0}-{1}'.format(theDate.year, int(theDate.strftime('%y'))+1)
else:
return '{0}-{1}'.format((theDate.year-1), theDate.strftime('%y'))
# example
theDate = datetime.strptime('01/03/2020', '%d/%m/%Y')
give_academic_year(theDate)
then it only remains to apply the function to the list, you can do it with a lambda expression.
regards.
Oh, #slothrop, that is elegant! I didn't know about those attributes.
So this seems to do the job for me :-
if utc_time.month>8:
acam_year=str(utc_time.year)+"-"+str((utc_time.year+1)-2000)
else:
acam_year=str(utc_time.year-1)+"-"+str((utc_time.year)-2000)
return acam
Just the ticket - thanks very much.

Given a list of dates, how can I combine/format them as a string like "Day1-Day3, Day5 Month Year"

Background:
I am working on a project that gives me properly formatted JSON data. For the purposes of this question, the data is not important. Specifically what is relevant to this question is the following example list of strings (dates, in the format YYYY-MM-DD):
dates = [ "2021-04-17", "2021-04-18", "2021-04-19", "2021-04-23" ]
What I want to do:
Given the example list dates above, I want to combine dates with the same month and year into a single line/string, with a format like so:
17-19, 23 APR 2021
What I tried:
I created a simple iteration on the dates list to create a dictionary, broken down like so: {year: {month: {days}}}. However this still leaves me with the issue of combining sequential days, e.g. [17, 18, 19, 23] needs to become 17-19, 23 when finally printing or creating the string. (See my comment below the code snippet)
It's beginning to feel like a sledgehammer solution for a nail problem. I feel like there has to be a better way to do this with list comprehension or something simpler.
Code snippet:
import datetime
dates = [ "2021-04-17", "2021-04-18", "2021-04-19", "2021-04-23" ]
parsed_dates = {}
for d in dates:
current_date = datetime.datetime.strptime(d, '%Y-%m-%d')
# local variables just for legibility
year = current_date.year
month = current_date.month
day = current_date.day
# if the current year is not in the dict, add it
if year not in parsed_dates:
parsed_dates[year] = {}
# if the current month is not in the year dict, add it
if month not in parsed_dates[year]:
parsed_dates[year][month] = set()
# for the day, simply add to the set (ignore duplicates)
parsed_dates[year][month].add(day)
# dictionary sorting omitted for legibility
# ...
print(parsed_dates)
I haven't continued trying to parse the dictionary into my desired format because as mentioned I feel like I'm going down the wrong rabbit hole here.
See my posted answer below for the solution, it is definitely not optimal. Hoping someone else has better ideas.
Output:
{2021: {4: {17, 18, 19, 23}}}
Some parameters for the potential solution:
I am not in control of the source data, this is provided to me
The date formats will always be a string, with format YYYY-MM-DD. Assume all dates are valid, and will successfully parse with datetime.datetime.strptime using the format string '%Y-%m-%d' (e.g. there are no whacky strings like 2021-50-98)
The dates are not guaranteed to be sequential days or months, or in order (e.g. I could have [ ... "2021-02-05", "2020-01-01", ...]
The dates list may contain duplicates (e.g. [ ... "2021-04-21", "2021-04-21", ... ])
No consideration needs to be given to localization (e.g. assume everything is in English, and everyone understands both styles of date format, DD MMM YYYY and YYYY-MM-DD)
The solution does not need to be "bullet-proof" (e.g. we can make assumptions on format and input, and that it will always look like the example above; the only variable is the number of dates per list)
The solution does not need to be extensible (e.g. I only need this as a solution to the above problem given the list of dates with no consideration for permutations or changes to data)
Please let me know if any other clarification or details are needed, or if the post is too long. I tried to give as much information as possible but I realize this might be overwhelming to quickly read and formulate potential solutions.
For anyone looking at this question, my current solution is as posted below. It is a ton of code to do something simple, so I'm sure there is a better solution out there. I am happy to accept someone else's answer if they can optimize this or point out a core concept I might be missing.
Note: I added "2021-03-18" to the dates list here to demonstrate breaking up the strings properly after formatting.
Code:
import datetime
import calendar
dates = [ "2021-04-17", "2021-04-18", "2021-03-18", "2021-04-19", "2021-04-23" ]
dates_parsed = [datetime.datetime.strptime(d, '%Y-%m-%d') for d in dates]
dates_dict = {}
for d in dates_parsed:
# local variables just for legibility
year = d.year
month = d.month
day = d.day
# if the current year is not in the dict, add it
if year not in dates_dict:
dates_dict[year] = {}
# if the current month is not in the year dict, add it
if month not in dates_dict[year]:
dates_dict[year][month] = set()
# for the day, simply add to the set (ignore duplicates)
dates_dict[year][month].add(day)
# begin looking through the dictionary for ranges to format
for year in dates_dict:
for month in dates_dict[year]:
days = list(dates_dict[year][month])
day_ranges = [] # a list of lists, used to step through and format as needed
curr_range = [] # the current list being operated on below
for day in days:
# check if current range is empty
if not curr_range:
curr_range = [day]
continue
# if the current range has elements, check if this day is the next one
# if so, add it to the current range
if curr_range[-1] + 1 == day:
curr_range.append(day)
# otherwise, we're not in a sequential day range
# push this list to the day_ranges list, and clear the curr_range list
# after clearning, add the current day to the new curr_range list
else:
day_ranges.append(curr_range.copy())
curr_range.clear()
curr_range = [day]
# if the curr_range isn't empty when exiting the loop, add it to the day_ranges list
if len(curr_range) > 0:
day_ranges.append(curr_range.copy())
# begin formatting the day ranges
# this formats something like [[17, 18, 19], [23]] to '17-19, 23'
day_strings = []
for d in day_ranges:
if len(d) > 1:
day_strings.append(f'{d[0]}-{d[-1]}')
else:
day_strings.append(str(d[0]))
# finally, join up each day ranges into one string
day_str = ', '.join([x for x in day_strings])
month_name = calendar.month_name[month]
print(f"{day_str} {month_name[:3].upper()} {year}")
Output:
17-19, 23 APR 2021
18 MAR 2021
(I'm not particularly concerned with the sorting of the year/month for this particular answer, that's easy enough to do elsewhere.)

When I use created_at__range to specify a range, the range is shifted by one day

I'm currently using the filter "created_at__range" to specify the first and last day of the month, but this code doesn't reflect the data registered today.
this_month = datetime.datetime.today()
first_day = datetime.datetime(this_month.year, this_month.month, 1)
this_month = this_month.strftime('%Y-%m-%d')
first_day = first_day.strftime('%Y-%m-%d')
time = obj.filter(created_at__range=(first_day, this_month)).aggregate(
time=Sum('time'))['time']
Currently, I'm using timedelta(days=1) to add a day, but if I do this, for example, if the date is 3/31, it will be 4/1 and the tally will be wrong.
this_month = datetime.datetime.today() + timedelta(days=1)
Why is this happening?
If anyone knows how to improve it, I'd appreciate it if you could let me know.
I assume that your field created_at is a DateTimeField. Quoting the warning from Django's documentation
Warning
Filtering a DateTimeField with dates won’t include items on the
last day, because the bounds are interpreted as “0am on the given
date”. If pub_date was a DateTimeField, the above expression
would be turned into this SQL:
SELECT ... WHERE pub_date BETWEEN '2005-01-01 00:00:00' and '2005-03-31 00:00:00';
Generally speaking, you can’t mix dates and datetimes.
I would like to add why even convert the datetime to a string simply use the object in your query:
this_month = datetime.datetime.today()
first_day = datetime.datetime(this_month.year, this_month.month, 1)
time = obj.filter(
created_at__range=(first_day, this_month)
).aggregate(time=Sum('time'))['time']
Edit: In fact to make this a little easier for yourself and if there is no object that would have a datetime in the future, just let the ORM and database do a little more work if needed by just comparing the day and the year:
today = datetime.datetime.today()
time = obj.filter(
created_at__year=today.year,
created_at__month=today.month,
).aggregate(time=Sum('time'))['time']

How format date into BYDAY (iCalendar specification)

I need to create RRULE string with BYDAY parameter, from my date value.
Is there any natural way to do it?
I wrote this utility code for this purpose:
import calendar
fweek_fday, mdays = calendar.monthrange(date.year, date.month)
# Get weekday of first day and total days in current month
mweeks = ((mdays+fweek_fday-1)//7)+1
# Get total weeks in current month
mday, wday = date.day, date.weekday()
# Get current day of month and current day as day of a week
week_days = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU']
week = ((mday+fweek_fday-1)//7)+(1 if wday>=fweek_fday else 0)
# Get current week no
week = -1 if week == mweeks else week
wday = week_days[wday]
output = "BYDAY=%d%s" % (week, wday)
as you said in comment there is no module that I've found yet to make a rule out from a set of constraints.
should it be possible for you, you may consider the RDATE rather than going for the BYDAY.
another option for you would be:
import datetime
(y,w,d ) = date.isocalendar()
#w will be the iso week number, d the day of week
(y2,wb,d2) = datetime.datetime(date.year,date.month,1).isocalendar()
wkcount = 1 if d>=d2 else 0
# you need to account whether your date is a weekday after the one of the first of the month or not
print "BYDAY=%d%s"%(w-wb+wkcount,date.strftime("%a").upper()[0:2])
however be careful should your rule also include WKST
The natural way to do this—unless you're writing an iCalendar library—is to use a pre-existing iCalendar library that has methods for dealing with recurrence rules, etc., as objects instead of doing string parsing and generation.
It's possible that there is no such library, in which case you're out of luck. But the first thing you should do is look for one.
A quick search at PyPI turns up many possibilities. The first one, iCalendar, is described as "a parser/generator of iCalendar files", which sounds like a good candidate. Skim the docs, or just pip install it and play with it, to see if it can do what you want.

Categories