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
from fractions import Fraction
from typing import cast, List, Optional, Sequence
from typing import cast, Sequence
if sys.version_info >= (3, 8):
from typing import Protocol
@@ -13,12 +13,12 @@ else:
class Edge(Protocol):
"""Any object that defines an edge (such as Layout)."""
size: Optional[int] = None
size: int | None
fraction: 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.
The returned list of integers should add up to total in most cases, unless it is
@@ -37,33 +37,37 @@ def layout_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
# Size of edge or None for yet to be determined
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
flexible_edges = [
(index, edge)
for index, (size, edge) in enumerate(zip(sizes, edges))
if size is None
# Get flexible edges and index to map these back on to sizes list
flexible_edges = [
(index, edge)
for index, (size, edge) in enumerate(zip(sizes, edges))
if size is None
]
# Remaining space in total
remaining = total - sum(size or 0 for size in sizes)
if remaining <= 0:
# No room for flexible edges
return [
((edge.min_size or 1) if size is None else size)
for size, edge in zip(sizes, edges)
]
# Remaining space in total
remaining = total - sum(size or 0 for size in sizes)
if remaining <= 0:
# No room for flexible edges
return [
((edge.min_size or 1) if size is None else size)
for size, edge in zip(sizes, edges)
]
_Fraction = Fraction
while None in sizes:
# Calculate number of characters in a ratio portion
portion = _Fraction(
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
for index, edge in flexible_edges:
for flexible_index, (index, edge) in enumerate(flexible_edges):
if portion * edge.fraction <= 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
break
else:
@@ -72,9 +76,8 @@ def layout_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
# to the following line
remainder = _Fraction(0)
for index, edge in flexible_edges:
size, remainder = divmod(portion * edge.fraction + remainder, 1)
sizes[index] = size
sizes[index], remainder = divmod(portion * edge.fraction + remainder, 1)
break
# 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