كيفية ترتيب السلاسل النصية أبجدياً حسب اللغة في JavaScript
استخدم Intl.Collator و localeCompare() لترتيب السلاسل النصية بشكل صحيح لأي لغة
مقدمة
عندما تقوم بترتيب مصفوفة من السلاسل النصية في JavaScript، فإن السلوك الافتراضي يقارن السلاسل النصية بقيم وحدات UTF-16 الخاصة بها. يعمل هذا مع نصوص ASCII الأساسية، لكنه يفشل عند ترتيب الأسماء أو عناوين المنتجات أو أي نص يحتوي على أحرف بعلامات تشكيل أو نصوص غير لاتينية أو أحرف مختلطة الحالة.
تمتلك اللغات المختلفة قواعد مختلفة للترتيب الأبجدي. تضع اللغة السويدية å و ä و ö في نهاية الأبجدية بعد z. تعامل اللغة الألمانية ä على أنها مكافئة لـ a في معظم السياقات. تتجاهل اللغة الفرنسية علامات التشكيل في أوضاع مقارنة معينة. تحدد هذه القواعد اللغوية كيف يتوقع الأشخاص رؤية القوائم المرتبة بلغتهم.
توفر JavaScript واجهتي برمجة تطبيقات لترتيب السلاسل النصية حسب اللغة. تتعامل طريقة 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). لا توجد طريقة لدى JavaScript لمعرفة أن المتحدثين بالسويدية يعتبرون Å حرفاً منفصلاً له موضع محدد في الأبجدية.
تُنشئ الأحرف المختلطة مشكلة أخرى.
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);
// Output: -1 (negative means 'a' comes before '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);
// Output: ['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);
// Output: ['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);
// Output: ['apple', 'Banana', 'Cherry']
يعمل هذا النهج بشكل جيد للمصفوفات التي تحتوي على بضع عشرات من العناصر. يكون تأثير الأداء ضئيلاً لمجموعات البيانات الصغيرة.
يمكنك أيضاً استخدام localeCompare() للتحقق مما إذا كانت سلسلة نصية تأتي قبل أخرى دون ترتيب مصفوفة كاملة.
const firstName = 'Åsa';
const secondName = 'Anna';
if (firstName.localeCompare(secondName, 'sv-SE') < 0) {
console.log(`${firstName} comes before ${secondName}`);
} else {
console.log(`${secondName} comes before ${firstName}`);
}
// Output: "Anna comes before Å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 = [/* array with 10,000 product names */];
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'];
// Swedish locale
const swedishSorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(swedishSorted);
// Output: ['Anna', 'Åsa', 'Ärla']
// German locale
const germanSorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(germanSorted);
// Output: ['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'));
// Output: 0 (equal)
console.log(collator.compare('a', 'á'));
// Output: 0 (equal)
console.log(collator.compare('a', 'b'));
// Output: -1 (different base characters)
يعامل هذا الوضع a و A و á كأحرف متطابقة لأنها تشترك في نفس الحرف الأساسي.
تأخذ حساسية accent العلامات الإعرابية في الاعتبار ولكنها تتجاهل حالة الأحرف.
const collator = new Intl.Collator('en-US', { sensitivity: 'accent' });
console.log(collator.compare('a', 'A'));
// Output: 0 (equal, case ignored)
console.log(collator.compare('a', 'á'));
// Output: -1 (different, accent matters)
تأخذ حساسية case حالة الأحرف في الاعتبار ولكنها تتجاهل العلامات الإعرابية.
const collator = new Intl.Collator('en-US', { sensitivity: 'case' });
console.log(collator.compare('a', 'A'));
// Output: -1 (different, case matters)
console.log(collator.compare('a', 'á'));
// Output: 0 (equal, accent ignored)
تأخذ حساسية variant (الافتراضية) جميع الاختلافات في الاعتبار.
const collator = new Intl.Collator('en-US', { sensitivity: 'variant' });
console.log(collator.compare('a', 'A'));
// Output: -1 (different)
console.log(collator.compare('a', 'á'));
// Output: -1 (different)
يوفر هذا الوضع المقارنة الأكثر صرامة، حيث يعامل أي اختلاف على أنه مهم.
كيفية فرز السلاسل النصية التي تحتوي على أرقام مضمنة
يتيح خيار numeric الفرز الرقمي للسلاسل النصية التي تحتوي على أرقام. عند تمكينه، تعامل المقارنة تسلسلات الأرقام كقيم رقمية بدلاً من مقارنتها حرفًا بحرف.
const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];
// Default sorting (wrong order)
const defaultSorted = [...files].sort();
console.log(defaultSorted);
// Output: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']
// Numeric sorting (correct order)
const collator = new Intl.Collator('en-US', { numeric: true });
const numericSorted = files.sort(collator.compare);
console.log(numericSorted);
// Output: ['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);
// Output: ['v1.2', 'v1.3', 'v1.10', 'v1.20']
كيفية التحكم في أي حالة أحرف تأتي أولاً
يحدد خيار caseFirst ما إذا كانت الأحرف الكبيرة أو الصغيرة تُرتب أولاً عند مقارنة السلاسل النصية التي تختلف فقط في حالة الأحرف. يقبل ثلاث قيم: upper أو lower أو false.
const words = ['apple', 'Apple', 'APPLE'];
// Uppercase first
const upperFirst = new Intl.Collator('en-US', { caseFirst: 'upper' });
const upperSorted = [...words].sort(upperFirst.compare);
console.log(upperSorted);
// Output: ['APPLE', 'Apple', 'apple']
// Lowercase first
const lowerFirst = new Intl.Collator('en-US', { caseFirst: 'lower' });
const lowerSorted = [...words].sort(lowerFirst.compare);
console.log(lowerSorted);
// Output: ['apple', 'Apple', 'APPLE']
// Default (locale-dependent)
const defaultCase = new Intl.Collator('en-US', { caseFirst: 'false' });
const defaultSorted = [...words].sort(defaultCase.compare);
console.log(defaultSorted);
// Output depends on locale
تستخدم القيمة false ترتيب حالة الأحرف الافتراضي للغة. تتعامل معظم اللغات مع السلاسل النصية التي تختلف فقط في حالة الأحرف على أنها متساوية عند استخدام إعدادات الحساسية الافتراضية.
يكون لهذا الخيار تأثير فقط عندما يسمح خيار sensitivity باختلافات حالة الأحرف.
كيفية تجاهل علامات الترقيم في الترتيب
يخبر خيار ignorePunctuation المرتب بتخطي علامات الترقيم عند مقارنة السلاسل النصية. يمكن أن يكون هذا مفيدًا عند ترتيب العناوين أو العبارات التي قد تتضمن أو لا تتضمن علامات ترقيم.
const titles = [
'The Old Man',
'The Old-Man',
'The Oldman',
];
// Default (punctuation matters)
const defaultCollator = new Intl.Collator('en-US');
const defaultSorted = [...titles].sort(defaultCollator.compare);
console.log(defaultSorted);
// Output: ['The Old Man', 'The Old-Man', 'The Oldman']
// Ignore punctuation
const noPunctCollator = new Intl.Collator('en-US', { ignorePunctuation: true });
const noPunctSorted = [...titles].sort(noPunctCollator.compare);
console.log(noPunctSorted);
// Output: ['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);
}
// User switches to Swedish
setLocale('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan'];
console.log(sortItems(names));
// Output: ['Anna', 'Åsa', 'Örjan']
// User switches to German
setLocale('de-DE');
const germanNames = ['Über', 'Uhr', 'Udo'];
console.log(sortItems(germanNames));
// Output: ['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 = [/* large array */];
const sorted = products.sort(collator.compare);
ينتج كلا النهجين نفس النتائج. يعتمد الاختيار على متطلبات الأداء وتفضيلات تنظيم الكود لديك.