TOMK / src /python /tom_world03 /interval.py
c0mbatduckzz's picture
Publish WQK 0.6 formal specification, use cases and verified release (part 2)
767a051 verified
Raw History Blame Contribute Delete
4.44 kB
"""Closed exact rational intervals and outward-exact arithmetic."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Iterable
from .rational import Q
@dataclass(frozen=True, slots=True)
class ClosedInterval:
lower: Q
upper: Q
def __post_init__(self) -> None:
lo = Q.from_value(self.lower)
hi = Q.from_value(self.upper)
if hi < lo:
raise ValueError(f"interval upper {hi} is below lower {lo}")
object.__setattr__(self, "lower", lo)
object.__setattr__(self, "upper", hi)
@classmethod
def point(cls, value: Any) -> "ClosedInterval":
q = Q.from_value(value)
return cls(q, q)
@classmethod
def from_value(cls, value: Any) -> "ClosedInterval":
if isinstance(value, cls):
return value
if isinstance(value, dict):
return cls(Q.from_value(value["lower"]), Q.from_value(value["upper"]))
if isinstance(value, (list, tuple)) and len(value) == 2:
return cls(Q.from_value(value[0]), Q.from_value(value[1]))
return cls.point(value)
@classmethod
def hull(cls, values: Iterable[Any]) -> "ClosedInterval":
items = [Q.from_value(v) for v in values]
if not items:
raise ValueError("cannot form interval hull of an empty sequence")
return cls(min(items), max(items))
def to_record(self) -> dict[str, dict[str, int]]:
return {"lower": self.lower.to_record(), "upper": self.upper.to_record()}
def to_text(self) -> str:
return f"[{self.lower.to_text()},{self.upper.to_text()}]"
def width(self) -> Q:
return self.upper - self.lower
def midpoint(self) -> Q:
return (self.lower + self.upper) / 2
def contains(self, value: Any) -> bool:
q = Q.from_value(value)
return self.lower <= q <= self.upper
def contains_zero(self) -> bool:
return self.contains(0)
def interior_contains_zero(self) -> bool:
return self.lower < 0 < self.upper
def is_point(self) -> bool:
return self.lower == self.upper
def sign_class(self) -> str:
if self.upper < 0:
return "negative"
if self.lower > 0:
return "positive"
zero = Q(0)
if self.lower == zero and self.upper == zero:
return "zero"
if self.lower == zero:
return "nonnegative"
if self.upper == zero:
return "nonpositive"
return "straddles-zero"
def excludes_zero(self) -> bool:
return self.upper < 0 or self.lower > 0
def intersection(self, other: Any) -> "ClosedInterval | None":
b = ClosedInterval.from_value(other)
lo = max(self.lower, b.lower)
hi = min(self.upper, b.upper)
return None if hi < lo else ClosedInterval(lo, hi)
def subset_of(self, other: Any) -> bool:
b = ClosedInterval.from_value(other)
return b.lower <= self.lower and self.upper <= b.upper
def __neg__(self) -> "ClosedInterval":
return ClosedInterval(-self.upper, -self.lower)
def __add__(self, other: Any) -> "ClosedInterval":
b = ClosedInterval.from_value(other)
return ClosedInterval(self.lower + b.lower, self.upper + b.upper)
def __radd__(self, other: Any) -> "ClosedInterval":
return self + other
def __sub__(self, other: Any) -> "ClosedInterval":
return self + (-ClosedInterval.from_value(other))
def __rsub__(self, other: Any) -> "ClosedInterval":
return ClosedInterval.from_value(other) - self
def __mul__(self, other: Any) -> "ClosedInterval":
b = ClosedInterval.from_value(other)
products = (
self.lower * b.lower,
self.lower * b.upper,
self.upper * b.lower,
self.upper * b.upper,
)
return ClosedInterval(min(products), max(products))
def __rmul__(self, other: Any) -> "ClosedInterval":
return self * other
def reciprocal(self) -> "ClosedInterval":
if self.contains_zero():
raise ZeroDivisionError("interval reciprocal is undefined across zero")
return ClosedInterval(1 / self.upper, 1 / self.lower)
def __truediv__(self, other: Any) -> "ClosedInterval":
return self * ClosedInterval.from_value(other).reciprocal()
def __str__(self) -> str:
return self.to_text()