import datetime from calendar import monthrange from os.path import commonprefix import math FIRST_SCROBBLE = int(datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).timestamp()) def register_scrobbletime(timestamp): global FIRST_SCROBBLE if timestamp < FIRST_SCROBBLE: FIRST_SCROBBLE = int(timestamp) def start_of_scrobbling(): global FIRST_SCROBBLE f = datetime.datetime.utcfromtimestamp(FIRST_SCROBBLE) return [f.year] def end_of_scrobbling(): global FIRST_SCROBBLE f = datetime.datetime.now() return [f.year] #def uri_to_internal(t): # return time_fix(t) # #def internal_to_uri(t): # if isinstance(t,list) or isinstance(t,tuple): # return "/".join(str(t)) # # return str(t) ### helpers # adjusting to sunday-first calendar # damn iso heathens class expandeddate(datetime.date): def chrweekday(self): return self.isoweekday() + 1 % 7 def chrcalendar(self): tomorrow = self + datetime.timedelta(days=1) cal = tomorrow.isocalendar() return (cal[0],cal[1],cal[2] % 7) date = expandeddate ### EVERYTHING NEW AGAIN # only for ranges, timestamps are separate class MRangeDescriptor: def __eq__(self,other): if not isinstance(other,MRangeDescriptor): return False return (self.first_stamp() == other.first_stamp() and self.last_stamp() == other.last_stamp()) # gives a hashable object that uniquely identifies this time range def hashable(self): return self.first_stamp(),self.last_stamp() def info(self): return { "fromstring":self.fromstr(), "tostr":self.tostr(), "uri":self.uri(), "fromstamp":self.first_stamp(), "tostamp":self.last_stamp(), "description":self.desc() } def uri(self): return "&".join(k + "=" + self.urikeys[k] for k in self.urikeys) def unlimited(self): return False # a range that is exactly a gregorian calendar unit (year, month or day) class MTime(MRangeDescriptor): def __init__(self,*ls): # in case we want to call with non-unpacked arguments if isinstance(ls[0],tuple) or isinstance(ls[0],list): ls = ls[0] self.tup = tuple(ls) self.precision = len(ls) self.year = ls[0] if len(ls)>1: self.month = ls[1] if len(ls)>2: self.day = ls[2] dt = [1970,1,1] dt[:len(ls)] = ls self.dateobject = date(dt[0],dt[1],dt[2]) def __str__(self): return "/".join(str(part) for part in self.tup) def fromstr(self): return str(self) def tostr(self): return str(self) # whether we currently live or will ever again live in this range def active(self): tod = date.today() if tod.year > self.year: return False if self.precision == 1: return True if tod.year == self.year: if tod.month > self.month: return False if self.precision == 2: return True if tod.month == self.month: if tod.day > self.day: return False return True def urikeys(self): return {"in":str(self)} def desc(self,prefix=False): if self.precision == 3: if prefix: return "on " + self.dateobject.strftime("%d. %B %Y") else: return self.dateobject.strftime("%d. %B %Y") if self.precision == 2: if prefix: return "in " + self.dateobject.strftime("%B %Y") else: return self.dateobject.strftime("%B %Y") if self.precision == 1: if prefix: return "in " + self.dateobject.strftime("%Y") else: return self.dateobject.strftime("%Y") def informal_desc(self): now = datetime.datetime.now(tz=datetime.timezone.utc) today = date(now.year,now.month,now.day) if self.precision == 3: diff = (today - dateobject).days if diff == 0: return "Today" if diff == 1: return "Yesterday" if diff < 7 and diff > 1: return timeobject.strftime("%A") #elif len(t) == 2: return self.desc() # describes only the parts that are different than another range object def contextual_desc(self,other): if isinstance(other,MTime): relevant = self.desc().split(" ") if self.year == other.year: relevant.pop() if self.precision > 1 and other.precision > 1 and self.month == other.month: relevant.pop() if self.precision > 2 and other.precision > 2 and self.day == other.day: relevant.pop() return " ".join(relevant) return self.desc() # gets object with one higher precision that starts this one def start(self): if self.precision == 1: return MTime(self.tup + (1,)) elif self.precision == 2: return MTime(self.tup + (1,)) # gets object with one higher precision that ends this one def end(self): if self.precision == 1: return MTime(self.tup + (12,)) elif self.precision == 2: return MTime(self.tup + (monthrange(self.year,self.month)[1],)) def first_day(self): if self.precision == 3: return self else: return self.start().first_day() def last_day(self): if self.precision == 3: return self else: return self.end().last_day() def first_stamp(self): day = self.first_day().dateobject return int(datetime.datetime.combine(day,datetime.time(tzinfo=datetime.timezone.utc)).timestamp()) def last_stamp(self): day = self.last_day().dateobject + datetime.timedelta(days=1) return int(datetime.datetime.combine(day,datetime.time(tzinfo=datetime.timezone.utc)).timestamp() - 1) # next range of equal length (not exactly same amount of days, but same precision level) def next(self,step=1): if abs(step) == math.inf: return None if self.precision == 1: return MTime(self.year + step) elif self.precision == 2: dt = [self.year,self.month] dt[1] += step while dt[1] > 12: dt[1] -= 12 dt[0] += 1 while dt[1] < 1: dt[1] += 12 dt[0] -= 1 return MTime(*dt) elif self.precision == 3: dt = self.dateobject d = datetime.timedelta(days=step) newdate = dt + d return MTime(newdate.year,newdate.month,newdate.day) # a range that is exactly one christian week (starting on sunday) class MTimeWeek(MRangeDescriptor): def __init__(self,year=None,week=None): self.year = year self.week = week # assume the first day of the first week of this year is 1/1 firstday = date(year,1,1) y,w,d = firstday.chrcalendar() if y == self.year: firstday -= datetime.timedelta(days=(d-1)) else: firstday += datetime.timedelta(days=8-d) # now we know the real first day, add the weeks we need firstday = firstday + datetime.timedelta(days=7*(week-1)) lastday = firstday + datetime.timedelta(days=6) # turn them into local overwritten date objects self.firstday = date(firstday.year,firstday.month,firstday.day) self.lastday = date(lastday.year,lastday.month,lastday.day) # now check if we're still in the same year y,w,_ = self.firstday.chrcalendar() self.year,self.week = y,w # firstday and lastday are already correct def __str__(self): return str(self.year) + "/W" + str(self.week) def fromstr(self): return str(self) def tostr(self): return str(self) # whether we currently live or will ever again live in this range def active(self): tod = date.today() if tod.year > self.year: return False if tod.year == self.year: if tod.chrcalendar()[1] > self.week: return False return True def urikeys(self): return {"in":str(self)} def desc(self,prefix=False): if prefix: return "in " + "Week " + str(self.week) + " " + str(self.year) else: return "Week " + str(self.week) + " " + str(self.year) def informal_desc(self): now = datetime.datetime.now(tz=datetime.timezone.utc) if now.year == self.year: return "Week " + str(self.week) return self.desc() def contextual_desc(self,other): if isinstance(other,MTimeWeek): if other.year == self.year: return "Week " + str(self.week) return self.desc() def start(self): return self.first_day() def end(self): return self.last_day() def first_day(self): return MTime(self.firstday.year,self.firstday.month,self.firstday.day) def last_day(self): return MTime(self.lastday.year,self.lastday.month,self.lastday.day) def first_stamp(self): day = self.firstday return int(datetime.datetime.combine(day,datetime.time(tzinfo=datetime.timezone.utc)).timestamp()) def last_stamp(self): day = self.lastday + datetime.timedelta(days=1) return int(datetime.datetime.combine(day,datetime.time(tzinfo=datetime.timezone.utc)).timestamp() - 1) def next(self,step=1): if abs(step) == math.inf: return None return MTimeWeek(self.year,self.week + step) # a range that is defined by separate start and end class MRange(MRangeDescriptor): def __init__(self,since=None,to=None): since,to = time_pad(since,to) self.since = since self.to = to if isinstance(self.since,MRange): self.since = self.since.start() if isinstance(self.to,MRange): self.to = self.to.end() def __str__(self): return str(self.since) + " - " + str(self.to) def fromstr(self): return str(self.since) def tostr(self): return str(self.to) # whether we currently live or will ever again live in this range def active(self): if self.to is None: return True return self.to.active() def unlimited(self): return (self.since is None and self.to is None) def urikeys(self): keys = {} if self.since is not None: keys["since"] = str(self.since) if self.to is not None: keys["to"] = str(self.to) return keys def desc(self,prefix=False): if self.since is not None and self.to is not None: if prefix: return "from " + self.since.contextual_desc(self.to) + " to " + self.to.desc() else: return self.since.contextual_desc(self.to) + " to " + self.to.desc() if self.since is not None and self.to is None: return "since " + self.since.desc() if self.since is None and self.to is not None: return "until " + self.to.desc() if self.since is None and self.to is None: return "" def informal_desc(self): # dis gonna be hard return "Not implemented" def start(self): return self.since def end(self): return self.to def first_day(self): return self.since.first_day() def last_day(self): return self.to.last_day() def first_stamp(self): if self.since is None: return FIRST_SCROBBLE else: return self.since.first_stamp() def last_stamp(self): if self.to is None: return int(datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).timestamp()) else: return self.to.last_stamp() def next(self,step=1): if abs(step) == math.inf: return None # hop from the start element by one until we reach the end element diff = 1 nxt = self.since while (nxt != self.to): diff += 1 nxt = nxt.next(step=1) newstart = self.since.next(step=diff*step) newend = self.to.next(step=diff*step) return MRange(newstart,newend) ## test w = MTimeWeek(2018,40) d = MTime(2019,4,9) m = MTime(2019,7) y = MTime(2020) def range_desc(r,**kwargs): if r is None: return "" return r.desc(**kwargs) def time_str(t): obj = time_fix(t) return obj.desc() # converts strings and stuff to objects def time_fix(t): if t is None: return None if isinstance(t,MRangeDescriptor): return t if isinstance(t, str): tod = datetime.datetime.utcnow() months = ["january","february","march","april","may","june","july","august","september","october","november","december"] weekdays = ["sunday","monday","tuesday","wednesday","thursday","friday","saturday"] if t.lower() in ["today","day"]: return today() elif t.lower() in ["month","thismonth"]: return thismonth() elif t.lower() in ["year","thisyear"]: return thisyear() elif t.lower() in ["week","thisweek"]: return thisweek() elif t.lower() in months: #diff = (tod.month - months.index(t.lower()) - 1) month = months.index(t.lower()) + 1 t = [tod.year,month] if month > tod.month: t[0] -= 1 elif t.lower() in weekdays: weekday = weekdays.index(t.lower()) diff = (tod.isoweekday() - weekday) % 7 dt = tod - datetime.timedelta(diff) t = [dt.year,dt.month,dt.day] if isinstance(t,str): t = t.split("/") #if isinstance(t,tuple): t = list(t) try: t = [int(p) for p in t] return MTime(t[:3]) except: pass if isinstance(t[1],str) and t[1].startswith("W"): try: year = int(t[0]) weeknum = int(t[1][1:]) return MTimeWeek(year=year,week=weeknum) except: raise def get_range_object(since=None,to=None,within=None): since,to,within = time_fix(since),time_fix(to),time_fix(within) # check if we can simplify if since is not None and to is not None and since == to: within = since # TODO if within is not None: return within else: return MRange(since,to) # makes times the same precision level def time_pad(f,t,full=False): if f is None or t is None: return f,t # week handling if isinstance(f,MTimeWeek) and isinstance(t,MTimeWeek): if full: return f.start(),t.end() else: return f,t if not isinstance(f,MTimeWeek) and isinstance(t,MTimeWeek): t = t.end() if isinstance(f,MTimeWeek) and not isinstance(t,MTimeWeek): f = f.start() while (f.precision < t.precision) or (full and f.precision < 3): f = f.start() while (f.precision > t.precision) or (full and t.precision < 3): t = t.end() return f,t ### TIMESTAMPS def timestamp_desc(t,short=False): if short: now = datetime.datetime.now(tz=datetime.timezone.utc) difference = int(now.timestamp() - t) if difference < 10: return "just now" if difference < 60: return str(difference) + " seconds ago" difference = int(difference/60) if difference < 60: return str(difference) + " minutes ago" if difference>1 else str(difference) + " minute ago" difference = int(difference/60) if difference < 24: return str(difference) + " hours ago" if difference>1 else str(difference) + " hour ago" difference = int(difference/24) timeobject = datetime.datetime.utcfromtimestamp(t) if difference < 5: return timeobject.strftime("%A") if difference < 31: return str(difference) + " days ago" if difference>1 else str(difference) + " day ago" if difference < 300 or timeobject.year == now.year: return timeobject.strftime("%B") #if difference < 300: return tim.strftime("%B %Y") return timeobject.strftime("%Y") else: timeobject = datetime.datetime.utcfromtimestamp(t) return timeobject.strftime("%d. %b %Y %I:%M %p") def time_stamps(since=None,to=None,within=None,range=None): if range is None: range = get_range_object(since=since,to=to,within=within) return range.first_stamp(),range.last_stamp() #print(range.desc()) # if (since==None): stamp1 = FIRST_SCROBBLE # else: stamp1 = range.first_stamp() # if (to==None): stamp2 = int(datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).timestamp()) # else: stamp2 = range.last_stamp() # return stamp1,stamp2 # if (since==None): stamp1 = FIRST_SCROBBLE # else: # stamp1 = since1 # since = time_fix(since) # date = [1970,1,1] # date[:len(since)] = since # stamp1 = int(datetime.datetime(date[0],date[1],date[2],tzinfo=datetime.timezone.utc).timestamp()) # # if (to==None): stamp2 = int(datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).timestamp()) # else: # to = time_fix(to) # to = _get_next(to) # date = [1970,1,1] # date[:len(to)] = to # stamp2 = int(datetime.datetime(date[0],date[1],date[2],tzinfo=datetime.timezone.utc).timestamp()) # # # return (stamp1,stamp2-1) def delimit_desc(step="month",stepn=1,trail=1): txt = "" if stepn is not 1: txt += _num(stepn) + "-" txt += {"year":"Yearly","month":"Monthly","week":"Weekly","day":"Daily"}[step.lower()] #if trail is not 1: txt += " " + _num(trail) + "-Trailing" if trail is math.inf: txt += " Cumulative" elif trail is not 1: txt += " Trailing" #we don't need all the info in the title return txt def day_from_timestamp(stamp): dt = datetime.datetime.utcfromtimestamp(stamp) return MTime(dt.year,dt.month,dt.day) def month_from_timestamp(stamp): dt = datetime.datetime.utcfromtimestamp(stamp) return MTime(dt.year,dt.month) def year_from_timestamp(stamp): dt = datetime.datetime.utcfromtimestamp(stamp) return MTime(dt.year) def week_from_timestamp(stamp): dt = datetime.datetime.utcfromtimestamp(stamp) d = date(dt.year,dt.month,dt.day) y,w,_ = d.chrcalendar() return MTimeWeek(y,w) def from_timestamp(stamp,unit): if unit == "day": return day_from_timestamp(stamp) if unit == "week": return week_from_timestamp(stamp) if unit == "month": return month_from_timestamp(stamp) if unit == "year": return year_from_timestamp(stamp) # since, to and within can accept old notations or objects. timerange can only be a new object. def ranges(since=None,to=None,within=None,timerange=None,step="month",stepn=1,trail=1,max_=None): (firstincluded,lastincluded) = time_stamps(since=since,to=to,within=within,range=timerange) d_start = from_timestamp(firstincluded,step) d_start = d_start.next(stepn-1) #last part of first included range i = 0 current_end = d_start #ranges = [] while current_end.first_stamp() <= lastincluded and (max_ is None or i < max_): current_start = current_end.next((stepn*trail-1)*-1) if current_start == current_end: yield current_start #ranges.append(current_start) else: yield MRange(current_start,current_end) #ranges.append(MRange(current_start,current_end)) current_end = current_end.next(stepn) i += 1 #return ranges def today(): tod = datetime.datetime.utcnow() return MTime(tod.year,tod.month,tod.day) def thisweek(): tod = datetime.datetime.utcnow() tod = date(tod.year,tod.month,tod.day) y,w,_ = tod.chrcalendar() return MTimeWeek(y,w) def thismonth(): tod = datetime.datetime.utcnow() return MTime(tod.year,tod.month) def thisyear(): tod = datetime.datetime.utcnow() return MTime(tod.year) #def _get_start_of(timestamp,unit): # date = datetime.datetime.utcfromtimestamp(timestamp) # if unit == "year": # #return [date.year,1,1] # return [date.year] # elif unit == "month": # #return [date.year,date.month,1] # return [date.year,date.month] # elif unit == "day": # return [date.year,date.month,date.day] # elif unit == "week": # change = (date.weekday() + 1) % 7 # d = datetime.timedelta(days=change) # newdate = date - d # return [newdate.year,newdate.month,newdate.day] # #def _get_next(time,unit="auto",step=1): # result = time[:] # if unit == "auto": # if is_week(time): unit = "week" # # see how long the list is, increment by the last specified unit # else: unit = [None,"year","month","day"][len(time)] # #while len(time) < 3: # # time.append(1) # # if unit == "year": # #return [time[0] + step,time[1],time[2]] # result[0] += step # return result # elif unit == "month": # #result = [time[0],time[1] + step,time[2]] # result[1] += step # while result[1] > 12: # result[1] -= 12 # result[0] += 1 # while result[1] < 1: # result[1] += 12 # result[0] -= 1 # return result # elif unit == "day": # dt = datetime.datetime(time[0],time[1],time[2]) # d = datetime.timedelta(days=step) # newdate = dt + d # return [newdate.year,newdate.month,newdate.day] # #eugh # elif unit == "week": # return _get_next(time,"day",step * 7) # # like _get_next(), but gets the last INCLUDED day / month whatever #def _get_end(time,unit="auto",step=1): # if step == 1: # if unit == "auto": return time[:] # if unit == "year" and len(time) == 1: return time[:] # if unit == "month" and len(time) == 2: return time[:] # if unit == "day" and len(time) == 3: return time[:] # exc = _get_next(time,unit,step) # inc = _get_next(exc,"auto",-1) # return inc # #def _is_past(date,limit): # date_, limit_ = date[:], limit[:] # while len(date_) != 3: date_.append(1) # while len(limit_) != 3: limit_.append(1) # if not date_[0] == limit_[0]: # return date_[0] > limit_[0] # if not date_[1] == limit_[1]: # return date_[1] > limit_[1] # return (date_[2] > limit_[2]) ##