المستوى: مبتدئ
لو كتبت 0.1 + 0.2 في الكود وطلعلك 0.30000000000000004، يبقى المقال ده ليك. هتفهم ليه ده بيحصل في كل لغات البرمجة (مش JavaScript بس)، وامتى لازم تتدخّل، وامتى عادي تتجاهل.
ليه 0.1 + 0.2 مش بيساوي 0.3 في الكمبيوتر
المشكلة باختصار
افتح Console في المتصفح واكتب الأمر ده:
console.log(0.1 + 0.2);
// النتيجة: 0.30000000000000004اعمل نفس الحاجة في Python:
print(0.1 + 0.2)
# النتيجة: 0.30000000000000004في Java و C++ و Go و Ruby و Swift و PHP و C#، النتيجة هي هي. ده مش bug في اللغة ولا في المعالج بتاعك. ده سلوك معتمد ومتفق عليه في كل المعالجات الحديثة من سنة 1985.
المثال البسيط — قسمة الكيكة على 3
تخيل إن عندك كيكة عايز تقسمها بالتساوي على 3 ناس. الجواب الحقيقي 0.333333... وعلامة "..." معناها أرقام بتفضل تتكرر للأبد. لكن لو معاك ورقة فيها 8 خانات بس بعد العلامة العشرية، هتكتب 0.33333333 وتقف. الرقم اللي كتبته أقل من الثلث الحقيقي بمقدار صغير جدًا. لو ضربت في 3 الإجابة هتطلع 0.99999999 مش 1 صحيحة.
الكمبيوتر بيعمل نفس الحاجة بالظبط، لكن بالنظام الثنائي (binary) مش العشري. الفرق المهم إن الكمبيوتر مش عنده مشكلة مع 0.5 أو 0.25 أو 0.75 لأنها كسور من قوى 2. المشكلة بتظهر مع 0.1 و 0.2 و 0.3، لأنها كسور دورية لا نهائية في binary.
التعريف العلمي — معيار IEEE 754
الكمبيوتر بيخزّن الأرقام العشرية بصيغة اسمها IEEE 754 double precision في 64 bit مقسومين كالآتي:
- 1 bit للإشارة (موجب أو سالب)
- 11 bit للأس (exponent)
- 52 bit للجزء الكسري (mantissa)
الرقم 0.1 في binary هو 0.0001100110011001100... وهكذا للأبد. التمثيل بـ 52 bit بيقطع التتابع، فيتخزّن كرقم أقرب وهو فعلياً 0.1000000000000000055511151231257827021181583404541015625. ده اللي بيخلّي 0.1 + 0.2 = 0.30000000000000004 بدل 0.3 المفترض.
الإصابة الحقيقية — فاتورة بـ 1 سنت غلط
فيه شركة fintech عربية كان عندها مشكلة في حساب فاتورة شهرية بسيطة، الكود كان شبه ده:
let total = 0;
for (let i = 0; i < 30; i++) {
total += 0.1; // كل يوم بـ 0.1 جنيه
}
console.log(total); // 2.9999999999999996
console.log(total === 3.0); // falseالتحقق total === 3.0 بيرجع false. المنطق التجاري كان مبني على المساواة دي عشان يقفل الفاتورة. النتيجة: العميل كان بيدفع 0.01 جنيه زيادة في 12,000 فاتورة شهرية. الخسارة الإجمالية: 120 جنيه شهرياً بدون أي سبب واضح في الـ logs، والفريق فضل شهرين يدوّر على السبب قبل ما يفهموا إن المشكلة في الـ float.
الحل — اختار الأداة الصح للموقف
مفيش حل واحد بيناسب كل الحالات. ده اللي بنستخدمه فعلاً في الإنتاج:
- للحسابات المالية: استخدم decimal type. في Python
from decimal import Decimal، في JavaBigDecimal، في JavaScript مكتبات زيdecimal.jsأوbig.js. - للمقارنات بين أرقام عشرية: ما تستخدمش
===أصلاً. استخدم تفاوت مقبول:Math.abs(a - b) < 0.0001. الرقم 0.0001 بيتسمى epsilon والقيمة بتعتمد على الـ domain. - للعملات تحديداً: اخزن الفلوس بالـ cents كأرقام صحيحة. 1 جنيه = 100 قرش = integer 100. الجمع والطرح والضرب كلهم على integers، ومافيش أخطاء على الإطلاق. ده اللي PayPal و Stripe بيعملوه.
مثال Python بـ Decimal:
from decimal import Decimal
result = Decimal('0.1') + Decimal('0.2')
print(result) # 0.3 بالظبط
print(result == Decimal('0.3')) # Trueلاحظ إن Decimal بياخد الأرقام كـ string مش كـ float، عشان لو كتبت Decimal(0.1) هتاخد الـ float المغشوش أصلاً، فاللي هيتخزن هو 0.1000000000000000055511...
Trade-offs — مفيش حل ببلاش
- Decimal أبطأ من float بـ 20x تقريباً. على Python 3.12، 1 مليون عملية جمع بالـ float بتاخد 35ms، نفسها بـ Decimal بتاخد حوالي 720ms.
- integer cents بيخلّي القسمة معقدة. 100 قرش ÷ 3 = 33.33 قرش، فيه فاقد قرش لازم تتعامل معاه يدوياً (rounding rules رسمية في كل دولة).
- Decimal بياخد ذاكرة أكتر. float = 8 bytes، أما
Decimal('0.1')= حوالي 104 bytes في Python. - المقارنة بـ epsilon بتفقد دقّة الـ test cases. الـ unit test ممكن يقبل قيم غلط بمقدار صغير لكنه فعلاً مهم في الـ domain بتاعك.
متى لا تتدخّل أصلاً
لو شغلك في graphics أو physics simulation أو machine learning، الـ float العادي كافي تماماً. الفرق بمقدار 0.0000000004 مش هيتلاحظ بصرياً ولا هيأثر في النتيجة النهائية. الحوادث المعروفة كلها في domains فيها equality قاطعة ومحاسبة دقيقة: حسابات مالية، أنظمة تذاكر الطيران، vote tallying في الانتخابات، قياسات هندسية بدقة عالية. لو domain شغلك مش واحد من دول، اتركه float واشتغل.
الخطوة التالية
افتح أكبر codebase عندك واعمل grep على 0.1 و === مع أرقام عشرية. كل match فيهم احتمال واقعي يكون فيه bug صامت. لو لقيت match في كود مالي، حوّله integer cents فوراً قبل ما تكتشف الفاتورة الغلط في حساب عميل مهم.
المصادر
- IEEE Standard 754-2019 for Floating-Point Arithmetic — المعيار الرسمي
- Python Tutorial: Floating Point Arithmetic — Issues and Limitations
- 0.30000000000000004.com — قائمة بأكتر من 60 لغة برمجة كلهم بيدّوا نفس النتيجة
- David Goldberg, "What Every Computer Scientist Should Know About Floating-Point Arithmetic", ACM Computing Surveys, March 1991
- Python decimal module documentation