Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
150 lines (101 sloc) 3.82 KB
from collections import Mapping, Iterable
from . import alphabets
__all__ = ['bx_encode', 'bx_decode', 'BaseKeyGenerator']
def bx_encode(n, alphabet):
"""\
Encodes an integer :attr:`n` in base ``len(alphabet)`` with
digits in :attr:`alphabet`.
::
# 'ba'
bx_encode(3, 'abc')
:param n: a positive integer.
:param alphabet: a 0-based iterable.
"""
if not isinstance(n, int):
raise TypeError('an integer is required')
base = len(alphabet)
if n == 0:
return alphabet[0]
digits = []
while n > 0:
digits.append(alphabet[n % base])
n = n // base
digits.reverse()
return ''.join(digits)
def bx_decode(string, alphabet, mapping=None):
"""\
Transforms a string in :attr:`alphabet` to an integer.
If :attr:`mapping` is provided, each key must map to its
positional value without duplicates.
::
mapping = {'a': 0, 'b': 1, 'c': 2}
# 3
bx_decode('ba', 'abc', mapping)
:param string: a string consisting of key from `alphabet`.
:param alphabet: a 0-based iterable.
:param mapping: a :class:`Mapping <collection.Mapping>`. If `None`,
the inverse of `alphabet` is used, with values mapped
to indices.
"""
mapping = mapping or dict([(d, i) for (i, d) in enumerate(alphabet)])
base = len(alphabet)
if not string:
raise ValueError('string cannot be empty')
if not isinstance(mapping, Mapping):
raise TypeError('a Mapping is required')
sum = 0
for digit in string:
try:
sum = base*sum + mapping[digit]
except KeyError:
raise ValueError(
"invalid literal for bx_decode with base %i: '%s'" % (base, digit))
return sum
class BaseKeyGenerator(Iterable):
"""\
A class which yields a unique string on every iteration (a `key`).
If the key generator is deterministic, then two generators should
yield the same keys given the same parameters.
`alphabet` can be any iterable, as long as each item is not
contained within any other item. For instance, ``('00', '0', '1')``
would be an ambiguous alphabet, since ``00`` could be interpreted as
two numbers.
Each yielded key is expected to be encoded with :meth:`encode`
::
class KeyGenerator(BaseKeyGenerator):
def __iter__(self):
...
yield self.encode(num)
Keys of a minimum length or starting at a certain *unencoded* value can be
generated by specifying :attr:`min_length` or :attr:`start`.
::
hexabet = '0123456789abcef'
keygen = KeyGenerator(start=255, alphabet=hexabet)
# ['ff', '100', '101']
[key for key in islice(k, 0, 3)]
.. admonition:: Subclassing
Subclasses should implement :meth:`__iter__ <BaseKeyGenerator.__iter__>`.
:param alphabet: an iterable.
:param start: the number to start iteration at.
:param min_length: the length of the string to start iteration at. Leading
zero-characters are not counted in the length.
Only one of `start` or `min_length` should be given.
"""
def __init__(self, alphabet=None, min_length=None, start=None):
min_length = max(1, min_length)
alphabet = alphabet or alphabets.DEFAULT
if start is None:
start = len(alphabet) ** (min_length-1)
# Raise an error if there are duplicates
seen = set()
for sym in alphabet:
if sym in seen:
msg = "alphabet contains duplicate symbol '{0}'".format(sym)
raise ValueError(msg)
seen.add(sym)
self.alphabet = alphabet
self.start = start
def encode(self, n):
return bx_encode(n, self.alphabet)
def __iter__(self):
raise NotImplementedError