Source code for diveplan.dive.formatters.runtime

"""Runtime-table formatter: the plan as a diver would write it on a slate.

Follows the dive-table convention (matching Subsurface's plan details with
"display transitions in deco" off): travel between deco stops is folded into
the following stop's row — its duration includes the ascent, its runtime is
the departure time. The ascent from the bottom to the first stop and the
final surfacing keep their own rows. Whole minutes throughout. Plain ASCII,
so the output pastes anywhere and prints from any encoding.

Use :meth:`~diveplan.dive.formatters.BaseFormatter.write` to save the sheet
as a ``.txt``.
"""

from datetime import timedelta

from diveplan.core.gas import Gas
from diveplan.dive.dive_report import DiveReport
from diveplan.dive.formatters import BaseFormatter

__all__ = ["RuntimeFormatter"]

# Row markers: v descent, - hold (bottom/stop), ^ ascent.
_MARK = {"DESCENT": "v", "FORCED_ASCENT": "^", "DECO_ASCENT": "^"}

_FOLDABLE_BEFORE_STOP = ("STOP", "GAS_SWITCH")


def _minutes(td: timedelta) -> int:
    return round(td.total_seconds() / 60)


[docs] class RuntimeFormatter(BaseFormatter): """Printable runtime sheet: depth / duration / runtime / gas.""" NAME = "runtime"
[docs] def format(self, report: DiveReport) -> str: """Render the runtime sheet as plain ASCII text.""" lines = [ f"DIVE PLAN - {report.model_name} | " f"runtime {_minutes(report.runtime)} min | " f"max depth {report.max_depth.depth_m:.0f} m", "generated by diveplan - DO NOT USE FOR REAL DIVES", "", " depth duration runtime gas", ] shown_gas = None for mark, depth_m, duration, runtime_end, gas in self._table_rows(report): gas_label = "" if gas != shown_gas: gas_label = f" {gas.name}" shown_gas = gas lines.append( f" {mark} {depth_m:3.0f}m {_minutes(duration):5d}min " f"{_minutes(runtime_end):6d}min{gas_label}" ) lines.append("") consumed = " | ".join( f"{gas.name} {litres:.0f} L" for gas, litres in report.consumption_l ) lines.append(f"gas used {consumed}") lines.append( f"sac bottom {report.sac_bottom:g} / " f"deco {report.sac_deco:g} L/min | " f"rock bottom x{report.sac_factor:g}" ) lines.append( f"rock bottom {report.rock_bottom_l:.0f} L " f"@ {report.max_depth.depth_m:.0f} m" ) lines.append(f"exposure CNS {report.cns:.0f}% | OTU {report.otus:.0f}") if report.tts_variations is not None: per_m = report.tts_variations.per_meter.total_seconds() / 60 per_min = report.tts_variations.per_minute.total_seconds() / 60 lines.append( f"variations +{per_min:.1f} min per extra min, " f"+{per_m:.1f} min per extra m" ) return "\n".join(lines)
@staticmethod def _table_rows( report: DiveReport, ) -> list[tuple[str, float, timedelta, timedelta, Gas]]: """Fold deco travel/switches into stop rows; keep phase boundaries. A DECO_ASCENT that departs from a stop is folded into the next row (dive-table convention); the first deco ascent (from the bottom) and the final surfacing keep their own rows. GAS_SWITCH segments always fold into the following stop — the gas column marks the change. """ rows = report.rows out: list[tuple[str, float, timedelta, timedelta, Gas]] = [] pending = timedelta(0) previous_kind: str | None = None for i, row in enumerate(rows): is_last = i == len(rows) - 1 if row.kind == "DECO_ASCENT" and not is_last: if previous_kind in _FOLDABLE_BEFORE_STOP: pending += row.duration # travel between stops: fold previous_kind = row.kind continue if row.kind == "GAS_SWITCH" and not is_last: pending += row.duration # switch time counts as stop time previous_kind = row.kind continue mark = _MARK.get(row.kind, "-") out.append( ( mark, row.end_depth_m if mark in ("v", "^") else row.start_depth_m, row.duration + pending, row.runtime, row.gas, ) ) pending = timedelta(0) previous_kind = row.kind return out