How does one implement report breaks in general and in Python specifically?
I’m referring to functionality that allows for inserting a title header before the record where some field changes, or a footer with totals after the change. Like in MS Access back in the day.
Currently using an iterable object and lambdas to check for breaks like this:
from typing import Literal, Callable
class BreakResult():
def __init__(self, name, state, slice_start, data=[]):
self.name = name
self.state = state
self.slice_start = slice_start
def __bool__(self):
# Return True if the list is non-empty
return self.state
# Define behavior when used in an integer context
def __int__(self):
return self.slice_start
def __str__(self):
return self.name if self.state else ""
class BreakForIterable():
def __init__(self, name, break_lambda:Callable, where:Literal['after', 'before']='after'):
self.name = name
self.break_lambda = break_lambda
self.where = where
self._slice_start = 0
self._state = False
@property
def slice_start(self):
pass
def __call__(self, i, data):
# returns true, slice_start or None, slice_end or none
if self.where == "before":
# return true if:
# - record number is 0
# - lambda returns different values for current record and the previous one
# and set the slice start to the current record
if i == 0:
slice_start = self._slice_start
self._slice_start = i
return BreakResult(self.name, True, slice_start)
elif self.break_lambda(data[i]) != self.break_lambda(data[i-1]):
slice_start = self._slice_start
self._slice_start = i
return BreakResult(self.name, True, slice_start)
else:
return BreakResult(self.name, False, None)
elif self.where == "after":
# return true if:
# - record number is equal to the last
# - lambda returns different values for current record and the next one
# and set the slice start to the current record
if i == 0:
return BreakResult(self.name, False, None)
elif i == len(data) - 1:
slice_start = self._slice_start
self._slice_start = i + 1
return BreakResult(self.name, True, slice_start)
elif self.break_lambda(data[i]) != self.break_lambda(data[i+1]):
slice_start = self._slice_start
self._slice_start = i + 1
return BreakResult(self.name, True, slice_start)
else:
return BreakResult(self.name, False, None)
return BreakResult(self.name, False, None)
class IterableWithBreaks():
def __init__(self, data):
self.data = data # this is the data list
self.breaks = [] # this is the list of breaks. Order is important
def add_break(self, name, break_lambda, where="after"):
self.breaks.append(BreakForIterable(name, break_lambda, where))
return self
def check_breaks(self, i, where:Literal['after', 'before']):
rl = []
for b in [ b for b in self.breaks if b.where == where]:
r = b(i, self.data)
rl.append(r)
return [ r for r in rl if r ]
def __iter__(self):
self._index = 0 # Initialize the index for iteration
return self
def __next__(self):
if self._index < len(self.data):
result = self.data[self._index]
self._index += 1
return result
else:
raise StopIteration # End of the iteration
def __getitem__(self, index):
return self.data[index]
# Define behavior for setting elements
def __setitem__(self, index, value):
self.data[index] = value
# Define behavior for deleting elements
def __delitem__(self, index):
del self.data[index]
but this smells to me like a naive approach – especially if I were to try adding totals to a header.
Is there a better approach? Or are there libraries that do that better? Anything from reporting tools (open source) to Python libraries (better, as I’d be able to better control graphics) would work.