from typing import List from .charts_ticks_types import TimeInterval from .dates_add_days import civil_from_days, days_from_civil, epoch_day_from_iso, iso_from_epoch_day # 1970-01-05, the first Monday on or after the epoch, as a day number. _FIRST_MONDAY = 4 _MAX_TICKS = 10000 def _ceil_div(a: int, b: int) -> int: return -((-a) // b) def time_ticks(start: str, stop: str, interval: TimeInterval, step: int) -> List[str]: """Calendar boundaries between two dates, inclusive, anchored to the calendar rather than to start so that panning does not move them: days by (day - 1) % step, weeks since 1970-01-05, months by (month - 1) % step, quarters by quarter of the year, years by year % step.""" if isinstance(step, bool) or not isinstance(step, int) or step < 1: raise ValueError(f"step must be a whole number of at least 1, got {step}") a = epoch_day_from_iso(start) b = epoch_day_from_iso(stop) reverse = b < a lo, hi = (b, a) if reverse else (a, b) days: List[int] = [] def add(day: int) -> None: if len(days) >= _MAX_TICKS: raise ValueError(f"too many ticks: more than {_MAX_TICKS}; use a longer interval or step") days.append(day) if interval == "day": for d in range(lo, hi + 1): if (civil_from_days(d).day - 1) % step == 0: add(d) elif interval == "week": k = _ceil_div(lo - _FIRST_MONDAY, 7) k = _ceil_div(k, step) * step d = _FIRST_MONDAY + 7 * k while d <= hi: add(d) d += 7 * step elif interval in ("month", "quarter", "year"): months = step if interval == "month" else 3 * step if interval == "quarter" else 12 first = civil_from_days(lo) year, month = first.year, first.month while True: d = days_from_civil(year, month, 1) if d > hi: break on_step = (month == 1 and year % step == 0) if interval == "year" else (month - 1) % months == 0 if d >= lo and on_step: add(d) month += 1 if month > 12: month = 1 year += 1 else: raise ValueError(f'unknown time interval "{interval}"') ticks = [iso_from_epoch_day(d) for d in days] if reverse: ticks.reverse() return ticks