เปรียบเทียบลอยใน php


156

ฉันต้องการเปรียบเทียบสองโฟลตใน PHP เหมือนในโค้ดตัวอย่างนี้:

$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';
}

ในรหัสนี้จะส่งคืนผลลัพธ์ของelseเงื่อนไขแทนifเงื่อนไขแม้ว่า$aและ$bจะเหมือนกัน มีวิธีพิเศษในการจัดการ / เปรียบเทียบลอยใน PHP หรือไม่

ถ้าใช่โปรดช่วยฉันแก้ปัญหานี้ด้วย

หรือมีปัญหากับการกำหนดค่าเซิร์ฟเวอร์ของฉัน?


a and b are sameฉันได้รับ เป็นรหัสเต็มของคุณหรือไม่
Pekka

รุ่นใด มันใช้งานได้ดีสำหรับฉัน
gblazex

@ อังเดรนี่น่าจะเป็นเพราะกรณีโลกแห่งความเป็นจริงมีแนวโน้มที่จะซับซ้อนกว่าตัวอย่างที่ยกมา ทำไมไม่เพิ่มมันเป็นคำตอบ?
Pekka

2
คุณอ่านfloating-pointคำอธิบายแท็กแล้วหรือยัง? stackoverflow.com/tags/floating-point/infoนั่นเป็นพฤติกรรมที่คุณอาจพบในภาษาการเขียนโปรแกรมใด ๆ เมื่อใช้ตัวเลขทศนิยม ดูเช่นstackoverflow.com/questions/588004/is-javascripts-math-broken
Piskvor ออกจากอาคาร

คำตอบ:


232

หากคุณทำเช่นนี้พวกเขาควรจะเหมือนกัน แต่โปรดทราบว่าคุณลักษณะของค่าจุดลอยตัวคือการคำนวณที่ดูเหมือนจะส่งผลให้ค่าเดียวกันไม่จำเป็นต้องเหมือนกัน ดังนั้นหาก$aเป็นตัวอักษร.17และ$bมาถึงที่นั่นด้วยการคำนวณมันจะแตกต่างกันแม้ว่าทั้งคู่จะแสดงค่าเดียวกัน

โดยปกติแล้วคุณไม่เคยเปรียบเทียบค่าทศนิยมเพื่อความเท่าเทียมเช่นนี้คุณต้องใช้ความแตกต่างที่ยอมรับได้น้อยที่สุด:

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

อะไรแบบนั้น.


21
ระวัง! การเลือก epsilon แบบคงที่เป็นวิธีที่ไม่ดีเพียงเพราะมันดูเล็กการเปรียบเทียบนี้จะกลับมาจริงในข้อผิดพลาดที่แม่นยำจำนวนมากเมื่อตัวเลขมีขนาดเล็ก วิธีที่ถูกต้องคือการตรวจสอบว่าข้อผิดพลาดสัมพัทธ์น้อยกว่า epsilon หรือไม่ abs($a-$b)>abs(($a-$b)/$b)
Piet Bijl

1
@Alexandru: ฉันรู้ว่าคุณหมายถึงอะไร แต่ PHP ไม่ได้อยู่คนเดียวในเรื่องนั้น คุณต้องแยกความแตกต่างระหว่างการใช้งานสองกรณีที่นี่: แสดงหมายเลขให้ผู้ใช้ ในกรณีที่การแสดงผล0.10000000000000000555111512312578270211815834045410156มักจะไม่มีจุดหมายและพวกเขาต้องการ0.1แทน และเขียนตัวเลขเพื่อให้สามารถอ่านได้อีกครั้งในลักษณะเดียวกัน อย่างที่คุณเห็นมันไม่ได้ชัดเจนเท่าที่คุณต้องการ และสำหรับบันทึกคุณยังคงต้องการเปรียบเทียบตัวเลขทศนิยมเช่นที่ฉันแสดงเพราะคุณสามารถมาถึง$aและ$bผ่านการคำนวณที่แตกต่างกันซึ่งสามารถทำให้พวกเขาแตกต่างกัน
Joey

2
ยังมีบางกรณีขอบซึ่งการทดสอบนี้ล้มเหลว เช่นa=b=0และถ้าaเป็นไปได้น้อยที่สุดไม่มีค่าบวกเป็นศูนย์และbเป็นไปได้น้อยที่สุดไม่ใช่ค่าลบศูนย์การทดสอบจะล้มเหลวอย่างไม่ถูกต้อง ข้อมูลที่ดีบางอย่างที่นี่: floating-point-gui.de/errors/comparison
Dom

13
ทำไมต้องหาร$b? คู่มือ PHPก็ไม่ได้คู่มือ MySQLยังไม่เหมือนกันif(abs($a-$b) < $epsilon) HAVING ABS(a - b) <= 0.0001
บัญชีم

1
@CaslavSabani: นี่เป็นญาติไม่ใช่ข้อผิดพลาดแน่นอน มันเสียยังคง (โดยเฉพาะเมื่อ$a == $b == 0แต่มันมีอยู่แล้วเป็นจำนวนมากโดยทั่วไปมากกว่าข้อผิดพลาดแน่นอน. ถ้า$aและ$bอยู่ในล้านแล้วคุณEPSILONจะต้องแตกต่างกันมากกว่าถ้า$aและ$bมีที่ไหนสักแห่งใกล้กับ0. ลิงค์ดู Dom ข้างต้นสำหรับการอภิปรายที่ดีขึ้นของ สิ่งนี้
Joey

65

อ่านคำเตือนสีแดงในคู่มือก่อน คุณต้องไม่เปรียบเทียบการลอยเพื่อความเท่าเทียมกัน คุณควรใช้เทคนิคเอปไซลอน

ตัวอย่างเช่น:

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

โดยที่PHP_FLOAT_EPSILONค่าคงที่แสดงจำนวนน้อยมาก (คุณต้องกำหนดใน PHP เวอร์ชันเก่าก่อน 7.2)


2
หากต้องการชี้แจงว่า EPSILON ในกรณีนี้คือ epsilon ของเครื่องซึ่งประมาณ 2.2204460492503E-16 และการเปรียบเทียบนี้ใช้กับขนาดใดก็ได้สองอัน
Michael Cordingley

1
@MichaelCordingley ไม่EPSILONนี่คือค่าคงที่ที่ผู้ใช้กำหนดเอง PHP ไม่มีค่าคงที่ในตัวที่แสดงถึงแนวคิดเฉพาะของ epsilon (ดูเพิ่มเติมget_defined_constants)
อธิการ

5
PHP_FLOAT_EPSILONจำนวนบวกที่น้อยที่สุดที่สามารถแทนได้ x เพื่อให้ x + 1.0! = 1.0 มีให้ตั้งแต่ PHP 7.2.0
Code4R7

2
สิ่งนี้ไม่สามารถใช้งานได้ในกรณีนี้: $ a = 270.10 + 20.10; $ b = 290.20; if (abs ($ a- $ b) <PHP_FLOAT_EPSILON) {echo 'เดียวกัน'; }
NemoXP

@NemoXP เนื่องจากนิพจน์เหล่านั้นให้ตัวเลขที่ต่างกัน echo $a - $b; /* 5.6843418860808E-14 */ echo PHP_FLOAT_EPSILON; /* 2.2204460492503E-16 */คำถามคือวิธีที่คุณต้องการกำหนด "เท่ากัน" สำหรับแอปพลิเคชันของคุณว่าควรจะใกล้เคียงกับจำนวนที่เท่าไหร่
Andrey

28

หรือลองใช้ฟังก์ชั่น bc math:

<?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 );

ผลลัพธ์:

0.17 == 0.17 (core comp oper): bool(false)
0.17 == 0.17 (with bc func)  : bool(true)

2
ในการใช้งาน bccomp คุณพลาด "สเกล" ดังนั้นคุณจึงเปรียบเทียบ 0 ถึง 0 ตามคู่มือ: php.net/manual/en/function.bccomp.php
stefancarlton

ฉันชอบสิ่งนี้ โซลูชันส่วนใหญ่ดูเหมือนจะพึ่งพาการปัดเศษและการสูญเสียความแม่นยำ แต่ฉันต้องจัดการกับพิกัดละติจูดและลองจิจูดด้วยความแม่นยำ 12 จุดและดูเหมือนว่าจะเปรียบเทียบได้อย่างแม่นยำโดยไม่ต้องปรับแต่ง
Rikaelus

แล้วประสิทธิภาพล่ะ bccomp()ใช้สตริงเป็นอาร์กิวเมนต์ อย่างไรก็ตามคุณสามารถใช้PHP_FLOAT_DIGสำหรับอาร์กิวเมนต์สเกลได้
Code4R7

19

ดังที่ได้กล่าวไว้ก่อนหน้านี้โปรดระมัดระวังเมื่อทำการเปรียบเทียบจุดลอยตัว (ไม่ว่าจะเท่ากับหรือมากกว่าหรือน้อยกว่า) ใน PHP อย่างไรก็ตามหากคุณสนใจตัวเลขเพียงไม่กี่ตัวคุณสามารถทำสิ่งต่อไปนี้:

$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';
}

การใช้การปัดเศษทศนิยม 2 ตำแหน่ง (หรือ 3 หรือ 4) จะทำให้เกิดผลลัพธ์ที่คาดหวัง


1
คำเตือนเพิ่มเติมฉันจะไม่แนะนำให้ทิ้ง codebase ของคุณด้วยข้อความเช่นนี้ หากคุณต้องการเปรียบเทียบการลอยแบบหลวม ๆ ให้ทำวิธีการเช่นนี้loose_float_compareเพื่อให้เห็นได้ชัดว่าเกิดอะไรขึ้น
Michael Butler

ภาษาพื้นเมืองของ PHP bccomp($a, $b, 2)ดีกว่าโซลูชันของคุณ ในตัวอย่างนี้ 2 คือความแม่นยำ คุณสามารถตั้งค่าเป็นจำนวนคะแนนลอยตัวที่คุณต้องการเปรียบเทียบ
John Miller

@JohnMiller ฉันไม่เห็นด้วยกับคุณ แต่ค่าเริ่มต้น bccomp ไม่สามารถใช้ได้ มันต้องการทั้งธงการรวบรวมที่จะเปิดใช้งานหรือติดตั้งส่วนขยาย ไม่ใช่ส่วนของแกนกลาง
Michael Butler

17

การใช้การเปรียบเทียบ PHP แบบเนทีฟจะดีกว่า:

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. 

ส่งคืน 0 ถ้าตัวถูกดำเนินการทั้งสองเท่ากัน 1 ถ้า left_operand ใหญ่กว่า right_operand หรือไม่เช่นนั้น -1


10

หากคุณมีค่าทศนิยมเพื่อเปรียบเทียบกับความเท่าเทียมกันวิธีง่ายๆในการหลีกเลี่ยงความเสี่ยงของกลยุทธ์การปัดเศษภายในของ OS ภาษาตัวประมวลผลหรืออื่น ๆ คือการเปรียบเทียบการแทนค่าสตริง

คุณสามารถใช้ข้อใดข้อหนึ่งต่อไปนี้เพื่อสร้างผลลัพธ์ที่ต้องการ: https://3v4l.org/rUrEq

การหล่อประเภทสตริง

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

การต่อสตริง

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

ฟังก์ชัน strval

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

การแทนค่าสตริงจะพิถีพิถันน้อยกว่าการลอยตัวเมื่อตรวจสอบความเสมอภาค


หรือถ้า (strval ($ a) === strval ($ b)) {…} ถ้าคุณไม่ต้องการแปลงค่าดั้งเดิม
Ekonoval

คำตอบเดิมของฉันคือ: ถ้า (''. $ a === ''. $ b) {…} แต่มีคนแก้ไขมัน ดังนั้น ...
Ame Nomade

1
@Ekonoval คุณช่วยอธิบายเพิ่มเติมเกี่ยวกับการดัดแปลงของคุณได้ไหม, ดูเหมือนว่าคุณกำลังยืนยันว่าการ(string)ดำเนินการร่ายนั้นดำเนินการโดยอ้างอิง, เปลี่ยนการประกาศดั้งเดิม ถ้าเป็นเช่นนั้นไม่ใช่ 3v4l.org/Craas
fyrye

@ fryry ใช่ฉันเดาฉันผิดทั้งสองวิธีให้ผลลัพธ์เดียวกัน
Ekonoval

อัปเดตคำตอบเพื่อให้ตัวอย่างการใช้งานและตัวอย่างทั้งหมดของการแก้ไขอื่น ๆ พร้อมกับต้นฉบับ
fyrye

4

หากคุณมีจำนวนทศนิยมที่ จำกัด จำนวนเล็กน้อยที่จะยอมรับได้สิ่งต่อไปนี้จะใช้งานได้ดี (แม้ว่าจะมีประสิทธิภาพที่ช้ากว่าโซลูชัน 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';
}

4

สิ่งนี้ใช้ได้กับฉันใน 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
}

3

สำหรับ PHP 7.2 คุณสามารถทำงานกับ PHP_FLOAT_EPSILON ( http://php.net/manual/en/reserved.constants.php ):

if(abs($a-$b) < PHP_FLOAT_EPSILON){
   echo 'a and b are same';
}

ทางออกที่ดี แต่: 1- ต้องใช้การอัปเดต PHP 7.2 ซึ่งทุกคนไม่สามารถทำอะไรได้อย่างง่ายดายสำหรับขนาดใหญ่ / ระบบเก่า 2 งานนี้เท่านั้นที่มีอยู่==และ!=แต่ไม่>, >=, <,<=
evilReiko

2

ถ้าคุณเขียนมันเหมือนว่ามันอาจจะใช้ได้ฉันคิดว่าคุณทำให้มันง่ายขึ้นสำหรับคำถาม (และการทำให้คำถามง่ายและกระชับเป็นปกติเป็นสิ่งที่ดีมาก)

แต่ในกรณีนี้ฉันจินตนาการว่าผลลัพธ์หนึ่งรายการเป็นการคำนวณและผลลัพธ์หนึ่งคือค่าคงที่

สิ่งนี้ละเมิดกฎสำคัญของการเขียนโปรแกรมจุดลอย: ไม่ทำการเปรียบเทียบความเท่าเทียมกัน

เหตุผลของเรื่องนี้ค่อนข้างบอบบาง1แต่สิ่งสำคัญที่ต้องจำคือพวกเขามักจะไม่ทำงาน (ยกเว้นแดกดันสำหรับค่าสำคัญ) และทางเลือกอื่นคือการเปรียบเทียบที่คลุมเครือตามแนวของ:

if abs(a - y) < epsilon



1.หนึ่งในปัญหาสำคัญเกี่ยวข้องกับวิธีที่เราเขียนตัวเลขในโปรแกรม เราเขียนมันเป็นสตริงทศนิยมและเศษส่วนส่วนใหญ่ที่เราเขียนไม่มีการแทนด้วยเครื่อง พวกมันไม่มีรูปแบบที่แน่นอนเพราะพวกมันทำซ้ำในระบบเลขฐานสอง ส่วนทุกเครื่องเป็นจำนวนจริงในรูปแบบ x / 2 n ตอนนี้ค่าคงที่เป็นทศนิยมและค่าคงที่ทศนิยมทุกค่าเป็นจำนวนตรรกยะของรูปแบบ x / (2 n * 5 m ) ตัวเลข5 ม.เป็นเลขคี่ดังนั้นจึงไม่มีปัจจัย2 nสำหรับพวกเขาใด ๆ เฉพาะเมื่อ m == 0 มีการแทนค่า จำกัด ทั้งในการขยายไบนารีและทศนิยมของเศษส่วน ดังนั้น 1.25 แน่นอนเพราะมันคือ 5 / (2 2 * 5 0) แต่ 0.1 ไม่ใช่เพราะมันเป็น 1 / (2 0 * 5 1 ) ในความเป็นจริงในซีรีส์ 1.01 .. 1.99 มีเพียง 3 ในตัวเลขเท่านั้นที่สามารถแทนได้: 1.25, 1.50 และ 1.75


DigitalRoss ค่อนข้างยากที่จะเข้าใจคำศัพท์บางคำในความคิดเห็นของคุณ แต่ใช่มันมีข้อมูลมาก และฉันจะไปที่ google ข้อกำหนดเหล่านี้ ขอบคุณ :)
Santosh Sonarikar

จะปลอดภัยหรือไม่ที่จะทำการเปรียบเทียบกับการลอยตัวหากคุณปัดเศษผลลัพธ์ทุกครั้งและมีตัวเลขที่สำคัญอยู่บ้าง กล่าวอีกนัยหนึ่งround($float, 3) == round($other, 3)
Michael Butler

2

นี่คือวิธีการในการเปรียบเทียบคะแนนทศนิยมหรือทศนิยม

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

ส่งdecimalตัวแปรไปstringและคุณจะไม่เป็นไร


1

การเปรียบเทียบลอยเพื่อความเท่าเทียมมีอัลกอริทึมไร้เดียงสา O (n)

คุณต้องแปลงค่าทศนิยมแต่ละค่าเป็นสตริงจากนั้นเปรียบเทียบแต่ละหลักเริ่มต้นจากด้านซ้ายของการแทนค่าสตริงแต่ละลอยโดยใช้ตัวดำเนินการเปรียบเทียบจำนวนเต็ม PHP จะทำการคำนวณตัวเลขอัตโนมัติในแต่ละตำแหน่งดัชนีให้เป็นจำนวนเต็มก่อนทำการเปรียบเทียบ หลักแรกที่ใหญ่กว่าอีกอันจะทำให้เกิดการวนซ้ำและประกาศลอยที่มันเป็นของใหญ่กว่าของทั้งสอง โดยเฉลี่ยจะมีการเปรียบเทียบ 1/2 * n สำหรับลอยเท่ากับกันจะมีการเปรียบเทียบ นี่เป็นสถานการณ์กรณีที่เลวร้ายที่สุดสำหรับอัลกอริทึม สถานการณ์กรณีที่ดีที่สุดคือตัวเลขตัวแรกของแต่ละการลอยนั้นแตกต่างกันทำให้เกิดการเปรียบเทียบเพียงครั้งเดียว

คุณไม่สามารถใช้ตัวดำเนินการเปรียบเทียบแบบ INTEGER กับผู้ดำเนินการค่าลอยตัวแบบเจตนาเพื่อสร้างผลลัพธ์ที่มีประโยชน์ ผลลัพธ์ของการดำเนินการดังกล่าวไม่มีความหมายเพราะคุณไม่ได้เปรียบเทียบจำนวนเต็ม คุณกำลังละเมิดโดเมนของผู้ให้บริการแต่ละรายที่สร้างผลลัพธ์ที่ไม่มีความหมาย นี่ถือเป็นการเปรียบเทียบเดลต้าเช่นกัน

ใช้โอเปอเรเตอร์การเปรียบเทียบจำนวนเต็มสำหรับสิ่งที่ออกแบบมาสำหรับ: การเปรียบเทียบจำนวนเต็ม

โซลูชั่นที่เรียบง่าย:

<?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;
  }
 }

?>

1

2019

TL; DR

ใช้ฟังก์ชั่นของฉันด้านล่างเช่นนี้ if(cmpFloats($a, '==', $b)) { ... }

  • ง่ายต่อการอ่าน / เขียน / เปลี่ยนแปลง: cmpFloats($a, '<=', $b)vsbccomp($a, $b) <= -1
  • ไม่จำเป็นต้องพึ่งพา
  • ทำงานร่วมกับ PHP เวอร์ชันใดก็ได้
  • ทำงานร่วมกับตัวเลขลบ
  • ทำงานร่วมกับทศนิยมที่ยาวที่สุดที่คุณสามารถจินตนาการได้
  • ข้อเสีย: ช้ากว่า bccomp เล็กน้อย ()

สรุป

ฉันจะเปิดเผยความลึกลับ

$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

ดังนั้นถ้าคุณลองด้านล่างมันจะเท่ากัน:

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

วิธีรับค่าลอยจริงหรือไม่

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

คุณเปรียบเทียบได้อย่างไร

  1. ใช้ฟังก์ชั่น BC คณิตศาสตร์ (คุณจะยังได้รับช่วงเวลา wtf-aha-gotcha จำนวนมาก)
  2. คุณอาจลองตอบ @ @ Gladhon โดยใช้ PHP_FLOAT_EPSILON (PHP 7.2)
  3. หากเปรียบเทียบการลอยด้วย==และ!=คุณสามารถพิมพ์ลงในสตริงได้ควรทำงานได้อย่างสมบูรณ์:

ประเภท cast ด้วยสตริง :

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

หรือพิมพ์ด้วย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"

คำเตือน:

หลีกเลี่ยงการแก้ปัญหาที่เกี่ยวข้องกับการจัดการลอยตัวทางคณิตศาสตร์ (การคูณการหาร ฯลฯ ) แล้วเปรียบเทียบส่วนใหญ่พวกเขาจะแก้ปัญหาและแนะนำปัญหาอื่น ๆ


โซลูชั่นที่แนะนำ

ฉันสร้างฟังก์ชั่น PHP ล้วนๆ (ไม่จำเป็นต้องพึ่งพา / ไลบรารี่ / ส่วนขยาย) ตรวจสอบและเปรียบเทียบแต่ละหลักเป็นสตริง ใช้ได้กับตัวเลขติดลบ

/**
 * 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)

1

ฟังก์ชั่นจาก@evilReikoมีข้อบกพร่องบางอย่างเช่นนี้:

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

ในฟังก์ชั่นของฉันฉันได้แก้ไขข้อบกพร่องเหล่านี้แล้ว แต่ในบางกรณีฟังก์ชันนี้คืนคำตอบที่ผิด:

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

ฟังก์ชั่นคงที่สำหรับการเปรียบเทียบลอย

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;
                        }
                    }
                }
            }
        }
    }
}

ตอบคำถามของคุณ

$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)

0

นี่คือคลาสที่มีประโยชน์จากห้องสมุดส่วนตัวของฉันสำหรับจัดการกับตัวเลขทศนิยม คุณสามารถปรับให้เข้ากับความชอบของคุณและแทรกวิธีแก้ปัญหาที่คุณชอบลงในคลาสของวิธีการ :-)

/**
 * 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;
    }
}
?>

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.