utils.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import math
  2. from functools import reduce, update_wrapper, wraps
  3. from inspect import signature
  4. from itertools import accumulate, chain, repeat
  5. from typing import Callable
  6. from ..utils.cells import combine_cells, fix_cells, mark_graphemes, split_graphemes
  7. def spinner_player(spinner):
  8. """Create an infinite generator that plays all cycles of a spinner indefinitely."""
  9. def inner_play():
  10. while True:
  11. yield from spinner() # instantiates a new cycle in each iteration.
  12. return inner_play() # returns an already initiated generator.
  13. def bordered(borders, default):
  14. """Decorator to include controllable borders in the outputs of a function."""
  15. def wrapper(fn):
  16. @wraps(fn)
  17. def inner_bordered(*args, **kwargs):
  18. content, right = fn(*args, **kwargs)
  19. return combine_cells(left_border, content, right or right_border)
  20. return inner_bordered
  21. left_border, right_border = extract_fill_graphemes(borders, default)
  22. return wrapper
  23. def extract_fill_graphemes(text, default):
  24. """Extract the exact same number of graphemes as default, filling missing ones."""
  25. text, default = (tuple(split_graphemes(c or '') for c in p) for p in (text or default, default))
  26. return (mark_graphemes(t or d) for t, d in zip(chain(text, repeat('')), default))
  27. def static_sliding_window(sep, gap, contents, length, right, initial):
  28. """Implement a sliding window over some content interspersed with a separator.
  29. It is very efficient, storing data in only one string.
  30. Note that the implementation is "static" in the sense that the content is pre-
  31. calculated and maintained static, but actually when the window slides both the
  32. separator and content seem to be moved.
  33. Also keep in mind that `right` is for the content, not the window.
  34. """
  35. def sliding_window():
  36. pos = initial
  37. while True:
  38. if pos < 0:
  39. pos += original
  40. elif pos >= original:
  41. pos -= original
  42. yield content[pos:pos + length]
  43. pos += step
  44. adjusted_sep = fix_cells((sep * math.ceil(gap / len(sep)))[:gap]) if gap else ''
  45. content = tuple(chain.from_iterable(chain.from_iterable(zip(repeat(adjusted_sep), contents))))
  46. original, step = len(content), -1 if right else 1
  47. assert length <= original, f'window slides inside content, {length} must be <= {original}'
  48. content += content[:length]
  49. return sliding_window()
  50. def overlay_sliding_window(background, gap, contents, length, right, initial):
  51. """Implement a sliding window over some content on top of a background.
  52. It uses internally a static sliding window, but dynamically swaps the separator
  53. characters for the background ones, thus making it appear immobile, with the
  54. contents sliding over it.
  55. """
  56. def overlay_window():
  57. for cells in window: # pragma: no cover
  58. yield tuple(b if c == '\0' else c for c, b in zip(cells, background))
  59. background = (background * math.ceil(length / len(background)))[:length]
  60. window = static_sliding_window('\0', gap, contents, length, right, initial)
  61. return overlay_window()
  62. def combinations(nums):
  63. """Calculate the number of total combinations a few spinners should have together,
  64. can be used for example with cycles or with frames played at the same time."""
  65. def lcm(a, b):
  66. """Calculate the lowest common multiple of two numbers."""
  67. return a * b // math.gcd(a, b)
  68. return reduce(lcm, nums)
  69. def split_options(options):
  70. """Split options that apply to dual elements, either duplicating or splitting."""
  71. return options if isinstance(options, tuple) else (options, options)
  72. def spread_weighted(actual_length, naturals):
  73. """Calculate the weighted spreading of the available space for all natural lengths."""
  74. total = sum(naturals)
  75. lengths = (actual_length / total * n for n in naturals)
  76. lengths = [round(x) for x in accumulate(lengths)] # needs to be resolved.
  77. lengths = tuple(map(lambda a, b: a - b, lengths, [0] + lengths))
  78. assert sum(lengths) == actual_length
  79. return lengths
  80. def fix_signature(func: Callable, source: Callable, skip_n_params: int):
  81. """Override signature to hide first n parameters."""
  82. original_doc = func.__doc__
  83. update_wrapper(func, source)
  84. if original_doc:
  85. func.__doc__ = f'{original_doc}\n{func.__doc__}'
  86. sig = signature(func)
  87. sig = sig.replace(parameters=tuple(sig.parameters.values())[skip_n_params:])
  88. func.__signature__ = sig
  89. return func
  90. def round_even(n):
  91. """Round a number to the nearest even integer."""
  92. r = int(n)
  93. return r + 1 if r & 1 else r