مستوى المقال: متوسط — مكتوب للمطوّر اللي بيكتب كود بيلمس أرقام أو فلوس.
ليه 0.1 + 0.2 مش بيساوي 0.3 في كل لغات البرمجة
لو جمعت 0.1 + 0.2 في بايثون أو جافاسكريبت ولقيت الناتج 0.30000000000000004، ده مش bug في لغتك ولا عطل في معالجك. ده سلوك متعمّد ومضبوط 100%. في آخر المقال هتعرف ليه بيحصل، وتقدر تمنعه يكسر حساب فلوس بسطر أو اتنين.
المشكلة باختصار
تخيّل عربة شراء فيها 3 منتجات سعر كل واحد 0.10 دولار. المفروض المجموع 0.30. لكن الكود بيقولك المجموع 0.30000000000000004، وبوابة الدفع بترفض العملية لأن الرقم مش مطابق. المشكلة دي بتظهر فجأة في الفلوس، والـ unit test اللي مكتوب فيه assert total == 0.3 بيفشل وانت مش فاهم ليه.
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
الحكاية ببساطة الأول
قبل أي كلام علمي، خد المثال ده. جرّب تكتب ثلث (1 ÷ 3) في النظام العشري اللي بنعدّ بيه: 0.3333 وهكذا. كل ما تزوّد تلاتات عمرك ما توصل لثلث بالظبط، لأنه كسر متكرر لا نهائي عندنا.
الكمبيوتر بيحسب بنظام تاني خالص: النظام الثنائي، أرقامه 0 و1 بس. وفيه كسور سهلة جدًا عندنا — زي 0.1 — بتطلع كسور متكررة لا نهائية عنده، بالظبط زي ما 1/3 لا نهائي عندنا. فالكمبيوتر بيخزّن أقرب رقم يقدر عليه، والفرق الصغير ده هو اللي بيطلع أول ما تجمع.
اللي بيحصل فعلاً جوّه IEEE 754
دلوقتي بالتفاصيل. أغلب اللغات بتخزّن الكسور العشرية في نوع اسمه double بمعيار IEEE 754، وحجمه 64 بت متقسّمين كالتالي: بت واحد للإشارة، 11 بت للأس (exponent)، و52 بت للجزء الكسري (mantissa).
الرقم 0.1 في النظام الثنائي بيساوي 0.0001100110011 وهكذا، والكتلة «0011» بتتكرر للأبد. بما إن عندك 52 بت بس تخزّن فيهم الكسر، المعالج بيقرّب لأقرب قيمة ممكنة. القيمة المخزّنة لـ 0.1 أكبر شوية من 0.1 الحقيقي، ولـ 0.2 كمان، فلما تجمعهم بيتراكم خطأ في حدود 5.5×10⁻¹⁷ ويظهر في آخر الأرقام.
الافتراض هنا إنك بتستخدم double، وهو الافتراضي في JavaScript و Python وأغلب الـ JSON. الـ double بيديك دقة 15 إلى 17 رقم عشري معنوي تقريبًا — كفاية لأغلب الحسابات، لكنه مش مضبوط بالمللي للكسور العشرية زي الفلوس.
الحل العملي: 3 طرق وكل واحدة بتمنها
ركّز على مبدأين: متقارنش أرقام عشرية بـ ==، ومتخزّنش فلوس في float أو double.
- احسب بالأعداد الصحيحة (أصغر وحدة). خزّن الفلوس بالقرش/السنت كأرقام صحيحة، والحساب بيطلع مضبوط 100%.
- استخدم Decimal. النوع ده بيحسب بنظام عشري حقيقي، فـ 0.1 + 0.2 بتطلع 0.3 بالظبط — بشرط تبعت قيمك كنص مش float.
- قارن بسماحية (tolerance). لو مش فلوس، استخدم
iscloseبدل==.
# 1) الأعداد الصحيحة: الأأمن للفلوس
cart = [10, 20, 70] # قروش
total = sum(cart) # 100 قرش = 1.00 بالظبط
print(total / 100) # 1.0
# 2) Decimal: لاحظ النصوص "0.1" مش 0.1
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2")) # 0.3
# 3) المقارنة بسماحية بدل ==
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True
وفي JavaScript نفس المبدأ:
(0.1 + 0.2).toFixed(2); // "0.30"
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON; // true
الـ trade-off هنا واضح: Decimal مضبوط لكنه أبطأ بمراحل من float (ممكن يوصل عشرات الأضعاف في الحسابات الكتيرة) وبياخد ذاكرة أكتر. طريقة الأعداد الصحيحة أسرع وأأمن، بس بتطلب انضباط إنك تفضل شغّال بالقرش في كل الكود وتحوّل للعرض بس في الآخر. والمقارنة بسماحية سهلة، لكنها مش حل للفلوس لأنها بتقبل فرق بسيط غير مقبول محاسبيًا.
float ولا double؟
فيه نوع أصغر اسمه float أحادي الدقة (32 بت): بت إشارة، 8 بت أس، 23 بت كسر. دقته أقل بكتير (6 إلى 7 أرقام معنوية بس)، فالخطأ بيظهر أسرع وأوضح.
القاعدة: استخدم double (الافتراضي) لأغلب الحسابات العامة. الـ float بيوفّر نص الذاكرة ومفيد في الرسوميات وتعلّم الآلة على ملايين القيم، بس متستخدموش لأي حاجة محتاجة دقة. ولا الاتنين مناسبين للفلوس — استخدم decimal أو أعداد صحيحة.
متى متشغلش بالك بالموضوع ده
لو شغلك مش فلوس ولا مقارنات دقيقة — زي نِسَب، أوزان تقريبية، إحداثيات رسم، أو متوسطات إحصائية — الخطأ في حدود 10⁻¹⁶ مالوش أي تأثير عملي، وتعقيد الكود بـ Decimal هيبقى مبالغة. سيب الـ double وامشي. الموضوع بيهمّك بس لما تقارن بـ == أو تخزّن مبالغ مالية.
الخطوة التالية
دوّر في كودك على أي == بين أرقام عشرية، وأي مكان بتخزّن فيه فلوس في float أو double. غيّر المقارنة لـ isclose، وحوّل تخزين الفلوس لأعداد صحيحة بالقرش أو لـ Decimal. لو الـ unit test اللي كان بيفشل على == 0.3 عدّى بعد التغيير، يبقى المشكلة اتقفلت.
المصادر
- Python Docs — Floating Point Arithmetic: Issues and Limitations
- IEEE 754 — Wikipedia
- Double-precision floating-point format — Wikipedia
- MDN — Number (JavaScript)
- David Goldberg — What Every Computer Scientist Should Know About Floating-Point Arithmetic
- 0.30000000000000004.com — نفس الناتج في كل اللغات