كيفية مقارنة السلاسل النصية مع تجاهل اختلافات حالة الأحرف

استخدم المقارنة المراعية للغة للتعامل مع المطابقة غير الحساسة لحالة الأحرف بشكل صحيح عبر اللغات المختلفة

مقدمة

تظهر مقارنة السلاسل النصية غير الحساسة لحالة الأحرف بشكل متكرر في تطبيقات الويب. يكتب المستخدمون استعلامات البحث بأحرف مختلطة، أو يدخلون أسماء المستخدمين بطريقة غير متناسقة في استخدام الأحرف الكبيرة، أو يملؤون النماذج دون مراعاة لحالة الأحرف. يحتاج تطبيقك إلى مطابقة هذه المدخلات بشكل صحيح بغض النظر عما إذا كان المستخدمون يكتبون بأحرف كبيرة أو صغيرة أو مختلطة.

النهج المباشر يحول كلا السلسلتين إلى أحرف صغيرة ويقارن بينهما. هذا يعمل للنص الإنجليزي ولكنه يفشل في التطبيقات الدولية. تمتلك اللغات المختلفة قواعد مختلفة للتحويل بين الأحرف الكبيرة والصغيرة. يمكن أن تنتج طريقة المقارنة التي تعمل للغة الإنجليزية نتائج خاطئة للغة التركية أو الألمانية أو اليونانية أو غيرها من اللغات.

توفر جافا سكريبت واجهة برمجة التطبيقات Intl.Collator للتعامل مع المقارنة غير الحساسة لحالة الأحرف بشكل صحيح عبر جميع اللغات. يشرح هذا الدرس سبب فشل التحويل البسيط للأحرف الصغيرة، وكيفية عمل المقارنة المراعية للغة، ومتى تستخدم كل نهج.

النهج البسيط باستخدام toLowerCase

تحويل كلا السلسلتين إلى أحرف صغيرة قبل المقارنة هو النهج الأكثر شيوعًا للمطابقة غير الحساسة لحالة الأحرف:

const str1 = "Hello";
const str2 = "HELLO";

console.log(str1.toLowerCase() === str2.toLowerCase());
// true

يعمل هذا النمط للنص ASCII والكلمات الإنجليزية. تعامل المقارنة الإصدارات ذات الأحرف الكبيرة والصغيرة من نفس الحرف على أنها متطابقة.

يمكنك استخدام هذا النهج للبحث التقريبي:

const query = "apple";
const items = ["Apple", "Banana", "APPLE PIE", "Orange"];

const matches = items.filter(item =>
  item.toLowerCase().includes(query.toLowerCase())
);

console.log(matches);
// ["Apple", "APPLE PIE"]

يجد الفلتر جميع العناصر التي تحتوي على استعلام البحث بغض النظر عن حالة الأحرف. هذا يوفر السلوك المتوقع للمستخدمين الذين يكتبون استعلامات دون التفكير في استخدام الأحرف الكبيرة.

لماذا يفشل النهج البسيط للنص الدولي

تقوم طريقة toLowerCase() بتحويل النص وفقًا لقواعد يونيكود، لكن هذه القواعد لا تعمل بنفس الطريقة في جميع اللغات. المثال الأشهر هو مشكلة حرف i في اللغة التركية.

في اللغة الإنجليزية، يتحول حرف i الصغير إلى حرف I الكبير. أما في اللغة التركية، فهناك حرفان متميزان:

  • حرف i الصغير المنقوط يتحول إلى حرف İ الكبير المنقوط
  • حرف ı الصغير غير المنقوط يتحول إلى حرف I الكبير غير المنقوط

هذا الاختلاف يؤدي إلى فشل المقارنة غير الحساسة لحالة الأحرف:

const word1 = "file";
const word2 = "FILE";

// في اللغة الإنجليزية (صحيح)
console.log(word1.toLowerCase() === word2.toLowerCase());
// true

// في اللغة التركية (غير صحيح)
console.log(word1.toLocaleLowerCase("tr") === word2.toLocaleLowerCase("tr"));
// false - "file" تصبح "fıle"

عند تحويل FILE إلى أحرف صغيرة باستخدام قواعد اللغة التركية، يتحول حرف I إلى ı (غير منقوط)، مما ينتج عنه fıle. وهذا لا يتطابق مع file (بحرف i منقوط)، لذلك تعود المقارنة بقيمة false على الرغم من أن السلسلتين تمثلان نفس الكلمة.

لغات أخرى لديها مشاكل مماثلة. اللغة الألمانية تحتوي على حرف ß الذي يتحول إلى SS عند الكتابة بأحرف كبيرة. اللغة اليونانية لديها أشكال متعددة لحرف سيجما الصغير (σ و ς) وكلاهما يتحول إلى Σ عند الكتابة بأحرف كبيرة. التحويل البسيط لحالة الأحرف لا يمكنه التعامل مع هذه القواعد الخاصة باللغات بشكل صحيح.

استخدام Intl.Collator مع حساسية أساسية للمقارنة غير الحساسة لحالة الأحرف

توفر واجهة برمجة التطبيقات Intl.Collator مقارنة للسلاسل النصية مع مراعاة اللغة المحلية مع إمكانية تكوين الحساسية. يتحكم خيار sensitivity في الاختلافات التي تهم أثناء المقارنة.

للمقارنة غير الحساسة لحالة الأحرف، استخدم sensitivity: "base":

const collator = new Intl.Collator("en", { sensitivity: "base" });

console.log(collator.compare("Hello", "hello"));
// 0 (السلاسل متساوية)

console.log(collator.compare("Hello", "HELLO"));
// 0 (السلاسل متساوية)

console.log(collator.compare("Hello", "Héllo"));
// 0 (السلاسل متساوية، تم تجاهل العلامات أيضًا)

الحساسية الأساسية تتجاهل اختلافات حالة الأحرف والعلامات. فقط الأحرف الأساسية هي التي تهم. تعيد المقارنة 0 عندما تكون السلاسل متكافئة في مستوى الحساسية هذا.

يتعامل هذا النهج مع مشكلة حرف i التركي بشكل صحيح:

const collator = new Intl.Collator("tr", { sensitivity: "base" });

console.log(collator.compare("file", "FILE"));
// 0 (تتطابق بشكل صحيح)

console.log(collator.compare("file", "FİLE"));
// 0 (تتطابق بشكل صحيح، حتى مع İ المنقوطة)

يطبق المقارن قواعد تحويل حالة الأحرف التركية تلقائيًا. كلتا المقارنتين تتعرفان على السلاسل كمتكافئة، بغض النظر عن أي حرف I كبير يظهر في المدخلات.

استخدام localeCompare مع خيار sensitivity

توفر طريقة localeCompare() طريقة بديلة لإجراء مقارنة غير حساسة لحالة الأحرف. وهي تقبل نفس الخيارات مثل Intl.Collator:

const str1 = "Hello";
const str2 = "HELLO";

console.log(str1.localeCompare(str2, "en", { sensitivity: "base" }));
// 0 (السلاسل النصية متساوية)

هذا ينتج نفس النتيجة كاستخدام Intl.Collator مع حساسية أساسية. تتجاهل المقارنة اختلافات حالة الأحرف وتعيد 0 للسلاسل النصية المتكافئة.

يمكنك استخدام هذا في تصفية المصفوفات:

const query = "apple";
const items = ["Apple", "Banana", "APPLE PIE", "Orange"];

const matches = items.filter(item =>
  item.localeCompare(query, "en", { sensitivity: "base" }) === 0 ||
  item.toLowerCase().includes(query.toLowerCase())
);

console.log(matches);
// ["Apple"]

ومع ذلك، فإن localeCompare() تعيد 0 فقط للتطابقات الدقيقة في مستوى الحساسية المحدد. وهي لا تدعم المطابقة الجزئية مثل includes(). للبحث عن سلسلة فرعية، لا تزال بحاجة إلى استخدام تحويل الأحرف الصغيرة أو تنفيذ خوارزمية بحث أكثر تطوراً.

الاختيار بين الحساسية الأساسية وحساسية العلامات

يقبل خيار sensitivity أربع قيم تتحكم في جوانب مختلفة من مقارنة السلاسل النصية:

الحساسية الأساسية

الحساسية الأساسية تتجاهل كلاً من حالة الأحرف والعلامات:

const collator = new Intl.Collator("en", { sensitivity: "base" });

console.log(collator.compare("cafe", "café"));
// 0 (العلامات متجاهلة)

console.log(collator.compare("cafe", "Café"));
// 0 (حالة الأحرف والعلامات متجاهلة)

console.log(collator.compare("cafe", "CAFÉ"));
// 0 (حالة الأحرف والعلامات متجاهلة)

هذا يوفر المطابقة الأكثر تساهلاً. المستخدمون الذين لا يستطيعون كتابة الأحرف المشكلة أو الذين يتخطونها للراحة سيحصلون على تطابقات صحيحة.

حساسية العلامات

حساسية العلامات تتجاهل حالة الأحرف ولكنها تأخذ العلامات في الاعتبار:

const collator = new Intl.Collator("en", { sensitivity: "accent" });

console.log(collator.compare("cafe", "café"));
// -1 (العلامات مهمة)

console.log(collator.compare("cafe", "Café"));
// -1 (العلامات مهمة، حالة الأحرف متجاهلة)

console.log(collator.compare("Café", "CAFÉ"));
// 0 (حالة الأحرف متجاهلة، العلامات متطابقة)

هذا يعامل الأحرف المشكلة وغير المشكلة على أنها مختلفة مع تجاهل حالة الأحرف. استخدم هذا عندما تكون اختلافات العلامات مهمة ولكن اختلافات حالة الأحرف ليست كذلك.

اختيار الحساسية المناسبة لحالة الاستخدام الخاصة بك

بالنسبة لمعظم احتياجات المقارنة غير الحساسة لحالة الأحرف، توفر الحساسية الأساسية أفضل تجربة للمستخدم:

  • وظائف البحث حيث يكتب المستخدمون استعلامات بدون علامات تشكيل
  • مطابقة اسم المستخدم حيث لا ينبغي أن تهم حالة الأحرف
  • البحث الضبابي حيث تريد أقصى قدر من المرونة
  • التحقق من صحة النماذج حيث يجب أن تتطابق Smith و smith

استخدم حساسية علامات التشكيل عندما:

  • تتطلب اللغة التمييز بين الأحرف المشكلة
  • تحتوي بياناتك على إصدارات مشكلة وغير مشكلة بمعانٍ مختلفة
  • تحتاج إلى مقارنة غير حساسة لحالة الأحرف ولكن تراعي علامات التشكيل

إجراء بحث غير حساس لحالة الأحرف باستخدام includes

توفر واجهة برمجة التطبيقات Intl.Collator مقارنة للسلاسل النصية الكاملة ولكنها لا توفر مطابقة للسلاسل الفرعية. للبحث غير الحساس لحالة الأحرف، لا يزال عليك الجمع بين المقارنة المراعية للغة مع أساليب أخرى.

أحد الخيارات هو استخدام toLowerCase() للبحث عن السلسلة الفرعية ولكن مع قبول قيودها للنصوص الدولية:

function caseInsensitiveIncludes(text, query, locale = "en") {
  return text.toLowerCase().includes(query.toLowerCase());
}

const text = "The Quick Brown Fox";
console.log(caseInsensitiveIncludes(text, "quick"));
// true

للبحث الأكثر تطوراً الذي يتعامل مع النصوص الدولية بشكل صحيح، تحتاج إلى التكرار خلال مواضع السلاسل الفرعية المحتملة واستخدام المقارن لكل مقارنة:

function localeAwareIncludes(text, query, locale = "en") {
  const collator = new Intl.Collator(locale, { sensitivity: "base" });

  for (let i = 0; i <= text.length - query.length; i++) {
    const substring = text.slice(i, i + query.length);
    if (collator.compare(substring, query) === 0) {
      return true;
    }
  }

  return false;
}

const text = "The Quick Brown Fox";
console.log(localeAwareIncludes(text, "quick"));
// true

يتحقق هذا النهج من كل سلسلة فرعية محتملة بالطول الصحيح ويستخدم المقارنة المراعية للغة لكل منها. يتعامل مع النصوص الدولية بشكل صحيح ولكن أداؤه أسوأ من includes() البسيط.

اعتبارات الأداء عند استخدام Intl.Collator

إنشاء نسخة من Intl.Collator يتضمن تحميل بيانات اللغة المحلية ومعالجة الخيارات. عندما تحتاج إلى إجراء مقارنات متعددة، قم بإنشاء المقارن مرة واحدة وأعد استخدامه:

// غير فعّال: ينشئ المقارن لكل عملية مقارنة
function badCompare(items, target) {
  return items.filter(item =>
    new Intl.Collator("en", { sensitivity: "base" }).compare(item, target) === 0
  );
}

// فعّال: ينشئ المقارن مرة واحدة، ويعيد استخدامه
function goodCompare(items, target) {
  const collator = new Intl.Collator("en", { sensitivity: "base" });
  return items.filter(item =>
    collator.compare(item, target) === 0
  );
}

الإصدار الفعّال ينشئ المقارن مرة واحدة قبل التصفية. كل مقارنة تستخدم نفس النسخة، مما يتجنب التكلفة المتكررة للتهيئة.

بالنسبة للتطبيقات التي تجري مقارنات متكررة، قم بإنشاء نسخ المقارن عند بدء تشغيل التطبيق وتصديرها للاستخدام في جميع أنحاء التعليمات البرمجية الخاصة بك:

// utils/collation.js
export const caseInsensitiveCollator = new Intl.Collator("en", {
  sensitivity: "base"
});

export const accentInsensitiveCollator = new Intl.Collator("en", {
  sensitivity: "accent"
});

// في كود التطبيق الخاص بك
import { caseInsensitiveCollator } from "./utils/collation";

const isMatch = caseInsensitiveCollator.compare(input, expected) === 0;

هذا النمط يعظم الأداء ويحافظ على سلوك مقارنة متسق عبر تطبيقك.

متى تستخدم toLowerCase مقابل Intl.Collator

بالنسبة للتطبيقات الإنجليزية فقط حيث تتحكم في محتوى النص وتعلم أنه يحتوي فقط على أحرف ASCII، فإن toLowerCase() توفر نتائج مقبولة:

// مقبول للنصوص الإنجليزية فقط، ونصوص ASCII فقط
const isMatch = str1.toLowerCase() === str2.toLowerCase();

هذا النهج بسيط وسريع ومألوف لمعظم المطورين. إذا كان تطبيقك حقًا لا يتعامل أبدًا مع النصوص الدولية، فقد لا توفر التعقيدات الإضافية للمقارنة المراعية للغة المحلية أي قيمة.

بالنسبة للتطبيقات الدولية أو التطبيقات التي يدخل فيها المستخدمون نصًا بأي لغة، استخدم Intl.Collator مع الحساسية المناسبة:

// مطلوب للنص الدولي
const collator = new Intl.Collator(userLocale, { sensitivity: "base" });
const isMatch = collator.compare(str1, str2) === 0;

هذا يضمن السلوك الصحيح بغض النظر عن اللغة التي يتحدث بها المستخدمون أو يكتبون بها. التكلفة الصغيرة للأداء عند استخدام Intl.Collator تستحق العناء لتجنب المقارنات غير الصحيحة.

حتى إذا كان تطبيقك يدعم حاليًا اللغة الإنجليزية فقط، فإن استخدام المقارنة المراعية للغة المحلية من البداية يجعل التدويل المستقبلي أسهل. إضافة دعم للغات جديدة لا يتطلب أي تغييرات في منطق المقارنة.

حالات استخدام عملية للمقارنة غير الحساسة لحالة الأحرف

تظهر المقارنة غير الحساسة لحالة الأحرف في العديد من السيناريوهات الشائعة:

مطابقة اسم المستخدم والبريد الإلكتروني

يكتب المستخدمون أسماء المستخدمين وعناوين البريد الإلكتروني بأحرف كبيرة غير متسقة:

const collator = new Intl.Collator("en", { sensitivity: "base" });

function findUserByEmail(users, email) {
  return users.find(user =>
    collator.compare(user.email, email) === 0
  );
}

const users = [
  { email: "[email protected]", name: "John" },
  { email: "[email protected]", name: "Jane" }
];

console.log(findUserByEmail(users, "[email protected]"));
// { email: "[email protected]", name: "John" }

هذا يجد المستخدم بغض النظر عن كيفية كتابة عنوان بريدهم الإلكتروني بأحرف كبيرة.

الإكمال التلقائي للبحث

تحتاج اقتراحات الإكمال التلقائي إلى مطابقة الإدخال الجزئي مع عدم حساسية حالة الأحرف:

const collator = new Intl.Collator("en", { sensitivity: "base" });

function getSuggestions(items, query) {
  const queryLower = query.toLowerCase();

  return items.filter(item =>
    item.toLowerCase().startsWith(queryLower)
  );
}

const items = ["Apple", "Apricot", "Banana", "Cherry"];
console.log(getSuggestions(items, "ap"));
// ["Apple", "Apricot"]

هذا يوفر اقتراحات بغض النظر عن حالة الأحرف التي يكتبها المستخدمون.

مطابقة العلامات والفئات

يقوم المستخدمون بتعيين علامات أو فئات للمحتوى دون استخدام أحرف كبيرة متسقة:

const collator = new Intl.Collator("en", { sensitivity: "base" });

function hasTag(item, tag) {
  return item.tags.some(itemTag =>
    collator.compare(itemTag, tag) === 0
  );
}

const article = {
  title: "My Article",
  tags: ["JavaScript", "Tutorial", "Web Development"]
};

console.log(hasTag(article, "javascript"));
// true

هذا يطابق العلامات بغض النظر عن اختلافات حالة الأحرف.