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

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

مقدمة

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

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

توفر JavaScript واجهة برمجة التطبيقات 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() بتحويل النص وفقاً لقواعد Unicode، لكن هذه القواعد لا تعمل بنفس الطريقة في جميع اللغات. المثال الأكثر شهرة هو مشكلة حرف i التركي.

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

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

هذا الاختلاف يعطل المقارنة غير الحساسة لحالة الأحرف:

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

// In English locale (correct)
console.log(word1.toLowerCase() === word2.toLowerCase());
// true

// In Turkish locale (incorrect)
console.log(word1.toLocaleLowerCase("tr") === word2.toLocaleLowerCase("tr"));
// false - "file" becomes "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 (strings are equal)

console.log(collator.compare("Hello", "HELLO"));
// 0 (strings are equal)

console.log(collator.compare("Hello", "Héllo"));
// 0 (strings are equal, accents ignored too)

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

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

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

console.log(collator.compare("file", "FILE"));
// 0 (correctly matches)

console.log(collator.compare("file", "FİLE"));
// 0 (correctly matches, even with dotted İ)

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

استخدام localeCompare مع خيار الحساسية

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

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

console.log(str1.localeCompare(str2, "en", { sensitivity: "base" }));
// 0 (strings are equal)

ينتج هذا نفس النتيجة عند استخدام 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 (accents ignored)

console.log(collator.compare("cafe", "Café"));
// 0 (case and accents ignored)

console.log(collator.compare("cafe", "CAFÉ"));
// 0 (case and accents ignored)

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

حساسية التشكيل

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

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

console.log(collator.compare("cafe", "café"));
// -1 (accents matter)

console.log(collator.compare("cafe", "Café"));
// -1 (accents matter, case ignored)

console.log(collator.compare("Café", "CAFÉ"));
// 0 (case ignored, accents match)

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

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

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

  • وظيفة البحث حيث يكتب المستخدمون الاستعلامات بدون تشكيل
  • مطابقة اسم المستخدم حيث لا يجب أن تهم حالة الأحرف
  • البحث الضبابي حيث تريد أقصى قدر من المرونة
  • التحقق من صحة النماذج حيث يجب أن يتطابق 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 تحميل بيانات اللغة ومعالجة الخيارات. عندما تحتاج إلى إجراء مقارنات متعددة، أنشئ المقارن مرة واحدة وأعد استخدامه:

// Inefficient: creates collator for every comparison
function badCompare(items, target) {
  return items.filter(item =>
    new Intl.Collator("en", { sensitivity: "base" }).compare(item, target) === 0
  );
}

// Efficient: creates collator once, reuses it
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"
});

// In your application code
import { caseInsensitiveCollator } from "./utils/collation";

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

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

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

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

// Acceptable for English-only, ASCII-only text
const isMatch = str1.toLowerCase() === str2.toLowerCase();

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

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

// Required for international text
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

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