-
Notifications
You must be signed in to change notification settings - Fork 1
/
utils.py
155 lines (112 loc) · 5.23 KB
/
utils.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
from datetime import datetime
from itertools import chain
from operator import itemgetter
import os
import re
from dateutil import parser as dparser
from more_itertools import groupby_transform
from CLIppy import get_from_file
DATETIME_SEP = ' @ '
error_str = '[ {} ]'
# xed_out_str = '\e[9m{}\e[0m'
def clean_time(t):
PATTERN = re.compile('(^.*[0-9] *((p|a)m)?).*$', re.I)
return re.sub(PATTERN, r'\1', t) # ignore any junk after "{a,p}m"
# (but don't assume present)
def filter_movies(movie_names, movie_times):
"""Filter movies that have no corresponding times
:movie_names: [str]
:movie_times: [[str], [str]]
:returns: (list of movie names, list of lists of movie times)
"""
assert len(movie_names) == len(movie_times), f'{len(movie_names)} != {len(movie_times)}'
is_empty = lambda lst: (all(map(is_empty, lst)) if isinstance(lst, list)
else False) # check if (nested) list is empty
movie_names, movie_times = (([], []) if is_empty(movie_times) else
zip(*((name, time) for name, time in
zip(movie_names, movie_times) if time)))
return list(movie_names), list(movie_times)
def filter_past(datetimes, cutoff=None, sep=DATETIME_SEP):
"""Filter datetimes before cutoff
:datetimes: list of strs ("date @ time") OR list of lists of strs
:cutoff: datetime str (default: now)
:returns: list of lists of (time) strs (or emptylist if past)
"""
cutoff = datetime.now() if cutoff is None else dparser.parse(cutoff)
if not datetimes:
return []
def clean_datetime(dt, sep=sep):
date, time = dt.split(sep)
return ', '.join((date, clean_time(time)))
is_past = lambda dt: (
dparser.parse(clean_datetime(dt))
- cutoff
).total_seconds() < 0
# date @ time -> time
PATTERN = re.compile(' *((a|p)m)')
strftime = lambda dt: re.sub(PATTERN, r'\1', # rm space before {a,p}m
dt.split(DATETIME_SEP)[-1].strip().lower())
is_nested_list = isinstance(datetimes[0], list)
return ([[strftime(dt) for dt in dts if not is_past(dt)]
for dts in datetimes] if is_nested_list else
[[strftime(dt)] # list of lists of "times"
if not is_past(dt) else [] for dt in datetimes])
def combine_times(movie_names, movie_times):
"""Combine times for duplicate movienames
:movie_names: [str]
:movie_times: [[str], [str]]
:returns: (list of movie names, list of lists of movie times)
"""
assert len(movie_names) == len(movie_times), f'{len(movie_names)} != {len(movie_times)}'
if not movie_names:
return [], []
movie_names, movie_times = zip(
*[(k, list(chain.from_iterable(g))) for k,g in groupby_transform(
sorted(zip(movie_names, movie_times)), itemgetter(0), # group by name
itemgetter(1))]) # output grouped times
return list(movie_names), list(movie_times)
def filter_by_rating(movie_names, movie_times, movie_ratings, threshold=0):
"""Filter movies by minimum rating
:movie_names: [strs]
:movie_times: [[strs]]
:movie_ratings: [floats]
:threshold: float [0, 1] or % (1, 100]
:returns: tuple(movie names, movie times, movie ratings)
"""
threshold = threshold / 100 if threshold > 1 else threshold # % -> float
tuple_in = (movie_names, movie_times, movie_ratings)
tuple_out = (zip(*((name, time, rating) for name, time, rating in zip(*tuple_in)
if rating >= threshold or rating < 0)) # only if above threshold or rating not found
if threshold > 0 else tuple_in) # else, don't bother to filter
tuple_out = tuple(tuple_out)
tuple_out = (tuple_out if tuple_out else ((), (), ())) # don't return empty tuple if all filtered out
return tuple_out
def get_theaters(city):
"""Get list of theaters by desired `city` from txt file
:city: str
:returns: list of theaters (str)
"""
dirname = os.path.dirname(os.path.realpath(__file__))
return get_from_file(suffix=city, prefix='theaters', dirname=dirname)
def index_into_days(days, date=None):
"""Get index into list of days that matches given `date`
:days: list of days (strs that are parseable by datetime)
:date: str (generally yyyy-mm-dd)
:returns: int
"""
date = dparser.parse(date) if date is not None else datetime.now()
# get offset from day0 (which is prob today, but not sure about cutoff for today vs tomorrow)
# this way, works for a list that says only: "wed, thu, fri, sat, sun, mon, tue, wed"
iday = (date - dparser.parse(days[0])).days
assert 0 <= iday <= len(days) - 1, '{} !<= {} !<= {}'.format(0, iday, len(days) - 1)
try: # BUT, sometimes will skip a day
assert (date - dparser.parse(days[iday])).days % 7 == 0 #, '{} != week multiple of {}'.format(days[iday], date)
except(AssertionError):
# then, fall back to direct indexing
try:
iday = [dparser.parse(day) for day in days].index(date)
except(ValueError): # date not in days
raise(AssertionError)
return iday
class NoMoviesException(Exception):
pass