From 527fedd7d592c30eb2e45e1cfffe61b88c1bf1f8 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Mon, 5 Oct 2026 14:30:05 +0000 Subject: [PATCH] TwAddresses: clean up bisect func, move to `algo` mod, add key-getting funcs --- mmgen/algo.py | 36 +++++++++++++++++ mmgen/tw/addresses.py | 51 ++++++++++------------- test/modtest_d/algo.py | 92 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 30 deletions(-) create mode 100755 mmgen/algo.py create mode 100755 test/modtest_d/algo.py diff --git a/mmgen/algo.py b/mmgen/algo.py new file mode 100755 index 00000000..80b183c8 --- /dev/null +++ b/mmgen/algo.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2026 The MMGen Project +# 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 diff --git a/mmgen/tw/addresses.py b/mmgen/tw/addresses.py index 2650c367..8404f461 100755 --- a/mmgen/tw/addresses.py +++ b/mmgen/tw/addresses.py @@ -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 diff --git a/test/modtest_d/algo.py b/test/modtest_d/algo.py new file mode 100755 index 00000000..1aaac0f4 --- /dev/null +++ b/test/modtest_d/algo.py @@ -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