def time2str(t, utc_offset=None): """Convert a time int into a textual representation. If utc_offset is None, the representation is in local time. Otherwise the given offset (in hours) is used. Use utc_offset=0 for UTC. In all cases the zone is explicit in the result. """ t = to_time_int(t) if this_is_js(): # pragma: no cover if utc_offset is None: utc_offset = -(Date(t * 1000).getTimezoneOffset() // 60) t += utc_offset * 3600 s = Date(t * 1000).toISOString() s = s.split(".")[0] if utc_offset == 0: s += "Z" else: s += f"{utc_offset:+03.0f}" else: # py import datetime if utc_offset is None: utc_offset = ( datetime.datetime.fromtimestamp(t) - datetime.datetime.utcfromtimestamp(t)).total_seconds() // 3600 tz = datetime.timezone(datetime.timedelta(hours=utc_offset)) dt = datetime.datetime.fromtimestamp(t, tz) if utc_offset == 0: s = dt.strftime("%Y-%m-%dT%H:%M:%SZ") else: s = dt.strftime("%Y-%m-%dT%H:%M:%S%z") return s
def get_timezone_info(t): d = Date(t * 1000) d_winter = Date(d.getFullYear(), 0, 1) d_summer = Date(d.getFullYear(), 6, 1) # offset = -d.getTimezoneOffset() / 60 offset_winter = -d_winter.getTimezoneOffset() / 60 offset_summer = -d_summer.getTimezoneOffset() / 60 return offset, offset_winter, offset_summer
def get_year_month_day(t): if this_is_js(): d = Date(t * 1000) return d.getFullYear(), d.getMonth() + 1, d.getDate() else: import datetime dt = datetime.datetime.fromtimestamp(t) return dt.year, dt.month, dt.day
def now(): """Get the current time in seconds, as a float.""" if this_is_js(): return Date().getTime() / 1000 else: import time return time.time()
def to_time_int(t): """Get a time (in int seconds since epoch), given float/str input. String inputs can be: * 'now': get current time. * E.g. '2018-04-24 11:23:00' for a local time (except in Safari :/). * E.g. '2018-04-24 11:23:00Z' for a time in UTC. * E.g. '2018-04-24 11:23:00+0200' for AMS summertime in this case. In the above, one can use a 'T' instead of a space between data and time to comply with ISO 8601. """ if this_is_js(): if isinstance(t, Date): t = t.getTime() / 1000 if isinstance(t, str): t = t.strip() if this_is_js(): # pragma: no cover if t.lower() == "now": t = Date().getTime() / 1000 else: if t.count(" ") == 1 and t[10] == " ": t = t.replace(" ", "T") # Otherwise Safari wont take it t = Date(t).getTime() / 1000 # Let browser handle date parsing else: # py import datetime t = t.replace("T", " ") if t.lower() == "now": t = datetime.datetime.now().timestamp() elif t.endswith("Z") or t[-5] in "-+": # ISO 8601 t = (t[:-1] + "+0000") if t.endswith("Z") else t t = datetime.datetime.strptime( t, "%Y-%m-%d %H:%M:%S%z").timestamp() else: t = datetime.datetime.strptime( t, "%Y-%m-%d %H:%M:%S").timestamp() if not isinstance(t, (int, float)): raise RuntimeError(f"Time must be a number, not {t!r}") return int(t)
def add(t, delta): """Add a delta to the given date. Delta can be an int (number of seconds), or a delta string like '4h', "21s", "2M" or "2Y". Works correctly for months and keeps leap years into account. """ if isinstance(delta, (float, int)): delta = str(delta) + "s" deltaName = delta[-1] deltaFactor = float(delta[:-1]) if isNaN(deltaFactor): raise RuntimeError(f"Cannot create delta from {delta!r}") d = Date(t * 1000) tup = ( d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), ) if deltaName == "s": tup[5] += deltaFactor elif deltaName == "m": tup[4] += deltaFactor elif deltaName == "h": tup[3] += deltaFactor elif deltaName == "D": tup[2] += deltaFactor elif deltaName == "W": tup[2] += deltaFactor * 7 elif deltaName == "M": tup[1] += deltaFactor elif deltaName == "Y": tup[0] += deltaFactor else: raise RuntimeError("Invalid datetime delta: " + delta) return Date(tup[0], tup[1], tup[2], tup[3], tup[4], tup[5]).getTime() / 1000
def get_weeknumber(t): """Get the ISO 8601 week number.""" # From https://weeknumber.net/how-to/javascript date = Date(t * 1000) # noqa RawJS(""" date.setHours(0, 0, 0, 0); // Thursday in current week decides the year. date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); // January 4 is always in week 1. var week1 = new Date(date.getFullYear(), 0, 4); // Adjust to Thursday in week 1 and count number of weeks from date to week1. var res = 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7); """) return res # noqa
def draw(self): PSCRIPT_OVERLOAD = False # noqa ctx = self.canvas.getContext("2d") # Prepare hidpi mode for canvas (flush state just in case) for i in range(4): ctx.restore() ctx.save() ctx.scale(self.pixel_ratio, self.pixel_ratio) # Flip y-axis ctx.scale(1, -1) ctx.translate(0, -self.height) # Clear bg ctx.clearRect(0, 0, self.width, self.height) # Determine drawing area x0 = 45 y0 = 35 width = self.width - x0 - 15 height = self.height - y0 - 5 data = data_per_db[self.dbname] if len(data) == 0: return # Get bounding box t1 = data[0].time_start t2 = data[-1].time_stop mi, ma = self._get_min_max() if ma <= mi: return hscale = width / (t2 - t1) vscale = height / (ma - mi) unix_from_utc_tuple = Date.UTC # avoid triggering new utc = get_hash_info().get("utc", False) xticks = {} # Prepare x ticks for hours (one hour is the smallest granularity) hourly_tick_units = (1, 3600), (2, 7200), (6, 21600) min_tick_dist = 60 for nhours, tick_unit in hourly_tick_units: if tick_unit * hscale >= min_tick_dist: break else: tick_unit = 0 # if tick_unit > 0: d = Date(t1 * 1000) if utc: tup = [ d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), ] tup[-1] = nhours * int(tup[-1] / nhours) t = unix_from_utc_tuple(tup[0], tup[1], tup[2], tup[3]) / 1000 else: tup = [ d.getFullYear(), d.getMonth(), d.getDate(), d.getHours() ] tup[-1] = nhours * int(tup[-1] / nhours) t = Date(tup[0], tup[1], tup[2], tup[3]).getTime() / 1000 while t <= t2: if t >= t1: d = Date(t * 1000) if utc: xticks[ t] = f"{d.getUTCHours():02i}:{d.getUTCMinutes():02i}" else: xticks[t] = f"{d.getHours():02i}:{d.getMinutes():02i}" t += tick_unit # Prepare x ticks for days/months day_tick_units = (2, 1), (2, 2), (2, 5), (1, 1), (1, 2), (1, 3), (0, 365) min_tick_dist = 60 for dindex, nsomething in day_tick_units: tick_unit = nsomething * [365 * 86400, 30 * 86400, 86400][dindex] if tick_unit * hscale >= min_tick_dist: break else: tick_unit = nsomething = 0 # n_date_ticks = 0 if nsomething > 0: d = Date(t1 * 1000) if utc: tup = [d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()] tup[dindex] = nsomething * int(tup[dindex] / nsomething) t = unix_from_utc_tuple(tup[0], tup[1], tup[2]) / 1000 else: tup = [d.getFullYear(), d.getMonth(), d.getDate()] tup[dindex] = nsomething * int(tup[dindex] / nsomething) t = Date(tup[0], tup[1], tup[2]).getTime() / 1000 while t <= t2: if t >= t1: n_date_ticks += 1 d = Date(t * 1000) if utc: dd = f"{d.getUTCDate():02i}" mm = f"{d.getUTCMonth()+1:02i}" yy = f"{d.getUTCFullYear()}" xticks[t] = f"{dd}-{mm}-{yy}" else: dd = f"{d.getDate():02i}" mm = f"{d.getMonth()+1:02i}" yy = f"{d.getFullYear()}" xticks[t] = f"{dd}-{mm}-{yy}" tup[dindex] += nsomething if utc: t = unix_from_utc_tuple(tup[0], tup[1], tup[2]) / 1000 else: t = Date(tup[0], tup[1], tup[2]).getTime() / 1000 # extra_x_tick = "" if n_date_ticks < 2: xtickskeys = xticks.keys() if len(xtickskeys) > 0 and hscale * (xtickskeys[0] - t1) < 30: xticks.pop(xtickskeys[0]) d = Date(t1 * 1000) if utc: extra_x_tick = ( f"{d.getUTCFullYear()}-{d.getUTCMonth()+1:02i}-{d.getUTCDate():02i}" ) else: extra_x_tick = ( f"{d.getFullYear()}-{d.getMonth()+1:02i}-{d.getDate():02i}" ) # Prepare y ticks yticks = self._get_ticks(vscale, mi, ma, 25) # text -> value # Prepare drawing ctx.lineWidth = 1 # Draw grid lines ctx.strokeStyle = "rgba(128, 128, 128, 0.3)" ctx.beginPath() for v, text in yticks.items(): y = y0 + (float(v) - mi) * vscale ctx.moveTo(x0, y) ctx.lineTo(x0 + width, y) ctx.stroke() # Draw x ticks ctx.strokeStyle = text_color ctx.fillStyle = text_color ctx.textAlign = "center" ctx.textBaseline = "top" # middle ctx.beginPath() for t, text in xticks.items(): x = x0 + (float(t) - t1) * hscale ctx.moveTo(x, y0) ctx.lineTo(x, y0 - 4) ctx.stroke() for t, text in xticks.items(): x = x0 + (float(t) - t1) * hscale angle = 0 # -0.15 * Math.PI x = min(x, x0 + width - 15) self._draw_text(ctx, text, x, y0 - 10, angle) if extra_x_tick: ctx.textAlign = "left" ctx.textBaseline = "bottom" self._draw_text(ctx, extra_x_tick, 0, 0) # Draw y ticks ctx.textAlign = "right" ctx.textBaseline = "middle" ctx.beginPath() for v, text in yticks.items(): y = y0 + (float(v) - mi) * vscale ctx.moveTo(x0 - 4, y) ctx.lineTo(x0, y) ctx.stroke() for v, text in yticks.items(): y = y0 + (float(v) - mi) * vscale self._draw_text(ctx, text, x0 - 8, y) # Draw axis ctx.strokeStyle = text_color ctx.beginPath() ctx.moveTo(x0, y0) ctx.lineTo(x0 + width, y0) ctx.moveTo(x0, y0) ctx.lineTo(x0, y0 + height) ctx.stroke() # Draw content self._draw_content(ctx, mi, ma, t1, t2, x0, y0, hscale, vscale) # Draw local / UTC ctx.fillStyle = "rgba(128, 128, 128, 0.5)" ctx.textAlign = "right" ctx.textBaseline = "bottom" self._draw_text(ctx, "UTC" if utc else "Local time", self.width, 0)
def _create_one_year_of_data(self, y): PSCRIPT_OVERLOAD = False # noqa now = dt.now() nowyear, nowmonth, nowday = dt.get_year_month_day(dt.now()) sy = str(y) rr = [] rpd = list(range(3, 9)) # number of records per day for m in range(1, 13): sm = f"{m:02}" # For each month, we select tags from the tag groups, # we may not have all, and we may have duplicates. tags = [] for i in range(1): tag_group = self._tag_groups1[int(random() * len(self._tag_groups1))] for tag in tag_group: tags.append(tag) for i in range(2): tag_group = self._tag_groups2[int(random() * len(self._tag_groups2))] for tag in tag_group: tags.append(tag) for d in range(1, 32): sd = f"{d:02}" # Don't make records in the current future if y > nowyear: continue elif y == nowyear and m > nowmonth: continue elif y == nowyear and m == nowmonth and d > nowday: continue # Is this date ok? if this_is_js(): # pragma: no cover weekday = Date(f"{sy}-{sm}-{sd}").getDay() # 0 is Sunday # Put some predictable stuff on today, whatever day it is. if y == nowyear and m == nowmonth and d == nowday: for start, stop, tag in [ ("08:51", "09:11", "#admin"), ("09:11", "10:27", "#client1 #meeting"), ("10:29", "11:52", "#client1 #code"), ("12:51", "13:32", "#client1 #code"), ("13:32", "14:28", "#client2 #meeting"), ("14:34", "16:11", "#client2 #design"), ]: t1 = dt.to_time_int(f"{sy}-{sm}-{sd}T{start}:00") t2 = dt.to_time_int(f"{sy}-{sm}-{sd}T{stop}:00") if t2 > now: continue works = [ "work", "stuff", "things", "administration" ] ds = "Did some " + works[int( random() * len(works))] ds += " " + tag record = self.records.create(t1, t2, ds) record.st = now rr.append(record) continue elif weekday not in (1, 2, 3, 4, 5): continue # no NaN (invalid date) or weekends else: if d > 28: continue # on Python, during tests t1 = dt.to_time_int(f"{sy}-{sm}-{sd}T08:00:00") for h in range(rpd[int(random() * len(rpd))]): tag = tags[int(random() * len(tags))] t1 += [0, 10 * 60, 20 * 60][int(random() * 3)] # pause in secs t2 = t1 + 60 * (60 + int(random() * 120)) # 1-3 hours if t2 > now - 60: break works = ["work", "stuff", "things", "administration"] ds = "Did some " + works[int(random() * len(works))] ds += " " + tag record = self.records.create(t1, t2, ds) record.st = now t1 = t2 # next rr.append(record) # Store records self.records._put_received(*rr)
def is_first_day_of_week(t): d = Date(t * 1000) return d.getDay() == 1 # Monday
def get_weekday_longname(t): d = Date(t * 1000) return DAYS_LONG[d.getDay()]
def get_weekday_shortname(t): d = Date(t * 1000) return DAYS_SHORT[ d.getDay()] # getDay starts at zero, which represents Sunday
def get_month_shortname(t): d = Date(t * 1000) return MONTHS_SHORT[d.getMonth()]
def floor(t, res): """Round the given date down to the nearest smaller resolution step.""" resName = res[-1] resFactor = float(res[:-1]) d = Date(t * 1000) tup = ( d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), ) if resName == "s": tup[-1] = int(tup[-1] / resFactor) * resFactor elif resName == "m": tup = tup[:5] tup[-1] = int(tup[-1] / resFactor) * resFactor elif resName == "h": tup = tup[:4] tup[-1] = int(tup[-1] / resFactor) * resFactor elif resName == "D": tup = tup[:3] tup[-1] = int( (tup[-1] - 1) / resFactor) * resFactor + 1 # days are 1-based elif resName == "W": d.setHours(0, 0, 0, 0) daysoff = (d.getDay() + 6) % 7 # Align to a monday d = Date(d.getTime() - 86_400_000 * daysoff) tup = d.getFullYear(), d.getMonth(), d.getDate() elif resName == "M": tup = tup[:2] # Note that it is zero-based (jan is zero) tup[-1] = int(tup[-1] / resFactor) * resFactor tup.extend([1]) elif resName == "Y": tup = tup[:1] tup[-1] = int(tup[-1] / resFactor) * resFactor tup.extend([0, 1]) else: raise RuntimeError("Invalid resolution: " + res) while len(tup) < 6: tup.append(0) return (Date(tup[0], tup[1], tup[2], tup[3], tup[4], tup[5]).getTime() / 1000) # cant do *tup