Initial Commit
[Projects.git] / pkgbuilds / pytivo / pkg / usr / share / pyTivo / Cheetah / Tools / MondoReport.py
blobf73e7fcbe283ee71828ad54a29ef239ad581fd66
1 #!/usr/bin/env python
2 """
3 @@TR: This code is pretty much unsupported.
5 MondoReport.py -- Batching module for Python and Cheetah.
7 Version 2001-Nov-18. Doesn't do much practical yet, but the companion
8 testMondoReport.py passes all its tests.
9 -Mike Orr (Iron)
11 TODO: BatchRecord.prev/next/prev_batches/next_batches/query, prev.query,
12 next.query.
14 How about Report: .page(), .all(), .summary()? Or PageBreaker.
15 """
16 import operator, types
17 try:
18 from Cheetah.NameMapper import valueForKey as lookup_func
19 except ImportError:
20 def lookup_func(obj, name):
21 if hasattr(obj, name):
22 return getattr(obj, name)
23 else:
24 return obj[name] # Raises KeyError.
26 ########## CONSTANTS ##############################
28 True, False = (1==1), (1==0)
29 numericTypes = types.IntType, types.LongType, types.FloatType
31 ########## PUBLIC GENERIC FUNCTIONS ##############################
33 class NegativeError(ValueError):
34 pass
36 def isNumeric(v):
37 return type(v) in numericTypes
39 def isNonNegative(v):
40 ret = isNumeric(v)
41 if ret and v < 0:
42 raise NegativeError(v)
44 def isNotNone(v):
45 return v is not None
47 def Roman(n):
48 n = int(n) # Raises TypeError.
49 if n < 1:
50 raise ValueError("roman numeral for zero or negative undefined: " + n)
51 roman = ''
52 while n >= 1000:
53 n = n - 1000
54 roman = roman + 'M'
55 while n >= 500:
56 n = n - 500
57 roman = roman + 'D'
58 while n >= 100:
59 n = n - 100
60 roman = roman + 'C'
61 while n >= 50:
62 n = n - 50
63 roman = roman + 'L'
64 while n >= 10:
65 n = n - 10
66 roman = roman + 'X'
67 while n >= 5:
68 n = n - 5
69 roman = roman + 'V'
70 while n < 5 and n >= 1:
71 n = n - 1
72 roman = roman + 'I'
73 roman = roman.replace('DCCCC', 'CM')
74 roman = roman.replace('CCCC', 'CD')
75 roman = roman.replace('LXXXX', 'XC')
76 roman = roman.replace('XXXX', 'XL')
77 roman = roman.replace('VIIII', 'IX')
78 roman = roman.replace('IIII', 'IV')
79 return roman
82 def sum(lis):
83 return reduce(operator.add, lis, 0)
85 def mean(lis):
86 """Always returns a floating-point number.
87 """
88 lis_len = len(lis)
89 if lis_len == 0:
90 return 0.00 # Avoid ZeroDivisionError (not raised for floats anyway)
91 total = float( sum(lis) )
92 return total / lis_len
94 def median(lis):
95 lis = lis[:]
96 lis.sort()
97 return lis[int(len(lis)/2)]
100 def variance(lis):
101 raise NotImplementedError()
103 def variance_n(lis):
104 raise NotImplementedError()
106 def standardDeviation(lis):
107 raise NotImplementedError()
109 def standardDeviation_n(lis):
110 raise NotImplementedError()
114 class IndexFormats:
115 """Eight ways to display a subscript index.
116 ("Fifty ways to leave your lover....")
118 def __init__(self, index, item=None):
119 self._index = index
120 self._number = index + 1
121 self._item = item
123 def index(self):
124 return self._index
126 __call__ = index
128 def number(self):
129 return self._number
131 def even(self):
132 return self._number % 2 == 0
134 def odd(self):
135 return not self.even()
137 def even_i(self):
138 return self._index % 2 == 0
140 def odd_i(self):
141 return not self.even_i()
143 def letter(self):
144 return self.Letter().lower()
146 def Letter(self):
147 n = ord('A') + self._index
148 return chr(n)
150 def roman(self):
151 return self.Roman().lower()
153 def Roman(self):
154 return Roman(self._number)
156 def item(self):
157 return self._item
161 ########## PRIVATE CLASSES ##############################
163 class ValuesGetterMixin:
164 def __init__(self, origList):
165 self._origList = origList
167 def _getValues(self, field=None, criteria=None):
168 if field:
169 ret = [lookup_func(elm, field) for elm in self._origList]
170 else:
171 ret = self._origList
172 if criteria:
173 ret = filter(criteria, ret)
174 return ret
177 class RecordStats(IndexFormats, ValuesGetterMixin):
178 """The statistics that depend on the current record.
180 def __init__(self, origList, index):
181 record = origList[index] # Raises IndexError.
182 IndexFormats.__init__(self, index, record)
183 ValuesGetterMixin.__init__(self, origList)
185 def length(self):
186 return len(self._origList)
188 def first(self):
189 return self._index == 0
191 def last(self):
192 return self._index >= len(self._origList) - 1
194 def _firstOrLastValue(self, field, currentIndex, otherIndex):
195 currentValue = self._origList[currentIndex] # Raises IndexError.
196 try:
197 otherValue = self._origList[otherIndex]
198 except IndexError:
199 return True
200 if field:
201 currentValue = lookup_func(currentValue, field)
202 otherValue = lookup_func(otherValue, field)
203 return currentValue != otherValue
205 def firstValue(self, field=None):
206 return self._firstOrLastValue(field, self._index, self._index - 1)
208 def lastValue(self, field=None):
209 return self._firstOrLastValue(field, self._index, self._index + 1)
211 # firstPage and lastPage not implemented. Needed?
213 def percentOfTotal(self, field=None, suffix='%', default='N/A', decimals=2):
214 rec = self._origList[self._index]
215 if field:
216 val = lookup_func(rec, field)
217 else:
218 val = rec
219 try:
220 lis = self._getValues(field, isNumeric)
221 except NegativeError:
222 return default
223 total = sum(lis)
224 if total == 0.00: # Avoid ZeroDivisionError.
225 return default
226 val = float(val)
227 try:
228 percent = (val / total) * 100
229 except ZeroDivisionError:
230 return default
231 if decimals == 0:
232 percent = int(percent)
233 else:
234 percent = round(percent, decimals)
235 if suffix:
236 return str(percent) + suffix # String.
237 else:
238 return percent # Numeric.
240 def __call__(self): # Overrides IndexFormats.__call__
241 """This instance is not callable, so we override the super method.
243 raise NotImplementedError()
245 def prev(self):
246 if self._index == 0:
247 return None
248 else:
249 length = self.length()
250 start = self._index - length
251 return PrevNextPage(self._origList, length, start)
253 def next(self):
254 if self._index + self.length() == self.length():
255 return None
256 else:
257 length = self.length()
258 start = self._index + length
259 return PrevNextPage(self._origList, length, start)
261 def prevPages(self):
262 raise NotImplementedError()
264 def nextPages(self):
265 raise NotImplementedError()
267 prev_batches = prevPages
268 next_batches = nextPages
270 def summary(self):
271 raise NotImplementedError()
275 def _prevNextHelper(self, start,end,size,orphan,sequence):
276 """Copied from Zope's DT_InSV.py's "opt" function.
278 if size < 1:
279 if start > 0 and end > 0 and end >= start:
280 size=end+1-start
281 else: size=7
283 if start > 0:
285 try: sequence[start-1]
286 except: start=len(sequence)
287 # if start > l: start=l
289 if end > 0:
290 if end < start: end=start
291 else:
292 end=start+size-1
293 try: sequence[end+orphan-1]
294 except: end=len(sequence)
295 # if l - end < orphan: end=l
296 elif end > 0:
297 try: sequence[end-1]
298 except: end=len(sequence)
299 # if end > l: end=l
300 start=end+1-size
301 if start - 1 < orphan: start=1
302 else:
303 start=1
304 end=start+size-1
305 try: sequence[end+orphan-1]
306 except: end=len(sequence)
307 # if l - end < orphan: end=l
308 return start,end,size
312 class Summary(ValuesGetterMixin):
313 """The summary statistics, that don't depend on the current record.
315 def __init__(self, origList):
316 ValuesGetterMixin.__init__(self, origList)
318 def sum(self, field=None):
319 lis = self._getValues(field, isNumeric)
320 return sum(lis)
322 total = sum
324 def count(self, field=None):
325 lis = self._getValues(field, isNotNone)
326 return len(lis)
328 def min(self, field=None):
329 lis = self._getValues(field, isNotNone)
330 return min(lis) # Python builtin function min.
332 def max(self, field=None):
333 lis = self._getValues(field, isNotNone)
334 return max(lis) # Python builtin function max.
336 def mean(self, field=None):
337 """Always returns a floating point number.
339 lis = self._getValues(field, isNumeric)
340 return mean(lis)
342 average = mean
344 def median(self, field=None):
345 lis = self._getValues(field, isNumeric)
346 return median(lis)
348 def variance(self, field=None):
349 raiseNotImplementedError()
351 def variance_n(self, field=None):
352 raiseNotImplementedError()
354 def standardDeviation(self, field=None):
355 raiseNotImplementedError()
357 def standardDeviation_n(self, field=None):
358 raiseNotImplementedError()
361 class PrevNextPage:
362 def __init__(self, origList, size, start):
363 end = start + size
364 self.start = IndexFormats(start, origList[start])
365 self.end = IndexFormats(end, origList[end])
366 self.length = size
369 ########## MAIN PUBLIC CLASS ##############################
370 class MondoReport:
371 _RecordStatsClass = RecordStats
372 _SummaryClass = Summary
374 def __init__(self, origlist):
375 self._origList = origlist
377 def page(self, size, start, overlap=0, orphan=0):
378 """Returns list of ($r, $a, $b)
380 if overlap != 0:
381 raise NotImplementedError("non-zero overlap")
382 if orphan != 0:
383 raise NotImplementedError("non-zero orphan")
384 origList = self._origList
385 origList_len = len(origList)
386 start = max(0, start)
387 end = min( start + size, len(self._origList) )
388 mySlice = origList[start:end]
389 ret = []
390 for rel in range(size):
391 abs_ = start + rel
392 r = mySlice[rel]
393 a = self._RecordStatsClass(origList, abs_)
394 b = self._RecordStatsClass(mySlice, rel)
395 tup = r, a, b
396 ret.append(tup)
397 return ret
400 batch = page
402 def all(self):
403 origList_len = len(self._origList)
404 return self.page(origList_len, 0, 0, 0)
407 def summary(self):
408 return self._SummaryClass(self._origList)
411 **********************************
412 Return a pageful of records from a sequence, with statistics.
414 in : origlist, list or tuple. The entire set of records. This is
415 usually a list of objects or a list of dictionaries.
416 page, int >= 0. Which page to display.
417 size, int >= 1. How many records per page.
418 widow, int >=0. Not implemented.
419 orphan, int >=0. Not implemented.
420 base, int >=0. Number of first page (usually 0 or 1).
422 out: list of (o, b) pairs. The records for the current page. 'o' is
423 the original element from 'origlist' unchanged. 'b' is a Batch
424 object containing meta-info about 'o'.
425 exc: IndexError if 'page' or 'size' is < 1. If 'origlist' is empty or
426 'page' is too high, it returns an empty list rather than raising
427 an error.
429 origlist_len = len(origlist)
430 start = (page + base) * size
431 end = min(start + size, origlist_len)
432 ret = []
433 # widow, orphan calculation: adjust 'start' and 'end' up and down,
434 # Set 'widow', 'orphan', 'first_nonwidow', 'first_nonorphan' attributes.
435 for i in range(start, end):
436 o = origlist[i]
437 b = Batch(origlist, size, i)
438 tup = o, b
439 ret.append(tup)
440 return ret
442 def prev(self):
443 # return a PrevNextPage or None
445 def next(self):
446 # return a PrevNextPage or None
448 def prev_batches(self):
449 # return a list of SimpleBatch for the previous batches
451 def next_batches(self):
452 # return a list of SimpleBatch for the next batches
454 ########## PUBLIC MIXIN CLASS FOR CHEETAH TEMPLATES ##############
455 class MondoReportMixin:
456 def batch(self, origList, size=None, start=0, overlap=0, orphan=0):
457 bat = MondoReport(origList)
458 return bat.batch(size, start, overlap, orphan)
459 def batchstats(self, origList):
460 bat = MondoReport(origList)
461 return bat.stats()
464 # vim: shiftwidth=4 tabstop=4 expandtab textwidth=79