|
import random |
|
|
|
class RandomCycler: |
|
""" |
|
Creates an internal copy of a sequence and allows access to its items in a constrained random |
|
order. For a source sequence of n items and one or several consecutive queries of a total |
|
of m items, the following guarantees hold (one implies the other): |
|
- Each item will be returned between m // n and ((m - 1) // n) + 1 times. |
|
- Between two appearances of the same item, there may be at most 2 * (n - 1) other items. |
|
""" |
|
|
|
def __init__(self, source): |
|
if len(source) == 0: |
|
raise Exception("Can't create RandomCycler from an empty collection") |
|
self.all_items = list(source) |
|
self.next_items = [] |
|
|
|
def sample(self, count: int): |
|
shuffle = lambda l: random.sample(l, len(l)) |
|
|
|
out = [] |
|
while count > 0: |
|
if count >= len(self.all_items): |
|
out.extend(shuffle(list(self.all_items))) |
|
count -= len(self.all_items) |
|
continue |
|
n = min(count, len(self.next_items)) |
|
out.extend(self.next_items[:n]) |
|
count -= n |
|
self.next_items = self.next_items[n:] |
|
if len(self.next_items) == 0: |
|
self.next_items = shuffle(list(self.all_items)) |
|
return out |
|
|
|
def __next__(self): |
|
return self.sample(1)[0] |
|
|
|
|