|
1 | 1 | import math |
2 | | -from snowfakery.plugins import SnowfakeryPlugin |
| 2 | +from random import randint, shuffle |
| 3 | +from types import SimpleNamespace |
| 4 | +from typing import List, Optional, Union |
| 5 | +from snowfakery.plugins import SnowfakeryPlugin, memorable, PluginResultIterator |
3 | 6 |
|
4 | 7 |
|
5 | 8 | class Math(SnowfakeryPlugin): |
6 | 9 | def custom_functions(self, *args, **kwargs): |
7 | 10 | "Expose math functions to Snowfakery" |
8 | 11 |
|
9 | | - class MathNamespace: |
10 | | - pass |
| 12 | + class MathNamespace(SimpleNamespace): |
| 13 | + @memorable |
| 14 | + def random_partition( |
| 15 | + self, |
| 16 | + total: int, |
| 17 | + *, |
| 18 | + min: int = 1, |
| 19 | + max: Optional[int] = None, |
| 20 | + step: int = 1, |
| 21 | + ): |
| 22 | + return GenericPluginResultIterator(False, parts(total, min, max, step)) |
11 | 23 |
|
12 | 24 | mathns = MathNamespace() |
13 | | - mathns.__dict__ = math.__dict__.copy() |
| 25 | + mathns.__dict__.update(math.__dict__.copy()) |
14 | 26 |
|
15 | 27 | mathns.pi = math.pi |
16 | 28 | mathns.round = round |
17 | 29 | mathns.min = min |
18 | 30 | mathns.max = max |
19 | | - |
| 31 | + mathns.context = self.context |
20 | 32 | return mathns |
| 33 | + |
| 34 | + |
| 35 | +class GenericPluginResultIterator(PluginResultIterator): |
| 36 | + def __init__(self, repeat, iterable): |
| 37 | + super().__init__(repeat) |
| 38 | + self.next = iter(iterable).__next__ |
| 39 | + |
| 40 | + |
| 41 | +def parts(total: int, min_: int = 1, max_=None, step=1) -> List[Union[int, float]]: |
| 42 | + """Split a number into a randomized set of 'pieces'. |
| 43 | + The pieces add up to the `total`. E.g. |
| 44 | +
|
| 45 | + parts(12) -> [3, 6, 3] |
| 46 | + parts(16) -> [8, 4, 2, 2] |
| 47 | +
|
| 48 | + The numbers generated will never be less than `min_`, if provided. |
| 49 | +
|
| 50 | + The numbers generated will never be less than `max_`, if provided. |
| 51 | +
|
| 52 | + The numbers generated will always be a multiple of `step`, if provided. |
| 53 | +
|
| 54 | + But...if you provide inconsistent constraints then your values |
| 55 | + will be inconsistent with them. e.g. if `total` is not a multiple |
| 56 | + of `step`. |
| 57 | + """ |
| 58 | + max_ = max_ or total |
| 59 | + factor = 0 |
| 60 | + |
| 61 | + if step < 1: |
| 62 | + assert step in [0.01, 0.5, 0.1, 0.20, 0.25, 0.50], step |
| 63 | + factor = step |
| 64 | + total = int(total / factor) |
| 65 | + step = int(total / factor) |
| 66 | + min_ = int(total / factor) |
| 67 | + max_ = int(total / factor) |
| 68 | + |
| 69 | + pieces = [] |
| 70 | + |
| 71 | + while sum(pieces) < total: |
| 72 | + remaining = total - sum(pieces) |
| 73 | + smallest = max(min_, step) |
| 74 | + if remaining < smallest: |
| 75 | + # try to add it to a random other piece |
| 76 | + for i, val in enumerate(pieces): |
| 77 | + if val + remaining <= max_: |
| 78 | + pieces[i] += remaining |
| 79 | + remaining = 0 |
| 80 | + break |
| 81 | + |
| 82 | + # just tack it on the end despite |
| 83 | + # it being too small...our |
| 84 | + # constraints must have been impossible |
| 85 | + # to fulfil |
| 86 | + if remaining: |
| 87 | + pieces.append(remaining) |
| 88 | + |
| 89 | + else: |
| 90 | + part = randint(smallest, min(remaining, max_)) |
| 91 | + round_up = part + step - (part % step) |
| 92 | + if round_up <= min(remaining, max_) and randint(0, 1): |
| 93 | + part = round_up |
| 94 | + else: |
| 95 | + part -= part % step |
| 96 | + |
| 97 | + pieces.append(part) |
| 98 | + |
| 99 | + assert sum(pieces) == total, pieces |
| 100 | + assert 0 not in pieces, pieces |
| 101 | + |
| 102 | + shuffle(pieces) |
| 103 | + if factor: |
| 104 | + pieces = [round(p * factor, 2) for p in pieces] |
| 105 | + return pieces |
0 commit comments