Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/rich/_ratio.py: 19%

73 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:35 +0000

1import sys 

2from fractions import Fraction 

3from math import ceil 

4from typing import cast, List, Optional, Sequence 

5 

6if sys.version_info >= (3, 8): 

7 from typing import Protocol 

8else: 

9 from typing_extensions import Protocol # pragma: no cover 

10 

11 

12class Edge(Protocol): 

13 """Any object that defines an edge (such as Layout).""" 

14 

15 size: Optional[int] = None 

16 ratio: int = 1 

17 minimum_size: int = 1 

18 

19 

20def ratio_resolve(total: int, edges: Sequence[Edge]) -> List[int]: 

21 """Divide total space to satisfy size, ratio, and minimum_size, constraints. 

22 

23 The returned list of integers should add up to total in most cases, unless it is 

24 impossible to satisfy all the constraints. For instance, if there are two edges 

25 with a minimum size of 20 each and `total` is 30 then the returned list will be 

26 greater than total. In practice, this would mean that a Layout object would 

27 clip the rows that would overflow the screen height. 

28 

29 Args: 

30 total (int): Total number of characters. 

31 edges (List[Edge]): Edges within total space. 

32 

33 Returns: 

34 List[int]: Number of characters for each edge. 

35 """ 

36 # Size of edge or None for yet to be determined 

37 sizes = [(edge.size or None) for edge in edges] 

38 

39 _Fraction = Fraction 

40 

41 # While any edges haven't been calculated 

42 while None in sizes: 

43 # Get flexible edges and index to map these back on to sizes list 

44 flexible_edges = [ 

45 (index, edge) 

46 for index, (size, edge) in enumerate(zip(sizes, edges)) 

47 if size is None 

48 ] 

49 # Remaining space in total 

50 remaining = total - sum(size or 0 for size in sizes) 

51 if remaining <= 0: 

52 # No room for flexible edges 

53 return [ 

54 ((edge.minimum_size or 1) if size is None else size) 

55 for size, edge in zip(sizes, edges) 

56 ] 

57 # Calculate number of characters in a ratio portion 

58 portion = _Fraction( 

59 remaining, sum((edge.ratio or 1) for _, edge in flexible_edges) 

60 ) 

61 

62 # If any edges will be less than their minimum, replace size with the minimum 

63 for index, edge in flexible_edges: 

64 if portion * edge.ratio <= edge.minimum_size: 

65 sizes[index] = edge.minimum_size 

66 # New fixed size will invalidate calculations, so we need to repeat the process 

67 break 

68 else: 

69 # Distribute flexible space and compensate for rounding error 

70 # Since edge sizes can only be integers we need to add the remainder 

71 # to the following line 

72 remainder = _Fraction(0) 

73 for index, edge in flexible_edges: 

74 size, remainder = divmod(portion * edge.ratio + remainder, 1) 

75 sizes[index] = size 

76 break 

77 # Sizes now contains integers only 

78 return cast(List[int], sizes) 

79 

80 

81def ratio_reduce( 

82 total: int, ratios: List[int], maximums: List[int], values: List[int] 

83) -> List[int]: 

84 """Divide an integer total in to parts based on ratios. 

85 

86 Args: 

87 total (int): The total to divide. 

88 ratios (List[int]): A list of integer ratios. 

89 maximums (List[int]): List of maximums values for each slot. 

90 values (List[int]): List of values 

91 

92 Returns: 

93 List[int]: A list of integers guaranteed to sum to total. 

94 """ 

95 ratios = [ratio if _max else 0 for ratio, _max in zip(ratios, maximums)] 

96 total_ratio = sum(ratios) 

97 if not total_ratio: 

98 return values[:] 

99 total_remaining = total 

100 result: List[int] = [] 

101 append = result.append 

102 for ratio, maximum, value in zip(ratios, maximums, values): 

103 if ratio and total_ratio > 0: 

104 distributed = min(maximum, round(ratio * total_remaining / total_ratio)) 

105 append(value - distributed) 

106 total_remaining -= distributed 

107 total_ratio -= ratio 

108 else: 

109 append(value) 

110 return result 

111 

112 

113def ratio_distribute( 

114 total: int, ratios: List[int], minimums: Optional[List[int]] = None 

115) -> List[int]: 

116 """Distribute an integer total in to parts based on ratios. 

117 

118 Args: 

119 total (int): The total to divide. 

120 ratios (List[int]): A list of integer ratios. 

121 minimums (List[int]): List of minimum values for each slot. 

122 

123 Returns: 

124 List[int]: A list of integers guaranteed to sum to total. 

125 """ 

126 if minimums: 

127 ratios = [ratio if _min else 0 for ratio, _min in zip(ratios, minimums)] 

128 total_ratio = sum(ratios) 

129 assert total_ratio > 0, "Sum of ratios must be > 0" 

130 

131 total_remaining = total 

132 distributed_total: List[int] = [] 

133 append = distributed_total.append 

134 if minimums is None: 

135 _minimums = [0] * len(ratios) 

136 else: 

137 _minimums = minimums 

138 for ratio, minimum in zip(ratios, _minimums): 

139 if total_ratio > 0: 

140 distributed = max(minimum, ceil(ratio * total_remaining / total_ratio)) 

141 else: 

142 distributed = total_remaining 

143 append(distributed) 

144 total_ratio -= ratio 

145 total_remaining -= distributed 

146 return distributed_total 

147 

148 

149if __name__ == "__main__": 

150 from dataclasses import dataclass 

151 

152 @dataclass 

153 class E: 

154 

155 size: Optional[int] = None 

156 ratio: int = 1 

157 minimum_size: int = 1 

158 

159 resolved = ratio_resolve(110, [E(None, 1, 1), E(None, 1, 1), E(None, 1, 1)]) 

160 print(sum(resolved))