18 March 2007

Fluid Strings

I just completed a piece of software — one part of the software sends text messages to cell phones. Each text message consists of several fields. And each field has a maximum limit on how long it can be. So if a field is longer than its limit, then it is cutoff. However, if the field smaller than its limit, then all the other fields should take up space that this field "gives up" — so, other fields can become longer than their limits in the final output. Here is what I did:
class FluidString(object):
    
    def __init__(self, actual, limit, prefix="", suffix=""):
        self.actual = actual
        self.limit = limit
        self.prefix = prefix
        self.suffix = suffix

    def increase_limit_by(self, amt):
        self.limit += amt

    def __str__(self):
        # adding newline is a hack, specific for my app,
        # where I want each fluid string to be
        # contained in a seperate line

        t = len(self.prefix) + len(self.suffix)
        if self.limit < t:
            return self.actual[:self.limit]
        
        s = self.actual[:self.limit-1-t]
        return self.prefix + s + self.suffix + "\n"


    # returns non-negative integer:
    def chars_left_over(self):
        return max(self.limit - len(str(self)), 0)

Fluid strings are assembled by containers:
# right now just fits pieces fluidly;
# if any piece underflows, other pieces
# evenly get what's left over.

class Container(object):
    
    def __init__(self, *items):
        self.strings = list(items)
        
    def add(self, fs):
        self.strings.append(fs)
        
    # unused:
    def on_underflow_of(self, target, *fs_pairs):
        pass
    
    def render(self):
        # find fluid strings that are underflowing.
        # split all those free spaces among strings that
        # are likely to overflow.

        anyleft = [x.chars_left_over() for x in self.strings]
        freespace = sum(anyleft)
        
        # how many to split freespace with:
        # `if not x': it means that the fluid string
        # will likely overflow. so split space among
        # those likely to overflow
        count = anyleft.count(0) #sum(1 for i in anyleft if i==0)
        
        if freespace and count:
            amount = freespace / count
            for i, x in enumerate(self.strings):
                if not anyleft[i]:
                    x.increase_limit_by(amount)

        final = "".join(str(x) for x in self.strings)
        #assert len(final) == sum(x.limit for x in self.strings)
        return final

    @staticmethod
    def test():
        c = Container()
        c.add(FluidString("01234567890123456789", 10))
        c.add(FluidString("asdf", 20))
        c.add(FluidString("hello world how are you", 10))
        return c.render()

Here is an usage sample (it is also above):
    def test():
        c = Container()
        c.add(FluidString("01234567890123456789", 10))
        c.add(FluidString("asdf", 20))
        c.add(FluidString("hello world how are you", 10))
        return c.render()
The output is:
01234567890123456
asdf
hello world how a
[newline]
(Note how the first & third FluidString added, shows more than its limit allows it — extra 7 characters each + 2 newlines.)