File size: 4,444 Bytes
767a051
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
"""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()