TwAddresses: clean up bisect func, move to algo mod, add key-getting funcs

This commit is contained in:
The MMGen Project 2026-10-05 14:30:05 +00:00
commit 527fedd7d5
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
3 changed files with 149 additions and 30 deletions

36
mmgen/algo.py Executable file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env python3
#
# MMGen Wallet, a terminal-based cryptocurrency wallet
# Copyright (C)2013-2026 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen-wallet
# https://gitlab.com/mmgen/mmgen-wallet
"""
algo: algorithms for the MMGen Wallet suite
"""
def get_start(*, data, bot, top, target, bisect_key_func, match_key_func):
"""
Bisecting algorithm to find first entry in a sorted list a substring of which, as
returned by match_key_func(), matches the requested target.
Return None if no match was found.
The target substring must sort below the desired start element but above any possible
previous element.
"""
assert bot <= top, f'get_start(): {bot=} > {top=}'
assert top < len(data), f'get_start(): {top=} >= {len(data)=}'
while True:
if bot == top:
return bot if match_key_func(data[bot]) == target else None
n = (top + bot) >> 1
if bisect_key_func(data[n]) < target:
bot = n + 1
else:
top = n

View file

@ -333,9 +333,20 @@ class TwAddresses(TwView):
return e.is_used
return None # addr not in tracking wallet
def get_target(self, key_in):
return key_in
@staticmethod
def get_bisect_key(d):
return d.twmmid
@staticmethod
def get_match_key(d):
return d.al_id
def get_change_address(
self,
key_in,
key,
*,
bot = None,
top = None,
@ -349,43 +360,23 @@ class TwAddresses(TwView):
False: no unused addresses in wallet with requested AddrListID
"""
def get_start(bot, top):
"""
bisecting algorithm to find first entry matching requested match key
Since 'btc' > 'F' and target sorts below the first twmmid of the match key
stringwise, we can just search on raw twmmids.
"""
# bisect target key sorts just below the desired target and never matches the bisect key
pre_target_key = key + ':0'
n = top >> 1
while True:
if bot == top:
return bot if self.data[bot].al_id == key else None
if self.data[n].twmmid < pre_target_key:
bot = n + 1
else:
top = n
n = (top + bot) >> 1
assert self.sort_key == 'twmmid'
assert self.reverse is False
key = key_in
from ..algo import get_start
target = self.get_target(key)
start = get_start(
bot = 0 if bot is None else bot,
top = len(self.data) - 1 if top is None else top)
data = self.data,
bot = 0 if bot is None else bot,
top = len(self.data) - 1 if top is None else top,
target = target,
bisect_key_func = self.get_bisect_key,
match_key_func = self.get_match_key)
if start is None:
return None
for d in self.data[start:]:
if d.al_id == key:
if self.get_match_key(d) == target:
if (
not d.is_used
and not d.twmmid in exclude

92
test/modtest_d/algo.py Executable file
View file

@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""
test.modtest_d.algo: algorithm unit tests for the MMGen suite
"""
from ..include.common import vmsg
class unit_tests:
def get_start(self, name, ut, desc='get_start() bisecting algorithm'):
from mmgen.algo import get_start
def gs(data, bot, top, target, bisect_key_func, match_key_func):
return get_start(
data = data,
bot = bot,
top = top,
target = target,
bisect_key_func = bisect_key_func,
match_key_func = match_key_func)
def bisect_key_func(d):
return d
def substr_3(d):
return d[:3]
fs = '{:<3} {:<3} {:<6} {:<14} {:<5} {}'
data = ('aaaa', 'aaba', 'aabb', 'aaca', 'aada', 'aaea', 'aaeb', 'aaec', 'aafa', 'aaga')
last = len(data) - 1
vmsg('\nDATA:\n {}\n {}\n'.format(
' '.join(str(n) for n in range(len(data))),
' '.join(data)))
vmsg(fs.format('BOT', 'TOP', 'TARGET', 'MATCH_KEY_FUNC', 'START', 'START_VAL'))
for bot, top, target, match_key_func, chk in (
(0, 0, 'aaa', substr_3, 0),
(0, 1, 'aaa', substr_3, 0),
(0, 2, 'aaa', substr_3, 0),
(0, last, 'aaa', substr_3, 0),
(0, last, 'aag', substr_3, 9),
(last, last, 'aag', substr_3, 9),
(7, last, 'aag', substr_3, 9),
(8, last, 'aag', substr_3, 9),
(0, last, 'aac', substr_3, 3),
(2, 3, 'aac', substr_3, 3),
(0, 3, 'aac', substr_3, 3),
(3, 3, 'aac', substr_3, 3),
(3, 4, 'aac', substr_3, 3),
(2, 4, 'aac', substr_3, 3),
(2, 5, 'aac', substr_3, 3),
(1, 3, 'aac', substr_3, 3),
(3, 5, 'aac', substr_3, 3),
(3, last, 'aac', substr_3, 3),
(4, 4, 'aad', substr_3, 4),
(4, 4, 'aae', substr_3, None),
(6, 6, 'aae', substr_3, 6),
(3, last, 'aae', substr_3, 5),
(5, 5, 'aae', substr_3, 5),
(4, 5, 'aae', substr_3, 5),
(3, 5, 'aae', substr_3, 5),
(3, 3, 'aae', substr_3, None),
(7, 7, 'aae', substr_3, 7),
(8, 8, 'aae', substr_3, None),
(8, last, 'aae', substr_3, None)):
start = gs(data, bot, top, target, bisect_key_func, match_key_func)
vmsg(fs.format(
bot, top, target, match_key_func.__name__,
'None' if start is None else start,
'-' if start is None else f'{data[start]}'))
assert start == chk, f'{start=} != {chk=}'
vmsg('\nTesting error handling:')
bad1 = lambda: gs(data, 1, 0, 'aa', bisect_key_func, substr_3)
bad2 = lambda: gs(data, 1, last+1, 'aa', bisect_key_func, substr_3)
bad_data = (
('bot > top', 'AssertionError', '>', bad1),
('top >= len(data)', 'AssertionError', '>=', bad2))
ut.process_bad_data(bad_data, pfx='')
return True