Functional Weave
Code in Python

charts.ticks@1.0.0

impl/python/time_ticks.py

2,374 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

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  ← from dates.add-days ^1.0.0 · built alongside by fune

# 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