Concept Test -- General Purpose String Output Formatting
localroger
Posts: 3,451
Are you tired of forgetting whether the output driver you're working wants TX or OUT for a byte, or whether it's the one you pasted in the fixed-width decimal output method? And of only being able to use those methods in parent objects of the output driver? Sure you can get over that problem by using the DAT-only video or serial driver, but then you need to remember whether that is the one you hacked to increase the buffer size or use a nonzero value to CLS.
The attached project is ostensibly a pretty little multi-format clock, but I created it as a proof of concept of a manner of using a general purpose universal string output object, strfmt.spin. Check it out to see how a single output format object, which can be quite rich because it only occurs once, is used all over the place and is also used to create more complex objects which can be used from multiple places. These objects use only local variables, so pasting them into OBJ declarations all over the program does not incur any overhead and they are even multi-cog thread safe (which the DAT-only output drivers aren't).
The basic technique involves setting up a buffer in a local variable for a PUB or PRI in the top object which also has access to the output driver. Methods are called which build the output in this buffer, and those methods can be as many levels deep as necessary to build complex functionality. Then the output driver's STR method is called to dispatch the output to the actual hardware.
For example, instead of this:
...which is very specific to the serial object, you could do the following:
This is more compact than it looks; the "ptr := func(ptr, " only adds 2 bytes to each operation because local variable access is so compact. Local buffers can be used as byte buffers even though they're allocated as longs, though it's important to remember they can't be returned as a function result since their stack space is freed for reuse when the function returns. I also tried using a pointer to the pointer variable so the format methods could update it directly, but that was both harder to read and bigger because of the need to address the pointer variable on each line. You could also use RESULT throughout but it doesn't make the program any smaller and is a lot harder to read.
If you find this interesting take a look at the example. I came up with this because I"m about to start coding a very elaborate application which will have lots and lots and lots of formatting, much of it shared between multiple output devices and enabled or disabled by mode settings. The usual methods of dealing with it were looking impractical.
(Demo is coded for VGA and the Demoboard, should also work with the TV object with minimal mods)
clocktest - Archive [Date 2013.07.05 Time 12.30].zip
The attached project is ostensibly a pretty little multi-format clock, but I created it as a proof of concept of a manner of using a general purpose universal string output object, strfmt.spin. Check it out to see how a single output format object, which can be quite rich because it only occurs once, is used all over the place and is also used to create more complex objects which can be used from multiple places. These objects use only local variables, so pasting them into OBJ declarations all over the program does not incur any overhead and they are even multi-cog thread safe (which the DAT-only output drivers aren't).
The basic technique involves setting up a buffer in a local variable for a PUB or PRI in the top object which also has access to the output driver. Methods are called which build the output in this buffer, and those methods can be as many levels deep as necessary to build complex functionality. Then the output driver's STR method is called to dispatch the output to the actual hardware.
For example, instead of this:
PUB PrintTime ser.str(string("The Time is ")) ser.rdec(hour, 2) ser.out(":") ser.rdec(minute, 2) ser.str(string(".",13))
...which is very specific to the serial object, you could do the following:
PUB PrintTime | ptr, buf[10] ptr := FmtTime(@buf) 'more ptr formatting if desired ser.str(@buf) 'This could even be in another object... PRI FmtTIme(ptr) ptr := sf.str(ptr, string("The Time is ")) ptr := sf.rdec(ptr, hour, 2)) ptr := sf.ch(ptr, ":") ptr := sf.rdec(ptr, minute, 2)) result := sf.str(ptr, string(".",13))
This is more compact than it looks; the "ptr := func(ptr, " only adds 2 bytes to each operation because local variable access is so compact. Local buffers can be used as byte buffers even though they're allocated as longs, though it's important to remember they can't be returned as a function result since their stack space is freed for reuse when the function returns. I also tried using a pointer to the pointer variable so the format methods could update it directly, but that was both harder to read and bigger because of the need to address the pointer variable on each line. You could also use RESULT throughout but it doesn't make the program any smaller and is a lot harder to read.
If you find this interesting take a look at the example. I came up with this because I"m about to start coding a very elaborate application which will have lots and lots and lots of formatting, much of it shared between multiple output devices and enabled or disabled by mode settings. The usual methods of dealing with it were looking impractical.
(Demo is coded for VGA and the Demoboard, should also work with the TV object with minimal mods)
clocktest - Archive [Date 2013.07.05 Time 12.30].zip
Comments
Wait, you mean people use their holidays for something other than Propeller development? What a waste of CPU cycles!
Ill have to go grill some raw meat over an open fire while I think about this.
I had to take some time out of Propeller programming to burn out a stump (kind of fun).
I'll try to find some time to take a look at to code.
I used formatting code like this for a lot of my touchscreen projects. I found it a lot easier to place strings which included data values as a complete string rather than trying to place the text bit with character, string and decimal calls mixed together.
Have you seen the guidelines for Gold Standard objects? I've adapted the formatting styles they suggested myself so code that doesn't follow the GS style looks a bit off to me (not that other styles are "wrong", I've just gotten used to seeing Spin written in GS style (since that's the style I use)). I'll see if I can find a link to the GS guidelines in case you're interested.
You use of local buffers for strings bugs me. Not that it's wrong to use local buffers. I think my problem with using local buffers to hold strings arose from my early Prop programming experience where I had used local buffers for strings but ended up getting myself confused when trying to access a single element in the buffer.
Besides the possible confusion of storing strings in an array of longs can cause, local variables don't show up in the memory used when pressing F8. If you have a significant amount of memory used by local buffers, you may be overestimating the amount of remaining memory available. This isn't really a big deal if one's aware of the memory used by local buffers but I thought I'd mention it since you're looking for comments.
Have you seen PST's "StrToBase" method? It's one of two private methods in PST (both of which would be more useful if they were public methods). You may be able to combine your dec and hex methods by using code similar to the "StrToBase" method.
I'm looking at to see how you handle negative signs when using a decimal point (this has given me issues). Okay, something must be wrong with your "fdec" method. It's much too simple to do what it claims. I've posted a method "decPoint" to the forum several times which does something similar, only I take twice as much code to do the same thing (at least it seems that way to me now).
I noticed you place the negative sign in front of a leading zero if it's needed to pad out the width. Any formatting methods I write which may include leading zeros, I try to also include the option of using leading spaces for padding. The problem with padding a number with leading spaces is the negative sign will look out of place if it's not directly in front of the leftmost digit. This makes the code to get a negative sign in the correct location require a bit more code if you make the padding character flexible (as either a zero or a space). (Edit: I notice now most of your methods include a pad character as a parameter.)
I see you have both "out" and "tx" methods in your formatting object but what about PST's "Char" method? You mention your code could be easily modified to use a TV object instead of the VGA object but I also think your formatting object would be very useful when combined with serial objects.
Well, I don't have time to find a link to the Gold Standard thread right now (my wife is waiting to go on a walk with me).
Thanks for posting this code. It looks extremely useful. I'm pretty sure your implementation of formatting methods are cleaner than similar methods I've written. I think this will likely become a commonly used object in my projects.
Edit: Here's a link to the Gold Standard Specification.
The thing about using local buffers is that they don't take up any memory at all -- they are released when the method returns. That's a huge advantage they bring. All you have to worry about is having enough stack space, which is rarely going to be a problem. And another advantage is that references to memory variables require embedding a word pointer in the Spin byte code, but local var references take a single byte. This makes the whole ptr := fn(ptr, ... thing much more practical, as it adds only 2 bytes to fn(... versus 6 bytes if ptr is a VAR.
FDEC lays out the decimal point and leading zeroes in the buffer, then uses the pri RDEC to write the decimal digits over, skipping over the decimal and skipping past any remaining lead zeroes before writing the minus sign. This is how instrument displays in my industry are usually formatted.
One could easily add "char" as an equivalent to out and tx; just didn't occur to me because I don't use PST. I specifically intended this code to be equally applicable to serial and other data output objects.