Merge fixes from branch 'xorn'
[geda-gaf.git] / xorn / src / python / hybridnum.py
blob27fd5b88477b37fc180584f50e6983f203d8d52a
1 # Copyright (C) 2013-2020 Roland Lutz
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 ## \namespace xorn.hybridnum
18 ## Hybrid fixed-/floating-point numbers.
20 # This module provides formatting and parsing functions for a hybrid
21 # number format which uses floating-point numbers as the base of a
22 # fixed-point notation. This way, decimal fractions down to an
23 # arbitrary (but fixed) number of digits can be represented exactly
24 # while still allowing the benefits from a floating-point number
25 # format.
27 # As an example, in a format with three fixed digits, both the number
28 # \c 1 (represented as the floating-point value \c 1000.0) and the
29 # number \c 0.001 (represented as the floating-point value \c 1.0) can
30 # be represented exactly, whereas the number \c 0.0001 (represented as
31 # the floating-point number \c 0.1) would be subject to conversion
32 # errors.
34 # To avoid the usual errors when converting a floating-point number to
35 # a string and vice versa, hexadecimal notation is used for the
36 # decimals to the floating-point representation.
38 ## Convert a floating-point number to its hybrid string representation.
40 # TODO: For efficiency reasons, this should probably be ported to C.
42 def format(x, decimal_digits):
43 if not isinstance(decimal_digits, int):
44 raise TypeError, 'number of decimals must be an integer'
45 if decimal_digits < 0:
46 raise ValueError, 'number of decimals must be non-negative'
48 s = float(x).hex()
50 if s[0] == '-':
51 sign = '-'
52 s = s[1:]
53 else:
54 sign = ''
56 assert s[0] == '0' and s[1] == 'x'
57 s = s[2:]
59 pos = s.index('p')
60 assert s[pos + 1] == '+' or s[pos + 1] == '-'
61 mant, exp = s[:pos], int(s[pos + 1:])
63 bits = []
64 if mant[0] == '0':
65 bits.append(0) # shouldn't normally happen, though
66 elif mant[0] == '1':
67 bits.append(1)
68 else:
69 raise AssertionError
70 assert mant[1] == '.'
71 for c in mant[2:]:
72 d = int(c, 16)
73 bits.append((d >> 3) & 1)
74 bits.append((d >> 2) & 1)
75 bits.append((d >> 1) & 1)
76 bits.append((d >> 0) & 1)
78 if exp < 0:
79 bits_before = []
80 bits_after = [0] * -(exp + 1) + bits
81 else:
82 while len(bits) < exp + 1:
83 bits.append(0)
84 bits_before = bits[:exp + 1]
85 bits_after = bits[exp + 1:]
87 while bits_after and bits_after[-1] == 0:
88 del bits_after[-1]
89 while len(bits_after) % 4 != 0:
90 bits_after.append(0)
92 before = str(sum(b << i for i, b in enumerate(reversed(bits_before))))
93 after = ''
94 for i in xrange(0, len(bits_after), 4):
95 after += '0123456789abcdef'[(bits_after[i] << 3) +
96 (bits_after[i + 1] << 2) +
97 (bits_after[i + 2] << 1) +
98 bits_after[i + 3]]
100 if decimal_digits == 0:
101 if after:
102 if before == '0':
103 return sign + ':' + after
104 return sign + before + ':' + after
105 if before == '0':
106 return '0' # signless zero
107 return sign + before
109 if len(before) < decimal_digits:
110 before = '0' * (decimal_digits - len(before)) + before
111 before0 = before[:-decimal_digits]
112 before1 = before[-decimal_digits:]
114 if before1 == '0' * decimal_digits:
115 if after:
116 if not before0:
117 return sign + ':' + after
118 return sign + before0 + '.' + before1 + ':' + after
119 else:
120 if not before0:
121 return '0' # signless zero
122 return sign + before0
123 else:
124 if after:
125 return sign + before0 + '.' + before1 + ':' + after
126 else:
127 return sign + before0 + '.' + before1.rstrip('0')
129 ## Convert a hybrid string representation to a floating-point number.
131 # TODO: For efficiency reasons, this should probably be ported to C.
133 def parse(s, decimal_digits):
134 if not isinstance(s, str) and not isinstance(s, unicode):
135 raise TypeError, 'invalid argument type (must be str or unicode)'
136 if not isinstance(decimal_digits, int):
137 raise TypeError, 'number of decimals must be an integer'
138 if decimal_digits < 0:
139 raise ValueError, 'number of decimals must be non-negative'
141 if s and s[0] == '-':
142 sign = -1
143 s = s[1:]
144 else:
145 sign = 1
147 try:
148 pos = s.index(':')
149 except ValueError:
150 after = None
151 if not s:
152 raise ValueError
153 else:
154 after = s[pos + 1:]
155 s = s[:pos]
156 if not after and not s:
157 raise ValueError
159 if decimal_digits == 0:
160 if '.' in s:
161 raise ValueError
162 before0 = s
163 before1 = ''
164 else:
165 try:
166 pos = s.index('.')
167 except ValueError:
168 if s and after is not None:
169 raise ValueError
170 before0 = s
171 before1 = ''
172 else:
173 before0 = s[:pos]
174 before1 = s[pos + 1:]
175 if not before0 and not before1:
176 raise ValueError
177 if len(before1) < decimal_digits:
178 if after is not None:
179 raise ValueError
180 before1 = before1 + (decimal_digits - len(before1)) * '0'
182 if len(before1) > decimal_digits:
183 raise ValueError
185 if not after:
186 after = '0'
188 for c in before0 + before1:
189 if c not in '0123456789':
190 raise ValueError
191 for c in after:
192 if c not in '0123456789abcdef':
193 raise ValueError
195 if not before0:
196 before0 = '0'
197 if not before1:
198 before1 = '0'
200 x = float(int(before0) * 10 ** decimal_digits + int(before1)) + \
201 float(int(after, 16)) / float(1 << len(after) * 4)
203 if not x:
204 return 0. # signless zero
205 return sign * x