لو كتبت في JavaScript أو Python الأمر 0.1 + 0.2 وطبعت النتيجة، هتلاقي 0.30000000000000004 مش 0.3. ده مش bug في اللغة، وده مش خطأ في الـ CPU. ده سلوك رسمي في معيار IEEE 754 اللي بتشتغل بيه كل اللغات تقريبًا. المقال ده هيفهّمك ليه ده بيحصل بمثال بسيط، ثم بالتفسير العلمي، وهيوريك سيناريو بيخسّر فلوس فعلًا في production، وطرق العلاج بكود شغّال.
المشكلة باختصار
الكمبيوتر بيخزّن الأرقام العشرية بصيغة ثنائية (binary)، مش عشرية. فيه أرقام عشرية بسيطة زي 0.1 مش ممكن تتمثّل بدقة كاملة في binary، زي ما 1÷3 مش ممكن تتكتب في decimal غير كـ 0.333... بلا نهاية. الكمبيوتر بيقرّب الرقم عند حد معيّن، والخطأ الصغير ده بيتراكم ويطلع في النتيجة.
الشرح المبسّط أولًا: قصة الثلث
تخيّل إنك ماسك آلة حاسبة وقلت لها احسب 1 ÷ 3. الناتج هيظهر 0.3333333، والآلة هتوقف عند عدد محدد من الخانات. لو جمعت النتيجة على نفسها 3 مرات، المفروض ترجع لـ 1، بس هتلاقيها 0.9999999. السبب: الآلة قطعت الرقم وضيّعت جزء منه.
ده بالظبط اللي بيحصل للكمبيوتر، لكن في نظام العدّ الثنائي. الرقم 0.1 بسيط جدًا في decimal، لكنه في binary بيطلع رقم متكرّر لا نهائي: 0.0001100110011001100... وبما إن الذاكرة محدودة (64 bit في double precision)، الكمبيوتر بيقطع الرقم عند حد معيّن، والباقي بيضيع. لمّا تجمع 0.1 + 0.2، كل واحد فيهم فيه خطأ تقريب، والخطأين بيتجمعوا، فيطلع الناتج 0.30000000000000004.
الفكرة ببساطة: عين الكمبيوتر بتشوف 0.1 كإنه 0.1000000000000000055511151231257827...، ولما بتضيف عليه 0.2 (اللي هو كمان مش مضبوط)، الخطأ بيبان في أول 17 خانة.
التفسير العلمي الدقيق: IEEE 754
المعيار اللي بيحكم الأرقام العشرية في JavaScript وPython وC وJava وGo وأغلب اللغات هو IEEE 754. الصيغة الأشهر هي double precision = 64 bit، متقسّمين كالتالي:
- 1 bit للإشارة (موجب أو سالب)
- 11 bit للأس (exponent)
- 52 bit للـ mantissa (الخانات العشرية الفعلية)
لإن الـ mantissa محدودة بـ 52 bit، أي رقم binary دوري بيتقطع عند الحد ده. أقل فرق ممكن يتمثّل قرب الرقم 1 اسمه Number.EPSILON في JavaScript وقيمته تقريبًا 2.220446049250313e-16. أي خطأ أصغر من ده الكمبيوتر مش بيعرف يميّزه.
سيناريو واقعي: خسارة 14 ألف جنيه في شهر
تخيّل موقع e-commerce بيبيع 50,000 طلب في الشهر، ومتوسط قيمة الطلب 300 جنيه، والضريبة 14%. لو حسبت الضريبة بـ price * 0.14 في JavaScript لكل طلب وخزّنت الناتج كـ float، كل طلب ممكن يكون غلط بمقدار جزء من الألف جنيه. على 50,000 طلب، الفرق بين اللي قيّدته والرقم اللي الـ auditor بيحسبه يدويًا ممكن يوصل 10-15 ألف جنيه في الشهر، وده بيفتحلك مشكلة محاسبية مع مصلحة الضرائب مش بس bug تقني.