المستوى: مبتدئ — المقال ده مكتوب لأي حد لسه في بداية طريقه في البرمجة، شاف نتيجة حسابية غريبة على الشاشة وعايز يفهم بيحصل إيه بالظبط، من غير رياضيات معقّدة.
ليه 0.1 + 0.2 مش بيساوي 0.3 في كل لغات البرمجة؟
لو كتبت 0.1 + 0.2 في أي لغة برمجة تقريبًا ولقيت الناتج 0.30000000000000004 بدل 0.3، فالكمبيوتر مش بايظ وانت مش غلطان. في آخر المقال ده هتعرف بالظبط ليه ده بيحصل، إمتى يبقى خطر حقيقي على شغلك، وإزاي تقفله في سطر أو سطرين.
المشكلة باختصار
افتح console المتصفح دلوقتي واكتب 0.1 + 0.2. الناتج هيكون 0.30000000000000004. وجرّب 0.1 + 0.2 === 0.3 هيرجّعلك false. ده مش bug في JavaScript ولا في Python ولا في Java. ده سلوك مكتوب في معيار رسمي اسمه IEEE 754، وكل المعالجات الحديثة بتنفّذه بنفس الطريقة بالظبط. السبب باختصار: الكمبيوتر بيخزّن الأرقام العشرية بنظام مالوش القدرة إنه يكتب 0.1 بدقة كاملة، فبيقرّبها، والتقريب الصغير ده بيطفو على السطح أول ما تجمع رقمين.
الأول، خلّينا نفهمها من غير كمبيوتر خالص
تعالى نلعب لعبة بسيطة بالورقة والقلم. قسّم 1 على 3. هيطلعلك 0.3333... والتلاتات مش بتخلص أبدًا. لو قلتلك "اكتبلي ناتج 1÷3 في أربع خانات بس بعد العلامة العشرية"، هتكتب 0.3333، وبكده تكون قرّبت الرقم وخسرت جزء صغير منه.
دلوقتي اضرب اللي كتبته في 3: 0.3333 × 3 = 0.9999. المفروض يطلع 1 بالظبط، بس طلع 0.9999. الفرق الصغير ده — 0.0001 — هو تمن إنك حاولت تكتب رقم تمثيله لا نهائي في مساحة محدودة.
الكمبيوتر بيقع في نفس الفخ بالظبط، بفرق واحد: هو مش بيحسب بنظام العشرة اللي احنا متعودين عليه، هو بيحسب بنظام الاتنين. وفي نظام الاتنين، الرقم اللي "تلاتاته مش بتخلص" مش هو 1÷3 — لأ، هو 0.1 نفسه.
طب ليه 0.1 بالذات بتعمل مشكلة في نظام الاتنين؟
في النظام العشري، الخانات بعد العلامة معناها أعشار، ثم أجزاء من مئة، ثم من ألف. في النظام الثنائي (نظام الاتنين)، الخانات بعد العلامة معناها نُص (1/2)، ثم رُبع (1/4)، ثم تُمن (1/8)، وهكذا. يعني الكمبيوتر يقدر يكتب بدقة كاملة أي رقم تقدر تركّبه من جمع كسور مقامها قوة للاتنين، وبس.
0.5سهل: هو نُص بالظبط، بيتكتب0.1في الثنائي.0.25سهل: هو رُبع، بيتكتب0.01.0.75سهل: نُص زائد رُبع، بيتكتب0.11.
لكن جرّب تركّب 0.1 العشري من جمع نُص ورُبع وتُمن وما بعدهم... مش هتقدر توصّلها بالظبط مهما ضفت خانات. التمثيل الثنائي للكسر 0.1 هو 0.0001100110011001100110011... والنمط 0011 بيفضل بيتكرر للأبد، زي التلاتات بالظبط في 1÷3. الصورة اللي في أول المقال دي بالضبط: كل مربّع بيمثّل خانة ثنائية واحدة في تمثيل 0.1، والنمط ماشي ومش بيقف.
التفسير الدقيق: إزاي الكمبيوتر بيخزّن الرقم فعلًا
النوع اللي بتتخزّن فيه الأرقام العشرية في أغلب اللغات اسمه double precision floating point، وبياخد 64 خانة (bit) في الذاكرة. الـ 64 خانة دول متقسّمين بالظبط زي ما في الرسم ده:
- خانة واحدة للإشارة (sign): بتحدّد الرقم موجب ولا سالب.
- 11 خانة للأس (exponent): بتحدّد حجم الرقم، يعني قوة الاتنين المضروب فيها.
- 52 خانة للمانتيسا (mantissa): بتحمل الأرقام المعنوية نفسها، وهي دي بالظبط مكمن المشكلة.
الرقم بيتخزّن بالصيغة دي: القيمة تساوي الإشارة مضروبة في 1.المانتيسا مضروبة في 2 أس (الأس ناقص 1023). المهم في الكلام ده إن المانتيسا فيها 52 خانة وبس. يعني عند الكمبيوتر مساحة ثابتة ومحدودة جدًا للأرقام المعنوية، وده بيدّيك دقة في حدود 15 إلى 17 رقم عشري معنوي لا أكتر.
وبما إن تمثيل 0.1 الثنائي لا نهائي، اللي بيحصل إن الكمبيوتر بياخد أقرب 52 خانة، يرمي الباقي، ويقرّب. النتيجة إن القيمة اللي بتتخزّن فعلًا أول ما تكتب 0.1 مش 0.1 بالظبط، هي:
0.1000000000000000055511151231257827021181583404541015625نفس الكلام بيحصل مع 0.2. كل واحد فيهم فيه خطأ تقريب ضئيل جدًا في حدود 10 أس سالب 17. ولما تجمعهم، الخطأين بيتجمّعوا، والناتج بيتقرّب لأقرب قيمة ممكنة، واللي بتظهر على الشاشة بالشكل 0.30000000000000004. الزيادة دي مقدارها تقريبًا 0.0000000000000000555 — رقم تافه فعلًا، بس موجود وبيكسر المقارنة.
جرّبها بنفسك: الكود اللي بيكشف الحكاية
الكود ده تقدر تنسخه وتجرّبه فورًا في console المتصفح:
// JavaScript
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
// المقارنة الصح: بفرق أصغر من هامش صغير بدل علامة التساوي
const isClose = Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON;
console.log(isClose); // true
ونفس التجربة في Python بتدّي نفس الناتج بالظبط، لأن الاتنين بيشتغلوا على معيار IEEE 754 نفسه:
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
>>> s = 0.0
>>> for _ in range(10): s += 0.1
>>> s
0.9999999999999999
لاحظ المثال الأخير: لما تجمع 0.1 عشر مرات المفروض يطلع 1.0، بس بيطلع 0.9999999999999999. ده معناه إن الخطأ مش بيحصل مرة واحدة، هو بيتراكم مع كل عملية.
القاعدة الذهبية اللي تطلع بيها من هنا: ممنوع تستخدم == في مقارنة رقمين عشريين. قارن دايمًا بفرق أصغر من هامش صغير (epsilon).
سيناريو من الواقع: لما القرشين الغلط يكسروا الفاتورة
الكلام ده يفضل نظري لحد ما يلمس الفلوس. تخيّل متجر إلكتروني عربي بياخد 8,000 طلب في اليوم، وكل طلب بيجمع سعر المنتج زائد الشحن. لو الحسابات دي ماشية بنوع float:
let total = 19.99 + 4.99;
console.log(total); // 24.979999999999997
console.log(total === 24.98); // false
الناتج المفروض يكون 24.98، بس طلع 24.979999999999997. دلوقتي تخيّل النظام بيقارن total === expectedTotal قبل ما يأكّد الدفع — المقارنة هتفشل، والطلب السليم هيتعلّق. على 8,000 طلب في اليوم، حتى لو 2% بس وقعوا في الفخ ده، يبقى 160 طلب متعطّل يوميًا وفريق دعم بيلاحق مشكلة وهمية. والأسوأ من كده: لو الأخطاء الصغيرة بتتجمّع في تقرير مالي شهري على ملايين العمليات، الإجمالي ممكن يخرج بفرق محسوس فعلًا، مش 10 أس سالب 17.
الحل: 3 طرق، وكل واحدة ليها تمنها
خزّن الفلوس كأرقام صحيحة (بالقرش بدل الجنيه). بدل ما تخزّن
19.99، خزّن1999قرش. كل الحسابات تبقى على أرقام صحيحة، والأرقام الصحيحة مفيهاش أي خطأ تقريب أصلًا. بتكسب: دقة كاملة 100% وسرعة عالية. بتخسر: لازم تفتكر تقسم على 100 وقت العرض بس، وتضرب في 100 وقت الإدخال. ده الحل الأخف، وهو اللي بتعتمد عليه أنظمة دفع كبيرة زي Stripe فعلًا.استخدم نوع Decimal مخصّص. Python فيها
decimal.Decimal، وقواعد البيانات فيها نوعDECIMALأوNUMERIC، وJavaScript ليها مكتبات زيdecimal.js. الأنواع دي بتحسب بالنظام العشري نفسه فمفيش تقريب ثنائي. بتكسب: دقة عشرية كاملة وكود واضح. بتخسر: أبطأ بشكل ملحوظ — العمليات على Decimal ممكن تكون أبطأ منfloatبعشرات المرات — وبتاخد ذاكرة أكتر. وانتبه لفخ مهم: لازم تبني الـ Decimal من نص زيDecimal("0.1")مش من رقمfloat، وإلا هتورّث نفس الخطأ من أول سطر.قرّب في اللحظة الصح. دوال زي
toFixed(2)في JavaScript أوround(x, 2)في Python بتقرّب الرقم لخانتين. بتكسب: أرخص حل وأسهله. بتخسر: ده مجرد بلاستر — بيخبّي المشكلة وقت العرض بس، ومش بيمنع تراكم الأخطاء جوّه الحسابات نفسها. وأكتر من كده، التقريب نفسه ممكن يتأثر بالمشكلة: في Pythonround(2.675, 2)بيطلع2.67مش2.68، لأن 2.675 نفسها متخزّنة أقل شوية من قيمتها. فاستخدم التقريب للعرض على الشاشة فقط، وممنوع تمامًا تعتمد عليه في المقارنات أو في القيم اللي بتتخزّن في الداتابيز.
متى ما تشغّلش بالك بالموضوع ده أصلًا
المقال ده ممكن يخوّفك من float بدون داعي، فخلّينا نوضّح الصورة. النوع float سريع جدًا ومدعوم من المعالج نفسه على مستوى العتاد، وهو الاختيار الصح في أغلب الشغل. خطأه في حدود 10 أس سالب 16، وده رقم تافه مالوش أي تأثير عملي في:
- الرسوميات والألعاب وحسابات الفيزياء والإحداثيات.
- تعلّم الآلة والإحصاء وقراءات الحسّاسات.
- أي قيمة بتتعرض في النهاية كرسمة أو نسبة تقريبية.
الفرضية هنا واضحة: المشكلة دي بتهمّ في تلات حالات بس — الفلوس، والمقارنات اللي محتاجة تساوي مظبوط بالظبط، وتراكم الأخطاء على ملايين العمليات. لو شغلك مش واحد من التلاتة دول، سيبك من Decimal واستخدم float وانت مرتاح، لأن استخدام Decimal في غير محلّه بيبطّأ برنامجك من غير أي فايدة.
الخطوة التالية
دلوقتي حالًا، افتح أي ملف في مشروعك بيتعامل مع فلوس. دوّر على أي متغير بيشيل مبلغ ونوعه float أو number أو double — ده علامة حمرا. ابدأ بأخطر نقطة: حساب الإجمالي، وأي مقارنة فيها == على مبلغ. حوّل التخزين يبقى بالقرش كعدد صحيح، أو لو الإطار اللي بتشتغل بيه بيدعم Decimal استخدمه. لو عملت ده في كل مكان بيمسّ الفلوس، تبقى قفلت الباب على فئة كاملة من الـ bugs الصامتة اللي بتظهر بعد شهور.
المصادر
- معيار IEEE 754-2019 الرسمي للحساب ذي الفاصلة العائمة (IEEE Standard for Floating-Point Arithmetic) — المرجع اللي بيحدّد بنية الـ 64 خانة وقواعد التقريب.
- David Goldberg، «What Every Computer Scientist Should Know About Floating-Point Arithmetic»، منشورة في ACM Computing Surveys سنة 1991 — المرجع الكلاسيكي لفهم أخطاء التقريب.
- توثيق Python الرسمي، «Floating-Point Arithmetic: Issues and Limitations» على الرابط docs.python.org/3/tutorial/floatingpoint.html — ومنه القيمة المخزّنة الدقيقة للكسر 0.1.
- MDN Web Docs، صفحة
Number.EPSILON— الطريقة الموصى بيها لمقارنة الأرقام العشرية في JavaScript. - الموقع المرجعي 0.30000000000000004.com اللي بيعرض نفس الناتج عبر أكتر من 100 لغة برمجة.