| import sys |
| from fractions import Fraction |
| from math import ceil |
| from typing import cast, List, Optional, Sequence |
| |
| if sys.version_info >= (3, 8): |
| from typing import Protocol |
| else: |
| from pip._vendor.typing_extensions import Protocol # pragma: no cover |
| |
| |
| class Edge(Protocol): |
| """Any object that defines an edge (such as Layout).""" |
| |
| size: Optional[int] = None |
| ratio: int = 1 |
| minimum_size: int = 1 |
| |
| |
| def ratio_resolve(total: int, edges: Sequence[Edge]) -> List[int]: |
| """Divide total space to satisfy size, ratio, and minimum_size, constraints. |
| |
| The returned list of integers should add up to total in most cases, unless it is |
| impossible to satisfy all the constraints. For instance, if there are two edges |
| with a minimum size of 20 each and `total` is 30 then the returned list will be |
| greater than total. In practice, this would mean that a Layout object would |
| clip the rows that would overflow the screen height. |
| |
| Args: |
| total (int): Total number of characters. |
| edges (List[Edge]): Edges within total space. |
| |
| Returns: |
| List[int]: Number of characters for each edge. |
| """ |
| # Size of edge or None for yet to be determined |
| sizes = [(edge.size or None) for edge in edges] |
| |
| _Fraction = Fraction |
| |
| # 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 |
| ] |
| # Remaining space in total |
| remaining = total - sum(size or 0 for size in sizes) |
| if remaining <= 0: |
| # No room for flexible edges |
| return [ |
| ((edge.minimum_size or 1) if size is None else size) |
| for size, edge in zip(sizes, edges) |
| ] |
| # Calculate number of characters in a ratio portion |
| portion = _Fraction( |
| remaining, sum((edge.ratio 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: |
| if portion * edge.ratio <= edge.minimum_size: |
| sizes[index] = edge.minimum_size |
| # New fixed size will invalidate calculations, so we need to repeat the process |
| break |
| else: |
| # Distribute flexible space and compensate for rounding error |
| # Since edge sizes can only be integers we need to add the remainder |
| # to the following line |
| remainder = _Fraction(0) |
| for index, edge in flexible_edges: |
| size, remainder = divmod(portion * edge.ratio + remainder, 1) |
| sizes[index] = size |
| break |
| # Sizes now contains integers only |
| return cast(List[int], sizes) |
| |
| |
| def ratio_reduce( |
| total: int, ratios: List[int], maximums: List[int], values: List[int] |
| ) -> List[int]: |
| """Divide an integer total in to parts based on ratios. |
| |
| Args: |
| total (int): The total to divide. |
| ratios (List[int]): A list of integer ratios. |
| maximums (List[int]): List of maximums values for each slot. |
| values (List[int]): List of values |
| |
| Returns: |
| List[int]: A list of integers guaranteed to sum to total. |
| """ |
| ratios = [ratio if _max else 0 for ratio, _max in zip(ratios, maximums)] |
| total_ratio = sum(ratios) |
| if not total_ratio: |
| return values[:] |
| total_remaining = total |
| result: List[int] = [] |
| append = result.append |
| for ratio, maximum, value in zip(ratios, maximums, values): |
| if ratio and total_ratio > 0: |
| distributed = min(maximum, round(ratio * total_remaining / total_ratio)) |
| append(value - distributed) |
| total_remaining -= distributed |
| total_ratio -= ratio |
| else: |
| append(value) |
| return result |
| |
| |
| def ratio_distribute( |
| total: int, ratios: List[int], minimums: Optional[List[int]] = None |
| ) -> List[int]: |
| """Distribute an integer total in to parts based on ratios. |
| |
| Args: |
| total (int): The total to divide. |
| ratios (List[int]): A list of integer ratios. |
| minimums (List[int]): List of minimum values for each slot. |
| |
| Returns: |
| List[int]: A list of integers guaranteed to sum to total. |
| """ |
| if minimums: |
| ratios = [ratio if _min else 0 for ratio, _min in zip(ratios, minimums)] |
| total_ratio = sum(ratios) |
| assert total_ratio > 0, "Sum of ratios must be > 0" |
| |
| total_remaining = total |
| distributed_total: List[int] = [] |
| append = distributed_total.append |
| if minimums is None: |
| _minimums = [0] * len(ratios) |
| else: |
| _minimums = minimums |
| for ratio, minimum in zip(ratios, _minimums): |
| if total_ratio > 0: |
| distributed = max(minimum, ceil(ratio * total_remaining / total_ratio)) |
| else: |
| distributed = total_remaining |
| append(distributed) |
| total_ratio -= ratio |
| total_remaining -= distributed |
| return distributed_total |
| |
| |
| if __name__ == "__main__": |
| from dataclasses import dataclass |
| |
| @dataclass |
| class E: |
| |
| size: Optional[int] = None |
| ratio: int = 1 |
| minimum_size: int = 1 |
| |
| resolved = ratio_resolve(110, [E(None, 1, 1), E(None, 1, 1), E(None, 1, 1)]) |
| print(sum(resolved)) |