Re:Pythonic monotonic
by pierrebai on Friday August 13, 2021 @10:05AM
(#28168975)
FWIW, here is my solution. I think it is grokable and does not rely on one knowing complex python, I think even someone without Python knowledge would understand what is going on:
[ Parent
| on HN ]
My updated version is more like a state machine. Your function and mine give the same answers both with the manual test cases and with a cross-comparison using random inputs:
def monotonic_direct(seq):
prev_value = None
prev_dir = 0 # 0 for increasing or decreasing, 1 for increasing, -1 for decreasing
group = []
#print("Run", seq)
for value in seq:
#print(prev_dir, prev_value, value)
if not group:
# Can only get here with the first element
prev_value = value
group.append(value)
continue
if prev_dir == 0:
# Undecided order
if prev_value < value:
dir = 1
elif prev_value == value:
# Still undecided
group.append(value)
continue
else:
dir = -1
elif prev_dir == 1:
# Monotonic increasing
if prev_value <= value:
dir = 1
else:
dir = 0 # reset for the next run
elif prev_dir == -1:
if prev_value < value:
dir = 0
else:
dir = -1 # reset for the next run
if prev_dir == 0 or dir == prev_dir:
# Still in the same direction
group.append(value)
else:
# New direction
if prev_dir == -1:
group.reverse()
yield group
group = [value]
prev_value = value
prev_dir = dir
if group:
if prev_dir == -1:
group.reverse()
yield group
def run_test_cases():
for dir_case, expected in (
([1], [[1]]),
([1, 2], [[1, 2]]),
([2, 1], [[1, 2]]), # because of the reverse
([1, 2, 3], [[1, 2, 3]]),
([1, 3, 2], [[1, 3], [2]]),
([1, 1, 2], [[1, 1, 2]]),
([1, 1, 0], [[0, 1, 1]]),
([1, 1, 2, 0], [[1, 1, 2], [0]]),
([1, 1, 0, 0], [[0, 0, 1, 1]]),
([3, 3, 2, 2, 1, 1], [[1, 1, 2, 2, 3, 3]]),
#([3, 3, 2, 3, 1, 1], [[2, 3, 3], [3], [1, 1]]),
([3, 3, 2, 3, 1, 1], [[2, 3, 3], [1, 1, 3]]),
([1, 2, 3, 2, 1, 4, 5, 6, 7], [[1, 2, 3], [1, 2], [4, 5, 6, 7]]),
("AABCBADCABC", [["A", "A", "B", "C"], ["A", "B"], ["A", "C", "D"], ["B", "C"]]),
):
got = list(monotonic_direct(dir_case))
assert got == expected, (dir_case, got, expected)
print("All test cases passed.")
def run_cross_test():
import random
for i in range(10000):
n = random.randrange(2, 10)
values = [random.randrange(10) for _ in range(n)]
result1 = list(monotonic_direct(values))
result2 = list(monotonic_increasing(values))
assert result1 == result2, (values, result1, result2)
print("Random cross-comparison tests passed.")
if __name__ == "__main__":
run_test_cases()
run_cross_test()I came up with a similar solution; using True/None/False instead of 1/0/-1 allows for more self-documenting code
def monotonics(seq):
run = []
last_v = None
increasing = None
for v in seq:
if last_v is None:
pass
elif increasing is None:
if v != last_v:
increasing = v > last_v
elif increasing:
if v < last_v:
increasing = None
yield run
run = []
else:
if v > last_v:
increasing = None
yield run[::-1]
run = []
last_v = v
run.append(v)
if run:
yield run
and, I'd add a two more test cases: [], and [1, 2, 3, 1, 2, 3]