2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2004 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
28 # test automatic long conversion
37 """rational class performing some basic rational arithmetics
38 the axis partitioning uses rational arithmetics (with infinite accuracy)
39 basically it contains self.num and self.denom"""
41 def initfromstring(self
, s
):
42 "converts a string 0.123 into a rational"
43 expparts
= s
.strip().replace("E", "e").split("e")
45 raise ValueError("multiple 'e' found in '%s'" % s
)
46 commaparts
= expparts
[0].split(".")
47 if len(commaparts
) > 2:
48 raise ValueError("multiple '.' found in '%s'" % expparts
[0])
49 if len(commaparts
) == 1:
50 commaparts
= [commaparts
[0], ""]
53 self
.denom
= 10 ** len(commaparts
[1])
55 self
.denom
= 10L ** len(commaparts
[1])
56 neg
= len(commaparts
[0]) and commaparts
[0][0] == "-"
58 commaparts
[0] = commaparts
[0][1:]
59 elif len(commaparts
[0]) and commaparts
[0][0] == "+":
60 commaparts
[0] = commaparts
[0][1:]
61 if len(commaparts
[0]):
62 if not commaparts
[0].isdigit():
63 raise ValueError("unrecognized characters in '%s'" % s
)
65 x
= int(commaparts
[0])
67 x
= long(commaparts
[0])
70 if len(commaparts
[1]):
71 if not commaparts
[1].isdigit():
72 raise ValueError("unrecognized characters in '%s'" % s
)
74 y
= int(commaparts
[1])
76 y
= long(commaparts
[1])
79 self
.num
= x
*self
.denom
+ y
82 if len(expparts
) == 2:
83 neg
= expparts
[1][0] == "-"
85 expparts
[1] = expparts
[1][1:]
86 elif expparts
[1][0] == "+":
87 expparts
[1] = expparts
[1][1:]
88 if not expparts
[1].isdigit():
89 raise ValueError("unrecognized characters in '%s'" % s
)
92 self
.denom
*= 10 ** int(expparts
[1])
94 self
.denom
*= 10L ** int(expparts
[1])
97 self
.num
*= 10 ** int(expparts
[1])
99 self
.num
*= 10L ** int(expparts
[1])
101 def initfromfloat(self
, x
, floatprecision
):
102 "converts a float into a rational with finite resolution"
103 if floatprecision
< 0:
104 raise RuntimeError("float resolution must be non-negative")
105 self
.initfromstring(("%%.%ig" % floatprecision
) % x
)
107 def __init__(self
, x
, power
=1, floatprecision
=10):
108 """initializes a rational
109 - rational=(num/denom)**power
111 - a string (like "1.2", "1.2e3", "1.2/3.4", etc.)
112 - a float (converted using floatprecision)
113 - a sequence of two integers
114 - a rational instance"""
120 # does x behave like a number
124 # does x behave like a string
129 self
.num
, self
.denom
= x
131 # otherwise it should have a num and denom
132 self
.num
, self
.denom
= x
.num
, x
.denom
135 fraction
= x
.split("/")
136 if len(fraction
) > 2:
137 raise ValueError("multiple '/' found in '%s'" % x
)
138 self
.initfromstring(fraction
[0])
139 if len(fraction
) == 2:
140 self
/= rational(fraction
[1])
143 self
.initfromfloat(x
, floatprecision
)
144 if not self
.denom
: raise ZeroDivisionError("zero denominator")
146 self
.num
, self
.denom
= self
.denom
, self
.num
149 self
.num
, self
.denom
= self
.denom
** (-power
), self
.num
** (-power
)
151 self
.num
, self
.denom
= long(self
.denom
) ** (-power
), long(self
.num
) ** (-power
)
154 self
.num
= self
.num
** power
155 self
.denom
= self
.denom
** power
157 self
.num
= long(self
.num
) ** power
158 self
.denom
= long(self
.denom
) ** power
160 def __cmp__(self
, other
):
162 return cmp(self
.num
* other
.denom
, other
.num
* self
.denom
)
164 return cmp(float(self
), other
)
167 return rational((abs(self
.num
), abs(self
.denom
)))
169 def __mul__(self
, other
):
170 return rational((self
.num
* other
.num
, self
.denom
* other
.denom
))
172 def __imul__(self
, other
):
173 self
.num
*= other
.num
174 self
.denom
*= other
.denom
177 def __div__(self
, other
):
178 return rational((self
.num
* other
.denom
, self
.denom
* other
.num
))
180 def __idiv__(self
, other
):
181 self
.num
*= other
.denom
182 self
.denom
*= other
.num
186 "caution: avoid final precision of floats"
187 return float(self
.num
) / self
.denom
190 return "%i/%i" % (self
.num
, self
.denom
)
193 class tick(rational
):
195 a tick is a rational enhanced by
196 - self.ticklevel (0 = tick, 1 = subtick, etc.)
197 - self.labellevel (0 = label, 1 = sublabel, etc.)
198 - self.label (a string) and self.labelattrs (a list, defaults to [])
199 When ticklevel or labellevel is None, no tick or label is present at that value.
200 When label is None, it should be automatically created (and stored), once the
201 an axis painter needs it. Classes, which implement _Itexter do precisely that."""
203 def __init__(self
, x
, ticklevel
=0, labellevel
=0, label
=None, labelattrs
=[], **kwargs
):
204 """initializes the instance
205 - see class description for the parameter description
206 - **kwargs are passed to the rational constructor"""
207 rational
.__init
__(self
, x
, **kwargs
)
208 self
.ticklevel
= ticklevel
209 self
.labellevel
= labellevel
211 self
.labelattrs
= labelattrs
213 def merge(self
, other
):
214 """merges two ticks together:
215 - the lower ticklevel/labellevel wins
216 - the ticks should be at the same position (otherwise it doesn't make sense)
217 -> this is NOT checked"""
218 if self
.ticklevel
is None or (other
.ticklevel
is not None and other
.ticklevel
< self
.ticklevel
):
219 self
.ticklevel
= other
.ticklevel
220 if self
.labellevel
is None or (other
.labellevel
is not None and other
.labellevel
< self
.labellevel
):
221 self
.labellevel
= other
.labellevel
222 if self
.label
is None:
223 self
.label
= other
.label
226 def mergeticklists(list1
, list2
, keepfirstifequal
=0):
227 """helper function to merge tick lists
228 - return a merged list of ticks out of list1 and list2
229 - CAUTION: original lists have to be ordered
230 (the returned list is also ordered)"""
231 # TODO: improve along the lines of http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/305269
233 # do not destroy original lists
238 while 1: # we keep on going until we reach an index error
239 while list2
[j
] < list1
[i
]: # insert tick
240 list1
.insert(i
, list2
[j
])
243 if list2
[j
] == list1
[i
]: # merge tick
244 if not keepfirstifequal
:
245 list1
[i
].merge(list2
[j
])
254 def maxlevels(ticks
):
255 "returns a tuple maxticklevel, maxlabellevel from a list of tick instances"
256 maxticklevel
= maxlabellevel
= 0
258 if tick
.ticklevel
is not None and tick
.ticklevel
>= maxticklevel
:
259 maxticklevel
= tick
.ticklevel
+ 1
260 if tick
.labellevel
is not None and tick
.labellevel
>= maxlabellevel
:
261 maxlabellevel
= tick
.labellevel
+ 1
262 return maxticklevel
, maxlabellevel