معضلات الجداول والعملات والنسب المئوية: ما الذي يحدث خلف الشاشة؟
كيف تُخزَّن العملات والنسب المئوية في قواعد البيانات؟ واتجاه رمز العملة وجداول الأرقام — معضلات حقيقية وحلول برمجية عملية.
عدد الكلمات: ~١٩٠٠ • مدة القراءة: ٩ دقائق
معضلات الجداول والعملات والنسب المئوية
ما الذي يحدث خلف الشاشة — وأين تختبئ الأخطاء؟
ملاحظة للقارئ: هذا المقال مستقلٌ تماماً ويمكن قراءته دون مقدمات، لكن إن أردت فهم كيف تعرض الأرقام والتواريخ تلقائياً بحسب المنطقة، فابدأ بـ توطين الأرقام والتواريخ بـ Intl.
في هذا المقال من منصة ذي يزن، ننتقل إلى الطبقة الأعمق من مشكلة التوطين: ليس كيف تعرض الأرقام على الشاشة، بل كيف تُخزَّن أصلاً في قاعدة البيانات. هذا هو المكان الذي تولد فيه معظم الأخطاء المالية الصامتة، تلك التي لا تظهر في الاختبار وتكتشفها فقط حين يشكو عميلٌ حقيقي.
ثلاث معضلاتٍ تستحق وقفةً مستقلة: تخزين العملات، وتخزين النسب المئوية، واتجاه رمز العملة في واجهة المستخدم. كل واحدةٍ منها قرارٌ تصميمي له تبعاتٌ بعيدة المدى.
أولاً: كيف تُخزَّن العملات في قاعدة البيانات؟
السؤال يبدو بسيطاً: عندي مبلغ ١٢٥٠٫٧٥ ريالاً سعودياً، كيف أخزّنه؟ لكن الإجابة تنطوي على قراراتٍ متشعبة.
المشكلة مع أنواع الأعداد العشرية
الخطأ الأكثر شيوعاً هو استخدام FLOAT أو DOUBLE لتخزين المبالغ المالية. هذان النوعان يستخدمان تمثيل الفاصلة العائمة (floating-point) الذي يُخزَّن داخلياً بالنظام الثنائي، وبعض الأعداد العشرية لا تُمثَّل بدقةٍ تامةٍ في هذا النظام:
// المشكلة الكلاسيكية للفاصلة العائمة في JavaScript
console.log(0.1 + 0.2);
// → 0.30000000000000004 ← ليس 0.3 !
// في قاعدة البيانات: نفس المشكلة
// SELECT 0.1 + 0.2 AS result → 0.30000000000000004
// إذا خزّنت 1250.75 كـ FLOAT قد تسترجع 1250.7499999...
في سياق عرض مقالٍ تقني، هذا الخطأ مثير للاهتمام. في سياق فاتورةٍ بها ألف بند، هذا الخطأ يعني أرقاماً ماليةً خاطئة.
الحل: DECIMAL أو INTEGER
لتخزين المبالغ المالية بأمان، لديك خياران صحيحان:
الخيار الأول: DECIMAL (أو NUMERIC): نوع بياناتٍ ذو دقةٍ ثابتةٍ تحدده أنت:
-- في SQL: عمود للمبالغ المالية بدقة 15 رقماً وخانتين عشريتين
CREATE TABLE invoices (
id INT PRIMARY KEY,
amount DECIMAL(15, 2), -- ✅ دقة ثابتة، آمن للمال
currency CHAR(3), -- رمز ISO: SAR, AED, EGP
created_at TIMESTAMP
);
-- مثال على التخزين
INSERT INTO invoices (amount, currency) VALUES (1250.75, 'SAR');
الخيار الثاني: INTEGER بأصغر وحدة: نهجٌ تعتمده كثيرٌ من أنظمة الدفع الكبرى (Stripe، PayPal)، وهو تخزين المبلغ بأصغر وحدةٍ نقديةٍ وتجنّب الكسور العشرية كلياً:
-- ١٢٥٠٫٧٥ ريال = 125075 هللة (أصغر وحدة = هللة = 1/100 ريال)
// في JavaScript
const amountInHalala = 125075; // INTEGER — لا أخطاء عشرية أبداً
// عند العرض: تحويل للعرض فقط
const amountForDisplay = amountInHalala / 100; // 1250.75
const formatted = new Intl.NumberFormat('ar-SA', {
style: 'currency',
currency: 'SAR'
}).format(amountForDisplay);
// ← ر.س ١٬٢٥٠٫٧٥
لكن انتبه: هذا النهج يفترض أن العملة لها خانتان عشريتان دائماً. الكويتي الدينار (KWD) له ثلاث خانات (فلس = 1/1000 دينار)، واليين الياباني لا خانات عشرية له أصلاً. لذا احتفظ دائماً بمعلومة عدد الخانات العشرية لكل عملة.
تخزين رمز العملة: لا تخزّن الرمز البصري
خطأ شائعٌ آخر: تخزين رمز العملة المرئي (“ر.س” أو “$”) في قاعدة البيانات بدلاً من رمز ISO الدولي:
| ❌ خاطئ — ما تخزّنه | ✅ صحيح — ما يجب تخزينه | السبب |
|---|---|---|
| ر.س | SAR | الرمز يختلف بحسب اللغة — Intl يتولى العرض |
| د.إ | AED | ISO 4217 معيار عالمي لا يتغيّر |
| ج.م | EGP | يسهّل التحويل والمقارنة والتقارير |
| $ | USD | $ تعني دولاراً أسترالياً وكندياً أيضاً |
قاعدة البيانات مخزن للحقائق، لا لتفضيلات العرض. خزّن رمز ISO وأترك لـ Intl مهمة تحويله إلى ما يراه المستخدم.
ثانياً: معضلة النسب المئوية — 15 أم 0.15؟
هذه المعضلة أبسط في ظاهرها لكنها مصدر أخطاءٍ مستمرةُ في الفرق البرمجية. السؤال: إذا كان الخصم ١٥٪، ماذا تخزّن في قاعدة البيانات؟
المدرستان وتبعاتهما
| المدرسة | ما يُخزَّن | حساب الخصم على ١٠٠٠ | العرض بـ Intl |
|---|---|---|---|
| القيمة الحرفية | 15 |
1000 * (15 / 100) |
format(15 / 100) |
| القيمة النسبية | 0.15 |
1000 * 0.15 |
format(0.15) |
كلا المدرستين صحيحتان — المشكلة تنشأ حين يخلط فريقٌ واحد بين الاثنتين دون توثيق. النتيجة هي خصم ١٥٠٠٪ بدلاً من ١٥٪ أو ضريبة ٠٫١٥٪ بدلاً من ١٥٪ — وكلاهما كارثةٌ صامتةٌ في نظام الفواتير.
التوصية العملية
المدرسة الأكثر توافقاً مع JavaScript وواجهة Intl هي تخزين القيمة النسبية (0.15)، لأن Intl.NumberFormat مع style: 'percent' تتوقع تلقائياً قيمةً بين ٠ و١:
// ✅ القيمة المخزّنة في DB: 0.15
const discountRate = 0.15;
const percentFormatter = new Intl.NumberFormat('ar-SA', {
style: 'percent',
maximumFractionDigits: 1,
});
console.log(percentFormatter.format(discountRate));
// ← ١٥٪
// حساب الخصم مباشرة — لا قسمة على 100
const originalPrice = 1000;
const discountAmount = originalPrice * discountRate; // 150
const finalPrice = originalPrice - discountAmount; // 850
أما إذا اخترت تخزين القيمة الحرفية (15) فوثّق ذلك صراحةً في مخطط قاعدة البيانات، وأضف طبقة تحويلٍ ثابتةٍ قبل أي عرض:
// القيمة المخزّنة في DB: 15 (موثَّق كـ "percentage literal")
const discountLiteral = 15;
// طبقة تحويل إلزامية قبل أي استخدام
const discountRate = discountLiteral / 100; // 0.15
console.log(percentFormatter.format(discountRate));
// ← ١٥٪
ثالثاً: اتجاه رمز العملة — قبل الرقم أم بعده؟
هذه المعضلة أكثر دقةً من سابقتيها لأنها تتقاطع مع اتجاه الكتابة (RTL/LTR) وأعراف كل دولةٍ في آنٍ واحد.
لا توجد قاعدةٌ عربيةٌ موحدة
الواقع أن الدول العربية لا تتفق على موضع رمز العملة:
| الدولة | الصيغة الشائعة | مثال | موضع الرمز |
|---|---|---|---|
| المملكة العربية السعودية | رمز ثم رقم | ر.س ١٬٢٥٠٫٠٠ | قبل الرقم (يميناً) |
| الإمارات العربية المتحدة | رمز ثم رقم | د.إ ١٬٢٥٠٫٠٠ | قبل الرقم (يميناً) |
| مصر | رقم ثم رمز | ١٬٢٥٠٫٠٠ ج.م | بعد الرقم (يساراً) |
| المغرب | رقم ثم رمز | 1.250,00 MAD | بعد الرقم (يساراً) |
الخبر الجيد: Intl.NumberFormat تعرف هذه الفروق وتطبّقها تلقائياً. لكن المشكلة تنشأ حين تحاول تجاوزها يدوياً أو حين تبني واجهة مستخدم تفترض موضعاً ثابتاً للرمز.
المشكلة مع التنسيق اليدوي
// ❌ خاطئ — تجميع يدوي يفترض موضعاً ثابتاً
function formatCurrency(amount, symbol) {
return `${symbol} ${amount.toFixed(2)}`;
// دائماً يضع الرمز يساراً — خاطئ للسعودية في RTL
}
// ✅ صحيح — اترك Intl يحدد الموضع
function formatCurrency(amount, locale, currency) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency,
}).format(amount);
}
console.log(formatCurrency(1250, 'ar-SA', 'SAR'));
// ← ر.س ١٬٢٥٠٫٠٠ (الرمز في موضعه الصحيح للسعودية)
console.log(formatCurrency(1250, 'ar-EG', 'EGP'));
// ← ١٬٢٥٠٫٠٠ ج.م (الرمز في موضعه الصحيح لمصر)
حين تحتاج تحكماً في موضع الرمز
أحياناً يفرض عليك تصميم الفاتورة عرض الرمز في موضعٍ بعينه بصرف النظر عن العرف. في هذه الحالة استخدم formatToParts() التي تعرفنا عليها في المقال السابق:
function getCurrencyParts(amount, locale, currency) {
const parts = new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).formatToParts(amount);
// استخراج الأجزاء منفصلة
const symbol = parts.find(p => p.type === 'currency')?.value;
const integer = parts.find(p => p.type === 'integer')?.value;
const decimal = parts.find(p => p.type === 'decimal')?.value;
const fraction= parts.find(p => p.type === 'fraction')?.value;
return { symbol, number: `${integer}${decimal}${fraction}` };
}
const { symbol, number } = getCurrencyParts(1250.75, 'ar-SA', 'SAR');
// symbol → 'ر.س'
// number → '١٬٢٥٠٫٧٥'
// الآن تحكّم في العرض كما تشاء في HTML
رابعاً: الجداول الحسابية — مشاكلٌ لا تظهر إلا عند الجمع
جداول الفواتير والتقارير المالية تضيف بُعداً آخر: جمع الأرقام المنسّقة أو التعامل مع أعمدة مختلطة الاتجاه.
لا تجمع الأرقام المنسّقة — الجمع للأرقام الخام فقط
const items = [
{ name: 'خدمة التصميم', amount: 1500.00 },
{ name: 'خدمة البرمجة', amount: 3200.50 },
{ name: 'الاستشارة', amount: 450.00 },
];
const formatter = new Intl.NumberFormat('ar-SA', {
style: 'currency',
currency: 'SAR',
});
// ✅ اجمع الأرقام الخام أولاً
const total = items.reduce((sum, item) => sum + item.amount, 0);
// total = 5150.50
// ثم نسّق النتيجة مرة واحدة للعرض
console.log(formatter.format(total));
// ← ر.س ٥٬١٥٠٫٥٠
// ❌ لا تجمع النصوص المنسّقة أبداً
// 'ر.س ١٬٥٠٠٫٠٠' + 'ر.س ٣٬٢٠٠٫٥٠' ← لا معنى له
خاصية tabular-nums في جداول الأرقام
حين تعرض عموداً من الأرقام المالية في جدول، المحاذاة البصرية مهمةٌ للقراءة السريعة. خاصية CSS font-variant-numeric: tabular-nums تجعل كل رقمٍ يشغل العرض ذاته بصرف النظر عن شكله:
/* عمود المبالغ في الجدول المالي */
.amount-column {
font-variant-numeric: tabular-nums;
text-align: end; /* يمين في LTR، يسار في RTL — منطقي لا مطلق */
font-feature-settings: "tnum" 1; /* دعم إضافي للخطوط القديمة */
}
لاحظ استخدام text-align: end بدلاً من right — وهي من الخصائص المنطقية في CSS التي تتكيف تلقائياً مع اتجاه الكتابة. (تعرفنا على هذا المبدأ بتفصيل في سلسلتنا السابقة حول الخصائص المنطقية في CSS.)
خامساً: ضريبة القيمة المضافة — معضلة التدوير
مسألةٌ أخيرةٌ تستحق الذكر لأنها تسبب خلافاتٍ حقيقيةٍ بين المطورين والمحاسبين: كيف تحسب الضريبة على بنودٍ متعددة؟
هناك طريقتان مختلفتان تُنتجان نتائج مختلفة:
const vatRate = 0.15; // ضريبة القيمة المضافة ١٥٪
const items = [100.10, 200.20, 300.30];
// طريقة ①: احسب ضريبة كل بند ثم اجمع
const taxPerItem = items.map(item =>
Math.round(item * vatRate * 100) / 100
);
// [15.02, 30.03, 45.05]
const totalTax1 = taxPerItem.reduce((a, b) => a + b, 0);
// totalTax1 = 90.10
// طريقة ②: اجمع الكل ثم احسب الضريبة مرة واحدة
const subtotal = items.reduce((a, b) => a + b, 0); // 600.60
const totalTax2 = Math.round(subtotal * vatRate * 100) / 100;
// totalTax2 = 90.09
// الفرق: ٠٫٠١ — ضئيل في فاتورة واحدة، مُضاعَف في آلاف الفواتير
لا توجد إجابةٌ “برمجية” صحيحةٌ هنا، فالقرار تحدده هيئة الزكاة والضريبة في السعودية مثلاً، أو الجهة المالية المختصة في كل دولة. لكن قرارك يجب أن يكون واحداً وموثّقاً في كل النظام.
خلاصة المقال والخطوة القادمة
المعضلات الثلاث التي استعرضناها تشترك في مبدأ واحد: افصل بين التخزين والعرض. قاعدة البيانات تخزّن الحقيقة الرياضية (DECIMAL، ISO 4217، قيمة نسبية)، وطبقة العرض تتولى تحويل هذه الحقيقة إلى ما يتوقعه المستخدم في منطقته.
في المقال القادم — وهو المشروع التطبيقي الختامي — سنبني نظام فواتيرٍ كاملاً يجمع كل ما تعلمناه: قاعدة بياناتٍ صحيحة البنية، وعرضٌ تلقائيٌ للعملات والتواريخ والنسب المئوية لثلاث دول ٍعربيةٍ في آنٍ واحد.
الخطوة التالية الموصى بها:
تابع معنا: مشروع تطبيقي: نظام فواتير متعدد الدول
المراجع والمصادر:
- معيار ISO 4217 لرموز العملات: iso.org — Currency Codes
- توثيق PostgreSQL — أنواع البيانات الرقمية: postgresql.org — Numeric Types
- نهج Stripe في تخزين المبالغ بأصغر وحدة: stripe.com — Zero-Decimal Currencies
- توثيق MDN —
Intl.NumberFormat.formatToParts: developer.mozilla.org - لوائح ضريبة القيمة المضافة — هيئة الزكاة والضريبة والجمارك: zatca.gov.sa
منصة ذي يزن © ٢٠٢٦
سلاسل التوطين
دليل معايرة البيانات المالية — ٤ مقالات
سلسلة دليل معايرة البيانات المالية — ٤ مقالات | منصة ذي يزن © ٢٠٢٦




