_ratio.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import sys
  2. from fractions import Fraction
  3. from math import ceil
  4. from typing import cast, List, Optional, Sequence
  5. if sys.version_info >= (3, 8):
  6. from typing import Protocol
  7. else:
  8. from pip._vendor.typing_extensions import Protocol # pragma: no cover
  9. class Edge(Protocol):
  10. """Any object that defines an edge (such as Layout)."""
  11. size: Optional[int] = None
  12. ratio: int = 1
  13. minimum_size: int = 1
  14. def ratio_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
  15. """Divide total space to satisfy size, ratio, and minimum_size, constraints.
  16. The returned list of integers should add up to total in most cases, unless it is
  17. impossible to satisfy all the constraints. For instance, if there are two edges
  18. with a minimum size of 20 each and `total` is 30 then the returned list will be
  19. greater than total. In practice, this would mean that a Layout object would
  20. clip the rows that would overflow the screen height.
  21. Args:
  22. total (int): Total number of characters.
  23. edges (List[Edge]): Edges within total space.
  24. Returns:
  25. List[int]: Number of characters for each edge.
  26. """
  27. # Size of edge or None for yet to be determined
  28. sizes = [(edge.size or None) for edge in edges]
  29. _Fraction = Fraction
  30. # While any edges haven't been calculated
  31. while None in sizes:
  32. # Get flexible edges and index to map these back on to sizes list
  33. flexible_edges = [
  34. (index, edge)
  35. for index, (size, edge) in enumerate(zip(sizes, edges))
  36. if size is None
  37. ]
  38. # Remaining space in total
  39. remaining = total - sum(size or 0 for size in sizes)
  40. if remaining <= 0:
  41. # No room for flexible edges
  42. return [
  43. ((edge.minimum_size or 1) if size is None else size)
  44. for size, edge in zip(sizes, edges)
  45. ]
  46. # Calculate number of characters in a ratio portion
  47. portion = _Fraction(
  48. remaining, sum((edge.ratio or 1) for _, edge in flexible_edges)
  49. )
  50. # If any edges will be less than their minimum, replace size with the minimum
  51. for index, edge in flexible_edges:
  52. if portion * edge.ratio <= edge.minimum_size:
  53. sizes[index] = edge.minimum_size
  54. # New fixed size will invalidate calculations, so we need to repeat the process
  55. break
  56. else:
  57. # Distribute flexible space and compensate for rounding error
  58. # Since edge sizes can only be integers we need to add the remainder
  59. # to the following line
  60. remainder = _Fraction(0)
  61. for index, edge in flexible_edges:
  62. size, remainder = divmod(portion * edge.ratio + remainder, 1)
  63. sizes[index] = size
  64. break
  65. # Sizes now contains integers only
  66. return cast(List[int], sizes)
  67. def ratio_reduce(
  68. total: int, ratios: List[int], maximums: List[int], values: List[int]
  69. ) -> List[int]:
  70. """Divide an integer total in to parts based on ratios.
  71. Args:
  72. total (int): The total to divide.
  73. ratios (List[int]): A list of integer ratios.
  74. maximums (List[int]): List of maximums values for each slot.
  75. values (List[int]): List of values
  76. Returns:
  77. List[int]: A list of integers guaranteed to sum to total.
  78. """
  79. ratios = [ratio if _max else 0 for ratio, _max in zip(ratios, maximums)]
  80. total_ratio = sum(ratios)
  81. if not total_ratio:
  82. return values[:]
  83. total_remaining = total
  84. result: List[int] = []
  85. append = result.append
  86. for ratio, maximum, value in zip(ratios, maximums, values):
  87. if ratio and total_ratio > 0:
  88. distributed = min(maximum, round(ratio * total_remaining / total_ratio))
  89. append(value - distributed)
  90. total_remaining -= distributed
  91. total_ratio -= ratio
  92. else:
  93. append(value)
  94. return result
  95. def ratio_distribute(
  96. total: int, ratios: List[int], minimums: Optional[List[int]] = None
  97. ) -> List[int]:
  98. """Distribute an integer total in to parts based on ratios.
  99. Args:
  100. total (int): The total to divide.
  101. ratios (List[int]): A list of integer ratios.
  102. minimums (List[int]): List of minimum values for each slot.
  103. Returns:
  104. List[int]: A list of integers guaranteed to sum to total.
  105. """
  106. if minimums:
  107. ratios = [ratio if _min else 0 for ratio, _min in zip(ratios, minimums)]
  108. total_ratio = sum(ratios)
  109. assert total_ratio > 0, "Sum of ratios must be > 0"
  110. total_remaining = total
  111. distributed_total: List[int] = []
  112. append = distributed_total.append
  113. if minimums is None:
  114. _minimums = [0] * len(ratios)
  115. else:
  116. _minimums = minimums
  117. for ratio, minimum in zip(ratios, _minimums):
  118. if total_ratio > 0:
  119. distributed = max(minimum, ceil(ratio * total_remaining / total_ratio))
  120. else:
  121. distributed = total_remaining
  122. append(distributed)
  123. total_ratio -= ratio
  124. total_remaining -= distributed
  125. return distributed_total
  126. if __name__ == "__main__":
  127. from dataclasses import dataclass
  128. @dataclass
  129. class E:
  130. size: Optional[int] = None
  131. ratio: int = 1
  132. minimum_size: int = 1
  133. resolved = ratio_resolve(110, [E(None, 1, 1), E(None, 1, 1), E(None, 1, 1)])
  134. print(sum(resolved))