# How to convert strings into a floating point number?

I’m going to be blunt. I’m looking for a way to convert numerical strings directly into their IEEE-754 (floating point) representation in binary. (Without converting the string into a number. “1000” → 1000 isn’t allowed!)
I’ll be using a `buffer` to store the data.

``````--example
function stringToFloat(str: string): buffer
--magic
end

local float = stringToFloat("1000.1") --buffer: 0 10001000 11110100000011001100110
``````

Here are some resources you may find helpful.

I’m open to all suggestions, even an explanation on how to do this would be great.

``````print(tonumber("1.54"))
``````

a very small line of code, i see you have experience with C

Unfortunately for my case, I can’t use `tonumber` to convert it into a number.

If you want to know the exact reason why: `tonumber` only works with strings within the range of a double. Any value exceeding what a double can hold will be interpreted as `inf`. The strings I’m working with will exceed the range of doubles.
I wrote this post in the most literal sense possible.

ohh understood, unfortunately my brain is stinky and small and i cant help you with this, but im very interested and i will try to find informations about. good luck!

1 Like

Written using ChatGPT:

``````function toIEEE754Binary(numString)
-- Helper function to convert an integer to a binary string
local function integerToBinary(integer)
local binary = ""
while integer > 0 do
binary = (integer % 2) .. binary
integer = math.floor(integer / 2)
end
return binary
end

-- Helper function to convert a decimal part to a binary string
local function fractionalToBinary(fractional)
local binary = ""
local precision = 23 -- Assuming single precision
while precision > 0 and fractional > 0 do
fractional = fractional * 2
if fractional >= 1 then
binary = binary .. "1"
fractional = fractional - 1
else
binary = binary .. "0"
end
precision = precision - 1
end
return binary
end

-- Parse the numerical string
local integerPart, fractionalPart = string.match(numString, "(%d+)%.?(%d*)")
integerPart = tonumber(integerPart) or 0
fractionalPart = tonumber("0." .. (fractionalPart or "0")) or 0.0

-- Convert to binary representation
local binaryInteger = integerToBinary(integerPart)
local binaryFractional = fractionalToBinary(fractionalPart)

-- Normalize the binary number
local normalizedBinary = binaryInteger .. binaryFractional

-- Calculate exponent and mantissa
local exponent = #binaryInteger - 1
local mantissa = string.sub(binaryInteger .. binaryFractional, 2, 24)

-- IEEE-754 single precision: 1 sign bit, 8 exponent bits, 23 mantissa bits
local sign = 0 -- Assuming positive number
local exponentBits = integerToBinary(exponent + 127) -- Bias of 127 for single precision

-- Ensure exponentBits is 8 bits long
exponentBits = string.rep("0", 8 - #exponentBits) .. exponentBits

-- Ensure mantissa is 23 bits long
mantissa = mantissa .. string.rep("0", 23 - #mantissa)

-- Combine sign, exponent, and mantissa
local ieee754Binary = sign .. exponentBits .. string.sub(mantissa, 1, 23)
return ieee754Binary
end

-- Example usage
local binaryRepresentation = toIEEE754Binary("1000")
print(binaryRepresentation) --output: 01000100011110100000000000000000

``````

I’m not sure if this is what you are looking for but I hope it helps.

1 Like

You should perhaps look into the work done by things like the Quake 3 quick square root. The code does somewhat whats asked, I have never worked with buffers, and the utility for this is quite low, as you probably will not face a number that is higher than a double that is 64 bits truly.

1 Like

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
``````

``````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
``````
1 Like

Holy crap! What an elegant and brilliant solution! You blew my mind! Thank you for the thorough explanation and code. I mean it.

1 Like