optimized layout_resolve added tests

This commit is contained in:
Will McGugan
2022-02-18 22:04:09 +00:00
parent 0c7c7ac964
commit 76c5d3a0e5
2 changed files with 90 additions and 23 deletions

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import sys import sys
from fractions import Fraction from fractions import Fraction
from typing import cast, List, Optional, Sequence from typing import cast, Sequence
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
from typing import Protocol from typing import Protocol
@@ -13,12 +13,12 @@ else:
class Edge(Protocol): class Edge(Protocol):
"""Any object that defines an edge (such as Layout).""" """Any object that defines an edge (such as Layout)."""
size: Optional[int] = None size: int | None
fraction: int = 1 fraction: int = 1
min_size: int = 1 min_size: int = 1
def layout_resolve(total: int, edges: Sequence[Edge]) -> List[int]: def layout_resolve(total: int, edges: Sequence[Edge]) -> list[int]:
"""Divide total space to satisfy size, fraction, and min_size, constraints. """Divide total space to satisfy size, fraction, and min_size, constraints.
The returned list of integers should add up to total in most cases, unless it is The returned list of integers should add up to total in most cases, unless it is
@@ -37,10 +37,9 @@ def layout_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
# Size of edge or None for yet to be determined # Size of edge or None for yet to be determined
sizes = [(edge.size or None) for edge in edges] sizes = [(edge.size or None) for edge in edges]
_Fraction = Fraction if None not in sizes:
return cast(list[int], sizes)
# While any edges haven't been calculated
while None in sizes:
# Get flexible edges and index to map these back on to sizes list # Get flexible edges and index to map these back on to sizes list
flexible_edges = [ flexible_edges = [
(index, edge) (index, edge)
@@ -55,15 +54,20 @@ def layout_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
((edge.min_size or 1) if size is None else size) ((edge.min_size or 1) if size is None else size)
for size, edge in zip(sizes, edges) for size, edge in zip(sizes, edges)
] ]
_Fraction = Fraction
while None in sizes:
# Calculate number of characters in a ratio portion # Calculate number of characters in a ratio portion
portion = _Fraction( portion = _Fraction(
remaining, sum((edge.fraction or 1) for _, edge in flexible_edges) remaining, sum((edge.fraction or 1) for _, edge in flexible_edges)
) )
# If any edges will be less than their minimum, replace size with the minimum # If any edges will be less than their minimum, replace size with the minimum
for index, edge in flexible_edges: for flexible_index, (index, edge) in enumerate(flexible_edges):
if portion * edge.fraction <= edge.min_size: if portion * edge.fraction <= edge.min_size:
sizes[index] = edge.min_size sizes[index] = edge.min_size
remaining -= edge.min_size
del flexible_edges[flexible_index]
# New fixed size will invalidate calculations, so we need to repeat the process # New fixed size will invalidate calculations, so we need to repeat the process
break break
else: else:
@@ -72,9 +76,8 @@ def layout_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
# to the following line # to the following line
remainder = _Fraction(0) remainder = _Fraction(0)
for index, edge in flexible_edges: for index, edge in flexible_edges:
size, remainder = divmod(portion * edge.fraction + remainder, 1) sizes[index], remainder = divmod(portion * edge.fraction + remainder, 1)
sizes[index] = size
break break
# Sizes now contains integers only # Sizes now contains integers only
return cast(List[int], sizes) return cast(list[int], sizes)

View File

@@ -0,0 +1,64 @@
from typing import NamedTuple
import pytest
from textual._layout_resolve import layout_resolve
class Edge(NamedTuple):
size: int | None = None
fraction: int = 1
min_size: int = 1
def test_single():
# One edge fixed size
assert layout_resolve(100, [Edge(10)]) == [10]
# One edge fraction of 1
assert layout_resolve(100, [Edge(None, 1)]) == [100]
# One edge fraction 3
assert layout_resolve(100, [Edge(None, 2)]) == [100]
# One edge, fraction1, min size 20
assert layout_resolve(100, [Edge(None, 1, 20)]) == [100]
# One edge fraction 1, min size 120
assert layout_resolve(100, [Edge(None, 1, 120)]) == [120]
def test_two():
# Two edges fixed size
assert layout_resolve(100, [Edge(10), Edge(20)]) == [10, 20]
# Two edges, fraction 1 each
assert layout_resolve(100, [Edge(None, 1), Edge(None, 1)]) == [50, 50]
# Two edges, one with fraction 2, one with fraction 1
# Note first value is rounded down, second is rounded up
assert layout_resolve(100, [Edge(None, 2), Edge(None, 1)]) == [66, 34]
# Two edges, both with fraction 2
assert layout_resolve(100, [Edge(None, 2), Edge(None, 2)]) == [50, 50]
# Two edges, one with fraction 3, one with fraction 1
assert layout_resolve(100, [Edge(None, 3), Edge(None, 1)]) == [75, 25]
# Two edges, one with fraction 3, one with fraction 1, second with min size of 30
assert layout_resolve(100, [Edge(None, 3), Edge(None, 1, 30)]) == [70, 30]
# Two edges, one with fraction 1 and min size 30, one with fraction 3
assert layout_resolve(100, [Edge(None, 1, 30), Edge(None, 3)]) == [30, 70]
@pytest.mark.parametrize(
"size, edges, result",
[
(10, [Edge(None, 1), Edge(None, 1), Edge(None, 1)], [3, 3, 4]),
(10, [Edge(5), Edge(None, 1), Edge(None, 1)], [5, 2, 3]),
(10, [Edge(None, 2), Edge(None, 1), Edge(None, 1)], [5, 2, 3]),
(10, [Edge(None, 2), Edge(3), Edge(None, 1)], [4, 3, 3]),
(
10,
[Edge(None, 2), Edge(None, 1), Edge(None, 1), Edge(None, 1)],
[4, 2, 2, 2],
),
(
10,
[Edge(None, 4), Edge(None, 1), Edge(None, 1), Edge(None, 1)],
[5, 2, 1, 2],
),
],
)
def test_multiple(size, edges, result):
assert layout_resolve(size, edges) == result