This might be my most complicated post yet.
This video explains how to convert a decimal number into a binary IEEE-754 representation. However, since you’re using numbers that exceed the limits of doubles, we’ll have to use Quadruple-precision floating-point formats instead. This format uses 16 bytes (128 bits).
First, we have to separate the parts of the number into the sign, the whole, and the fraction.
local function stringToFloat(str: string): buffer
local sign, whole, fraction = string.match(str, "^(%--)(%d+)%.?(%d*)")
end
Next, we’ll calculate the binary representation for the whole
number.
To convert the whole
into binary, you must divide by two, grab the remainder, and repeat the process until you reach zero. Make sure that when dividing by two, drop the fractional part.
However, division doesn’t work properly on numbers beyond Doubles, so we’ll have to create our own makeshift division by two system with a predefined table.
local dividedBy2 = {
--formatted as: [number: string] = {wholeNumber: number, decimal: number}
["0"] = {0, 0}, --0.0
["1"] = {0, 5}, --0.5
["2"] = {1, 0}, --1.0
["3"] = {1, 5}, --1.5
["4"] = {2, 0}, --2.0
["5"] = {2, 5}, --2.5
["6"] = {3, 0}, --3.0
["7"] = {3, 5}, --3.5
["8"] = {4, 0}, --4.0
["9"] = {4, 5} --4.5
}
local function floorDivide2(str: string): (string, number) --returns the value and the remainder
local result, remainder = "", 0
for i = 1, #str do --go through every digit in str
local character = string.sub(str, i, i)
local divisionResult = dividedBy2[character] --get item in dividedBy2 table
result ..= tostring(divisionResult[1] + remainder) --append digit to result
remainder = divisionResult[2] --set remainder
end
result = string.gsub(result, "^0+", "") --remove prepended zeroes. this will remove the entire number if the string only contains zeroes
return result, remainder
end
Now, we can use this division system to convert any number into binary.
local function wholeNumberToBinary(str: string): string
local result, remainder = "", 0
while str ~= "" do
str, remainder = floorDivide2(str)
result = math.sign(remainder) .. result --prepend digit. if remainder is 5 it equals one due to the math.sign() function
end
return result
end
And, we will update our stringToFloat
function.
local function stringToFloat(str: string): buffer
local sign, whole, fraction = string.match(str, "^(%--)(%d+)%.?(%d*)")
local wholeInBinary = wholeNumberToBinary(whole)
end
Secondly, we’ll calculate the binary representation for the fraction
. It is simply multiplying by two, taking the digit in the one’s place, and setting it back to zero, and repeating the process with that number instead. This process must be repeated 112 times, since the Quadruple floating point format stores 112 bits for the fraction.
--[[
0.3 * 2 = 0.6 | 0
0.6 * 2 = 1.2 | 1 -- 1.2 turns into 0.2
0.2 * 2 = 0.4 | 0
...
]]
We now have to create another makeshift function for multiplying by two. Again, with a predefined table. This is to avoid floating point errors.
local multipliedBy2 = {
--formatted as: [number: string] = {wholeNumber: number, carryOn: number}
["0"] = {0, 0}, --0
["1"] = {2, 0}, --2
["2"] = {4, 0}, --4
["3"] = {6, 0}, --6
["4"] = {8, 0}, --8
["5"] = {0, 1}, --10
["6"] = {2, 1}, --12
["7"] = {4, 1}, --14
["8"] = {6, 1}, --16
["9"] = {8, 1} --18
}
local function multiplyBy2(str: string): string --this only works for whole numbers
local result, carry = "", 0
for i = #str, 1, -1 do --go through every digit in str backwards
local character = string.sub(str, i, i)
local multipliedResult = multipliedBy2[character] --get item in multipliedBy2 table
result = tostring(multipliedResult[1] + carry) .. result --prepend digit to result
carry = multipliedResult[2] --set carry
end
if carry > 0 then result = tostring(carry) .. result end --prepend carry to result
return result
end
Now, we can turn a fraction into binary.
local function fractionToBinary(str: string): string
local result = ""
for _ = 1, 112 do
str = multiplyBy2(str)
local digit = string.sub(str, 1, 1)
result ..= digit
str = `0{string.sub(str, 2)}`
end
return result
end
And, we will update the stringToFloat
function again.
local function stringToFloat(str: string): buffer
local sign, whole, fraction = string.match(str, "^(%--)(%d+)%.?(%d*)")
local wholeInBinary = wholeNumberToBinary(whole)
local fractionInBinary = fractionToBinary(`0{fraction}`) --always prepend a zero at the start
end
Afterwards, we’ll need to find the exponent. First, we can combine both the wholeInBinary
and the fractionInBinary
variables together. And, we’ll locate the first 1
in that string and take it’s index.
Afterwards, we’ll use that number to subtract it from the length of the wholeInBinary
variable.
Then, take that result and use that to subtract it from the exponent bias which is 16383. Therefore, the equation is 16383 + (len of wholeInBinary - index of the first one in the string)
local function stringToFloat(str: string): buffer
...
local binaryRepresentation = wholeInBinary .. fractionInBinary
local index = string.find(binaryRepresentation, "1")
local exponent = 127 + (#wholeInBinary - index)
end
Afterwards, convert the exponent
into 15 bits.
local function stringToFloat(str: string): buffer
...
exponent = wholeNumberToBinary(tostring(exponent))
for _ = 1, 15 - #exponent do
exponent = "0" .. exponent
end
end
Next, we’ll truncate the binaryRepresentation
into a string of 112 bits where the starting point is after the first 1
. This is the mantissa.
local function stringToFloat(str: string): buffer
...
binaryRepresentation = string.sub(binaryRepresentation, index + 1, 113 + index)
end
Now, we’ll convert the sign
into either a 1 or 0, depending on whether the number is negative or positive.
local function stringToFloat(str: string): buffer
local sign, whole, fraction = string.match(str, "^(%--)(%d+)%.?(%d*)")
...
sign = sign == "-" and "1" or "0"
end
We have our sign, exponent, and mantissa, so we can combine them together to get a Quadruple precision floating point number!
local function stringToFloat(str: string): buffer
...
local binaryResult = `{sign}{exponent}{binaryRepresentation}`
end
Finally, we can put them inside a buffer type, which will store our number.
local function stringToFloat(str: string): buffer
...
local numberBuffer = buffer.create(16) --create the buffer
local encodedString = ""
for i = 1, 128, 8 do --convert the binary result into a string
local byte = tonumber(string.sub(binaryResult, i, i + 7), 2)
encodedString ..= string.char(byte)
end
buffer.writestring(numberBuffer, 0, encodedString) --write it to the buffer
return numberBuffer
end
Full code (Contains no comments):
local dividedBy2 = {
["0"] = {0, 0},
["1"] = {0, 5},
["2"] = {1, 0},
["3"] = {1, 5},
["4"] = {2, 0},
["5"] = {2, 5},
["6"] = {3, 0},
["7"] = {3, 5},
["8"] = {4, 0},
["9"] = {4, 5}
}
local multipliedBy2 = {
["0"] = {0, 0},
["1"] = {2, 0},
["2"] = {4, 0},
["3"] = {6, 0},
["4"] = {8, 0},
["5"] = {0, 1},
["6"] = {2, 1},
["7"] = {4, 1},
["8"] = {6, 1},
["9"] = {8, 1}
}
local function floorDivide2(str: string): (string, number)
local result, remainder = "", 0
for i = 1, #str do
local character = string.sub(str, i, i)
local divisionResult = dividedBy2[character]
result ..= tostring(divisionResult[1] + remainder)
remainder = divisionResult[2]
end
result = string.gsub(result, "^0+", "")
return result, remainder
end
local function multiplyBy2(str: string): string
local result, carry = "", 0
for i = #str, 1, -1 do
local character = string.sub(str, i, i)
local multipliedResult = multipliedBy2[character]
result = tostring(multipliedResult[1] + carry) .. result
carry = multipliedResult[2]
end
if carry > 0 then result = tostring(carry) .. result end
return result
end
local function wholeNumberToBinary(str: string): string
local result, remainder = "", 0
while str ~= "" do
str, remainder = floorDivide2(str)
result = math.sign(remainder) .. result
end
return result
end
local function fractionToBinary(str: string): string
local result = ""
for _ = 1, 112 do
str = multiplyBy2(str)
local digit = string.sub(str, 1, 1)
result ..= digit
str = `0{string.sub(str, 2)}`
end
return result
end
local function stringToFloat(str: string): buffer
local sign, whole, fraction = string.match(str, "^(%--)(%d+)%.?(%d*)")
local wholeInBinary = wholeNumberToBinary(whole)
local fractionInBinary = fractionToBinary(`0{fraction}`)
local binaryRepresentation = wholeInBinary .. fractionInBinary
local index = string.find(binaryRepresentation, "1")
local exponent = 16383 + (#wholeInBinary - index)
exponent = wholeNumberToBinary(tostring(exponent))
for _ = 1, 15 - #exponent do
exponent = "0" .. exponent
end
binaryRepresentation = string.sub(binaryRepresentation, index + 1, index + 113)
sign = sign == "-" and "1" or "0"
local binaryResult = `{sign}{exponent}{binaryRepresentation}`
local numberBuffer = buffer.create(16)
local encodedString = ""
for i = 1, 128, 8 do
local byte = tonumber(string.sub(binaryResult, i, i + 7), 2)
encodedString ..= string.char(byte)
end
buffer.writestring(numberBuffer, 0, encodedString)
return numberBuffer
end