maloja/maloja/malojatime.py

587 lines
16 KiB
Python
Raw Normal View History

2021-12-11 06:56:53 +03:00
from datetime import timezone, timedelta, date, time, datetime
from calendar import monthrange
2019-03-03 00:55:22 +03:00
from os.path import commonprefix
import math
2021-12-19 23:10:55 +03:00
from .globalconf import malojaconfig
2021-12-19 23:10:55 +03:00
OFFSET = malojaconfig["TIMEZONE"]
TIMEZONE = timezone(timedelta(hours=OFFSET))
2021-12-11 06:56:53 +03:00
UTC = timezone.utc
2021-12-11 06:56:53 +03:00
FIRST_SCROBBLE = int(datetime.utcnow().replace(tzinfo=UTC).timestamp())
2019-03-03 00:55:22 +03:00
def register_scrobbletime(timestamp):
global FIRST_SCROBBLE
if timestamp < FIRST_SCROBBLE:
FIRST_SCROBBLE = int(timestamp)
2019-04-10 12:52:34 +03:00
2021-12-10 23:08:44 +03:00
# Object that represents a contextual time range relevant for displaying chart information
# there is no smaller unit than days
# also, two distinct objects could represent the same timerange
# (e.g. 2019/03 is not the same as 2019/03/01 - 2019/03/31)
2019-04-10 12:52:34 +03:00
2021-12-10 23:31:09 +03:00
# Generic Time Range
class MTRangeGeneric:
2021-12-10 23:08:44 +03:00
# despite the above, ranges that refer to the exact same real time range should evaluate as equal
def __eq__(self,other):
2021-12-10 23:31:09 +03:00
if not isinstance(other,MTRangeGeneric): return False
return (self.first_stamp() == other.first_stamp() and self.last_stamp() == other.last_stamp())
2019-04-12 18:57:28 +03:00
# gives a hashable object that uniquely identifies this time range
def hashable(self):
return self.first_stamp(),self.last_stamp()
def info(self):
2021-12-10 23:08:44 +03:00
return {**self.__json__(),"uri":self.uri()}
2019-05-08 18:42:56 +03:00
def __json__(self):
return {
"fromstring":self.fromstr(),
"tostr":self.tostr(),
"fromstamp":self.first_stamp(),
"tostamp":self.last_stamp(),
"description":self.desc()
}
def uri(self):
2021-01-01 04:52:05 +03:00
return "&".join(k + "=" + self.urikeys()[k] for k in self.urikeys())
def unlimited(self):
return False
2021-12-10 23:08:44 +03:00
# whether we currently live or will ever again live in this range
2019-06-11 11:08:55 +03:00
def active(self):
2021-12-11 06:56:53 +03:00
return (self.last_stamp() > datetime.utcnow().timestamp())
2019-06-11 11:08:55 +03:00
2021-12-10 23:31:09 +03:00
# Any range that has one defining base unit, whether week, year, etc.
class MTRangeSingular(MTRangeGeneric):
def fromstr(self):
return str(self)
def tostr(self):
return str(self)
def urikeys(self):
return {"in":str(self)}
# a range that is exactly a gregorian calendar unit (year, month or day)
2021-12-10 23:31:09 +03:00
class MTRangeGregorian(MTRangeSingular):
2019-04-10 12:52:34 +03:00
def __init__(self,*ls):
# in case we want to call with non-unpacked arguments
2021-12-10 23:08:44 +03:00
if isinstance(ls[0], (tuple, list)): ls = ls[0]
2019-04-10 12:52:34 +03:00
self.tup = tuple(ls)
self.precision = len(ls)
2021-12-10 23:08:44 +03:00
2019-04-10 12:52:34 +03:00
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
2021-12-11 06:56:53 +03:00
self.dateobject = date(dt[0],dt[1],dt[2])
2019-04-10 12:52:34 +03:00
def __str__(self):
return "/".join(str(part) for part in self.tup)
2021-12-10 23:31:09 +03:00
2019-04-10 12:52:34 +03:00
2019-04-12 18:57:28 +03:00
# whether we currently live or will ever again live in this range
2021-12-10 23:08:44 +03:00
# USE GENERIC SUPER METHOD INSTEAD
# def active(self):
# tod = datetime.datetime.utcnow().date()
# 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 and tod.day > self.day: return False
# return True
2019-04-12 18:57:28 +03:00
2019-04-10 12:52:34 +03:00
def desc(self,prefix=False):
2021-12-10 23:08:44 +03:00
prefixes = (None,'in ','in ','on ')
formats = ('%Y','%B','%d')
timeformat = ' '.join(reversed(formats[0:self.precision]))
if prefix: return prefixes[self.precision] + self.dateobject.strftime(timeformat)
else: return self.dateobject.strftime(timeformat)
2019-04-10 12:52:34 +03:00
def informal_desc(self):
2021-12-10 23:08:44 +03:00
# TODO: ignore year when same year etc
2021-12-11 06:56:53 +03:00
now = datetime.now(tz=timezone.utc)
today = date(now.year,now.month,now.day)
2019-04-10 12:52:34 +03:00
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):
2021-12-10 23:08:44 +03:00
# TODO: more elegant maybe?
2021-12-10 23:31:09 +03:00
if not isinstance(other, MTRangeGregorian): return self.desc()
2021-12-10 23:08:44 +03:00
Refactoring (#83) * Merge isinstance calls * Inline variable that is immediately returned * Replace set() with comprehension * Replace assignment with augmented assignment * Remove unnecessary else after guard condition * Convert for loop into list comprehension * Replace unused for index with underscore * Merge nested if conditions * Convert for loop into list comprehension * Convert for loop into set comprehension * Remove unnecessary else after guard condition * Replace if statements with if expressions * Simplify sequence comparison * Replace multiple comparisons with in operator * Merge isinstance calls * Merge nested if conditions * Add guard clause * Merge duplicate blocks in conditional * Replace unneeded comprehension with generator * Inline variable that is immediately returned * Remove unused imports * Replace unneeded comprehension with generator * Remove unused imports * Remove unused import * Inline variable that is immediately returned * Swap if/else branches and remove unnecessary else * Use str.join() instead of for loop * Multiple refactors - Remove redundant pass statement - Hoist repeated code outside conditional statement - Swap if/else to remove empty if body * Inline variable that is immediately returned * Simplify generator expression * Replace if statement with if expression * Multiple refactoring - Replace range(0, x) with range(x) - Swap if/else branches - Remove unnecessary else after guard condition * Use str.join() instead of for loop * Hoist repeated code outside conditional statement * Use str.join() instead of for loop * Inline variables that are immediately returned * Merge dictionary assignment with declaration * Use items() to directly unpack dictionary values * Extract dup code from methods into a new one
2021-10-19 15:58:24 +03:00
relevant = self.desc().split(" ")
if self.year == other.year:
relevant.pop()
if self.precision > 1 and other.precision > 1 and self.month == other.month:
2019-04-10 12:52:34 +03:00
relevant.pop()
Refactoring (#83) * Merge isinstance calls * Inline variable that is immediately returned * Replace set() with comprehension * Replace assignment with augmented assignment * Remove unnecessary else after guard condition * Convert for loop into list comprehension * Replace unused for index with underscore * Merge nested if conditions * Convert for loop into list comprehension * Convert for loop into set comprehension * Remove unnecessary else after guard condition * Replace if statements with if expressions * Simplify sequence comparison * Replace multiple comparisons with in operator * Merge isinstance calls * Merge nested if conditions * Add guard clause * Merge duplicate blocks in conditional * Replace unneeded comprehension with generator * Inline variable that is immediately returned * Remove unused imports * Replace unneeded comprehension with generator * Remove unused imports * Remove unused import * Inline variable that is immediately returned * Swap if/else branches and remove unnecessary else * Use str.join() instead of for loop * Multiple refactors - Remove redundant pass statement - Hoist repeated code outside conditional statement - Swap if/else to remove empty if body * Inline variable that is immediately returned * Simplify generator expression * Replace if statement with if expression * Multiple refactoring - Replace range(0, x) with range(x) - Swap if/else branches - Remove unnecessary else after guard condition * Use str.join() instead of for loop * Hoist repeated code outside conditional statement * Use str.join() instead of for loop * Inline variables that are immediately returned * Merge dictionary assignment with declaration * Use items() to directly unpack dictionary values * Extract dup code from methods into a new one
2021-10-19 15:58:24 +03:00
if self.precision > 2 and other.precision > 2 and self.day == other.day:
2019-04-10 12:52:34 +03:00
relevant.pop()
Refactoring (#83) * Merge isinstance calls * Inline variable that is immediately returned * Replace set() with comprehension * Replace assignment with augmented assignment * Remove unnecessary else after guard condition * Convert for loop into list comprehension * Replace unused for index with underscore * Merge nested if conditions * Convert for loop into list comprehension * Convert for loop into set comprehension * Remove unnecessary else after guard condition * Replace if statements with if expressions * Simplify sequence comparison * Replace multiple comparisons with in operator * Merge isinstance calls * Merge nested if conditions * Add guard clause * Merge duplicate blocks in conditional * Replace unneeded comprehension with generator * Inline variable that is immediately returned * Remove unused imports * Replace unneeded comprehension with generator * Remove unused imports * Remove unused import * Inline variable that is immediately returned * Swap if/else branches and remove unnecessary else * Use str.join() instead of for loop * Multiple refactors - Remove redundant pass statement - Hoist repeated code outside conditional statement - Swap if/else to remove empty if body * Inline variable that is immediately returned * Simplify generator expression * Replace if statement with if expression * Multiple refactoring - Replace range(0, x) with range(x) - Swap if/else branches - Remove unnecessary else after guard condition * Use str.join() instead of for loop * Hoist repeated code outside conditional statement * Use str.join() instead of for loop * Inline variables that are immediately returned * Merge dictionary assignment with declaration * Use items() to directly unpack dictionary values * Extract dup code from methods into a new one
2021-10-19 15:58:24 +03:00
return " ".join(relevant)
2019-04-10 12:52:34 +03:00
Refactoring (#83) * Merge isinstance calls * Inline variable that is immediately returned * Replace set() with comprehension * Replace assignment with augmented assignment * Remove unnecessary else after guard condition * Convert for loop into list comprehension * Replace unused for index with underscore * Merge nested if conditions * Convert for loop into list comprehension * Convert for loop into set comprehension * Remove unnecessary else after guard condition * Replace if statements with if expressions * Simplify sequence comparison * Replace multiple comparisons with in operator * Merge isinstance calls * Merge nested if conditions * Add guard clause * Merge duplicate blocks in conditional * Replace unneeded comprehension with generator * Inline variable that is immediately returned * Remove unused imports * Replace unneeded comprehension with generator * Remove unused imports * Remove unused import * Inline variable that is immediately returned * Swap if/else branches and remove unnecessary else * Use str.join() instead of for loop * Multiple refactors - Remove redundant pass statement - Hoist repeated code outside conditional statement - Swap if/else to remove empty if body * Inline variable that is immediately returned * Simplify generator expression * Replace if statement with if expression * Multiple refactoring - Replace range(0, x) with range(x) - Swap if/else branches - Remove unnecessary else after guard condition * Use str.join() instead of for loop * Hoist repeated code outside conditional statement * Use str.join() instead of for loop * Inline variables that are immediately returned * Merge dictionary assignment with declaration * Use items() to directly unpack dictionary values * Extract dup code from methods into a new one
2021-10-19 15:58:24 +03:00
2021-12-10 23:08:44 +03:00
# get objects with one higher precision that start or end this one
def start(self):
2021-12-10 23:31:09 +03:00
if self.precision in [1, 2]: return MTRangeGregorian(*self.tup,1)
2021-12-10 23:08:44 +03:00
return self
2019-04-10 12:52:34 +03:00
def end(self):
2021-12-10 23:31:09 +03:00
if self.precision == 1: return MTRangeGregorian(*self.tup,12)
elif self.precision == 2: return MTRangeGregorian(*self.tup,monthrange(self.year,self.month)[1])
2021-12-10 23:08:44 +03:00
return self
2019-04-10 12:52:34 +03:00
2021-12-10 23:08:44 +03:00
# get highest precision objects (day) that start or end this one
2019-04-10 12:52:34 +03:00
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()
2021-12-10 23:08:44 +03:00
# get first or last timestamp of this range
2019-04-10 12:52:34 +03:00
def first_stamp(self):
day = self.first_day().dateobject
2021-12-11 06:56:53 +03:00
return int(datetime.combine(day,time(tzinfo=TIMEZONE)).timestamp())
2019-04-10 12:52:34 +03:00
def last_stamp(self):
2021-12-11 06:56:53 +03:00
day = self.last_day().dateobject + timedelta(days=1)
return int(datetime.combine(day,time(tzinfo=TIMEZONE)).timestamp() - 1)
2019-04-10 12:52:34 +03:00
# 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:
2021-12-10 23:31:09 +03:00
return MTRangeGregorian(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
2021-12-10 23:31:09 +03:00
return MTRangeGregorian(*dt)
elif self.precision == 3:
2021-12-11 06:56:53 +03:00
newdate = self.dateobject + timedelta(days=step)
2021-12-10 23:31:09 +03:00
return MTRangeGregorian(newdate.year,newdate.month,newdate.day)
2021-12-10 23:08:44 +03:00
def prev(self,step=1):
return self.next(step*(-1))
# a range that is exactly one christian week (starting on sunday)
2021-12-10 23:31:09 +03:00
class MTRangeWeek(MTRangeSingular):
2019-04-10 12:52:34 +03:00
def __init__(self,year=None,week=None):
2021-01-15 22:09:34 +03:00
# do this so we can construct the week with overflow (eg 2020/-3)
2021-12-11 06:56:53 +03:00
thisisoyear_firstday = date.fromchrcalendar(year,1,1)
self.firstday = thisisoyear_firstday + timedelta(days=7*(week-1))
self.firstday = date(self.firstday.year,self.firstday.month,self.firstday.day)
2021-01-15 22:09:34 +03:00
# for compatibility with pre python3.8 (https://bugs.python.org/issue32417)
2021-01-15 20:39:52 +03:00
2021-12-11 06:56:53 +03:00
self.lastday = self.firstday + timedelta(days=6)
2021-01-15 20:39:52 +03:00
2021-01-15 20:45:42 +03:00
# now get the actual year and week number (in case of overflow)
self.year,self.week,_ = self.firstday.chrcalendar()
2021-01-15 20:39:52 +03:00
2019-04-10 12:52:34 +03:00
def __str__(self):
2021-12-10 23:08:44 +03:00
return f"{self.year}/W{self.week}"
2019-04-10 12:52:34 +03:00
2021-12-10 23:31:09 +03:00
2019-04-10 12:52:34 +03:00
def desc(self,prefix=False):
if prefix:
2021-12-10 23:08:44 +03:00
return f"in Week {self.week} {self.year}"
2019-04-10 12:52:34 +03:00
else:
2021-12-10 23:08:44 +03:00
return f"Week {self.week} {self.year}"
2019-04-10 12:52:34 +03:00
def informal_desc(self):
2021-12-11 06:56:53 +03:00
now = datetime.now(tz=timezone.utc)
2021-12-10 23:08:44 +03:00
if now.year == self.year: return f"Week {self.week}"
2019-04-10 12:52:34 +03:00
return self.desc()
def contextual_desc(self,other):
2021-12-10 23:31:09 +03:00
if isinstance(other, MTRangeWeek) and other.year == self.year: return f"Week {self.week}"
2019-04-10 12:52:34 +03:00
return self.desc()
def start(self):
return self.first_day()
def end(self):
return self.last_day()
def first_day(self):
2021-12-10 23:31:09 +03:00
return MTRangeGregorian(self.firstday.year,self.firstday.month,self.firstday.day)
2019-04-10 12:52:34 +03:00
def last_day(self):
2021-12-10 23:31:09 +03:00
return MTRangeGregorian(self.lastday.year,self.lastday.month,self.lastday.day)
2019-04-10 12:52:34 +03:00
def first_stamp(self):
day = self.firstday
2021-12-11 06:56:53 +03:00
return int(datetime.combine(day,time(tzinfo=TIMEZONE)).timestamp())
2019-04-10 12:52:34 +03:00
def last_stamp(self):
2021-12-11 06:56:53 +03:00
day = self.lastday + timedelta(days=1)
return int(datetime.combine(day,time(tzinfo=TIMEZONE)).timestamp() - 1)
2019-04-10 12:52:34 +03:00
def next(self,step=1):
if abs(step) == math.inf: return None
2021-12-10 23:31:09 +03:00
return MTRangeWeek(self.year,self.week + step)
2019-04-10 12:52:34 +03:00
# a range that is defined by separate start and end
2021-12-10 23:31:09 +03:00
class MTRangeComposite(MTRangeGeneric):
2019-04-10 12:52:34 +03:00
def __init__(self,since=None,to=None):
since,to = time_pad(since,to)
self.since = since
self.to = to
2021-12-10 23:31:09 +03:00
if isinstance(self.since,MTRangeComposite): self.since = self.since.start()
if isinstance(self.to,MTRangeComposite): self.to = self.to.end()
2019-04-10 12:52:34 +03:00
def __str__(self):
2021-12-10 23:08:44 +03:00
return f"{self.since} - {self.to}"
def fromstr(self):
return str(self.since)
def tostr(self):
return str(self.to)
2019-04-10 12:52:34 +03:00
2019-04-12 18:57:28 +03:00
# 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
2019-04-10 12:52:34 +03:00
def desc(self,prefix=False):
if self.since is not None and self.to is not None:
if prefix:
2021-12-10 23:08:44 +03:00
return f"from {self.since.contextual_desc(self.to)} to {self.to.desc()}"
2019-04-10 12:52:34 +03:00
else:
2021-12-10 23:08:44 +03:00
return f"{self.since.contextual_desc(self.to)} to {self.to.desc()}"
2019-04-10 12:52:34 +03:00
if self.since is not None and self.to is None:
2021-12-10 23:08:44 +03:00
return f"since {self.since.desc()}"
2019-04-10 12:52:34 +03:00
if self.since is None and self.to is not None:
2021-12-10 23:08:44 +03:00
return f"until {self.to.desc()}"
if self.since is None and self.to is None:
return ""
2019-04-10 12:52:34 +03:00
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()
2019-04-10 12:52:34 +03:00
def last_stamp(self):
2021-12-11 06:56:53 +03:00
if self.to is None: return int(datetime.utcnow().replace(tzinfo=timezone.utc).timestamp())
else: return self.to.last_stamp()
def next(self,step=1):
if abs(step) == math.inf: return None
if self.since is None or self.to is None: 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)
2021-12-10 23:31:09 +03:00
return MTRangeComposite(newstart,newend)
2019-04-10 12:52:34 +03:00
2020-09-04 14:59:04 +03:00
def today():
2021-12-11 06:56:53 +03:00
tod = datetime.now(tz=TIMEZONE)
2021-12-10 23:31:09 +03:00
return MTRangeGregorian(tod.year,tod.month,tod.day)
2020-09-04 14:59:04 +03:00
def thisweek():
2021-12-11 06:56:53 +03:00
tod = datetime.now(tz=TIMEZONE)
tod = date(tod.year,tod.month,tod.day)
2020-09-04 14:59:04 +03:00
y,w,_ = tod.chrcalendar()
2021-12-10 23:31:09 +03:00
return MTRangeWeek(y,w)
2020-09-04 14:59:04 +03:00
def thismonth():
2021-12-11 06:56:53 +03:00
tod = datetime.now(tz=TIMEZONE)
2021-12-10 23:31:09 +03:00
return MTRangeGregorian(tod.year,tod.month)
2020-09-04 14:59:04 +03:00
def thisyear():
2021-12-11 06:56:53 +03:00
tod = datetime.now(tz=TIMEZONE)
2021-12-10 23:31:09 +03:00
return MTRangeGregorian(tod.year)
2020-09-04 14:59:04 +03:00
def alltime():
2021-12-10 23:31:09 +03:00
return MTRangeComposite(None,None)
2020-09-04 14:59:04 +03:00
def range_desc(r,**kwargs):
if r is None: return ""
return r.desc(**kwargs)
2019-04-10 12:52:34 +03:00
def time_str(t):
obj = time_fix(t)
return obj.desc()
2019-04-04 21:01:06 +03:00
2020-09-04 14:59:04 +03:00
currenttime_string_representations = (
(today,["today","day"]),
(thisweek,["week","thisweek"]),
(thismonth,["month","thismonth"]),
(thisyear,["year","thisyear"]),
(lambda:None,["alltime"])
)
month_string_representations = (
["january","jan"],
["february","feb"],
["march","mar"],
["april","apr"],
["may"],
["june","jun"],
["july","jul"],
["august","aug"],
["september","sep"],
["october","oct"],
["november","nov"],
["december","dec"],
)
weekday_string_representations = (
["sunday","sun"],
["monday","mon"],
["tuesday","tue"],
["wednesday","wed"],
["thursday","thu"],
["friday","fri"],
["saturday","sat"]
)
def get_last_instance(category,current,target,amount):
offset = (target-current) % -(amount)
return category().next(offset)
str_to_time_range = {
**{s:callable for callable,strlist in currenttime_string_representations for s in strlist},
2021-12-11 06:56:53 +03:00
**{s:(lambda i=index:get_last_instance(thismonth,datetime.utcnow().month,i,12)) for index,strlist in enumerate(month_string_representations,1) for s in strlist},
**{s:(lambda i=index:get_last_instance(today,datetime.utcnow().isoweekday()+1%7,i,7)) for index,strlist in enumerate(weekday_string_representations,1) for s in strlist}
2020-09-04 14:59:04 +03:00
}
2019-04-10 12:52:34 +03:00
# converts strings and stuff to objects
2019-03-03 00:55:22 +03:00
def time_fix(t):
2021-12-10 23:31:09 +03:00
if t is None or isinstance(t,MTRangeGeneric): return t
2019-03-03 00:55:22 +03:00
2019-03-04 15:43:19 +03:00
if isinstance(t, str):
2020-09-04 14:59:04 +03:00
t = t.lower()
if t in str_to_time_range:
return str_to_time_range[t]()
2019-03-03 00:55:22 +03:00
if isinstance(t,str): t = t.split("/")
#if isinstance(t,tuple): t = list(t)
try:
t = [int(p) for p in t]
2021-12-10 23:31:09 +03:00
return MTRangeGregorian(t[:3])
except:
pass
if isinstance(t[1],str) and t[1].startswith("w"):
try:
year = int(t[0])
weeknum = int(t[1][1:])
2021-12-10 23:31:09 +03:00
return MTRangeWeek(year=year,week=weeknum)
except:
raise
2019-04-10 12:52:34 +03:00
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:
2021-12-10 23:31:09 +03:00
return MTRangeComposite(since,to)
2019-04-10 12:52:34 +03:00
2019-03-03 00:55:22 +03:00
# makes times the same precision level
def time_pad(f,t,full=False):
2019-04-10 12:52:34 +03:00
if f is None or t is None: return f,t
# week handling
2021-12-10 23:31:09 +03:00
if isinstance(f,MTRangeWeek) and isinstance(t,MTRangeWeek):
2019-04-10 12:52:34 +03:00
if full: return f.start(),t.end()
else: return f,t
2021-12-10 23:31:09 +03:00
if not isinstance(f,MTRangeWeek) and isinstance(t,MTRangeWeek):
2019-04-10 12:52:34 +03:00
t = t.end()
2021-12-10 23:31:09 +03:00
if isinstance(f,MTRangeWeek) and not isinstance(t,MTRangeWeek):
2019-04-10 12:52:34 +03:00
f = f.start()
2019-04-10 12:52:34 +03:00
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()
2019-04-10 12:52:34 +03:00
return f,t
2019-03-03 00:55:22 +03:00
2019-03-03 15:38:53 +03:00
2019-04-10 12:52:34 +03:00
### TIMESTAMPS
2019-03-03 15:38:53 +03:00
2019-04-10 12:52:34 +03:00
def timestamp_desc(t,short=False):
2021-12-11 06:56:53 +03:00
timeobj = datetime.fromtimestamp(t,tz=TIMEZONE)
2021-12-10 23:54:11 +03:00
2021-12-20 02:15:22 +03:00
if not short: return timeobj.strftime(malojaconfig["TIME_FORMAT"])
2021-12-11 22:40:52 +03:00
difference = int(datetime.now().timestamp() - t)
thresholds = (
(10,"just now"),
(2*60,f"{difference} seconds ago"),
(2*60*60,f"{difference/60:.0f} minutes ago"),
(2*24*60*60,f"{difference/(60*60):.0f} hours ago"),
(5*24*60*60,f"{timeobj.strftime('%A')}"),
(31*24*60*60,f"{difference/(60*60*24):.0f} days ago"),
(12*31*24*60*60,f"{timeobj.strftime('%B')}"),
(math.inf,f"{timeobj.strftime('%Y')}")
)
for t,s in thresholds:
if difference < t: return s
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()
2021-12-10 23:08:44 +03:00
def delimit_desc_p(d):
return delimit_desc(**d)
def delimit_desc(step="month",stepn=1,trail=1):
2019-03-03 00:55:22 +03:00
txt = ""
if stepn != 1: txt += str(stepn) + "-"
txt += {"year":"Yearly","month":"Monthly","week":"Weekly","day":"Daily"}[step.lower()]
if trail is math.inf: txt += " Cumulative"
elif trail != 1: txt += " Trailing" #we don't need all the info in the title
2019-03-03 00:55:22 +03:00
return txt
def day_from_timestamp(stamp):
2021-12-11 06:56:53 +03:00
dt = datetime.fromtimestamp(stamp,tz=TIMEZONE)
2021-12-10 23:31:09 +03:00
return MTRangeGregorian(dt.year,dt.month,dt.day)
def month_from_timestamp(stamp):
2021-12-11 06:56:53 +03:00
dt = datetime.fromtimestamp(stamp,tz=TIMEZONE)
2021-12-10 23:31:09 +03:00
return MTRangeGregorian(dt.year,dt.month)
def year_from_timestamp(stamp):
2021-12-11 06:56:53 +03:00
dt = datetime.fromtimestamp(stamp,tz=TIMEZONE)
2021-12-10 23:31:09 +03:00
return MTRangeGregorian(dt.year)
def week_from_timestamp(stamp):
2021-12-11 06:56:53 +03:00
dt = datetime.fromtimestamp(stamp,tz=TIMEZONE)
d = date(dt.year,dt.month,dt.day)
y,w,_ = d.chrcalendar()
2021-12-10 23:31:09 +03:00
return MTRangeWeek(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
2019-09-20 19:53:00 +03:00
current_start = current_end.next((stepn*trail-1)*-1)
2019-04-11 18:44:33 +03:00
#ranges = []
2019-09-26 19:24:42 +03:00
while current_end.first_stamp() < lastincluded and (max_ is None or i < max_):
2019-09-20 19:53:00 +03:00
2019-04-11 18:44:33 +03:00
if current_start == current_end:
yield current_start
2019-04-11 18:44:33 +03:00
#ranges.append(current_start)
else:
2021-12-10 23:31:09 +03:00
yield MTRangeComposite(current_start,current_end)
#ranges.append(MTRangeComposite(current_start,current_end))
current_end = current_end.next(stepn)
2019-09-20 19:53:00 +03:00
current_start = current_end.next((stepn*trail-1)*-1)
2019-04-11 18:44:33 +03:00
i += 1
2019-04-11 18:44:33 +03:00
#return ranges