【问题标题】:how can i get numerator and denominator from a fractional number?如何从小数中获取分子和分母?
【发布时间】:2018-06-21 06:47:01
【问题描述】:

如何从小数中获取分子和分母?例如,从“1.375”我想得到“1375/1000”或“11/8”作为结果。我怎么能用 C++ 做到这一点? 我试图通过分隔点之前和之后的数字来做到这一点,但它不知道如何获得我想要的输出。

【问题讨论】:

  • 你知道用铅笔和一张纸怎么做吗?
  • 开始:1375/1000 = 1 和 375/1000(小数部分)
  • 乘以 10,直到它不是整数得到 1375/1000 [然后将两个数字除以它们的 GCD 得到 11/8]
  • 需要一点小心。许多看起来不错的分数数字并不能产生特别令人愉快的浮点数。见Is floating point math broken?
  • 您想要const char *std::string 还是其他东西(doubleboost::rational<int>?)。您的源对象(字符串文字)"1.375" 或(双重文字)1.375 还是其中一种类型的变量?

标签: c++


【解决方案1】:

您并没有真正指定是否需要将浮点数或字符串转换为比率,所以我将假设前者。

您可以直接使用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

【讨论】:

    【解决方案2】:

    在 C++ 中,您可以使用 Boost 理性类。但是你需要给出分子和分母。

    为此,您需要找出输入字符串中小数点后的数字。您可以通过字符串操作函数来做到这一点。逐个字符读取输入的字符,发现.后面没有字符

    char inputstr[30]; 
    int noint=0, nodec=0;
    char intstr[30], dec[30];
    int decimalfound = 0;
    int denominator = 1;
    int numerator;
    
    scanf("%s",inputstr);
    
    len = strlen(inputstr);
    
    for (int i=0; i<len; i++)
    {
        if (decimalfound ==0)
        {
            if (inputstr[i] == '.')
            {
                decimalfound = 1;
            }
            else
            {
                intstr[noint++] = inputstr[i];
            }
         }
         else
         {
            dec[nodec++] = inputstr[i];
            denominator *=10;
         }
    }
    dec[nodec] = '\0';
    intstr[noint] = '\0';
    
    numerator = atoi(dec) + (atoi(intstr) * 1000);
    
    // You can now use the numerator and denominator as the fraction, 
    // either in the Rational class or you can find gcd and divide by 
    // gcd.
    

    【讨论】:

    • 您没有输入验证。如果用户输入的长度超过您为inputstr 分配的大小,您可能会遇到分段错误。并且没有检查以确保输入是有效的十进制值。
    • decimalfound 需要初始化为0denominator1(不是10)。有其他人实际测试过这段代码吗?
    • @DavidCollins - 我已经制作了一个基本代码,而不是所有检查的东西。所以没有添加长度检查和输入有效性检查。我已经解决了您提到的decimalfounddenominator 的问题。
    【解决方案3】:

    这个简单的代码怎么样:

    double n = 1.375;
    int num = 1, den = 1;
    double frac = (num * 1.f / den);
    double margin = 0.000001;
    while (abs(frac - n) > margin){
        if (frac > n){
            den++;
        }
        else{
            num++;
        }
        frac = (num * 1.f / den);
    }
    

    我并没有真正测试太多,这只是一个想法。

    【讨论】:

      【解决方案4】:

      我希望我发布一个使用“仅C 语言”的答案会被原谅。我知道你用C++ 标记了这个问题 - 但我无法拒绝诱饵,抱歉。这仍然是有效的 C++ 至少(尽管它确实主要使用 C 字符串处理技术)。

      int num_string_float_to_rat(char *input, long *num, long *den) {
          char *tok = NULL, *end = NULL;
          char buf[128] = {'\0'};
          long a = 0, b = 0;
          int den_power = 1;
      
          strncpy(buf, input, sizeof(buf) - 1);
      
          tok = strtok(buf, ".");
          if (!tok) return 1;
          a = strtol(tok, &end, 10);
          if (*end != '\0') return 2;
          tok = strtok(NULL, ".");
          if (!tok) return 1;
          den_power = strlen(tok);  // Denominator power of 10
          b = strtol(tok, &end, 10);
          if (*end != '\0') return 2;
      
          *den = static_cast<int>(pow(10.00, den_power));
          *num = a * *den + b;
      
          num_simple_fraction(num, den);
      
          return 0;
      }
      

      示例用法:

      int rc = num_string_float_to_rat("0015.0235", &num, &den);
      // Check return code -> should be 0!
      printf("%ld/%ld\n", num, den);
      

      输出:

      30047/2000
      

      http://codepad.org/CFQQEZkc 上的完整示例。

      注意事项:

      • strtok() 用于将输入解析为令牌(在这方面无需重新发明轮子)。 strtok() 修改其输入 - 因此使用临时缓冲区以确保安全
      • 它检查无效字符 - 如果找到,将返回一个非零返回码
      • 已使用 strtol() 代替 atoi() - 因为它可以检测输入中的非数字字符
      • scanf() 没有被用于 slurp 输入 - 由于浮点数的舍入问题
      • strtol() 的基数已明确设置为 10,以避免前导零出现问题(否则前导零会导致数字被解释为八进制)
      • 它使用 num_simple_fraction() 帮助器(未显示) - 反过来又使用 gcd() 帮助器(也未显示) - 将结果转换为简单分数
      • 分子的log10()是通过计算小数点后token的长度来确定的

      【讨论】:

        【解决方案5】:

        我会分三步完成。

        1) 找到小数点,这样你就知道分母有多大了。

        2) 得到分子。就是去掉了小数点的原文。

        3) 得到分母。如果没有小数点,则分母为 1。否则,分母为 10^n,其中 n 是(现已删除的)小数点右侧的位数。

        struct fraction {
            std::string num, den;
        };
        
        fraction parse(std::string input) {
            // 1:
            std::size_t dec_point = input.find('.');
        
            // 2:
            if (dec_point == std::string::npos)
                dec_point = 0;
            else {
                dec_point = input.length() - dec_point;
                input.erase(input.begin() + dec_point);
            }
        
            // 3:
            int denom = 1;
            for (int i = 1; i < dec_point; ++i)
                denom *= 10;
            string result = { input, std::to_string(denom) };
            return result;
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-09-25
          • 2012-10-13
          • 2017-12-16
          相关资源
          最近更新 更多