-
Notifications
You must be signed in to change notification settings - Fork 575
/
length.py
97 lines (82 loc) · 3.86 KB
/
length.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# coding=utf-8
#
# This file is part of Hypothesis, which may be found at
# https://github.com/HypothesisWorks/hypothesis-python
#
# Most of this work is copyright (C) 2013-2018 David R. MacIver
# (david@drmaciver.com), but it contains contributions by others. See
# CONTRIBUTING.rst for a full list of people who may hold copyright, and
# consult the git log if you need to determine who owns an individual
# contribution.
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at http://mozilla.org/MPL/2.0/.
#
# END HEADER
from __future__ import division, print_function, absolute_import
from hypothesis.internal.conjecture.shrinking.common import Shrinker, \
find_integer
"""
This module implements a length minimizer for sequences.
That is, given some sequence of elements satisfying some predicate, it tries to
find a strictly shorter one satisfying the same predicate.
Doing so perfectly is provably exponential. This only implements a linear time
worst case algorithm which guarantees certain minimality properties of the
fixed point.
"""
class Length(Shrinker):
"""Attempts to find a smaller sequence satisfying f. Will only perform
linearly many evaluations, and does not loop to a fixed point.
Guarantees made at a fixed point:
1. No individual element may be deleted.
2. No *adjacent* pair of elements may be deleted.
"""
def make_immutable(self, value):
return tuple(value)
def short_circuit(self):
return self.consider(()) or len(self.current) <= 1
def left_is_better(self, left, right):
return len(left) < len(right)
def run_step(self):
# Try to delete as many elements from the sequence as possible, in
# (roughly) one pass, from right to left.
#
# Starting from the end of the sequence, delete as many consecutive
# elements as possible. When we reach an element that we can't delete
# this way, skip over it, and repeat the process for the remaining
# elements to its left.
#
# This diagram shows the layout of the non-deletable elements that
# we've previously skipped, the remaining elements that might be
# deletable, and the (k) elements we are currently trying to delete.
# The two slices show how we create a new sequence that omits those
# (k) elements.
# <=================|
# remaining skipped
# /^^^^^^^^^^^^^^^^^^^^^^^^^\ /^^^^^^^^^^^^^^^^^\
# +---+---+---+---+---+---+---+---+---+---+---+---+
# | | | | | | | | | | | | |
# +---+---+---+---+---+---+---+---+---+---+---+---+
# \_________________/ \#####/ \_________________/
# [:remaining-k] k [remaining:]
# <=====|
# Number of elements at the end of the sequence that we were
# unable to delete.
skipped = 0
# When all elements have been deleted or skipped, the pass is complete.
while skipped < len(self.current):
# Number of elements that might still be deletable.
remaining = len(self.current) - skipped
start = self.current
# Delete as many of the remaining elements as possible, from right
# to left. Always retain the skipped elements at the end.
find_integer(
lambda k: k <= remaining and self.consider(
start[:remaining - k] + start[remaining:]
)
)
# Skip over the element we couldn't delete, and continue.
# (If we deleted everything, this will put j > len(self.current),
# which is harmless because the loop is about to end anyway.)
skipped += 1