Напишите свою собственную реализацию функции математического пола, C

Я думал о функции floor, доступной в math.h. Его очень легко использовать:

#include <stdio.h>
#include <math.h>

int main(void)
{
  for (double a = 12.5; a < 13.4; a += 0.1)
    printf("floor of  %.1lf is  %.1lf\n", a, floor(a));
  return 0;
}

Что, если я хотел бы написать свою собственную реализацию? Будет ли это выглядеть так:

#include <stdio.h>
#include <math.h>

double my_floor(double num)
{
    return (int)num;
}

int main(void)
{
    double a;

    for (a = 12.5; a < 13.4; a += 0.1)
        printf("floor of  %.1lf is  %.1lf\n", a, floor(a));

    printf("\n\n");

    for (a = 12.5; a < 13.4; a += 0.1)
        printf("floor of  %.1lf is  %.1lf\n", a, my_floor(a));

    return 0;
}

?

Кажется, он не работает с отрицательными числами (my_floor), но второй кажется прекрасным (my_floor_2):

#include <stdio.h>
#include <math.h>

double my_floor(double num)
{
    return (int)num;
}

double my_floor_2(double num)
{
    if(num < 0)
        return (int)num - 1;
    else
        return (int)num;
}

int main(void)
{
    double a1 = -12.5;

    printf("%lf\n", floor(a1));
    printf("%lf\n", my_floor(a1));
    printf("%lf\n", my_floor_2(a1));

    return 0;
}

выход программы:

img

Является ли один из них в конечном итоге правильным или нет?

Ответ 1

Нет, вы не можете справиться с этим так. Лучший способ написать свою собственную реализацию - украсть ту, что из стандартной библиотеки C на вашей платформе. Но обратите внимание, что могут содержать специфические нюансы платформы, поэтому они могут быть не переносимыми.

Функция стандартной библиотеки C floor, как правило, умна тем, что она не работает, перейдя к интегральному типу. Если это произошло, вы рискуете переполнением целого числа signed, поведение которого равно undefined. (Обратите внимание, что наименьший возможный диапазон для int составляет от -32767 до +32767).

Точная реализация также зависит от схемы с плавающей точкой, используемой на вашей платформе.

Для платформы с плавающей точкой IEEE754 и типа long long вы можете использовать эту схему:

  • Если величина числа больше 53-й степени 2, верните ее назад (поскольку она уже является интегральной).
  • Else, добавьте к 64-битовому типу (длинный длинный) и верните его.

Ответ 2

Обе ваши попытки имеют ограничения:

  • Если значение double находится за пределами диапазона типа int, преобразование в int определяется реализацией.
  • Если значение double отрицательное, но целое, возврат (int)num - 1 неверен.

Вот (почти) переносная версия, которая пытается обрабатывать все случаи:

double my_floor_2(double num) {
    if (num >= LLONG_MAX || num <= LLONG_MIN || num != num) {
        /* handle large values, infinities and nan */
        return num;
    }
    long long n = (long long)num;
    double d = (double)n;
    if (d == num || num >= 0)
        return d;
    else
        return d - 1;
}

Это должно быть правильно, если тип long long имеет больше битов значений, чем тип double, что имеет место в большинстве современных систем.

Ответ 3

В С++ и 32-битной арифметике это можно сделать, например, следующим образом:

//---------------------------------------------------------------------------
// IEEE 754 double MSW masks
const DWORD _f64_sig    =0x80000000;    // sign
const DWORD _f64_exp    =0x7FF00000;    // exponent
const DWORD _f64_exp_sig=0x40000000;    // exponent sign
const DWORD _f64_exp_bia=0x3FF00000;    // exponent bias
const DWORD _f64_exp_lsb=0x00100000;    // exponent LSB
const DWORD _f64_exp_pos=        20;    // exponent LSB bit position
const DWORD _f64_man    =0x000FFFFF;    // mantisa
const DWORD _f64_man_msb=0x00080000;    // mantisa MSB
const DWORD _f64_man_bits=       52;    // mantisa bits
// IEEE 754 single masks
const DWORD _f32_sig    =0x80000000;    // sign
const DWORD _f32_exp    =0x7F800000;    // exponent
const DWORD _f32_exp_sig=0x40000000;    // exponent sign
const DWORD _f32_exp_bia=0x3F800000;    // exponent bias
const DWORD _f32_exp_lsb=0x00800000;    // exponent LSB
const DWORD _f32_exp_pos=        23;    // exponent LSB bit position
const DWORD _f32_man    =0x007FFFFF;    // mantisa
const DWORD _f32_man_msb=0x00400000;    // mantisa MSB
const DWORD _f32_man_bits=       23;    // mantisa bits
//---------------------------------------------------------------------------
double f64_floor(double x)
    {
    const int h=1;      // may be platform dependent MSB/LSB order
    const int l=0;
    union _f64          // semi result
        {
        double f;       // 64bit floating point
        DWORD u[2];     // 2x32 bit uint
        } y;
    DWORD m,a;
    int sig,exp,sh;
    y.f=x;
    // extract sign
    sig =y.u[h]&_f64_sig;
    // extract exponent
    exp =((y.u[h]&_f64_exp)>>_f64_exp_pos)-(_f64_exp_bia>>_f64_exp_pos);
    // floor bit shift
    sh=_f64_man_bits-exp; a=0;
    if (exp<0)
        {
        a=y.u[l]|(y.u[h]&_f64_man);
        if (sig) return -1.0;
        return 0.0;
        }
    // LSW
    if (sh>0)
        {
        if (sh<32) m=(0xFFFFFFFF>>sh)<<sh; else m=0;
        a=y.u[l]&(m^0xFFFFFFFF); y.u[l]&=m;
        }
    // MSW
    sh-=32;
    if (sh>0)
        {
        if (sh<_f64_exp_pos) m=(0xFFFFFFFF>>sh)<<sh; else m=_f64_sig|_f64_exp;
        a|=y.u[h]&(m^0xFFFFFFFF); y.u[h]&=m;
        }
    if ((sig)&&(a)) y.f--;
    return y.f;
    }
//---------------------------------------------------------------------------
float f32_floor(float x)
    {
    union               // semi result
        {
        float f;        // 32bit floating point
        DWORD u;        // 32 bit uint
        } y;
    DWORD m,a;
    int sig,exp,sh;
    y.f=x;
    // extract sign
    sig =y.u&_f32_sig;
    // extract exponent
    exp =((y.u&_f32_exp)>>_f32_exp_pos)-(_f32_exp_bia>>_f32_exp_pos);
    // floor bit shift
    sh=_f32_man_bits-exp; a=0;
    if (exp<0)
        {
        a=y.u&_f32_man;
        if (sig) return -1.0;
        return 0.0;
        }
    if (sh>0)
        {
        if (sh<_f32_exp_pos) m=(0xFFFFFFFF>>sh)<<sh; else m=_f32_sig|_f32_exp;
        a|=y.u&(m^0xFFFFFFFF); y.u&=m;
        }
    if ((sig)&&(a)) y.f--;
    return y.f;
    }
//---------------------------------------------------------------------------

Цель состоит в том, чтобы сделать маску, которая очистит десятичные биты от мантиссы, а в случае отрицательного ввода и ненулевые очищенные биты уменьшат результат. Чтобы получить доступ к отдельным битам, вы можете преобразовать значение с плавающей запятой в интегральное представление с использованием объединения (например, в примере) или вместо этого использовать указатели.

Я тестировал это в простом VCL приложении:

float f32;
double f64;
AnsiString txt="";
// 64 bit
txt+="[double]\r\n";
for (f64=-10.0;f64<=10.0;f64+=0.1)
 if (fabs(floor(f64)-f64_floor(f64))>1e-6)
    {
    txt+=AnsiString().sprintf("%5.3lf %5.3lf %5.3lf\r\n",f64,floor(f64),f64_floor(f64));
    f64_floor(f64);
    }
for (f64=1;f64<=1e307;f64*=1.1)
    {
    if (fabs(floor( f64)-f64_floor( f64))>1e-6) { txt+=AnsiString().sprintf("%lf lf lf\r\n", f64,floor( f64),f64_floor( f64));
    f64_floor( f64); }
    if (fabs(floor(-f64)-f64_floor(-f64))>1e-6) { txt+=AnsiString().sprintf("%lf lf lf\r\n",-f64,floor(-f64),f64_floor(-f64));
    f64_floor(-f64); }
    }
// 32 bit
txt+="[float]\r\n";
for (f32=-10.0;f32<=10.0;f32+=0.1)
 if (fabs(floor(f32)-f32_floor(f32))>1e-6)
    {
    txt+=AnsiString().sprintf("%5.3lf %5.3lf %5.3lf\r\n",f32,floor(f32),f32_floor(f32));
    f32_floor(f32);
    }
for (f32=1;f32<=1e37;f32*=1.1)
    {
    if (fabs(floor( f32)-f32_floor( f32))>1e-6) { txt+=AnsiString().sprintf("%lf lf lf\r\n", f32,floor( f32),f32_floor( f32));
    f32_floor( f32); }
    if (fabs(floor(-f32)-f32_floor(-f32))>1e-6) { txt+=AnsiString().sprintf("%lf lf lf\r\n",-f32,floor(-f32),f32_floor(-f32));
    f32_floor(-f32); }
    }
mm_log->Lines->Add(txt);

без разностного результата (поэтому во всех проверенных случаях он соответствует значениям math.h floor(). Если вы хотите сделать этот снимок вне VCL, просто измените AnsiString на любой тип строки вы получили под рукой и изменили вывод с TMemo::mm_log на все, что у вас было (например, console cout или что-то еще)

Двойной вызов fxx_floor() в случае разницы - для целей отладки (вы можете поместить точку останова и шаг в случае ошибки напрямую).

[Примечание]

Остерегайтесь того, что порядок слов (MSW, LSW) зависит от платформы, поэтому вы должны соответствующим образом скорректировать константы h,l. Этот код не оптимизирован, поэтому его легко понять, поэтому не ожидайте, что он будет быстрым.