zvoase (owner)

Revisions

gist: 21166 Download_button fork
public
Public Clone URL: git://gist.github.com/21166.git
Embed All Files: show embed
baseconv.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
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2008 Zachary Voase
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
 
 
import copy
 
 
class Base(object):
    
    """
Generic class for representing different bases.
The ``Base`` class holds the information on a particular numerical base,
such as the *words* and the *name*. The name exists solely for convenience
when using the library in the interactive interpreter.
The words are what make up a base. Decimal, for example, uses 10 words;
these are (in order) `0`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, and `9`.
Binary has only `0` and `1`. Any other numbers, strings or even arbitrary
objects can be used as words, but order is important; if, for example, you
use [`1`, `0`] as the word list for binary, all of the numbers you receive
will be printed the wrong way; if, for example, you try to convert
1010011010 into decimal, you will get 357 instead of the actual value 666.
You may also optionally attach a ``format`` attribute to an instance of
``Base``. This should be a function which accepts one argument, a
``Number`` instance, and returns a string representation of that number.
For example, the ``HEXADECIMAL`` base in the top-level of this module
has a formatter which will prefix the hex representation of a number with
``'0x'``.
Here is an example of how to create a base, taken from the actual
``HEXADECIMAL`` variable found in the top-level of this module::
>>> from baseconv import *
>>> HEXADECIMAL = Base('0123456789ABCDEF', name='HEXADECIMAL')
>>> HEXADECIMAL.format = (
... lambda n: '0x' + ''.join(map(str, n.values)))
"""
    
    def __init__(self, words, name=''):
        self.__words = words
        if not name:
            # Make a sensible default. Because it's nice to have a name.
            name = 'base%d' % (self.length,)
        self.name = name
    
    def __call__(self, values):
        # Remember, ``self`` is the base, not the number.
        return Number(self, values=values, indices=False)
    
    def __repr__(self):
        return 'Base(%r, %r)' % (self.words, self.name)
    
    def __get_words(self):
        return self.__words
    
    def __get_length(self):
        return len(self.words)
    
    def is_valid(self, values):
        return set(values).issubset(set(self.words))
    
    words = property(__get_words)
    length = property(__get_length)
 
 
class Number(object):
    
    """
Represent a specific number in a base, and provide methods for conversion.
The ``Number`` class holds a representation of a number in a particular
base. This base is an instance of the ``Base`` class, and the number is
accessible through several descriptors:
``decimal``
The ``decimal`` attribute holds a Python ``int`` (or ``long``)
with the value of the number in base-10, which is the typical
counting system. ``decimal`` may be set to a value also, which
will update the number to represent the new value.
``indices``
Indices represent a number in a particular base. For example, if
a base's words are ``a``,``b``, ``c`` and ``d``, the number
``dbbca`` would be represented by the list ``[3, 1, 1, 2, 0]``.
The integers in this list all point to positions within the base's
word list. Internally, all numbers are stored like this.
``values``
This is similar to the index representation, only it uses the
actual values from the base's word list; following on from the
previous example, ``[3, 1, 1, 2, 0]`` would be replaced by
``['d', 'b', 'b', 'c', 'a']``.
The base of a number can be accessed via the ``base`` descriptor.
Accessing this will return the instance of the base. You can also convert
a number's base by changing it's ``base`` attribute::
>>> from baseconv import *
>>> num = Number(BINARY, '1010011010')
>>> print num
0b1010011010
>>> num.base = DECIMAL
>>> print num
666
>>> num.base = HEXADECIMAL
>>> print num
0x29A
Hint: as a shortcut to instantiating a ``Number`` instance with a base and
list of values, you can call the ``Base`` instance. For example::
>>> from baseconv import *
>>> num1 = Number(BINARY, '1010011010')
>>> num2 = BINARY('1010011010')
>>> num1.decimal == num2.decimal
True
"""
    
    def __init__(self, base, values=[], indices=False):
        """
Initialize a ``Number`` instance.
This function takes a ``Base`` instance, an optional list of values,
and a flag specifying whether those values are actually words from the
base or indices of words in the base's wordlist.
The first positional argument should be a ``Base`` instance. Several
common bases are provided in the top-level of this module; these are
``BINARY``, ``DECIMAL``, ``HEXADECIMAL`` and ``OCTAL``. Note that
``Base`` instances are callable; ``base.__call__(values)`` is the same
as ``Number(base, values=values, indices=False)``.
The ``values`` keyword is the list of words which represent the
number. They will be checked for validity, and invalid numbers will
raise an ``AssertionError``. With the ``indices`` keyword argument set
to ``True``, the value list should be a list of integers, each of
which will represent a position within the given base's wordlist.
"""
        self.__base = base
        if indices:
            self.indices = values
        else:
            self.values = values
    
    def __repr__(self):
        return 'Number(%s, %r)' % (self.base.name, self.values)
    
    def __str__(self):
        # This allows bases to define their own ways of printing numbers. For
        # example, hexadecimal is shown as '0xf0ff0f', etc.
        if hasattr(self.base, 'format'):
            return self.base.format(self)
        return ''.join(map(str, self.values))
    
    def zfill(self, length):
        """
Show a number with at least a certain number of zeros before.
This is pretty much the same as
"""
        s = str(self)
        while len(s) < length:
            if len(self.base.words[0]) > (length - len(s)):
                return s
            s = self.base.words[0] + s
        return s
    
    @classmethod
    def from_decimal(cls, base, decimal):
        """Convert a Python ``int`` into a ``Number`` of a given base."""
        # Get a ``DECIMAL`` instance, and just change its base.
        new = DECIMAL(str(decimal))
        new.base = base
        return new
    
    def __get_base(self):
        return self.__base
    
    def __set_base(self, base):
        # Pretty simple; the number's decimal value won't change, and then the
        # other parts will fall into place when re-setting this via the
        # descriptor.
        old_decimal = copy.copy(self.decimal)
        self.__base = base
        self.decimal = old_decimal
    
    def __get_decimal(self):
        decimal = 0
        for index, value in enumerate(reversed(self.__indices)):
            decimal += value * (self.base.length ** index)
        return decimal
 
    def __set_decimal(self, decimal):
        indices = []
        # Copy it to stop this method from mutating another variable.
        number = copy.copy(decimal)
        while number:
            indices.insert(0, number % self.base.length)
            number = number // self.base.length
        self.indices = indices
    
    def __get_indices(self):
        return self.__indices
    
    def __set_indices(self, indices):
        new_indices = []
        for index in indices:
            # Check that the index is valid within the base's wordlist.
            if index >= self.base.length:
                raise KeyError('Index')
            new_indices.append(index)
        self.__indices = new_indices
    
    def __get_values(self):
        values = map(self.base.words.__getitem__, self.indices)
        # Make sure all words in the base are single characters. Otherwise,
        # joining the list could give an erroneous representation.
        if all(isinstance(x, basestring) for x in self.base.words) and set(
            map(len, self.base.words)) == set([1]):
            # If all is OK, we return a string.
            return ''.join(values)
        # If this is going to be a problem, act safely and return a list.
        return values
            
    
    def __set_values(self, values):
        indices = []
        for value in values:
            # Check the value for validity.
            if value not in self.base.words:
                raise ValueError('%r not in wordlist for %r' %
                    (value, self.base))
            # We're assuming that the base's wordlist has no duplicates.
            indices.append(self.base.words.index(value))
        self.__indices = indices
    
    base = property(__get_base, __set_base)
    decimal = property(__get_decimal, __set_decimal)
    indices = property(__get_indices, __set_indices)
    values = property(__get_values, __set_values)
 
 
DECIMAL = Base('0123456789', 'DECIMAL')
 
BINARY = Base('01', 'BINARY')
BINARY.format = (
    lambda n: '0b' + ''.join(map(str, n.values)))
 
HEXADECIMAL = Base('0123456789ABCDEF', 'HEXADECIMAL')
HEXADECIMAL.format = (
    lambda n: '0x' + ''.join(map(str, n.values)))
 
OCTAL = Base('01234567', 'OCTAL')
OCTAL.format = (
    lambda n: '0o' + ''.join(map(str, n.values)))
 
ALPHA_LOWER = Base('abcdefghijklmnopqrstuvwxyz', 'ALPHA_LOWER')
ALPHA_UPPER = Base('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'ALPHA_UPPER')
ALPHA = Base('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 'ALPHA')
 
if __name__ == '__main__':
    import doctest
    # Test this module:
    doctest.testmod()