Hey everyone! I made a some little helper functions for allowing some basic handling of positive integers greater than 32767. Feel free to expand/modify these. Use for whatever, no attribution required.
This could be useful for a score counter in a game or experience points in an RPG. There's a minor overhead in code-size and in performance, so these are when you really need a little bit of of extra precision for your numbers.
Unsigned Integer Decimal Strings
This first method handles numeric strings holding arbitrary unsigned integers. I haven't added sign handling or fraction handling, so this is for unsigned integers only, but may come in handy for score / experience counter sort of things. I also added a comparison function so you can tell if a decimal string is greater than/less than/equal to another one.
The disadvantage is number of allocations required for calculating intermediate results, lots of concenation and substring ops. But if you're doing this for non-time critical functionality, and want the extra digits, it's pretty handy.
function decdigit(s,i) local c=sub(s,-i,-i) return c~='' and 0+c or 0 end function declen(s) for i=1,#s do if(sub(s,i,i)~='0')return #s-i+1 end return 1 end function decadd(a,b) local s='' local c=0 for i=1,max(declen(a),declen(b)) do local v=decdigit(a,i)+decdigit(b,i)+c c=0 if(v>9)v-=10 c=1 if(#s>0 or v>0 or c>0 or i==1)s=v..s end if(c>0)s='1'..s return s end function deccmp(a,b) local m=declen(a) local n=declen(b) if(m<n)return -1 if(m>n)return 1 for i=1,n do local u=decdigit(a,i) local v=decdigit(b,i) if(u<v)return -1 if(u>v)return 1 end return 0 end -- Addition test cases print(decadd('1','1')) -- 2 print(decadd('999','1')) -- 1000 print(decadd('123456789','99999')) -- 123556788 print(decadd('10009','20009')) -- 30018 -- Comparison test cases print(deccmp('2000','200')) -- 1 (a>b) print(deccmp('2000','1000')) -- 1 (a>b) print(deccmp('2000','2000')) -- 0 (equal) print(deccmp('1','00001')) -- 0 (equal) print(deccmp('200','2000')) -- -1 (a<b) print(deccmp('1000','2000')) -- -1 (a<b) |
Binary Coded Decimals
A second method takes advantage of a fairly ancient number format, called Binary Coded Decimal notation. It stores values in so-called "packed BCD notation", where one decimal digit 0-9 is stored per 4-bit hex digit of the number. The format used by this library supports numbers up to 7 places (0 .. 9999999). To accomplish this, it uses 3 nybbles from the integer part and 4 nybbles from the fractional part of the number. In this notation, 1 is 0x0.0001, 123 is 0x0.0123, 12345 is 0x1.2345, 1234567 is 0x123.4567, which is a bit clumsy to write, but keeps numbers to single tokens.
The possible advantage that BCD has over numeric strings is that it avoids allocations and string slicing/coercion, and just manipulates nybbles of numbers directly. Also, because the BCD value is still a number, regular comparison operations can be used (as long as both sides are in BCD format), saving some tokens.
When printing, the numbers must be converted back into strings. Also, as mentioned before, there are only 7 digits to work with.
function bcddigit(a,n) local r=n<4 and shl(a,(4-n)*4) or shr(a,(n-4)*4) return band(r,15) end function bcdtostr(a) local s='' for i=6,0,-1 do local v=bcddigit(a,i) if(#s>0 or v>0 or i==0)s=s..v end return s end function bcdfromint(n) return 0+('0x'..flr(n/10000)..'.'..sub(10000+n%10000,-4)) end function bcdadd(a,b) local r=0 local c=0 for i=0,6 do local v=bcddigit(a,i)+bcddigit(b,i)+c c=0 if(v>9)v-=10 c=1 r+=shl(shr(v,16),i*4) end if(c>0)return 0x999.9999 return r end function bcdsub(a,b) local r=0 local c=0 for i=0,6 do local v=bcddigit(a,i)-bcddigit(b,i)-c c=0 if(v<0)v+=10 c=1 r+=shl(shr(v,16),i*4) end if(c>0)return 0 return r end -- Examples: print(bcdtostr(0x000.0000)) -- 0 print(bcdtostr(0x000.0001)) -- 1 print(bcdtostr(0x10.0000)) -- 100000 print(bcdtostr(0x123.4567)) -- 1234567 print(bcdtostr(bcdadd(0x1.0009, 0x2.0009))) -- 10009 + 20009 = 30018 print(bcdtostr(bcdsub(0x0.0090, 0x0.0017))) -- 90 - 17 = 73 |
So far, I've written support to add/subtract two BCD numbers, as well as convert from BCD representation to a string. Note that this only handles positive integers (no sign bit or fractional part). It also does not error-check, so make sure all values passed are already in packed BCD notation it expects.
Ideas:
- (code removal) To perform arithmetic with wraparound rather than clamping when out-of-range, remove the carry-checks after the for loops in bcdadd/bcdsub.
- (code removal) If you don't need subtraction, you can delete the code for it. Acceptable for score systems or RPG experience, but probably not gold.
- (code removal) If you never need to convert normal integers into BCD format, you can remove the code for it.
- (small code required) To allow even larger integers, the carry flag could also be returned and somehow fed into further calculations. Perhaps an optional parameter to bcdadd/sub that specifies the initial carry flag (defaults to 0), and have an optional second return value for the result carry. Then you could chain together additions to make numbers with even more digits. However, when you print this, you will need a way to force leading zeros to be included on any lower "words" of the number, or it will incorrectly skip them.
- (medium/large code required) Another way to allow even larger BCD integers without exposing the rest of the code to the carry flag would be to always pass multiple "words", like say two values (one containing the upper 7 digits, one containing the lower 7 digits), and return them all as one unit.
- (lots of code) put them in a array of 7-digit values and go for a full-on variable-length bigint type. Would require too much code to be worth it, probably.
- (small code change, possibly new code if comparision is required) You could use 8 digits instead of 7 digits inside the BCD value with some minor adjustments, but then you will need to provide your own order-comparison functions rather than using the builtin comparison operators, because of the sign bit. Equality checks should still work though. Score counters would probably be fine without a compare operator, but RPG experience and gold would require a new function to handle comparing 8-digit BCD.
I'm surprised this hasn't received any comments. Just today I am learning that PICO is limited to 32767 and only displays 32768 to represent "infinity."
Your code here definitely does work for 9-digit numbers and higher and that's the kind of thing that's perfect for veteran experience points or the VERY wealthy player in Goldpieces. :)
Yes, I should have found this post when I was looking for a similar solution!
[Please log in to post a comment]