Bandingkan mengapung di php

157

Saya ingin membandingkan dua float di PHP, seperti dalam kode contoh ini:

$a = 0.17;
$b = 1 - 0.83; //0.17
if($a == $b ){
 echo 'a and b are same';
}
else {
 echo 'a and b are not same';
}

Dalam kode ini ia mengembalikan hasil elsekondisi bukannya ifkondisi, meskipun $adan $bsama. Apakah ada cara khusus untuk menangani / membandingkan float di PHP?

Jika ya maka tolong bantu saya untuk menyelesaikan masalah ini.

Atau apakah ada masalah dengan konfigurasi server saya?

Santosh Sonarikar
sumber
Saya mengerti a and b are same. Apakah ini kode lengkap Anda?
Pekka
versi apa? itu bekerja dengan baik untuk saya.
gblazex
@Andrey ini mungkin karena kasus dunia nyata cenderung lebih kompleks daripada contoh yang dikutip. Mengapa tidak menambahkannya sebagai jawaban?
Pekka
2
Apakah Anda membaca floating-pointdeskripsi tag? stackoverflow.com/tags/floating-point/info Itulah perilaku yang mungkin Anda temui dalam bahasa pemrograman apa pun, saat menggunakan angka floating-point. Lihat misalnya stackoverflow.com/questions/588004/is-javascripts-math-broken
Piskvor meninggalkan gedung

Jawaban:

232

Jika Anda melakukannya seperti ini mereka harus sama. Tetapi perhatikan bahwa karakteristik nilai floating-point adalah bahwa perhitungan yang tampaknya menghasilkan nilai yang sama tidak perlu benar-benar identik. Jadi jika $aitu literal .17dan $btiba di sana melalui perhitungan, bisa jadi keduanya berbeda, meskipun keduanya menampilkan nilai yang sama.

Biasanya Anda tidak pernah membandingkan nilai floating-point untuk kesetaraan seperti ini, Anda perlu menggunakan perbedaan terkecil yang dapat diterima:

if (abs(($a-$b)/$b) < 0.00001) {
  echo "same";
}

Sesuatu seperti itu.

Joey
sumber
21
AWAS! Memilih epsilon yang diperbaiki adalah cara yang buruk hanya karena terlihat kecil, perbandingan ini akan kembali benar dalam banyak kesalahan presisi ketika angkanya kecil. Cara yang benar adalah dengan memeriksa apakah kesalahan relatif lebih kecil dari epsilon. abs($a-$b)>abs(($a-$b)/$b)
Piet Bijl
1
@Alexandru: Saya tahu maksud Anda, tetapi PHP tidak sendirian dalam hal itu. Anda perlu membedakan dua use case di sini: Menampilkan nomor kepada pengguna. Dalam hal ini menampilkan 0.10000000000000000555111512312578270211815834045410156biasanya tidak ada gunanya dan mereka lebih suka 0.1sebaliknya. Dan menulis nomor sehingga dapat dibaca lagi dengan cara yang persis sama. Seperti yang Anda lihat, ini tidak sejelas yang Anda bayangkan. Dan sebagai catatan, Anda masih ingin membandingkan angka floating-point seperti yang saya tunjukkan karena Anda dapat mencapai $adan $bmelalui perhitungan berbeda yang dapat membuatnya berbeda.
Joey
2
Masih ada beberapa kasus tepi yang gagal tes ini. Seperti a=b=0dan jika aadalah yang terkecil yang mungkin tidak ada nilai positif nol dan bmerupakan nilai terkecil yang mungkin bukan nol negatif, tes tersebut akan gagal. Beberapa informasi bagus di sini: floating-point-gui.de/errors/comparison
Dom
13
Mengapa membagi $b? yang pengguna PHP hanya melakukan if(abs($a-$b) < $epsilon) yang manual MySQL juga melakukan hal yang samaHAVING ABS(a - b) <= 0.0001
Akuntan م
1
@CaslavSabani: Ini relatif, bukan kesalahan absolut. Itu masih rusak (terutama ketika $a == $b == 0, tetapi sudah jauh lebih umum daripada kesalahan absolut. Jika $adan $bada dalam jutaan, maka Anda EPSILONharus sangat berbeda daripada jika $adan $bberada di suatu tempat dekat 0. Lihat tautan Dom di atas untuk diskusi yang lebih baik tentang ini
Joey
65

Baca peringatan merah di manual terlebih dahulu. Anda tidak boleh membandingkan pelampung untuk kesetaraan. Anda harus menggunakan teknik epsilon.

Sebagai contoh:

if (abs($a-$b) < PHP_FLOAT_EPSILON) {  }

di mana PHP_FLOAT_EPSILONkonstanta mewakili angka yang sangat kecil (Anda harus mendefinisikannya dalam versi PHP lama sebelum 7.2)

Andrey
sumber
2
Untuk memperjelas, apakah EPSILON dalam hal ini adalah mesin epsilon, yang kira-kira 2.2204460492503E-16? Dan, apakah perbandingan ini berfungsi untuk dua pelampung dengan besaran berapa pun?
Michael Cordingley
1
@MichaelCordingley Tidak, di EPSILONsini adalah konstanta yang ditentukan pengguna secara sewenang-wenang. PHP tidak memiliki konstanta bawaan yang mewakili ide spesifik arsitektur epsilon. (Lihat juga get_defined_constants.)
Uskup
5
PHP_FLOAT_EPSILONAngka positif terkecil yang dapat diwakili x, sehingga x + 1.0! = 1.0. Tersedia pada PHP 7.2.0.
Code4R7
2
Ini sebenarnya tidak berfungsi dalam kasus ini: $ a = 270.10 + 20.10; $ b = 290.20; if (abs ($ a- $ b) <PHP_FLOAT_EPSILON) {echo 'same'; }
NemoXP
@NemoXP karena ekspresi itu menghasilkan angka yang berbeda. echo $a - $b; /* 5.6843418860808E-14 */ echo PHP_FLOAT_EPSILON; /* 2.2204460492503E-16 */Pertanyaannya adalah bagaimana Anda ingin mendefinisikan "sama" untuk aplikasi Anda, seberapa dekat angkanya harus dianggap sama.
Andrey
29

Atau coba gunakan fungsi bc matematika:

<?php
$a = 0.17;
$b = 1 - 0.83; //0.17

echo "$a == $b (core comp oper): ", var_dump($a==$b);
echo "$a == $b (with bc func)  : ", var_dump( bccomp($a, $b, 3)==0 );

Hasil:

0.17 == 0.17 (core comp oper): bool(false)
0.17 == 0.17 (with bc func)  : bool(true)
Mario
sumber
2
dalam penggunaan bccomp, Anda telah melewatkan "skala" sehingga Anda benar-benar membandingkan 0 hingga 0 menurut manual: php.net/manual/en/function.bccomp.php
stefancarlton
Saya suka ini. Sebagian besar solusi tampaknya mengandalkan pembulatan dan kehilangan presisi, tetapi saya berurusan dengan koordinat lintang dan bujur dengan 12 titik presisi dan ini tampaknya membandingkan mereka secara akurat tanpa perlu penyesuaian.
Rikaelus
Bagaimana dengan kinerja? bccomp()mengambil string sebagai argumen. Pokoknya Anda bisa menggunakan PHP_FLOAT_DIGuntuk argumen skala.
Code4R7
19

Seperti yang dikatakan sebelumnya, berhati-hatilah saat melakukan perbandingan floating point (apakah sama dengan, lebih besar, atau kurang dari) di PHP. Namun, jika Anda hanya tertarik pada beberapa digit signifikan, Anda dapat melakukan sesuatu seperti:

$a = round(0.17, 2);
$b = round(1 - 0.83, 2); //0.17
if($a == $b ){
    echo 'a and b are same';
}
else {
    echo 'a and b are not same';
}

Penggunaan pembulatan ke 2 tempat desimal (atau 3, atau 4) akan menyebabkan hasil yang diharapkan.

Michael Butler
sumber
1
Kata peringatan tambahan, saya tidak akan merekomendasikan mengotori basis kode Anda dengan pernyataan seperti ini. Jika Anda ingin melakukan perbandingan float longgar, buat metode seperti loose_float_comparesehingga jelas apa yang sedang terjadi.
Michael Butler
PHP asli bccomp($a, $b, 2)lebih unggul daripada solusi Anda. Dalam contoh ini, 2 adalah ketepatan. Anda dapat mengaturnya ke jumlah floating point yang ingin Anda bandingkan.
John Miller
@JohnMiller Saya tidak setuju dengan Anda, tetapi bccomp tidak tersedia secara default. Ini membutuhkan salah satu flag kompilasi untuk diaktifkan, atau ekstensi diinstal. Bukan bagian dari inti.
Michael Butler
17

Akan lebih baik menggunakan perbandingan PHP asli :

bccomp($a, $b, 3)
// Third parameter - the optional scale parameter
// is used to set the number of digits after the decimal place
// which will be used in the comparison. 

Mengembalikan 0 jika kedua operan sama, 1 jika left_operand lebih besar dari right_operand, -1 sebaliknya.

FieryCat
sumber
10

Jika Anda memiliki nilai floating point untuk dibandingkan dengan kesetaraan, cara sederhana untuk menghindari risiko strategi pembulatan internal OS, bahasa, prosesor atau sebagainya, adalah membandingkan representasi string dari nilai-nilai tersebut.

Anda dapat menggunakan salah satu dari yang berikut ini untuk menghasilkan hasil yang diinginkan: https://3v4l.org/rUrEq

Pengecoran Tipe String

if ( (string) $a === (string) $b) {  }

Penggabungan string

if ('' . $a === '' . $b) {  }

fungsi strval

if (strval($a) === strval($b)) {  }

Representasi string jauh lebih rewel daripada mengapung ketika datang untuk memeriksa kesetaraan.

Ame Nomade
sumber
atau jika (strval ($ a) === strval ($ b)) {...} jika Anda tidak ingin mengonversi nilai asli
Ekonoval
Yah, jawaban asli saya adalah: if (''. $ A === ''. $ B) {...} tetapi seseorang mengeditnya. Jadi ...
Ame Nomade
1
@Ekonoval Bisakah Anda uraikan modifikasi Anda, Tampaknya Anda menegaskan bahwa (string)operasi pemeran dilakukan dengan referensi, mengubah deklarasi asli? Jika demikian, ini bukan kasus 3v4l.org/Craas
fyrye
@fyrye Ya saya kira saya salah, kedua pendekatan menghasilkan hasil yang sama.
Ekonoval
Memperbarui jawaban untuk memberikan contoh penggunaan dan semua contoh suntingan lainnya bersama dengan aslinya
fyrye
4

Jika Anda memiliki jumlah kecil angka desimal yang terbatas yang dapat diterima, berikut ini berfungsi dengan baik (meskipun dengan kinerja lebih lambat daripada solusi epsilon):

$a = 0.17;
$b = 1 - 0.83; //0.17

if (number_format($a, 3) == number_format($b, 3)) {
    echo 'a and b are same';
} else {
    echo 'a and b are not same';
}
dtbarne
sumber
4

Ini berfungsi untuk saya di PHP 5.3.27.

$payments_total = 123.45;
$order_total = 123.45;

if (round($payments_total, 2) != round($order_total, 2)) {
   // they don't match
}
Crmpicco
sumber
3

Untuk PHP 7.2, Anda dapat bekerja dengan PHP_FLOAT_EPSILON ( http://php.net/manual/en/reserved.constants.php ):

if(abs($a-$b) < PHP_FLOAT_EPSILON){
   echo 'a and b are same';
}
Gladhon
sumber
Solusi yang bagus Tapi: 1- Membutuhkan memperbarui PHP 7.2 yang tidak semua orang bisa melakukannya dengan mudah untuk besar / sistem lama 2- ini bekerja hanya untuk yang ada ==dan !=tapi tidak >, >=, <,<=
evilReiko
2

Jika Anda menulisnya seperti itu mungkin akan berhasil, jadi saya bayangkan Anda menyederhanakannya untuk pertanyaan. (Dan menjaga pertanyaan tetap sederhana dan ringkas biasanya adalah hal yang sangat baik.)

Tetapi dalam kasus ini saya membayangkan satu hasil adalah perhitungan dan satu hasil adalah konstan.

Ini melanggar aturan utama pemrograman titik mengambang: Jangan pernah melakukan perbandingan kesetaraan.

Alasan untuk ini agak 1 halus tetapi yang penting untuk diingat adalah bahwa mereka biasanya tidak berfungsi (kecuali, ironisnya, untuk nilai integral) dan bahwa alternatifnya adalah perbandingan fuzzy di sepanjang baris:

if abs(a - y) < epsilon



1. Salah satu masalah utama melibatkan cara kita menulis angka dalam program. Kami menulisnya sebagai string desimal, dan akibatnya sebagian besar fraksi yang kami tulis tidak memiliki representasi mesin yang tepat. Mereka tidak memiliki bentuk hingga yang pasti karena mereka ulangi dalam bentuk biner. Setiap fraksi mesin adalah bilangan rasional dari bentuk x / 2 n . Sekarang, konstanta adalah desimal dan setiap konstanta desimal adalah bilangan rasional dari bentuk x / (2 n * 5 m ). Angka 5 m itu ganjil, jadi tidak ada faktor 2 n untuk mereka. Hanya ketika m == 0 adakah representasi terbatas dalam ekspansi biner dan desimal dari fraksi. Jadi, 1,25 tepat karena 5 / (2 2 * 5 0) tetapi 0,1 bukan karena itu 1 / (2 0 * 5 1 ). Faktanya, dalam seri 1.01 .. 1.99 hanya 3 angka yang benar-benar mewakili: 1.25, 1.50, dan 1.75.

DigitalRoss
sumber
DigitalRoss cukup sulit untuk memahami beberapa istilah dalam komentar Anda, tetapi ya, sangat informatif. Dan saya akan google istilah ini. Terima kasih :)
Santosh Sonarikar
Tidakkah cukup aman untuk melakukan perbandingan di atas kendaraan hias, asalkan Anda membulatkan hasilnya setiap waktu dan berada dalam beberapa digit signifikan? Dengan kata lainround($float, 3) == round($other, 3)
Michael Butler
2

Berikut adalah solusi untuk membandingkan titik mengambang atau angka desimal

//$fd['someVal'] = 2.9;
//$i for loop variable steps 0.1
if((string)$fd['someVal']== (string)$i)
{
    //Equal
}

Keluarkan decimalvariabel ke stringdan Anda akan baik-baik saja.

Natalie Rey
sumber
1

Membandingkan float untuk kesetaraan memiliki algoritma O (n) naif.

Anda harus mengonversi setiap nilai float ke string, lalu membandingkan setiap digit mulai dari sisi kiri setiap representasi string float menggunakan operator perbandingan integer. PHP akan melakukan autocast digit pada setiap posisi indeks ke integer sebelum perbandingan. Digit pertama yang lebih besar dari yang lain akan memutus loop dan menyatakan float bahwa itu milik sebagai yang lebih besar dari keduanya. Rata-rata, akan ada perbandingan 1/2 * n. Untuk mengapung yang sama satu sama lain, akan ada n perbandingan. Ini adalah skenario terburuk untuk algoritma. Skenario kasus terbaik adalah bahwa digit pertama dari setiap float berbeda, hanya menyebabkan satu perbandingan.

Anda tidak dapat menggunakan OPERATOR PERBANDINGAN INTEGER pada nilai float mentah dengan tujuan menghasilkan hasil yang bermanfaat. Hasil dari operasi tersebut tidak memiliki arti karena Anda tidak membandingkan bilangan bulat. Anda melanggar domain masing-masing operator yang menghasilkan hasil yang tidak berarti. Ini berlaku untuk perbandingan delta juga.

Gunakan operator perbandingan integer untuk apa yang mereka dirancang untuk: membandingkan integer.

SOLUSI YANG DISEDIAKAN:

<?php

function getRand(){
  return ( ((float)mt_rand()) / ((float) mt_getrandmax()) );
 }

 $a = 10.0 * getRand();
 $b = 10.0 * getRand();

 settype($a,'string');
 settype($b,'string');

 for($idx = 0;$idx<strlen($a);$idx++){
  if($a[$idx] > $b[$idx]){
   echo "{$a} is greater than {$b}.<br>";
   break;
  }
  else{
   echo "{$b} is greater than {$a}.<br>";
   break;
  }
 }

?>
Kyle
sumber
1

2019

TL; DR

Gunakan fungsi saya di bawah, seperti ini if(cmpFloats($a, '==', $b)) { ... }

  • Mudah dibaca / tulis / ubah: cmpFloats($a, '<=', $b)vs.bccomp($a, $b) <= -1
  • Tidak diperlukan dependensi.
  • Bekerja dengan versi PHP apa pun.
  • Bekerja dengan angka negatif.
  • Bekerja dengan desimal terpanjang yang dapat Anda bayangkan.
  • Kelemahan: Sedikit lebih lambat dari bccomp ()

Ringkasan

Saya akan mengungkap misterinya.

$a = 0.17;
$b = 1 - 0.83;// 0.17 (output)
              // but actual value internally is: 0.17000000000000003996802888650563545525074005126953125
if($a == $b) {
    echo 'same';
} else {
    echo 'different';
}
// Output: different

Jadi jika Anda mencoba yang di bawah ini, itu akan sama:

if($b == 0.17000000000000003) {
    echo 'same';
} else {
    echo 'different';
}
// Output "same"

Bagaimana cara mendapatkan nilai float yang sebenarnya?

$b = 1 - 0.83;
echo $b;// 0.17
echo number_format($a, 100);// 0.1700000000000000399680288865056354552507400512695312500000000000000000000000000000000000000000000000

Bagaimana Anda bisa membandingkan?

  1. Gunakan fungsi BC Math . (Anda masih akan mendapatkan banyak momen wtf-aha-gotcha)
  2. Anda dapat mencoba jawaban @ Gladhon, menggunakan PHP_FLOAT_EPSILON (PHP 7.2).
  3. Jika membandingkan float dengan ==dan !=, Anda bisa mengetikkannya ke string, itu akan bekerja dengan sempurna:

Ketik cast dengan string :

$b = 1 - 0.83;
if((string)$b === (string)0.17) {
    echo 'if';
} else {
    echo 'else';
}
// it will output "if"

Atau typecast dengan number_format():

$b = 1 - 0.83;
if(number_format($b, 3) === number_format(0.17, 3)) {
    echo 'if';
} else {
    echo 'else';
}
// it will output "if"

Peringatan:

Hindari solusi yang melibatkan memanipulasi float secara matematis (mengalikan, membagi, dll) kemudian membandingkan, sebagian besar mereka akan memecahkan beberapa masalah dan memperkenalkan masalah lain.


Solusi yang Disarankan

Saya telah membuat fungsi PHP murni (tidak perlu depenedies / pustaka / ekstensi). Memeriksa dan membandingkan setiap digit sebagai string. Juga berfungsi dengan angka negatif.

/**
 * Compare numbers (floats, int, string), this function will compare them safely
 * @param Float|Int|String  $a         (required) Left operand
 * @param String            $operation (required) Operator, which can be: "==", "!=", ">", ">=", "<" or "<="
 * @param Float|Int|String  $b         (required) Right operand
 * @param Int               $decimals  (optional) Number of decimals to compare
 * @return boolean                     Return true if operation against operands is matching, otherwise return false
 * @throws Exception                   Throws exception error if passed invalid operator or decimal
 */
function cmpFloats($a, $operation, $b, $decimals = 15) {
    if($decimals < 0) {
        throw new Exception('Invalid $decimals ' . $decimals . '.');
    }
    if(!in_array($operation, ['==', '!=', '>', '>=', '<', '<='])) {
        throw new Exception('Invalid $operation ' . $operation . '.');
    }

    $aInt = (int)$a;
    $bInt = (int)$b;

    $aIntLen = strlen((string)$aInt);
    $bIntLen = strlen((string)$bInt);

    // We'll not used number_format because it inaccurate with very long numbers, instead will use str_pad and manipulate it as string
    $aStr = (string)$a;//number_format($a, $decimals, '.', '');
    $bStr = (string)$b;//number_format($b, $decimals, '.', '');

    // If passed null, empty or false, then it will be empty string. So change it to 0
    if($aStr === '') {
        $aStr = '0';
    }
    if($bStr === '') {
        $bStr = '0';
    }

    if(strpos($aStr, '.') === false) {
        $aStr .= '.';
    }
    if(strpos($bStr, '.') === false) {
        $bStr .= '.';
    }

    $aIsNegative = strpos($aStr, '-') !== false;
    $bIsNegative = strpos($bStr, '-') !== false;

    // Append 0s to the right
    $aStr = str_pad($aStr, ($aIsNegative ? 1 : 0) + $aIntLen + 1 + $decimals, '0', STR_PAD_RIGHT);
    $bStr = str_pad($bStr, ($bIsNegative ? 1 : 0) + $bIntLen + 1 + $decimals, '0', STR_PAD_RIGHT);

    // If $decimals are less than the existing float, truncate
    $aStr = substr($aStr, 0, ($aIsNegative ? 1 : 0) + $aIntLen + 1 + $decimals);
    $bStr = substr($bStr, 0, ($bIsNegative ? 1 : 0) + $bIntLen + 1 + $decimals);

    $aDotPos = strpos($aStr, '.');
    $bDotPos = strpos($bStr, '.');

    // Get just the decimal without the int
    $aDecStr = substr($aStr, $aDotPos + 1, $decimals);
    $bDecStr = substr($bStr, $bDotPos + 1, $decimals);

    $aDecLen = strlen($aDecStr);
    //$bDecLen = strlen($bDecStr);

    // To match 0.* against -0.*
    $isBothZeroInts = $aInt == 0 && $bInt == 0;

    if($operation === '==') {
        return $aStr === $bStr ||
               $isBothZeroInts && $aDecStr === $bDecStr;
    } else if($operation === '!=') {
        return $aStr !== $bStr ||
               $isBothZeroInts && $aDecStr !== $bDecStr;
    } else if($operation === '>') {
        if($aInt > $bInt) {
            return true;
        } else if($aInt < $bInt) {
            return false;
        } else {// Ints equal, check decimals
            if($aDecStr === $bDecStr) {
                return false;
            } else {
                for($i = 0; $i < $aDecLen; ++$i) {
                    $aD = (int)$aDecStr[$i];
                    $bD = (int)$bDecStr[$i];
                    if($aD > $bD) {
                        return true;
                    } else if($aD < $bD) {
                        return false;
                    }
                }
            }
        }
    } else if($operation === '>=') {
        if($aInt > $bInt ||
           $aStr === $bStr ||
           $isBothZeroInts && $aDecStr === $bDecStr) {
            return true;
        } else if($aInt < $bInt) {
            return false;
        } else {// Ints equal, check decimals
            if($aDecStr === $bDecStr) {// Decimals also equal
                return true;
            } else {
                for($i = 0; $i < $aDecLen; ++$i) {
                    $aD = (int)$aDecStr[$i];
                    $bD = (int)$bDecStr[$i];
                    if($aD > $bD) {
                        return true;
                    } else if($aD < $bD) {
                        return false;
                    }
                }
            }
        }
    } else if($operation === '<') {
        if($aInt < $bInt) {
            return true;
        } else if($aInt > $bInt) {
            return false;
        } else {// Ints equal, check decimals
            if($aDecStr === $bDecStr) {
                return false;
            } else {
                for($i = 0; $i < $aDecLen; ++$i) {
                    $aD = (int)$aDecStr[$i];
                    $bD = (int)$bDecStr[$i];
                    if($aD < $bD) {
                        return true;
                    } else if($aD > $bD) {
                        return false;
                    }
                }
            }
        }
    } else if($operation === '<=') {
        if($aInt < $bInt || 
           $aStr === $bStr ||
           $isBothZeroInts && $aDecStr === $bDecStr) {
            return true;
        } else if($aInt > $bInt) {
            return false;
        } else {// Ints equal, check decimals
            if($aDecStr === $bDecStr) {// Decimals also equal
                return true;
            } else {
                for($i = 0; $i < $aDecLen; ++$i) {
                    $aD = (int)$aDecStr[$i];
                    $bD = (int)$bDecStr[$i];
                    if($aD < $bD) {
                        return true;
                    } else if($aD > $bD) {
                        return false;
                    }
                }
            }
        }
    }
}

$a = 1 - 0.83;// 0.17
$b = 0.17;
if($a == $b) {
    echo 'same';
} else {
    echo 'different';
}
// Output: different (wrong)

if(cmpFloats($a, '==', $b)) {
    echo 'same';
} else {
    echo 'different';
}
// Output: same (correct)
evilReiko
sumber
1

Fungsi dari @evilReiko memiliki beberapa bug seperti ini:

cmpFloats(-0.1, '==', 0.1); // Expected: false, actual: true
cmpFloats(-0.1, '<', 0.1); // Expected: true, actual: false
cmpFloats(-4, '<', -3); // Expected: true, actual: true
cmpFloats(-5.004, '<', -5.003); // Expected: true, actual: false

Dalam fungsi saya, saya telah memperbaiki bug ini, tetapi bagaimanapun juga dalam beberapa kasus fungsi ini mengembalikan jawaban yang salah:

cmpFloats(0.0000001, '==', -0.0000001); // Expected: false, actual: true
cmpFloats(843994202.303411, '<', 843994202.303413); // Expected: true, actual: false
cmpFloats(843994202.303413, '>', 843994202.303411); // Expected: true, actual: false

Fungsi tetap untuk membandingkan mengapung

function cmpFloats($a, $operation, $b, $decimals = 15)
{
    if ($decimals < 0) {
        throw new Exception('Invalid $decimals ' . $decimals . '.');
    }
    if (!in_array($operation, ['==', '!=', '>', '>=', '<', '<='])) {
        throw new Exception('Invalid $operation ' . $operation . '.');
    }

    $aInt = (int)$a;
    $bInt = (int)$b;

    $aIntLen = strlen((string)$aInt);
    $bIntLen = strlen((string)$bInt);

    // We'll not used number_format because it inaccurate with very long numbers, instead will use str_pad and manipulate it as string
    $aStr = (string)$a;//number_format($a, $decimals, '.', '');
    $bStr = (string)$b;//number_format($b, $decimals, '.', '');

    // If passed null, empty or false, then it will be empty string. So change it to 0
    if ($aStr === '') {
        $aStr = '0';
    }
    if ($bStr === '') {
        $bStr = '0';
    }

    if (strpos($aStr, '.') === false) {
        $aStr .= '.';
    }
    if (strpos($bStr, '.') === false) {
        $bStr .= '.';
    }

    $aIsNegative = strpos($aStr, '-') !== false;
    $bIsNegative = strpos($bStr, '-') !== false;

    // Append 0s to the right
    $aStr = str_pad($aStr, ($aIsNegative ? 1 : 0) + $aIntLen + 1 + $decimals, '0', STR_PAD_RIGHT);
    $bStr = str_pad($bStr, ($bIsNegative ? 1 : 0) + $bIntLen + 1 + $decimals, '0', STR_PAD_RIGHT);

    // If $decimals are less than the existing float, truncate
    $aStr = substr($aStr, 0, ($aIsNegative ? 1 : 0) + $aIntLen + 1 + $decimals);
    $bStr = substr($bStr, 0, ($bIsNegative ? 1 : 0) + $bIntLen + 1 + $decimals);

    $aDotPos = strpos($aStr, '.');
    $bDotPos = strpos($bStr, '.');

    // Get just the decimal without the int
    $aDecStr = substr($aStr, $aDotPos + 1, $decimals);
    $bDecStr = substr($bStr, $bDotPos + 1, $decimals);

    $aDecLen = strlen($aDecStr);
    //$bDecLen = strlen($bDecStr);

    // To match 0.* against -0.*
    $isBothZeroInts = $aInt == 0 && $bInt == 0;

    if ($operation === '==') {
        return $aStr === $bStr ||
            ($isBothZeroInts && $aDecStr === $bDecStr && $aIsNegative === $bIsNegative);
    } elseif ($operation === '!=') {
        return $aStr !== $bStr ||
            $isBothZeroInts && $aDecStr !== $bDecStr;
    } elseif ($operation === '>') {
        if ($aInt > $bInt) {
            return true;
        } elseif ($aInt < $bInt) {
            return false;
        } else {// Ints equal, check decimals
            if ($aIsNegative !== $bIsNegative) {
                return (!$aIsNegative && $bIsNegative);
            }

            if ($aDecStr === $bDecStr) {
                return false;
            } else {
                for ($i = 0; $i < $aDecLen; ++$i) {
                    $aD = (int)$aDecStr[$i];
                    $bD = (int)$bDecStr[$i];

                    if ($aIsNegative && $bIsNegative) {
                        if ($aD < $bD) {
                            return true;
                        } elseif ($aD > $bD) {
                            return false;
                        }
                    } else {
                        if ($aD > $bD) {
                            return true;
                        } elseif ($aD < $bD) {
                            return false;
                        }
                    }
                }
            }
        }
    } elseif ($operation === '>=') {
        if ($aInt > $bInt ||
            $aStr === $bStr ||
            $isBothZeroInts && $aDecStr === $bDecStr) {
            return true;
        } elseif ($aInt < $bInt) {
            return false;
        } else {// Ints equal, check decimals
            if ($aIsNegative !== $bIsNegative) {
                return (!$aIsNegative && $bIsNegative);
            }

            if ($aDecStr === $bDecStr) {// Decimals also equal
                return true;
            } else {
                for ($i = 0; $i < $aDecLen; ++$i) {
                    $aD = (int)$aDecStr[$i];
                    $bD = (int)$bDecStr[$i];

                    if ($aIsNegative && $bIsNegative) {
                        if ($aD < $bD) {
                            return true;
                        } elseif ($aD > $bD) {
                            return false;
                        }
                    } else {
                        if ($aD > $bD) {
                            return true;
                        } elseif ($aD < $bD) {
                            return false;
                        }
                    }
                }
            }
        }
    } elseif ($operation === '<') {
        if ($aInt < $bInt) {
            return true;
        } elseif ($aInt > $bInt) {
            return false;
        } else {// Ints equal, check decimals
            if ($aIsNegative !== $bIsNegative) {
                return ($aIsNegative && !$bIsNegative);
            }

            if ($aDecStr === $bDecStr) {
                return false;
            } else {
                for ($i = 0; $i < $aDecLen; ++$i) {
                    $aD = (int)$aDecStr[$i];
                    $bD = (int)$bDecStr[$i];

                    if ($aIsNegative && $bIsNegative) {
                        if ($aD > $bD) {
                            return true;
                        } elseif ($aD < $bD) {
                            return false;
                        }
                    } else {
                        if ($aD < $bD) {
                            return true;
                        } elseif ($aD > $bD) {
                            return false;
                        }
                    }
                }
            }
        }
    } elseif ($operation === '<=') {
        if ($aInt < $bInt ||
            $aStr === $bStr ||
            $isBothZeroInts && $aDecStr === $bDecStr) {
            return true;
        } elseif ($aInt > $bInt) {
            return false;
        } else {// Ints equal, check decimals
            if ($aIsNegative !== $bIsNegative) {
                return ($aIsNegative && !$bIsNegative);
            }

            if ($aDecStr === $bDecStr) {// Decimals also equal
                return true;
            } else {
                for ($i = 0; $i < $aDecLen; ++$i) {
                    $aD = (int)$aDecStr[$i];
                    $bD = (int)$bDecStr[$i];

                    if ($aIsNegative && $bIsNegative) {
                        if ($aD > $bD) {
                            return true;
                        } elseif ($aD < $bD) {
                            return false;
                        }
                    } else {
                        if ($aD < $bD) {
                            return true;
                        } elseif ($aD > $bD) {
                            return false;
                        }
                    }
                }
            }
        }
    }
}

Jawab untuk pertanyaan Anda

$a = 1 - 0.83;// 0.17
$b = 0.17;
if($a == $b) {
    echo 'same';
} else {
    echo 'different';
}
// Output: different (wrong)

if(cmpFloats($a, '==', $b)) {
    echo 'same';
} else {
    echo 'different';
}
// Output: same (correct)
Volodymyr
sumber
0

Ini adalah kelas yang berguna dari perpustakaan pribadi saya untuk berurusan dengan angka floating point. Anda dapat menyesuaikannya sesuai keinginan Anda dan memasukkan solusi apa pun yang Anda suka ke metode kelas :-).

/**
 * A class for dealing with PHP floating point values.
 * 
 * @author Anthony E. Rutledge
 * @version 12-06-2018
 */
final class Float extends Number
{
    // PHP 7.4 allows for property type hints!

    private const LESS_THAN = -1;
    private const EQUAL = 0;
    private const GREATER_THAN = 1;

    public function __construct()
    {

    }

    /**
     * Determines if a value is an float.
     * 
     * @param mixed $value
     * @return bool
     */
    public function isFloat($value): bool
    {
        return is_float($value);
    }

    /**
     * A method that tests to see if two float values are equal.
     * 
     * @param float $y1
     * @param float $y2
     * @return bool
     */
    public function equals(float $y1, float $y2): bool
    {
        return (string) $y1 === (string) $y2;
    }

    /**
     * A method that tests to see if two float values are not equal.
     * 
     * @param float $y1
     * @param float $y2
     * @return bool
     */
    public function isNotEqual(float $y1, float $y2): bool
    {
        return !$this->equals($y1, $y2);
    }

    /**
     * Gets the bccomp result.
     * 
     * @param float $y1
     * @param float $y2
     * @return int
     */
    private function getBccompResult(float $y1, float $y2): int
    {
        $leftOperand = (string) $y1;
        $rightOperand = (string) $y2;

        // You should check the format of the float before using it.

        return bccomp($leftOperand, $rightOperand);
    }

    /**
     * A method that tests to see if y1 is less than y2.
     * 
     * @param float $y1
     * @param float $y2
     * @return bool
     */
    public function isLess(float $y1, float $y2): bool
    {
        return ($this->getBccompResult($y1, $y2) === self::LESS_THAN);
    }

    /**
     * A method that tests to see if y1 is less than or equal to y2.
     * 
     * @param float $y1
     * @param float $y2
     * @return bool
     */
    public function isLessOrEqual(float $y1, float $y2): bool
    {
        $bccompResult = $this->getBccompResult($y1, $y2);
        return ($bccompResult === self::LESS_THAN || $bccompResult === self::EQUALS);
    }

    /**
     * A method that tests to see if y1 is greater than y2.
     * 
     * @param float $y1
     * @param float $y2
     * @return bool
     */
    public function isGreater(float $y1, float $y2): bool
    {
        return ($this->getBccompResult($y1, $y2) === self::GREATER_THAN);
    }

    /**
     * A method that tests to see if y1 is greater than or equal to y2.
     * 
     * @param float $y1
     * @param float $y2
     * @return bool
     */
    public function isGreaterOrEqual(float $y1, float $y2): bool
    {
        $bccompResult = $this->getBccompResult($y1, $y2);
        return ($bccompResult === self::GREATER_THAN || $bccompResult === self::EQUALS);
    }

    /**
     * Returns a valid PHP float value, casting if necessary.
     * 
     * @param mixed $value
     * @return float
     *
     * @throws InvalidArgumentException
     * @throws UnexpectedValueException
     */
    public function getFloat($value): float
    {
        if (! (is_string($value) || is_int($value) || is_bool($value))) {
            throw new InvalidArgumentException("$value should not be converted to float!");
        }

        if ($this->isFloat($value)) {
            return $value;
        }

        $newValue = (float) $value;

        if ($this->isNan($newValue)) {
            throw new UnexpectedValueException("The value $value was converted to NaN!");
        }

        if (!$this->isNumber($newValue)) {
            throw new UnexpectedValueException("The value $value was converted to something non-numeric!");
        }

        if (!$this->isFLoat($newValue)) {
            throw new UnexpectedValueException("The value $value was not converted to a floating point value!");
        }

        return $newValue;
    }
}
?>
Anthony Rutledge
sumber
0

Jawaban sederhana:

if( floatval( (string) $a ) >= floatval( (string) $b) ) { //do something }
Nader
sumber