كيفية فرز السلاسل النصية أبجدياً حسب اللغة في جافا سكريبت
استخدم Intl.Collator و localeCompare() لفرز السلاسل النصية بشكل صحيح لأي لغة
مقدمة
عند فرز مصفوفة من السلاسل النصية في جافا سكريبت، فإن السلوك الافتراضي يقارن السلاسل النصية حسب قيم وحدات الترميز UTF-16 الخاصة بها. هذا يعمل للنصوص الأساسية بترميز ASCII، لكنه يفشل عند فرز الأسماء، أو عناوين المنتجات، أو أي نص يحتوي على أحرف مشكّلة، أو نصوص غير لاتينية، أو أحرف مختلطة الحالة.
تمتلك اللغات المختلفة قواعد مختلفة للترتيب الأبجدي. تضع السويدية الأحرف å وä وö في نهاية الأبجدية بعد حرف z. تعامل الألمانية حرف ä كمكافئ لحرف a في معظم السياقات. تتجاهل الفرنسية علامات التشكيل في أوضاع مقارنة معينة. هذه القواعد اللغوية تحدد كيف يتوقع الناس رؤية القوائم المرتبة في لغتهم.
توفر جافا سكريبت واجهتي برمجة تطبيقات لفرز السلاسل النصية مع مراعاة اللغة. تتعامل طريقة String.prototype.localeCompare() مع المقارنات البسيطة. توفر واجهة Intl.Collator أداءً أفضل عند فرز المصفوفات الكبيرة. يشرح هذا الدرس كيفية عمل كلتا الطريقتين، ومتى تستخدم كل منهما، وكيفية تكوين سلوك الفرز للغات المختلفة.
لماذا يفشل الفرز الافتراضي مع النصوص الدولية
تقارن طريقة Array.sort() الافتراضية السلاسل النصية حسب قيم وحدات ترميز UTF-16 الخاصة بها. هذا يعني أن الأحرف الكبيرة تأتي دائمًا قبل الأحرف الصغيرة، والأحرف المشكّلة تُفرز بعد حرف z.
const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort();
console.log(sorted);
// Output: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']
هذه النتيجة خاطئة بالنسبة للغة السويدية. في السويدية، تعتبر الأحرف å وä وö أحرفًا منفصلة تنتمي إلى نهاية الأبجدية. الترتيب الصحيح يجب أن يضع Anna أولاً، ثم Bengt، ثم Åsa وÄrla وÖrjan.
تحدث المشكلة لأن الفرز الافتراضي يقارن قيم نقاط الترميز، وليس المعنى اللغوي. الحرف Å له نقطة ترميز U+00C5، وهي أكبر من نقطة ترميز z (U+007A). لا تملك جافا سكريبت طريقة لمعرفة أن المتحدثين بالسويدية يعتبرون Å حرفًا منفصلاً له موقع محدد في الأبجدية.
تخلق حالة الأحرف المختلطة مشكلة أخرى.
const words = ['zebra', 'Apple', 'banana', 'Zoo'];
const sorted = words.sort();
console.log(sorted);
// Output: ['Apple', 'Zoo', 'banana', 'zebra']
جميع الأحرف الكبيرة لها قيم نقاط ترميز أقل من الأحرف الصغيرة. هذا يتسبب في ظهور Apple وZoo قبل banana، وهذا ليس ترتيبًا أبجديًا في أي لغة.
كيفية فرز localeCompare للسلاسل النصية وفقًا للقواعد اللغوية
تقوم طريقة localeCompare() بمقارنة سلسلتين نصيتين وفقًا لقواعد الترتيب الخاصة بلغة محددة. تُرجع رقمًا سالبًا إذا كانت السلسلة الأولى تأتي قبل الثانية، وصفرًا إذا كانتا متكافئتين، ورقمًا موجبًا إذا كانت السلسلة الأولى تأتي بعد الثانية.
const result = 'a'.localeCompare('b', 'en-US');
console.log(result);
// الناتج: -1 (القيمة السالبة تعني أن 'a' تأتي قبل 'b')
يمكنك استخدام localeCompare() مباشرة مع Array.sort() عن طريق تمريرها كدالة مقارنة.
const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(sorted);
// الناتج: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']
اللغة السويدية تضع Anna و Bengt أولاً لأنهما يستخدمان أحرفًا لاتينية قياسية. ثم تأتي Åsa و Ärla و Örjan بأحرفها السويدية الخاصة في النهاية.
نفس القائمة عند فرزها باستخدام اللغة الألمانية تنتج نتائج مختلفة.
const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(sorted);
// الناتج: ['Anna', 'Ärla', 'Åsa', 'Bengt', 'Örjan']
اللغة الألمانية تعامل حرف ä كمكافئ لحرف a لأغراض الفرز. هذا يضع Ärla مباشرة بعد Anna، بدلاً من وضعها في النهاية كما تفعل اللغة السويدية.
متى تستخدم localeCompare
استخدم localeCompare() عندما تحتاج إلى فرز مصفوفة صغيرة أو مقارنة سلسلتين نصيتين. توفر واجهة برمجة تطبيقات بسيطة دون الحاجة إلى إنشاء وإدارة كائن مقارنة.
const items = ['Banana', 'apple', 'Cherry'];
const sorted = items.sort((a, b) => a.localeCompare(b, 'en-US'));
console.log(sorted);
// الناتج: ['apple', 'Banana', 'Cherry']
يعمل هذا النهج بشكل جيد للمصفوفات التي تحتوي على بضع عشرات من العناصر. تأثير الأداء ضئيل بالنسبة لمجموعات البيانات الصغيرة.
يمكنك أيضًا استخدام localeCompare() للتحقق مما إذا كانت سلسلة نصية تأتي قبل أخرى دون فرز مصفوفة كاملة.
const firstName = 'Åsa';
const secondName = 'Anna';
if (firstName.localeCompare(secondName, 'sv-SE') < 0) {
console.log(`${firstName} تأتي قبل ${secondName}`);
} else {
console.log(`${secondName} تأتي قبل ${firstName}`);
}
// الناتج: "Anna تأتي قبل Åsa"
تحترم هذه المقارنة الترتيب الأبجدي السويدي دون الحاجة إلى فرز مصفوفة كاملة.
كيف يحسن Intl.Collator الأداء
تقوم واجهة برمجة التطبيقات Intl.Collator بإنشاء دالة مقارنة قابلة لإعادة الاستخدام ومُحسّنة للاستخدام المتكرر. عند فرز مصفوفات كبيرة أو إجراء العديد من المقارنات، يكون المُرتِّب أسرع بكثير من استدعاء localeCompare() لكل مقارنة.
const collator = new Intl.Collator('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort(collator.compare);
console.log(sorted);
// Output: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']
تُرجع خاصية collator.compare دالة مقارنة تعمل مباشرة مع Array.sort(). لا تحتاج إلى تغليفها في دالة سهمية.
إنشاء مُرتِّب مرة واحدة وإعادة استخدامه لعمليات متعددة يتجنب النفقات العامة المتعلقة بالبحث عن بيانات اللغة المحلية في كل مقارنة.
const collator = new Intl.Collator('de-DE');
const germanCities = ['München', 'Berlin', 'Köln', 'Hamburg'];
const sortedCities = germanCities.sort(collator.compare);
const germanNames = ['Müller', 'Schmidt', 'Schröder', 'Fischer'];
const sortedNames = germanNames.sort(collator.compare);
console.log(sortedCities);
// Output: ['Berlin', 'Hamburg', 'Köln', 'München']
console.log(sortedNames);
// Output: ['Fischer', 'Müller', 'Schmidt', 'Schröder']
يتعامل نفس المُرتِّب مع كلتا المصفوفتين دون الحاجة إلى إنشاء نسخة جديدة.
متى تستخدم Intl.Collator
استخدم Intl.Collator عند فرز مصفوفات تحتوي على مئات أو آلاف العناصر. تزداد فائدة الأداء مع حجم المصفوفة لأن دالة المقارنة يتم استدعاؤها عدة مرات أثناء الفرز.
const collator = new Intl.Collator('en-US');
const products = [/* مصفوفة تحتوي على 10,000 اسم منتج */];
const sorted = products.sort(collator.compare);
بالنسبة للمصفوفات التي يزيد حجمها عن بضع مئات من العناصر، يمكن أن يكون المُرتِّب أسرع بعدة مرات من localeCompare().
استخدم أيضًا Intl.Collator عندما تحتاج إلى فرز مصفوفات متعددة بنفس اللغة المحلية والخيارات. إنشاء المُرتِّب مرة واحدة وإعادة استخدامه يلغي عمليات البحث المتكررة عن بيانات اللغة المحلية.
const collator = new Intl.Collator('fr-FR');
const firstNames = ['Amélie', 'Bernard', 'Émilie', 'François'];
const lastNames = ['Dubois', 'Martin', 'Lefèvre', 'Bernard'];
const sortedFirstNames = firstNames.sort(collator.compare);
const sortedLastNames = lastNames.sort(collator.compare);
يعمل هذا النمط بشكل جيد عند بناء عروض الجداول أو واجهات أخرى تعرض قوائم مرتبة متعددة.
كيفية تحديد اللغة المحلية
تقبل كل من localeCompare() و Intl.Collator معرّف اللغة المحلية كوسيط أول. يستخدم هذا المعرّف تنسيق BCP 47، والذي يجمع عادةً بين رمز اللغة ورمز المنطقة الاختياري.
const names = ['Åsa', 'Anna', 'Ärla'];
// اللغة المحلية السويدية
const swedishSorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(swedishSorted);
// النتيجة: ['Anna', 'Åsa', 'Ärla']
// اللغة المحلية الألمانية
const germanSorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(germanSorted);
// النتيجة: ['Anna', 'Ärla', 'Åsa']
تحدد اللغة المحلية قواعد الترتيب التي يتم تطبيقها. تمتلك اللغتان السويدية والألمانية قواعد مختلفة للحرفين å و ä، مما ينتج عنه ترتيبات مختلفة.
يمكنك حذف اللغة المحلية لاستخدام اللغة المحلية الافتراضية للمستخدم من المتصفح.
const collator = new Intl.Collator();
const names = ['Åsa', 'Anna', 'Ärla'];
const sorted = names.sort(collator.compare);
يحترم هذا النهج تفضيلات لغة المستخدم دون تحديد لغة محلية معينة. سيتطابق الترتيب المرتب مع ما يتوقعه المستخدم بناءً على إعدادات المتصفح الخاصة به.
يمكنك أيضًا تمرير مصفوفة من اللغات المحلية لتوفير خيارات احتياطية.
const collator = new Intl.Collator(['sv-SE', 'sv', 'en-US']);
تستخدم واجهة برمجة التطبيقات أول لغة محلية مدعومة من المصفوفة. إذا كانت اللغة السويدية من السويد غير متوفرة، فإنها تحاول استخدام اللغة السويدية العامة، ثم تعود إلى اللغة الإنجليزية الأمريكية.
كيفية التحكم في حساسية الحالة
يحدد خيار sensitivity كيفية تعامل المقارنة مع الاختلافات في الحالة والعلامات. يقبل أربع قيم: base، وaccent، وcase، وvariant.
حساسية base تتجاهل كلاً من الحالة والعلامات، وتقارن فقط الأحرف الأساسية.
const collator = new Intl.Collator('en-US', { sensitivity: 'base' });
console.log(collator.compare('a', 'A'));
// النتيجة: 0 (متساوية)
console.log(collator.compare('a', 'á'));
// النتيجة: 0 (متساوية)
console.log(collator.compare('a', 'b'));
// النتيجة: -1 (أحرف أساسية مختلفة)
يعامل هذا الوضع a و A و á على أنها متطابقة لأنها تشترك في نفس الحرف الأساسي.
حساسية accent تأخذ في الاعتبار العلامات ولكنها تتجاهل الحالة.
const collator = new Intl.Collator('en-US', { sensitivity: 'accent' });
console.log(collator.compare('a', 'A'));
// النتيجة: 0 (متساوية، تم تجاهل الحالة)
console.log(collator.compare('a', 'á'));
// النتيجة: -1 (مختلفة، العلامة مهمة)
حساسية case تأخذ في الاعتبار الحالة ولكنها تتجاهل العلامات.
const collator = new Intl.Collator('en-US', { sensitivity: 'case' });
console.log(collator.compare('a', 'A'));
// النتيجة: -1 (مختلفة، الحالة مهمة)
console.log(collator.compare('a', 'á'));
// النتيجة: 0 (متساوية، تم تجاهل العلامة)
حساسية variant (الافتراضية) تأخذ في الاعتبار جميع الاختلافات.
const collator = new Intl.Collator('en-US', { sensitivity: 'variant' });
console.log(collator.compare('a', 'A'));
// النتيجة: -1 (مختلفة)
console.log(collator.compare('a', 'á'));
// النتيجة: -1 (مختلفة)
يوفر هذا الوضع المقارنة الأكثر صرامة، حيث يعامل أي اختلاف على أنه مهم.
كيفية فرز السلاسل النصية التي تحتوي على أرقام مضمنة
يتيح خيار numeric الفرز الرقمي للسلاسل النصية التي تحتوي على أرقام. عند تفعيله، تتعامل المقارنة مع تسلسلات الأرقام كقيم رقمية بدلاً من مقارنتها حرفًا بحرف.
const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];
// الفرز الافتراضي (ترتيب خاطئ)
const defaultSorted = [...files].sort();
console.log(defaultSorted);
// النتيجة: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']
// الفرز الرقمي (ترتيب صحيح)
const collator = new Intl.Collator('en-US', { numeric: true });
const numericSorted = files.sort(collator.compare);
console.log(numericSorted);
// النتيجة: ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']
بدون الفرز الرقمي، يتم فرز السلاسل النصية حرفًا بحرف. السلسلة النصية 10 تأتي قبل 2 لأن الحرف الأول 1 له نقطة رمز أقل من 2.
عند تفعيل الفرز الرقمي، يتعرف المقارن على 10 كرقم عشرة و2 كرقم اثنين. وهذا ينتج ترتيب الفرز المتوقع حيث يأتي 2 قبل 10.
هذا الخيار مفيد لفرز أسماء الملفات، أرقام الإصدارات، أو أي سلاسل نصية تمزج بين النص والأرقام.
const versions = ['v1.10', 'v1.2', 'v1.20', 'v1.3'];
const collator = new Intl.Collator('en-US', { numeric: true });
const sorted = versions.sort(collator.compare);
console.log(sorted);
// النتيجة: ['v1.2', 'v1.3', 'v1.10', 'v1.20']
كيفية التحكم في أي حالة أحرف تأتي أولاً
يحدد خيار caseFirst ما إذا كانت الأحرف الكبيرة أو الصغيرة تُفرز أولاً عند مقارنة السلاسل النصية التي تختلف فقط في حالة الأحرف. يقبل ثلاث قيم: upper (الأحرف الكبيرة)، lower (الأحرف الصغيرة)، أو false.
const words = ['apple', 'Apple', 'APPLE'];
// الأحرف الكبيرة أولاً
const upperFirst = new Intl.Collator('en-US', { caseFirst: 'upper' });
const upperSorted = [...words].sort(upperFirst.compare);
console.log(upperSorted);
// النتيجة: ['APPLE', 'Apple', 'apple']
// الأحرف الصغيرة أولاً
const lowerFirst = new Intl.Collator('en-US', { caseFirst: 'lower' });
const lowerSorted = [...words].sort(lowerFirst.compare);
console.log(lowerSorted);
// النتيجة: ['apple', 'Apple', 'APPLE']
// الافتراضي (يعتمد على اللغة المحلية)
const defaultCase = new Intl.Collator('en-US', { caseFirst: 'false' });
const defaultSorted = [...words].sort(defaultCase.compare);
console.log(defaultSorted);
// النتيجة تعتمد على اللغة المحلية
القيمة false تستخدم ترتيب الحالة الافتراضي للغة المحلية. معظم اللغات المحلية تعامل السلاسل النصية التي تختلف فقط في حالة الأحرف على أنها متساوية عند استخدام إعدادات الحساسية الافتراضية.
هذا الخيار له تأثير فقط عندما يسمح خيار sensitivity باعتبار اختلافات حالة الأحرف مهمة.
كيفية تجاهل علامات الترقيم في الفرز
يخبر خيار ignorePunctuation المقارن بتخطي علامات الترقيم عند مقارنة السلاسل النصية. يمكن أن يكون هذا مفيدًا عند فرز العناوين أو العبارات التي قد تتضمن أو لا تتضمن علامات ترقيم.
const titles = [
'The Old Man',
'The Old-Man',
'The Oldman',
];
// الافتراضي (علامات الترقيم مهمة)
const defaultCollator = new Intl.Collator('en-US');
const defaultSorted = [...titles].sort(defaultCollator.compare);
console.log(defaultSorted);
// النتيجة: ['The Old Man', 'The Old-Man', 'The Oldman']
// تجاهل علامات الترقيم
const noPunctCollator = new Intl.Collator('en-US', { ignorePunctuation: true });
const noPunctSorted = [...titles].sort(noPunctCollator.compare);
console.log(noPunctSorted);
// النتيجة: ['The Old Man', 'The Old-Man', 'The Oldman']
عند تجاهل علامات الترقيم، تتعامل المقارنة مع الشرطة في "Old-Man" كما لو أنها غير موجودة، مما يجعل السلاسل النصية تُقارن كما لو كانت جميعها "TheOldMan".
فرز أسماء المستخدمين من بلدان مختلفة
عند فرز أسماء المستخدمين من جميع أنحاء العالم، استخدم اللغة المفضلة للمستخدم لاحترام توقعاتهم اللغوية.
const userLocale = navigator.language;
const collator = new Intl.Collator(userLocale);
const users = [
{ name: 'Müller', country: 'Germany' },
{ name: 'Martin', country: 'France' },
{ name: 'Andersson', country: 'Sweden' },
{ name: 'García', country: 'Spain' },
];
const sorted = users.sort((a, b) => collator.compare(a.name, b.name));
sorted.forEach(user => {
console.log(`${user.name} (${user.country})`);
});
يكتشف هذا الكود لغة المستخدم من المتصفح ويفرز الأسماء وفقًا لذلك. يرى المستخدم الألماني القائمة مرتبة حسب القواعد الألمانية، بينما يرى المستخدم السويدي القائمة مرتبة حسب القواعد السويدية.
الفرز مع تبديل اللغة
عندما يسمح تطبيقك للمستخدمين بتبديل اللغات، قم بتحديث المقارن عند تغيير اللغة.
let currentLocale = 'en-US';
let collator = new Intl.Collator(currentLocale);
function setLocale(newLocale) {
currentLocale = newLocale;
collator = new Intl.Collator(currentLocale);
}
function sortItems(items) {
return items.sort(collator.compare);
}
// المستخدم يبدل إلى السويدية
setLocale('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan'];
console.log(sortItems(names));
// النتيجة: ['Anna', 'Åsa', 'Örjan']
// المستخدم يبدل إلى الألمانية
setLocale('de-DE');
const germanNames = ['Über', 'Uhr', 'Udo'];
console.log(sortItems(germanNames));
// النتيجة: ['Udo', 'Uhr', 'Über']
يضمن هذا النمط تحديث القوائم المرتبة لتتطابق مع اللغة المختارة للمستخدم.
الاختيار بين localeCompare و Intl.Collator
استخدم localeCompare() عندما تحتاج إلى مقارنة سريعة لمرة واحدة أو عند فرز مصفوفة صغيرة تحتوي على أقل من 100 عنصر. الصيغة البسيطة أسهل للقراءة والفرق في الأداء ضئيل بالنسبة لمجموعات البيانات الصغيرة.
const items = ['banana', 'Apple', 'cherry'];
const sorted = items.sort((a, b) => a.localeCompare(b, 'en-US'));
استخدم Intl.Collator عند فرز مصفوفات كبيرة، أو إجراء العديد من المقارنات، أو فرز مصفوفات متعددة بنفس اللغة والخيارات. إنشاء المقارن مرة واحدة وإعادة استخدامه يوفر أداءً أفضل.
const collator = new Intl.Collator('en-US', { sensitivity: 'base', numeric: true });
const products = [/* مصفوفة كبيرة */];
const sorted = products.sort(collator.compare);
كلا النهجين ينتجان نفس النتائج. يعتمد الاختيار على متطلبات الأداء وتفضيلات تنظيم الكود لديك.