Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 83 additions & 3 deletions av/rational.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
# type: ignore
import sys
from decimal import Decimal
from fractions import Fraction
from numbers import Rational
from numbers import Rational, Real

import cython
from cython.cimports import libav as lib

_INT32_MAX: cython.longlong = 2147483647
_INF: cython.double = float("inf")
_HASH_INF = sys.hash_info.inf
_C_NEG_INF: cython.int = -1
_C_FINITE: cython.int = 0
_C_POS_INF: cython.int = 1
_C_UNDEF: cython.int = 2
_C_NAN: cython.int = 100 # foreign NaN: unordered, every comparison is False
_C_UNKNOWN: cython.int = 101 # not a real number: NotImplemented

_LT: cython.int = 0
_LE: cython.int = 1
_GT: cython.int = 2
_GE: cython.int = 3


@cython.final
Expand Down Expand Up @@ -76,7 +91,10 @@ def __float__(self):

def __hash__(self):
if self.den == 0:
return hash((self.num, 0))
# ``1/0 == float("inf")``, so the two must hash alike.
if self.num == 0:
return hash((0, 0))
return _HASH_INF if self.num > 0 else -_HASH_INF
return hash(Fraction(self.num, self.den))

def __reduce__(self):
Expand All @@ -87,19 +105,34 @@ def __eq__(self, other):
o: AVRational = other
return self.num == o.num and self.den == o.den
if self.den == 0:
return False
cb: cython.int = _order_class(other)
if cb == _C_UNKNOWN:
return NotImplemented
return _order_class(self) == cb
return Fraction(self.num, self.den).__eq__(other)

def __lt__(self, other):
cb: cython.int = _order_class(other)
if self.den == 0 or cb != _C_FINITE:
return _cmp(self, cb, _LT)
return Fraction(self.num, self.den).__lt__(other)

def __le__(self, other):
cb: cython.int = _order_class(other)
if self.den == 0 or cb != _C_FINITE:
return _cmp(self, cb, _LE)
return Fraction(self.num, self.den).__le__(other)

def __gt__(self, other):
cb: cython.int = _order_class(other)
if self.den == 0 or cb != _C_FINITE:
return _cmp(self, cb, _GT)
return Fraction(self.num, self.den).__gt__(other)

def __ge__(self, other):
cb: cython.int = _order_class(other)
if self.den == 0 or cb != _C_FINITE:
return _cmp(self, cb, _GE)
return Fraction(self.num, self.den).__ge__(other)

def __neg__(self):
Expand Down Expand Up @@ -144,6 +177,53 @@ def __rsub__(self, other):
return Fraction(self.num, self.den).__rsub__(other)


@cython.cfunc
def _order_class(v) -> cython.int:
if type(v) is AVRational:
o: AVRational = v
if o.den != 0:
return _C_FINITE
if o.num == 0:
return _C_UNDEF
return _C_POS_INF if o.num > 0 else _C_NEG_INF
if isinstance(v, Rational): # int, Fraction, ...
return _C_FINITE
if isinstance(v, Decimal):
if v.is_nan():
return _C_NAN
if v.is_infinite():
return _C_POS_INF if v > 0 else _C_NEG_INF
return _C_FINITE
if isinstance(v, Real): # float, numpy scalars, ...
f: cython.double = v
if f != f:
return _C_NAN
if f == _INF:
return _C_POS_INF
if f == -_INF:
return _C_NEG_INF
return _C_FINITE
return _C_UNKNOWN


# Returns object, not bint: NotImplemented must reach Python intact so the
# reflected operator gets a turn.
@cython.cfunc
def _cmp(a: AVRational, cb: cython.int, op: cython.int):
if cb == _C_UNKNOWN:
return NotImplemented
if cb == _C_NAN:
return False
ca: cython.int = _order_class(a)
if op == _LT:
return ca < cb
if op == _LE:
return ca <= cb
if op == _GT:
return ca > cb
return ca >= cb


@cython.cfunc
def from_avrational(q: lib.AVRational) -> AVRational:
obj: AVRational = AVRational.__new__(AVRational)
Expand Down
138 changes: 137 additions & 1 deletion tests/test_rational.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pickle
from decimal import Decimal
from fractions import Fraction

import pytest
Expand Down Expand Up @@ -69,7 +70,7 @@ def test_setters_accept_avrational() -> None:
import av

cc = av.codec.CodecContext.create("mpeg4", "w")
cc.time_base = AVRational(1001, 30000) # type: ignore[assignment]
cc.time_base = AVRational(1001, 30000)
assert cc.time_base == Fraction(1001, 30000)


Expand All @@ -86,3 +87,138 @@ def test_pickle_and_repr() -> None:
assert pickle.loads(pickle.dumps(r)) == r
assert repr(r) == "AVRational(30000, 1001)"
assert str(r) == "30000/1001"


def test_nonfinite_equality() -> None:
inf, nan = float("inf"), float("nan")

# Equality between two AVRationals is structural.
assert AVRational(1, 0) == AVRational(2, 0)
assert AVRational(0, 0) == AVRational(0, 0)
assert AVRational(1, 0) != AVRational(-1, 0)
assert AVRational(1, 0) != AVRational(0, 0)

# Against another numeric type it is by value, so the hashes must agree.
assert AVRational(1, 0) == inf and hash(AVRational(1, 0)) == hash(inf)
assert AVRational(-1, 0) == -inf and hash(AVRational(-1, 0)) == hash(-inf)
assert AVRational(1, 0) == Decimal("Infinity")
assert hash(AVRational(1, 0)) == hash(Decimal("Infinity"))
assert {AVRational(1, 0)} == {inf}

# 0/0 equals no value of another type, not even a NaN.
assert AVRational(0, 0) != nan
assert AVRational(0, 0) != Decimal("NaN")
assert AVRational(0, 0) != 0 and AVRational(0, 0) != Fraction(0, 1)

assert AVRational(1, 0) != "x" and AVRational(1, 0) is not None


def test_nonfinite_ordering() -> None:
inf = float("inf")
neg, pos, undef, half = (
AVRational(-1, 0),
AVRational(1, 0),
AVRational(0, 0),
AVRational(1, 2),
)

assert neg < half < pos < undef
assert undef > pos > half > neg
assert not (pos < pos) and pos <= pos and pos >= pos
assert not (undef < undef) and undef <= undef and undef >= undef

# A finite AVRational against a non-finite one, either way round.
assert half < pos and pos > half
assert half > neg and neg < half
assert not (half >= pos) and not (pos <= half)

# Against plain numbers, including infinities of another type.
assert pos > 10**9 and pos > 1e308 and pos > Fraction(10**9)
assert neg < -(10**9) and neg < -1e308
assert pos <= inf and pos >= inf and not (pos < inf) and not (pos > inf)
assert neg <= -inf and neg >= -inf
assert half < inf and half > -inf
assert undef > inf and undef > 0 and not (undef < inf)


def test_nonfinite_ordering_is_consistent() -> None:
values = [
AVRational(0, 0),
AVRational(1, 0),
AVRational(-1, 0),
AVRational(1, 2),
AVRational(0, 1),
Fraction(1, 2),
0,
7,
-2.5,
float("inf"),
float("-inf"),
Decimal("0.5"),
Decimal("Infinity"),
]
for a in (v for v in values if isinstance(v, AVRational)):
for b in values:
eq, lt, le, gt, ge = a == b, a < b, a <= b, a > b, a >= b
assert le == (lt or eq), (a, b)
assert ge == (gt or eq), (a, b)
assert not (lt and gt), (a, b)
if isinstance(b, AVRational):
assert (lt, le, eq) == (b > a, b >= a, b == a), (a, b)
assert lt + eq + gt == 1, (a, b)
if eq:
assert hash(a) == hash(b), (a, b)


def test_nan_is_unordered() -> None:
rationals = (AVRational(1, 2), AVRational(1, 0), AVRational(0, 0))
for value in (float("nan"), Decimal("NaN"), Decimal("sNaN")):
for r in rationals:
assert not (r < value) and not (r <= value)
assert not (r > value) and not (r >= value)

for value in (float("nan"), Decimal("NaN")):
for r in rationals:
assert r != value


def test_nonfinite_sorting() -> None:
values = [
AVRational(1, 0),
AVRational(0, 0),
AVRational(1, 2),
AVRational(-1, 0),
AVRational(3, 1),
AVRational(-3, 1),
]
expected = [
AVRational(-1, 0),
AVRational(-3, 1),
AVRational(1, 2),
AVRational(3, 1),
AVRational(1, 0),
AVRational(0, 0),
]
assert sorted(values) == expected
backwards = values[::-1]
assert sorted(backwards) == expected
assert max(values) == AVRational(0, 0)
assert min(values) == AVRational(-1, 0)


def test_unorderable_types() -> None:
for value in ("x", None, 1j, object()):
for r in (AVRational(1, 2), AVRational(1, 0), AVRational(0, 0)):
with pytest.raises(TypeError):
r < value # noqa: B015
with pytest.raises(TypeError):
r >= value # noqa: B015


def test_fraction_on_the_left_is_asymmetric() -> None:
assert Fraction(1, 2) < AVRational(1, 0)
assert Fraction(1, 2) > AVRational(-1, 0)
assert Fraction(1, 2) >= AVRational(0, 0)
assert not (AVRational(0, 0) <= Fraction(1, 2))
assert not (Fraction(1, 2) < AVRational(0, 0))
assert AVRational(0, 0) > Fraction(1, 2)
Loading