codekingpro/portable-devtools
114k
1from fractions import Fraction2from math import ceil3from typing import cast, List, Optional, Sequence, Protocol4 5 6class Edge(Protocol):7 """Any object that defines an edge (such as Layout)."""8 9 size: Optional[int] = None10 ratio: int = 111 minimum_size: int = 112 13 14def ratio_resolve(total: int, edges: Sequence[Edge]) -> List[int]:15 """Divide total space to satisfy size, ratio, and minimum_size, constraints.16 17 The returned list of integers should add up to total in most cases, unless it is18 impossible to satisfy all the constraints. For instance, if there are two edges19 with a minimum size of 20 each and `total` is 30 then the returned list will be20 greater than total. In practice, this would mean that a Layout object would21 clip the rows that would overflow the screen height.22 23 Args:24 total (int): Total number of characters.25 edges (List[Edge]): Edges within total space.26 27 Returns:28 List[int]: Number of characters for each edge.29 """30 # Size of edge or None for yet to be determined31 sizes = [(edge.size or None) for edge in edges]32 33 _Fraction = Fraction34 35 # While any edges haven't been calculated36 while None in sizes:37 # Get flexible edges and index to map these back on to sizes list38 flexible_edges = [39 (index, edge)40 for index, (size, edge) in enumerate(zip(sizes, edges))41 if size is None42 ]43 # Remaining space in total44 remaining = total - sum(size or 0 for size in sizes)45 if remaining <= 0:46 # No room for flexible edges47 return [48 ((edge.minimum_size or 1) if size is None else size)49 for size, edge in zip(sizes, edges)50 ]51 # Calculate number of characters in a ratio portion52 portion = _Fraction(53 remaining, sum((edge.ratio or 1) for _, edge in flexible_edges)54 )55 56 # If any edges will be less than their minimum, replace size with the minimum57 for index, edge in flexible_edges:58 if portion * edge.ratio <= edge.minimum_size:59 sizes[index] = edge.minimum_size60 # New fixed size will invalidate calculations, so we need to repeat the process61 break62 else:63 # Distribute flexible space and compensate for rounding error64 # Since edge sizes can only be integers we need to add the remainder65 # to the following line66 remainder = _Fraction(0)67 for index, edge in flexible_edges:68 size, remainder = divmod(portion * edge.ratio + remainder, 1)69 sizes[index] = size70 break71 # Sizes now contains integers only72 return cast(List[int], sizes)73 74 75def ratio_reduce(76 total: int, ratios: List[int], maximums: List[int], values: List[int]77) -> List[int]:78 """Divide an integer total in to parts based on ratios.79 80 Args:81 total (int): The total to divide.82 ratios (List[int]): A list of integer ratios.83 maximums (List[int]): List of maximums values for each slot.84 values (List[int]): List of values85 86 Returns:87 List[int]: A list of integers guaranteed to sum to total.88 """89 ratios = [ratio if _max else 0 for ratio, _max in zip(ratios, maximums)]90 total_ratio = sum(ratios)91 if not total_ratio:92 return values[:]93 total_remaining = total94 result: List[int] = []95 append = result.append96 for ratio, maximum, value in zip(ratios, maximums, values):97 if ratio and total_ratio > 0:98 distributed = min(maximum, round(ratio * total_remaining / total_ratio))99 append(value - distributed)100 total_remaining -= distributed101 total_ratio -= ratio102 else:103 append(value)104 return result105 106 107def ratio_distribute(108 total: int, ratios: List[int], minimums: Optional[List[int]] = None109) -> List[int]:110 """Distribute an integer total in to parts based on ratios.111 112 Args:113 total (int): The total to divide.114 ratios (List[int]): A list of integer ratios.115 minimums (List[int]): List of minimum values for each slot.116 117 Returns:118 List[int]: A list of integers guaranteed to sum to total.119 """120 if minimums:121 ratios = [ratio if _min else 0 for ratio, _min in zip(ratios, minimums)]122 total_ratio = sum(ratios)123 assert total_ratio > 0, "Sum of ratios must be > 0"124 125 total_remaining = total126 distributed_total: List[int] = []127 append = distributed_total.append128 if minimums is None:129 _minimums = [0] * len(ratios)130 else:131 _minimums = minimums132 for ratio, minimum in zip(ratios, _minimums):133 if total_ratio > 0:134 distributed = max(minimum, ceil(ratio * total_remaining / total_ratio))135 else:136 distributed = total_remaining137 append(distributed)138 total_ratio -= ratio139 total_remaining -= distributed140 return distributed_total141 142 143if __name__ == "__main__":144 from dataclasses import dataclass145 146 @dataclass147 class E:148 size: Optional[int] = None149 ratio: int = 1150 minimum_size: int = 1151 152 resolved = ratio_resolve(110, [E(None, 1, 1), E(None, 1, 1), E(None, 1, 1)])153 print(sum(resolved))154 