You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

154 lines
5.2 KiB

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