您并没有真正指定是否需要将浮点数或字符串转换为比率,所以我将假设前者。
您可以直接使用IEEE-754 编码的属性,而不是尝试基于字符串或算术的方法。
浮点数(按标准称为binary32)在内存中编码如下:
S EEEEEEEE MMMMMMMMMMMMMMMMMMMMMMM
^ ^
bit 31 bit 0
其中S 是符号位,Es 是指数位(其中 8 个)Ms 是尾数位(23 位)。
数字可以这样解码:
value = (-1)^S * significand * 2 ^ expoenent
where:
significand = 1.MMMMMMMMMMMMMMMMMMMMMMM (as binary)
exponent = EEEEEEEE (as binary) - 127
(注意:这是所谓的“正常数”,还有零、次正规、无穷大和 NaN - 请参阅我链接的 Wikipedia 页面)
这里可以使用。我们可以把上面的等式改写成这样:
(-1)^S * significand * exponent = (-1)^s * (significand * 2^23) * 2 ^ (exponent - 23)
重点是significand * 2^23 是一个整数(等于1.MMMMMMMMMMMMMMMMMMMMMMM,二进制 - 通过乘以 2^23,我们将点向右移动了 23 位)。2 ^ (exponent - 23) 显然也是一个整数。
换句话说:我们可以将数字写成:
(significand * 2^23) / 2^(-(exponent - 23)) (when exponent - 23 < 0)
or
[(significand * 2^23) * 2^(exponent - 23)] / 1 (when exponent - 23 >= 0)
所以我们有分子和分母——直接来自数字的二进制表示。
以上所有内容都可以在 C++ 中这样实现:
struct Ratio
{
int64_t numerator; // numerator includes sign
uint64_t denominator;
float toFloat() const
{
return static_cast<float>(numerator) / denominator;
}
static Ratio fromFloat(float v)
{
// First, obtain bitwise representation of the value
const uint32_t bitwiseRepr = *reinterpret_cast<uint32_t*>(&v);
// Extract sign, exponent and mantissa bits (as stored in memory) for convenience:
const uint32_t signBit = bitwiseRepr >> 31u;
const uint32_t expBits = (bitwiseRepr >> 23u) & 0xffu; // 8 bits set
const uint32_t mntsBits = bitwiseRepr & 0x7fffffu; // 23 bits set
// Handle some special cases:
if(expBits == 0 && mntsBits == 0)
{
// special case: +0 and -0
return {0, 1};
}
else if(expBits == 255u && mntsBits == 0)
{
// special case: +inf, -inf
// Let's agree that infinity is always represented as 1/0 in Ratio
return {signBit ? -1 : 1, 0};
}
else if(expBits == 255u)
{
// special case: nan
// Let's agree, that if we get NaN, we returns max int64_t by 0
return {std::numeric_limits<int64_t>::max(), 0};
}
// mask lowest 23 bits (mantissa)
uint32_t significand = (1u << 23u) | mntsBits;
const int64_t signFactor = signBit ? -1 : 1;
const int32_t exp = expBits - 127 - 23;
if(exp < 0)
{
return {signFactor * static_cast<int64_t>(significand), 1u << static_cast<uint32_t>(-exp)};
}
else
{
return {signFactor * static_cast<int64_t>(significand * (1u << static_cast<uint32_t>(exp))), 1};
}
}
};
(希望上面的 cmets 和描述可以理解 - 如果有需要改进的地方,请告诉我)
为简单起见,我省略了对超出范围值的检查。
我们可以这样使用它:
float fv = 1.375f;
Ratio rv = Ratio::fromFloat(fv);
std::cout << "fv = " << fv << ", rv = " << rv << ", rv.toFloat() = " << rv.toFloat() << "\n";
输出是:
fv = 1.375, rv = 11534336/8388608, rv.toFloat() = 1.375
如您所见,两端的值完全相同。
问题在于分子和分母都很大。这是因为代码总是将有效位乘以 2^23,即使较小的值足以使其成为整数(这相当于将 0.2 写为 2000000/10000000 而不是 2/10 - 这是同一件事,只是写法不同) .
这可以通过将代码更改为将有效数乘以(并除以指数)乘以最小数字来解决,如下所示(省略号代表与上述相同的部分):
// counts number of subsequent least significant bits equal to 0
// example: for 1001000 (binary) returns 3
uint32_t countTrailingZeroes(uint32_t v)
{
uint32_t counter = 0;
while(counter < 32 && (v & 1u) == 0)
{
v >>= 1u;
++counter;
}
return counter;
}
struct Ratio
{
...
static Ratio fromFloat(float v)
{
...
uint32_t significand = (1u << 23u) | mntsBits;
const uint32_t nTrailingZeroes = countTrailingZeroes(significand);
significand >>= nTrailingZeroes;
const int64_t signFactor = signBit ? -1 : 1;
const int32_t exp = expBits - 127 - 23 + nTrailingZeroes;
if(exp < 0)
{
return {signFactor * static_cast<int64_t>(significand), 1u << static_cast<uint32_t>(-exp)};
}
else
{
return {signFactor * static_cast<int64_t>(significand * (1u << static_cast<uint32_t>(exp))), 1};
}
}
};
现在,对于以下代码:
float fv = 1.375f;
Ratio rv = Ratio::fromFloat(fv);
std::cout << "fv = " << fv << ", rv = " << rv << ", rv.toFloat() = " << rv.toFloat() << "\n";
我们得到:
fv = 1.375,rv = 11/8,rv.toFloat() = 1.375