1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2005 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
27 # test automatic long conversion
36 """rational class performing some basic rational arithmetics
37 the axis partitioning uses rational arithmetics (with infinite accuracy)
38 basically it contains self.num and self.denom"""
40 def initfromstring(self
, s
):
41 "converts a string 0.123 into a rational"
42 expparts
= s
.strip().replace("E", "e").split("e")
44 raise ValueError("multiple 'e' found in '%s'" % s
)
45 commaparts
= expparts
[0].split(".")
46 if len(commaparts
) > 2:
47 raise ValueError("multiple '.' found in '%s'" % expparts
[0])
48 if len(commaparts
) == 1:
49 commaparts
= [commaparts
[0], ""]
52 self
.denom
= 10 ** len(commaparts
[1])
54 self
.denom
= 10 ** len(commaparts
[1])
55 neg
= len(commaparts
[0]) and commaparts
[0][0] == "-"
57 commaparts
[0] = commaparts
[0][1:]
58 elif len(commaparts
[0]) and commaparts
[0][0] == "+":
59 commaparts
[0] = commaparts
[0][1:]
60 if len(commaparts
[0]):
61 if not commaparts
[0].isdigit():
62 raise ValueError("unrecognized characters in '%s'" % s
)
64 x
= int(commaparts
[0])
66 x
= int(commaparts
[0])
69 if len(commaparts
[1]):
70 if not commaparts
[1].isdigit():
71 raise ValueError("unrecognized characters in '%s'" % s
)
73 y
= int(commaparts
[1])
75 y
= int(commaparts
[1])
78 self
.num
= x
*self
.denom
+ y
81 if len(expparts
) == 2:
82 neg
= expparts
[1][0] == "-"
84 expparts
[1] = expparts
[1][1:]
85 elif expparts
[1][0] == "+":
86 expparts
[1] = expparts
[1][1:]
87 if not expparts
[1].isdigit():
88 raise ValueError("unrecognized characters in '%s'" % s
)
91 self
.denom
*= 10 ** int(expparts
[1])
93 self
.denom
*= 10 ** int(expparts
[1])
96 self
.num
*= 10 ** int(expparts
[1])
98 self
.num
*= 10 ** int(expparts
[1])
100 def initfromfloat(self
, x
, floatprecision
):
101 "converts a float into a rational with finite resolution"
102 if floatprecision
< 0:
103 raise RuntimeError("float resolution must be non-negative")
104 self
.initfromstring(("%%.%ig" % floatprecision
) % x
)
106 def __init__(self
, x
, power
=1, floatprecision
=10):
107 """initializes a rational
108 - rational=(num/denom)**power
110 - a string (like "1.2", "1.2e3", "1.2/3.4", etc.)
111 - a float (converted using floatprecision)
112 - a sequence of two integers
113 - a rational instance"""
119 # does x behave like a number
123 # does x behave like a string
128 self
.num
, self
.denom
= x
130 # otherwise it should have a num and denom
131 self
.num
, self
.denom
= x
.num
, x
.denom
134 fraction
= x
.split("/")
135 if len(fraction
) > 2:
136 raise ValueError("multiple '/' found in '%s'" % x
)
137 self
.initfromstring(fraction
[0])
138 if len(fraction
) == 2:
139 self
/= rational(fraction
[1])
142 self
.initfromfloat(x
, floatprecision
)
143 if not self
.denom
: raise ZeroDivisionError("zero denominator")
145 self
.num
, self
.denom
= self
.denom
, self
.num
148 self
.num
, self
.denom
= self
.denom
** (-power
), self
.num
** (-power
)
150 self
.num
, self
.denom
= int(self
.denom
) ** (-power
), int(self
.num
) ** (-power
)
153 self
.num
= self
.num
** power
154 self
.denom
= self
.denom
** power
156 self
.num
= int(self
.num
) ** power
157 self
.denom
= int(self
.denom
) ** power
159 def __cmp__(self
, other
):
161 return cmp(self
.num
* other
.denom
, other
.num
* self
.denom
)
163 return cmp(float(self
), other
)
166 return rational((abs(self
.num
), abs(self
.denom
)))
168 def __add__(self
, other
):
169 assert abs(other
) < 1e-10
172 def __mul__(self
, other
):
173 return rational((self
.num
* other
.num
, self
.denom
* other
.denom
))
175 def __imul__(self
, other
):
176 self
.num
*= other
.num
177 self
.denom
*= other
.denom
180 def __div__(self
, other
):
181 return rational((self
.num
* other
.denom
, self
.denom
* other
.num
))
183 __truediv__
= __div__
185 def __idiv__(self
, other
):
186 self
.num
*= other
.denom
187 self
.denom
*= other
.num
191 "caution: avoid final precision of floats"
192 return float(self
.num
) / self
.denom
195 return "%i/%i" % (self
.num
, self
.denom
)
198 class tick(rational
):
200 a tick is a rational enhanced by
201 - self.ticklevel (0 = tick, 1 = subtick, etc.)
202 - self.labellevel (0 = label, 1 = sublabel, etc.)
203 - self.label (a string) and self.labelattrs (a list, defaults to [])
204 When ticklevel or labellevel is None, no tick or label is present at that value.
205 When label is None, it should be automatically created (and stored), once the
206 an axis painter needs it. Classes, which implement _Itexter do precisely that."""
208 def __init__(self
, x
, ticklevel
=0, labellevel
=0, label
=None, labelattrs
=[], **kwargs
):
209 """initializes the instance
210 - see class description for the parameter description
211 - **kwargs are passed to the rational constructor"""
212 rational
.__init
__(self
, x
, **kwargs
)
213 self
.ticklevel
= ticklevel
214 self
.labellevel
= labellevel
216 self
.labelattrs
= labelattrs
218 def merge(self
, other
):
219 """merges two ticks together:
220 - the lower ticklevel/labellevel wins
221 - the ticks should be at the same position (otherwise it doesn't make sense)
222 -> this is NOT checked"""
223 if self
.ticklevel
is None or (other
.ticklevel
is not None and other
.ticklevel
< self
.ticklevel
):
224 self
.ticklevel
= other
.ticklevel
225 if self
.labellevel
is None or (other
.labellevel
is not None and other
.labellevel
< self
.labellevel
):
226 self
.labellevel
= other
.labellevel
227 if self
.label
is None:
228 self
.label
= other
.label
231 def mergeticklists(list1
, list2
, mergeequal
=1):
232 """helper function to merge tick lists
233 - return a merged list of ticks out of list1 and list2
234 - CAUTION: original lists have to be ordered
235 (the returned list is also ordered)"""
236 # TODO: improve along the lines of http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/305269
238 # do not destroy original lists
243 while 1: # we keep on going until we reach an index error
244 while list2
[j
] < list1
[i
]: # insert tick
245 list1
.insert(i
, list2
[j
])
248 if list2
[j
] == list1
[i
]: # merge tick
250 list1
[i
].merge(list2
[j
])
259 def maxlevels(ticks
):
260 "returns a tuple maxticklevel, maxlabellevel from a list of tick instances"
261 maxticklevel
= maxlabellevel
= 0
263 if tick
.ticklevel
is not None and tick
.ticklevel
>= maxticklevel
:
264 maxticklevel
= tick
.ticklevel
+ 1
265 if tick
.labellevel
is not None and tick
.labellevel
>= maxlabellevel
:
266 maxlabellevel
= tick
.labellevel
+ 1
267 return maxticklevel
, maxlabellevel