I don't know how inefficient or bad this is, but my tests are passing. There's probably many other better ways, but the original quad value is never converted to a double in this case.
Code: Select all
Procedure.s FormatCents(value.q, decimals.i = 2, point.c = '.', separator.c = ',')
If (value = 0)
; no need to process anything, easy.
ProcedureReturn "0" + Chr(point) + "00"
EndIf
; remember that we have a sign, but we do the math in non-negative space
Protected sign = #False
If (value < 0)
value * -1
sign = #True
EndIf
Protected decimals_mod.q = Pow(10, decimals) ; calcualte the rhs for the modulo
Protected cents.q = value % decimals_mod ; extract what we put as cents later
Protected segment.q = 0
value / decimals_mod ; remove the cents we just extracted
Protected int_digits = Int(Round(Log10(value), #PB_Round_Down) + 1) ; the number of digits, at least. log10 fails at a certain point, but it's never lower.
If value <= 0 : int_digits = 1 : EndIf ; if the remaining value was 0, log10 is -Inf, but we know there will be 1 digit: 0
Protected segments = Int(Round(int_digits / 3.0, #PB_Round_Up)) ; the number of thousands segments
; calculate the memory we need
Protected required_characters = (int_digits + (segments - 1) + (1 + decimals))
If (sign) : required_characters + 1 : EndIf
; put that memory on the stack, as it is (probably) never that much.
Protected Dim stack_memory.c(required_characters + 1)
stack_memory(required_characters) = 0
; initialize our cursor, we work from the right to left.
Protected *current.Character = @stack_memory() + required_characters * SizeOf(Character)
; insert the decimal point and the decimals
*current - (1 + decimals) * SizeOf(Character)
*current\c = point
PokeS(*current + SizeOf(Character), RSet(Str(cents), decimals, "0"), -1, #PB_String_NoZero)
If (value = 0)
; while loop will never enter, just put '0'
*current - SizeOf(Character)
*current\c = '0'
EndIf
While (value)
segment = value % 1000 ; get thousandth segment
value / 1000 ; remove thousandth segment from value
If value
; there is still a value remaining after this, so we create a ,NNN segment in the string
*current - 4 * SizeOf(Character)
*current\c = separator
PokeS(*current + SizeOf(Character), RSet(Str(segment), 3, "0"), -1, #PB_String_NoZero)
Else
; put whatever is left at the beginning
Protected last_segment.s = Str(segment)
*current - StringByteLength(last_segment)
PokeS(*current, last_segment, -1, #PB_String_NoZero)
EndIf
Wend
If (sign)
; there was a sign, so add that as a last operation
*current - SizeOf(Character)
*current\c = '-'
EndIf
; peek from where we left off, which will usually be at the start of the allocated
; memory. In some cases, though, i.e. with 999999999999999999 and -999999999999999999
; more memory than needed will be allocated because log10 actually returns the number
; of digits for those values.
ProcedureReturn PeekS(*current)
EndProcedure
Structure TestCase
value.q
expected.s
line_number.i
EndStructure
NewList TestCases.TestCase()
Macro AddTestCase(_V_, _E_)
; no sign
AddElement(TestCases())
TestCases()\value = _V_
TestCases()\expected = _E_
TestCases()\line_number = #PB_Compiler_Line
; sign
If (_V_ <> 0)
AddElement(TestCases())
TestCases()\value = -_V_
TestCases()\expected = "-" + _E_
TestCases()\line_number = #PB_Compiler_Line
EndIf
EndMacro
AddTestCase(000, "0.00")
AddTestCase(087, "0.87")
AddTestCase(100, "1.00")
AddTestCase(001, "0.01")
AddTestCase(150087, "1,500.87")
AddTestCase(150001, "1,500.01")
AddTestCase(100, "1.00")
AddTestCase(250, "2.50")
AddTestCase(12345, "123.45")
AddTestCase(987654, "9,876.54")
AddTestCase(5000, "50.00")
AddTestCase(888888, "8,888.88")
AddTestCase(999999, "9,999.99")
AddTestCase(1000000, "10,000.00")
AddTestCase(1234567, "12,345.67")
AddTestCase(98765, "987.65")
AddTestCase(123, "1.23")
AddTestCase(456, "4.56")
AddTestCase(789, "7.89")
AddTestCase(2501, "25.01")
AddTestCase(87654321, "876,543.21")
AddTestCase(8888, "88.88")
AddTestCase(54321, "543.21")
AddTestCase(111111, "1,111.11")
AddTestCase(999, "9.99")
AddTestCase(123456, "1,234.56")
AddTestCase(98765432, "987,654.32")
AddTestCase(987654321, "9,876,543.21")
AddTestCase(1000000000, "10,000,000.00")
AddTestCase(123456789, "1,234,567.89")
AddTestCase(500, "5.00")
AddTestCase(3210, "32.10")
AddTestCase(9876543210, "98,765,432.10")
AddTestCase(55555, "555.55")
AddTestCase(666666, "6,666.66")
AddTestCase(7777777, "77,777.77")
AddTestCase(1234, "12.34")
AddTestCase(7890, "78.90")
AddTestCase(987654321987654321, "9,876,543,219,876,543.21")
AddTestCase(123456789012345678, "1,234,567,890,123,456.78")
AddTestCase(999999999999999999, "9,999,999,999,999,999.99")
AddTestCase(1111111111111111111, "11,111,111,111,111,111.11")
AddTestCase(8446744073709551616, "84,467,440,737,095,516.16")
AddTestCase(5555555555555555555, "55,555,555,555,555,555.55")
AddTestCase(6666666666666666666, "66,666,666,666,666,666.66")
AddTestCase(7777777777777777777, "77,777,777,777,777,777.77")
AddTestCase(8888888888888888888, "88,888,888,888,888,888.88")
AddTestCase(1234567890123456789, "12,345,678,901,234,567.89")
AddTestCase(1234567890987654321, "12,345,678,909,876,543.21")
AddTestCase(1111111100000000000, "11,111,111,000,000,000.00")
AddTestCase(987654321000000000, "9,876,543,210,000,000.00")
AddTestCase(1230000000, "12,300,000.00")
AddTestCase(111, "1.11")
AddTestCase(8765432100000, "87,654,321,000.00")
AddTestCase(987654321987654321, "9,876,543,219,876,543.21")
#ONLY_GOOD = #False
#ONLY_FAIL = #False
ForEach (TestCases())
result.s = FormatCents(TestCases()\value, 2)
If (result = TestCases()\expected)
CompilerIf #ONLY_GOOD Or Not #ONLY_FAIL
Debug "[GOOD](" + RSet(Str(TestCases()\line_number), 4, "0") + ") " + Str(TestCases()\value) + ": " + TestCases()\expected + " = " + result
CompilerEndIf
Else
CompilerIf #ONLY_FAIL Or Not #ONLY_GOOD
Debug "[FAIL](" + RSet(Str(TestCases()\line_number), 4, "0") + ") " + Str(TestCases()\value) + ": " + TestCases()\expected + " != " + result
CompilerEndIf
EndIf
Next