forked from cool-RR/python_toolbox
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfunctions.py
More file actions
195 lines (149 loc) · 7.03 KB
/
functions.py
File metadata and controls
195 lines (149 loc) · 7.03 KB
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# Copyright 2009-2017 Ram Rachum.
# This program is distributed under the MIT license.
'''Module for doing a binary search in a sequence.'''
# Todo: wrap all things in tuples?
#
# todo: add option to specify `cmp`.
#
# todo: i think `binary_search_by_index` should have the core logic, and the
# other one will use it. I think this will save many sequence accesses, and
# some sequences can be expensive.
#
# todo: ensure there are no `if variable` checks where we're thinking of None
# but the variable might be False
from python_toolbox import misc_tools
from .roundings import (Rounding, roundings, LOW, LOW_IF_BOTH,
LOW_OTHERWISE_HIGH, HIGH, HIGH_IF_BOTH,
HIGH_OTHERWISE_LOW, EXACT, CLOSEST, CLOSEST_IF_BOTH,
BOTH)
def binary_search_by_index(sequence, value,
function=misc_tools.identity_function,
rounding=CLOSEST):
'''
Do a binary search, returning answer as index number.
For all rounding options, a return value of None is returned if no matching
item is found. (In the case of `rounding=BOTH`, either of the items in the
tuple may be `None`)
You may optionally pass a key function as `function`, so instead of the
objects in `sequence` being compared, their outputs from `function` will be
compared. If you do pass in a function, it's assumed that it's strictly
rising.
Note: This function uses `None` to express its inability to find any
matches; therefore, you better not use it on sequences in which None is a
possible item.
Similiar to `binary_search` (refer to its documentation for more info). The
difference is that instead of returning a result in terms of sequence
items, it returns the indexes of these items in the sequence.
For documentation of rounding options, check `binary_search.roundings`.
'''
my_range = range(len(sequence))
fixed_function = lambda index: function(sequence[index])
result = binary_search(my_range, value, function=fixed_function,
rounding=rounding)
return result
def _binary_search_both(sequence, value,
function=misc_tools.identity_function):
'''
Do a binary search through a sequence with the `BOTH` rounding.
You may optionally pass a key function as `function`, so instead of the
objects in `sequence` being compared, their outputs from `function` will be
compared. If you do pass in a function, it's assumed that it's strictly
rising.
Note: This function uses `None` to express its inability to find any
matches; therefore, you better not use it on sequences in which `None` is a
possible item.
'''
# todo: i think this should be changed to return tuples
### Preparing: ############################################################
# #
get = lambda number: function(sequence[number])
low = 0
high = len(sequence) - 1
# #
### Finished preparing. ###################################################
### Handling edge cases: ##################################################
# #
if not sequence:
return (None, None)
low_value, high_value = get(low), get(high)
if value in (low_value, high_value):
return tuple((value, value))
elif low_value > value:
return tuple((None, sequence[low]))
elif high_value < value:
return (sequence[high], None)
# #
### Finished handling edge cases. #########################################
# Now we know the value is somewhere inside the sequence.
assert low_value < value < high_value
while high - low > 1:
medium = (low + high) // 2
medium_value = get(medium)
if medium_value > value:
high, high_value = medium, medium_value
continue
if medium_value < value:
low, low_value = medium, medium_value
continue
if medium_value == value:
return (sequence[medium], sequence[medium])
return (sequence[low], sequence[high])
def binary_search(sequence, value, function=misc_tools.identity_function,
rounding=CLOSEST):
'''
Do a binary search through a sequence.
For all rounding options, a return value of None is returned if no matching
item is found. (In the case of `rounding=BOTH`, either of the items in the
tuple may be `None`)
You may optionally pass a key function as `function`, so instead of the
objects in `sequence` being compared, their outputs from `function` will be
compared. If you do pass in a function, it's assumed that it's strictly
rising.
Note: This function uses `None` to express its inability to find any
matches; therefore, you better not use it on sequences in which None is a
possible item.
For documentation of rounding options, check `binary_search.roundings`.
'''
from .binary_search_profile import BinarySearchProfile
binary_search_profile = BinarySearchProfile(sequence, value,
function=function)
return binary_search_profile.results[rounding]
def make_both_data_into_preferred_rounding(
both, value, function=misc_tools.identity_function, rounding=BOTH):
'''
Convert results gotten using `BOTH` to a different rounding option.
This function takes the return value from `binary_search` (or other such
functions) with `rounding=BOTH` as the parameter `both`. It then gives the
data with a different rounding, specified with the parameter `rounding`.
'''
# todo optimize and organize: break to individual functions, put in
# `BinarySearchProfile`
if rounding is BOTH:
return both
elif rounding is LOW:
return both[0]
elif rounding is LOW_IF_BOTH:
return both[0] if both[1] is not None else None
elif rounding is LOW_OTHERWISE_HIGH:
return both[0] if both[0] is not None else both[1]
elif rounding is HIGH:
return both[1]
elif rounding is HIGH_IF_BOTH:
return both[1] if both[0] is not None else None
elif rounding is HIGH_OTHERWISE_LOW:
return both[1] if both[1] is not None else both[0]
elif rounding is EXACT:
results = [item for item in both if
(item is not None and function(item) == value)]
return results[0] if results else None
elif rounding in (CLOSEST, CLOSEST_IF_BOTH):
if rounding is CLOSEST_IF_BOTH:
if None in both:
return None
if both[0] is None: return both[1]
if both[1] is None: return both[0]
distances = [abs(function(item)-value) for item in both]
if distances[0] <= distances[1]:
return both[0]
else:
return both[1]