فصل 9عبارات باقاعده

بعضی‌ها، وقتی با مشکلی روبرو می‌شوند، فکر می‌کنند "خب، راه حل را می‌دانم، استفاده از عبارات باقاعده. " ولی حالا با دو مشکل روبرو هستند.

جیمی زاوینسکی

یوان‌ما گفت، 'وقتی چوب را برخلاف جهت الیافش برش می‌دهید، نیروی بیشتری نیاز دارید. و هنگامی‌ که بر خلاف روش صحیح حل مسئله، برنامه‌نویسی می‌کنید، به کد بیشتری نیاز دارید.’

استاد یوان‌ما, کتاب برنامه‌نویسی
A railroad diagram

ابزار‌ها و تکنیک‌های برنامه‌نویسی در طول زمان به شکلی نامنظم و تکاملی حفظ می‌شوند و گسترش می‌یابند. این‌طور نیست که همیشه آن‌هایی که درخشان یا خوب هستند برنده شوند؛ بلکه تکنیک‌ها و ابزار‌هایی باقی می‌مانند که در یک حوزه‌ی مناسب به اندازه‌ی کافی خوب عمل می‌کنند یا این ویژگی را دارند که با تکنولوژی موفق دیگری به خوبی یکپارچه و تلفیق می‌شوند.

در این فصل، درباره‌ی یکی از این ابزار‌های موفق، عبارات باقاعده، صحبت خواهم کرد. عبارات باقاعده روشی برای توصیف الگو‌ها در داده‌های متنی (رشته‌ای) می‌باشند. این عبارات، زبانی کوچک و مجزا را تشکیل می‌دهند که بخشی از زبان جاوااسکریپت و خیلی زبان‌ها و سیستم‌های دیگر محسوب می‌شوند.

عبارات باقاعده، به طور همزمان هم خیلی بی‌قواره و هم فوق‌العاده کاربردی هستند. قواعد دستوری آن‌ها رمزگونه و رابط برنامه‌نویسی آن‌ها در جاوااسکریپت کمی نچسب است. اما ابزار بسیار قدرتمندی برای پردازش و وارسی رشته‌ها محسوب می‌شوند. درک صحیح عبارات باقاعده، شما را به برنامه‌نویس بهتری تبدیل می‌کند.

ایجاد عبارات باقاعده

یک عبارت باقاعده یک نوع شیء است. می‌توان آن را هم با سازنده‌ی RegExp و هم به طور مستقیم با قرار دادن یک الگو بین دو کاراکتر اسلش (/) ایجاد نمود.

let re1 = new RegExp("abc");
let re2 = /abc/;

هر دوی عبارت‌های باقاعده‌ی بالا نمایانگر یک الگو می‌باشند: کاراکتر a که بعد از آن b و بعد c می آید.

زمانی که از سازنده‌ی RegExp استفاده می‌شود، الگو به صورت رشته‌ی معمولی نوشته می‌شود؛ بنابراین قوانین معمول برای کاراکتر بک‌اسلش برقرار است.

در روش دوم که در آن الگو بین دو کاراکتر اسلش ظاهر می‌شود، تفسیر بک اسلش کمی متفاوت است. اول اینکه، به دلیل اینکه کاراکتر اسلش نشان دهنده‌ی پایان الگو است، باید یک بک اسلش را قبل از اسلشی که می‌خواهیم به عنوان بخشی از الگو تفسیر شود قرار دهیم. افزون بر آن، بک اسلش‌هایی که بخشی از کدکاراکتر‌های خاص (مانند \n) محسوب نمی‌شوند، بر خلاف حالت رشته‌ای، حفظ شده و باعث تغییر در معنای الگو خواهند شد. بعضی کاراکتر‌ها مثل علامت سوال یا مثبت، معانی خاصی در عبارات باقاعده دارند و اگر قرار است نمایانگر کاراکتر خودشان باشند، باید قبلشان یک بک اسلش قرار داده شود.

let eighteenPlus = /eighteen\+/;

آزمایش تطبیق الگو

اشیاء عبارات باقاعده دارای تعدادی متد می‌باشند. ساده‌ترین آن‌ها متد test است. اگر به این متد یک رشته ارسال کنید، با برگرداندن یک مقدار بولی، به شما خواهد گفت که آیا در رشته‌ی داده شده، نمونه‌ای مطابق الگوی عبارت باقاعده، وجود دارد یا خیر.

console.log(/abc/.test("abcde"));
// → true
console.log(/abc/.test("abxde"));
// → false

اگر در عبارات باقاعده هیچ کاراکتر خاصی استفاده نشود، آن عبارت معادل همان دنباله‌ی کاراکتر‌ها می‌باشد. اگر abc در هر جای رشته‌ای که مورد آزمایش قرار داده‌ایم قرار گرفته باشد (نه فقط در شروع رشته)، متد test مقدار true را تولید می‌کند.

مجموعه‌های کاراکتر

فهمیدن اینکه آیا یک رشته حاوی abc هست یا خیر را می‌توان به خوبی با متد indexOf نیز انجام داد. عبارات باقاعده به ما امکان تولید الگو‌های پیچیده‌تری را می‌دهند.

فرض کنید قصد داریم همه اعداد را شناسایی کنیم. در یک عبارت باقاعده، قرار دادن یک مجموعه کاراکتر درون براکت باعث می‌شود که آن بخش از عبارت با هر کاراکتری که بین براکت‌ها آمده است تطبیق یابد.

هر دوی عبارت‌های زیر همه‌ی رشته‌هایی که دارای رقم هستند را شامل می‌شود:

console.log(/[0123456789]/.test("in 1992"));
// → true
console.log(/[0-9]/.test("in 1992"));
// → true

برای مشخص کردن یک بازه از کاراکتر‌ها می‌توان درون براکت‌ها از یک کاراکتر (-) بین دو کاراکتر استفاده کرد که ترتیب کاراکتر‌ها توسط کد یونیکد آن‌ها مشخص می‌شود. کاراکتر‌های 0 تا 9 کنار هم و در بازه‌ی یونیکد (کد‌های 48 تا 57) قرار دارند بنابراین [0-9] همه‌ی آن‌ها را پوشش داده و هر رقمی را شامل می‌شود.

برای بعضی از گروه‌های کاراکتری روش کوتاهتری هم از پیش تعریف شده است. اعداد یکی از آن‌ها هستند: مثلا \d معنایی مشابه [0-9] دارد.

\dهر کاراکتر عددی
\wیک کاراکتر از نوع عدد یا حرف الفبا (“کاراکتر کلمه”)
\sهمه‌ی کاراکترهای فضای‌خالی ( فاصله، تب، خط جدید، و مشابه آن‌ها)
\Dکاراکتری که از نوع عدد نباشد
\Wکاراکتری که عدد و حرف الفبا نباشد
\Sکاراکتری که فضای خالی محسوب نشود
.همه‌ی کاراکترها به جز کاراکتر خط جدید

بنابراین می‌توانید فرمت تاریخ و زمانی شبیه به 01-30-2003 15:20 را با عبارت زیر شناسایی کنید:

let dateTime = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/;
console.log(dateTime.test("01-30-2003 15:20"));
// → true
console.log(dateTime.test("30-jan-2003 15:20"));
// → false

ظاهر عبارت بالا خیلی بی‌قواره است، درست است؟ نیمی از آن بک‌اسلش است که الگو را بیش از حد شلوغ کرده و تشخیص معنای آن را سخت نموده است. در ادامه با نسخه‌ای از آن که کمی بهبود یافته است آشنا خواهیم شد.

این کد‌های بک‌اسلش را همچنین می‌توان درون براکت استفاده کرد. به عنوان مثال، [\d.] به معنای یک رقم یا یک کاراکتر نقطه است. اما خود نقطه وقتی داخل براکت قرار می‌گیرد معنای خاصش را از دست می‌دهد. این قضیه برای دیگر کاراکتر‌های خاص مثل + هم برقرار است.

برای معکوس کردن یک مجموعه‌ی کاراکتر – به این معنا که شما قصد دارید هر کاراکتری بجز آن‌هایی که در مجموعه مشخص شده‌اند را بیان کنید – می‌توانید از یک کاراکتر (^) بعد از براکت شروع بازه استفاده کنید.

let notBinary = /[^01]/;
console.log(notBinary.test("1100100010100110"));
// → false
console.log(notBinary.test("1100100010200110"));
// → true

تکرار بخش‌هایی از یک الگو

می‌دانیم که چگونه یک عدد یا رقم را شناسایی کنیم. چه باید کرد اگر بخواهیم که یک عدد کامل – دنباله‌ای از یک یا بیشتر رقم – را هدف قرار بدهیم؟

زمانی که یک علامت مثبت (+) را بعد از چیزی در یک عبارت باقاعده قرار می‌دهید، این علامت نشان می‌دهد که آن عنصر ممکن است یک بار یا بیشتر تکرار شود. بنابراین، /\d+/ به معنای مطابقت عبارت با تعداد یک یا بیشتر از کاراکتر‌های عددی خواهد بود.

console.log(/'\d+'/.test("'123'"));
// → true
console.log(/'\d+'/.test("''"));
// → false
console.log(/'\d*'/.test("'123'"));
// → true
console.log(/'\d*'/.test("''"));
// → true

کاراکتر ستاره (*) معنای مشابهی دارد با این تفاوت که به الگو اجازه می‌دهد تا صفر بار تکرار (نبودن کاراکتر) را هم شامل شود. اگر بعد از چیزی کاراکتر ستاره قرار گیرد، باعث می‌شود که الگو همیشه چیزی برای مطابقت پیدا کند – در صورتی که نتواند متنی برای مطابقت پیدا کند، با نبود آن عنصر مطابقت خواهد داد.

استفاده از علامت سوال (?) در یک الگو به معنای اختیاری بودن است، یعنی ممکن است که آن عنصر نباشد یا یک بار حاضر باشد. در مثال پیش رو، کاراکتر u اختیاری است و می‌تواند باشد و در صورت نبودن هم الگو صدق خواهد کرد.

let neighbor = /neighbou?r/;
console.log(neighbor.test("neighbour"));
// → true
console.log(neighbor.test("neighbor"));
// → true

برای مشخص کردن این موضوع که یک الگو باید به تعداد دقیقی رخ دهد، می‌توانید از کروشه استفاده کنید؛ به عنوان مثال، قرار دادن {4} بعد از یک عنصر، باعث می‌شود که الگو انتظار داشته باشد آن عنصر دقیقا 4 مرتبه رخ داده باشد. همچنین می‌توان یک بازه را نیز مشخص نمود: {2,4} به این معنا است که این عنصر باید حداقل دو مرتبه و حداکثر چهار مرتبه رخ دهد.

اینجا نسخه‌ی دیگر از الگوی تشخیص تاریخ و زمان را داریم که امکان تشخیص روز، ماه و ساعت به هر دو فرمت تک رقمی و دو رقمی را دارد. همچنین درک این الگو کمی راحت‌تر از الگوی پیشین است.

let dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/;
console.log(dateTime.test("1-30-2003 8:45"));
// → true

همچنین می‌توانید بازه‌هایی که انتهایی باز دارند را نیز مشخص کنید. این کار با حذف رقم پس از ویرگول انجام می‌شود. بنابراین، {5,} به معنای پنج یا بیشتر می‌باشد.

دسته‌بندی زیرعبارات

برای استفاده از یک عملگر مانند * یا + روی بیش از یک عنصر در آنِ واحد، باید از پرانتز استفاده کنید. از دید عملگر‌هایی که بعد از عبارت‌های داخل پرانتز قرار می‌گیرند، هر عبارت محصور بین پرانتز به عنوان یک عنصر در نظر گرفته می‌شود.

let cartoonCrying = /boo+(hoo+)+/i;
console.log(cartoonCrying.test("Boohoooohoohooo"));
// → true

کاراکتر‌های + اول و دوم فقط به o دوم از boo و hoo اعمال می‌شوند. کاراکتر + سوم به کل گروه (hoo+) اعمال می‌شود و یک یا بیش از یک بار تکرار آن الگو را شامل می‌شود.

کاراکتر i که در انتهای عبارت مثال آمده است باعث می‌شود که عبارت باقاعده به بزرگی و کوچکی حروف حساس نباشد، یعنی کاراکتر B بزرگ هم در رشته‌ی ورودی تطبیق خواهد خورد، با وجود اینکه الگو خودش به حروف کوچک نوشته شده است.

تطبیق‌ها و گروه‌ها

متد test ساده‌ترین راهی است که برای تطبیق یک عبارت باقاعده استفاده می‌شود. این متد فقط تطبیق و عدم تطبیق عبارت را مشخص می‌کند و دیگر هیچ. عبارات باقاعده همچنین متدی به نام exec (به معنای اجرا) دارند که در صورت نبود تطبیق، مقدار null را بر‌می گرداند و در صورت وجود تطبیق، شیئی شامل اطلاعاتی راجع به آن تولید می‌کند.

let match = /\d+/.exec("one two 100");
console.log(match);
// → ["100"]
console.log(match.index);
// → 8

شیئی که از یک متد exec برگردانده می‌شود خاصیتی به نام index دارد که نقطه شروع تطبیق پیدا شده را در رشته به ما می‌نشان می‌دهد. علاوه بر آن، این شیء شبیه به (و در واقع یک) آرایه‌ای از رشته‌ها است، که عنصر اولش رشته‌ای است که با الگو مطابقت داشته است – در مثال قبل، دنباله‌ای از اعداد که به دنبال آن بودیم.

مقدار‌های رشته‌ای متدی به نام match دارند که به شکل مشابهی عمل می‌کند.

console.log("one two 100".match(/\d+/));
// → ["100"]

زمانی که یک عبارت باقاعده شامل زیرعبارت‌هایی باشد که با پرانتز گروه‌بندی شده‌اند، متن‌هایی که با آن گروه‌ها مطابقت دارند نیز درون یک آرایه نمایش داده خواهد شد. تطبیق کامل همیشه در همان عنصر اول است. عنصر بعدی آرایه متعلق به بخشی است که توسط اولین گروه تطبیق یافته است (گروهی که پرانتز شروعش در عبارت اول آمده است)، سپس گروه دوم و الی آخر.

let quotedText = /'([^']*)'/;
console.log(quotedText.exec("she said 'hello'"));
// → ["'hello'", "hello"]

زمانی که برای یک گروه تطبیقی در رشته پیدا نمی‌شود (به عنوان مثال، زمانی که بعد از گروه علامت سوال قرار گرفته باشد) موقعیت آن در آرایه‌ی خروجی به صورت undefined خواهد بود. به طور مشابه، اگر یک گروه چندین تطبیق داشته باشد، فقط آخرین آن‌ها در آرایه قرار خواهد گرفت.

console.log(/bad(ly)?/.exec("bad"));
// → ["bad", undefined]
console.log(/(\d)+/.exec("123"));
// → ["123", "3"]

از قابلیت گروه‌ها می‌توان برای استخراج قسمت‌های یک رشته استفاده کرد. به عنوان مثال، زمانی که فقط بودن یک تاریخ در یک رشته برای ما مهم نیست و قصد داریم تا آن را از دل آن استخراج کرده و شیئی حاوی آن بسازیم، می‌توانیم با استفاده از پرانتز در الگوی ارقام، به طور مستقیم آن را در نتیجه‌ی exec مجزا کنیم.

اما ابتدا، یک فاصله‌ی کوتاه بگیریم و کمی در رابطه با روش از پیش تعریف شده برای نمایش مقادیر زمان و تاریخ در جاوااسکریپت صحبت کنیم.

کلاس Date

جاوااسکریپت کلاس استانداردی برای نمایش تاریخ‌ها – یا به عبارتی نقاطی در زمان – دارد. این کلاس Date نامیده می‌شود. اگر با new یک کلاس تاریخ ایجاد کنید، زمان و تاریخ فعلی را خواهید گرفت.

console.log(new Date());
// → Mon Nov 13 2017 16:19:11 GMT+0100 (CET)

همچنین می‌توانید یک شیء برای یک تاریخ مشخص ایجاد کنید.

console.log(new Date(2009, 11, 9));
// → Wed Dec 09 2009 00:00:00 GMT+0100 (CET)
console.log(new Date(2009, 11, 9, 12, 59, 59, 999));
// → Wed Dec 09 2009 12:59:59 GMT+0100 (CET)

جاوااسکریپت از قراردادی استفاده می‌کند که در آن ماه‌ها از صفر شروع می‌شوند (بنابراین ماه دسامبر برابر 11 خواهد شد)، اما روز‌ها از یک شروع می‌شوند. این به نظر گیج‌کننده و احمقانه می‌رسد. پس دقت داشته باشید.

چهار آرگومان آخر (hours، minutes، seconds و milliseconds) اختیاری هستند و اگر مشخص نشوند با صفر مقداردهی می‌شوند.

برچسب‌های ثبت زمان (timestamp) به عنوان تعداد هزارم ثانیه‌هایی ذخیره می‌شوند که از شروع سال 1970 میلادی در ناحیه زمانی UTC می‌گذرد. این روش بر اساس “Unix time” است که خود حدود همان سال اختراع شد. می‌توانید برای زمان‌های قبل از 1970 از اعداد منفی استفاده کنید. متد getTime روی یک شیء Date این عدد را تولید می‌کند. این عدد همانطور که می‌توانید حدس بزنید رقم بزرگی است.

console.log(new Date(2013, 11, 19).getTime());
// → 1387407600000
console.log(new Date(1387407600000));
// → Thu Dec 19 2013 00:00:00 GMT+0100 (CET)

اگر به تابع سازنده‌ی Date یک آرگومان ارسال نمایید، این آرگومان به عنوان همان شمارش هزارم ثانیه‌ها تفسیر می‌شود. می‌توانید تعداد هزام ‌ثانیه‌های لحظه‌ی کنونی را با ایجاد یک شیء جدید Date و فراخوانی متد getTime روی آن یا با فراخوانی تابع Date.now بدست بیاورید.

اشیاء Date متد‌هایی مانند getFullYear، getMonth، getDate، getHours، getMinutes، و getSeconds را فراهم می‌کنند که بتوان اجزای یک تاریخ را به وسیله‌ی آن‌ها استخراج کرد. در کنار متد getFullYear، متدی به نام getYear نیز وجود دارد، که سال را با کسر از 1900 تولید می‌کند (مثل 98 یا 119) که تقریبا کاربردی ندارد.

با قراردادن پرانتز دور بخش‌های عبارتی که به آن نیاز داریم، می‌توانیم شیء تاریخ را از یک رشته ایجاد کنیم.

function getDate(string) {
  let [_, month, day, year] =
    /(\d{1,2})-(\d{1,2})-(\d{4})/.exec(string);
  return new Date(year, month - 1, day);
}
console.log(getDate("1-30-2003"));
// → Thu Jan 30 2003 00:00:00 GMT+0100 (CET)

کاراکتر خط زیرین (_) که در مثال به عنوان یک متغیر استفاده شده است، در اینجا استفاده‌ای ندارد و فقط برای عبور از خانه‌ی اول آرایه‌ی تولیدی exec استفاده شده است.

مرز‌های واژه و رشته

متاسفانه، متد getDate همچنین تاریخ‌های غلطی مانند 00-1-3000 را از رشته‌ی "100-1-30000" استخراج می‌کند. یک تطبیق ممکن است در هرجای رشته رخ بدهد، بنابراین در این مورد، از کاراکتر دوم این رشته شروع می‌شود و در کاراکتر یکی مانده به پایان، تمام می‌شود.

اگر بخواهیم تطبیق شامل کل رشته باشد، باید با استفاده از نشانگر‌های ^ و $ این کار را انجام دهیم. کاراکتر ^، شروع رشته‌ی ورودی را مشخص می‌کند، در حالیکه کاراکتر $، این کار را برای پایان انجام می‌دهد. بنابراین /^\d+$/ رشته‌ای را تطبیق خواهد داد که کلا دارای یک یا بیش از یک رقم باشد، /^!/ شامل همه‌ی رشته‌هایی می‌شود که با یک علامت تعجب شروع شده باشند، و /x^/ هیچ رشته‌ای را شامل نخواهد شد (نمی‌توان یک کاراکتر x را قبل از کاراکتر شروع یک رشته تصور کرد).

اگر، از سوی دیگر، بخواهیم مطمئن شویم که تاریخ مورد نظر در مرز‌های یک کلمه شروع و پایان می‌یابد، می‌توانیم از نشانگر \b استفاده کنیم. یک مرز کلمه می‌تواند شروع یا پایان یک رشته یا هر نقطه‌ای در رشته باشد که یک کارکتر از نوع کلمه (حرف الفبا یا رقم مثل \w) در یک سمت داشته باشد و یک کاراکتر غیر‌کلمه‌ای در سمت دیگر داشته باشد.

console.log(/cat/.test("concatenate"));
// → true
console.log(/\bcat\b/.test("concatenate"));
// → false

توجه داشته باشید که یک نشانگر تعیین مرز (حدود) خود کاراکتری را تطبیق نمی‌دهد. این نشانگر فقط باعث می‌شود که عبارت باقاعده فقط زمانی تطبیق بخورد که یک شرط مشخص در نقطه‌ای که نشانگر در الگو قرار گرفته برقرار باشد.

الگوهای انتخاب

فرض کنید بخواهیم بدانیم که در یک رشته‌ی متنی عددی وجود دارد که بعد از آن یکی از کلمه‌های pig، cow، یا chicken به صورت مفرد یا جمع آمده باشد.

می‌توانیم سه عبارت باقاعده‌ی مجزا نوشته و هر کدام را به نوبت روی نوشته آزمایش کنیم. اما یک راه بهتر نیز وجود دارد. کاراکتر پایپ (|) امکان انتخاب بین الگوی سمت راست و چپش را فراهم می‌کند. بنابراین می‌توانیم بنویسیم:

let animalCount = /\b\d+ (pig|cow|chicken)s?\b/;
console.log(animalCount.test("15 pigs"));
// → true
console.log(animalCount.test("15 pigchickens"));
// → false

می‌توان با استفاده از پرانتز بخش‌هایی از الگو که عملگر پایپ روی آن‌ها اعمال می‌شود را محدود کرد، و نیز می‌توان چندین عملگر پایپ را کنار هم قرار داد تا امکان انتخاب بین بیش از دو جایگزین را فراهم نمود.

مکانیزم تطبیق‌دهی

از نظر مفهومی، زمانی که از متد exec یا test استفاده می‌کنید، موتور عبارت باقاعده به دنبال تطبیقی در رشته‌ی شما می‌گردد و سعی دارد این کار را با تطبیق دادن عبارت از ابتدای رشته انجام دهد، سپس از کاراکتر دوم، و همین طور ادامه می‌دهد تا اینکه تطبیقی پیدا کند یا به انتهای رشته‌ی داده شده برسد. در پایان رشته، یا اولین تطبیق ممکن را برمی‌گرداند یا جستجو با شکست روبرو می‌شود.

موتور جاوااسکریپت برای انجام تطبیق، با عبارت باقاعده مانند یک نمودار جریان برخورد می‌کند. نمودار پایین برای عبارت مربوط به مثال حیوانات است:

Visualization of /\b\d+ (pig|cow|chicken)s?\b/

عبارت ما موفق به تطبیق خواهد شد اگر بتوانیم مسیری از سمت چپ نمودار به سمت راست آن بیابیم. موقعیت فعلی را در رشته حفظ می‌کنیم، و هر بار که به سمت یک مستطیل حرکت می‌کنیم، مطمئن می‌شویم که بخشی از رشته که بعد از موقعیت فعلی ما قرار دارد با آن مستطیل تطبیق دارد.

بنابراین اگر سعی کنیم که رشته‌ی "the 3 pigs" را از موقعیت 4 تطبیق دهیم، پیشروی ما در نمودار چیزی شبیه به زیر می‌شود:

عقب‌گرد

عبارت باقاعده‌ی /\b([01]+b|[\da-f]+h|\d+)\b/ یکی از اعداد زیر را تطبیق می‌دهد: یک عدد دودویی که بعد از آن یک b آمده باشد، یک عدد هگزادسیمال (عددی در مبنای 16 که دارای حروف a تا f است که برای اعداد 10 تا 15 استفاده می‌شوند) که بعد از آن یک h قرار گرفته، یا یک عدد ده‌دهی معمولی که هیچ پسوندی ندارد. نمودار زیر مربوط به این عبارت است:

Visualization of /\b([01]+b|\d+|[\da-f]+h)\b/

در زمان تطبیق این عبارت، اغلب اینگونه می‌شود که علی رغم اینکه ممکن است ورودی دارای عدد دودویی نباشد، اما شاخه‌ی بالایی (دودویی) انتخاب می‌شود. در زمان تطبیق رشته‌ی "103" به عنوان مثال، فقط زمانی متوجه می‌شویم که در شاخه‌ی اشتباهی قرار داریم که به کاراکتر 3 برسیم. رشته با عبارت تطبیق دارد اما نه لزوما با شاخه‌ای که در حال حاضر در آن قرار گرفته‌ایم.

بنابراین تطبیق‌دهنده عقب‌گرد انجام می‌دهد. هنگام ورود به یک شاخه، موقعیت کنونی خودش را به خاطر می‌سپارد (در اینجا، در ابتدای رشته، درست قبل از اولین مستطیل مرز (محدوده) در نمودار) با این کار می‌تواند به عقب برگردد و اگر شاخه‌ی فعلی جواب نداد به سراغ شاخه‌ی دیگری برود. برای رشته‌ی "103" بعد از مواجه با کاراکتر 3، به سراغ شاخه‌ی اعداد هگزادسیمال می‌رود، که نتیجه‌ای نخواهد داشت به این دلیل که بعد از عدد، هیج کاراکتر h ای وجود ندارد. بنابراین به سراغ شاخه‌ی عدد ده‌دهی می‌رود. این شاخه انتخاب درستی است و یک تطبیق در پایان گزارش داده می‌شود.

تطبیق‌گر به محض اینکه یک تطبیق کامل پیدا می‌کند متوقف می‌شود. معنای این کار این است که اگر چندین شاخه‌ی بالقوه برای تطبیق یک رشته موجود باشد، فقط اولین شاخه (به ترتیبی که شاخه در عبارت منظم قرار گرفته است) استفاده می‌شود.

عقب‌گرد همچنین برای عملگرهای تکرار مثل + و * نیز اتفاق می افتد. اگر الگوی /^.*x/ را روی رشته‌ی "abcxe" تطبیق دهید، قسمت .*، ابتدا سعی می‌کند که تمام رشته را مصرف کند. موتور سپس متوجه می‌شود که نیاز به یک x دارد تا بتواند الگو را تطبیق دهد. چون هیچ x ای قبل از پایان رشته وجود ندارد، عملگر * سعی می‌کند تا یک کاراکتر کمتر را تطبیق دهد. اما تطبیق‌گر، x را بعد از abcx نیز پیدا نمی‌کند بنابراین عقب‌گرد دوباره اتفاق می‌افتد که موجب می‌شود عملگر ستاره فقط abc را تطبیق دهد. اکنون یک x درست جایی که لازمش دارد پیدا می‌کند و آن را به عنوان یک تطبیق موفق از موقعیت 0 تا 4 گزارش می‌دهد.

می‌توان عبارات باقاعده‌ای نوشت که در آن‌ها تعداد زیادی عقب‌گرد انجام شود. این مشکل زمانی رخ می‌دهد که یک الگو می‌تواند یک ورودی را به شیوه‌های زیاد و متفاوتی تطبیق دهد. به عنوان مثال، اگر هنگام نوشتن یک عبارت باقاعده برای یک عدد دودویی حواسمان نباشد، ممکن است تصادفا چیزی شبیه /([01]+)+b/ بنویسیم.

Visualization of /([01]+)+b/

اگر این الگو سعی کند که سری‌های بلندی از صفر و یک‌ها را بدون کاراکتر پایانی b تطبیق دهد، تطبیق‌گر ابتدا سراغ حلقه‌ی درونی می‌رود تا اینکه تمامی اعداد تمام شوند. سپس متوجه می‌شود که کاراکتر b وجود ندارد، بنابراین یک مکان (موقعیت) عقب‌گرد می‌کند، یک بار به سراغ حلقه‌ی بیرونی می‌رود و نتیجه‌ای نمی‌گیرد، دوباره برای خروج از حلقه‌ی درونی عقب‌گرد انجام می‌دهد. یعنی مقدار کار انجام شده به ازای هر کاراکتر دو برابر می‌شود. حتی برای چند دوجین کاراکتر، عمل تطبیق در واقع برای همیشه طول خواهد کشید.

متد replace

مقادیر رشته‌ای دارای متدی به نام replace هستند که می‌توان از آن برای جایگزینی بخشی از رشته با رشته‌ای دیگر استفاده کرد.

console.log("papa".replace("p", "m"));
// → mapa

آرگومان اول این متد همچنین می‌تواند یک عبارت باقاعده باشد، که در این صورت، اولین تطبیق پیدا شده توسط عبارت باقاعده، با رشته‌ی مورد نظر جایگزین می‌شود. زمانی که گزینه‌ی g (سراسری) به عبارت باقاعده اضافه شود، به جای جایگزینی اولین مورد، تمامی تطبیق‌های پیداشده در رشته، جایگزین خواهند شد.

console.log("Borobudur".replace(/[ou]/, "a"));
// → Barobudur
console.log("Borobudur".replace(/[ou]/g, "a"));
// → Barabadar

بهتر به نظر می‌رسید اگر گزینه‌ی انتخاب بین جایگزینی همه‌ی تطبیق‌ها یا یک تطبیق، به شکل یک آرگومان مجزا برای متد replace تعریف می‌شد یا اینکه متدی متفاوت برای آن در نظر گرفته می‌شد؛ مانند replaceAll. اما از بد روزگار، این گزینه وابسته به خاصیتی در عبارت باقاعده می‌باشد.

قدرت اصلی استفاده از عبارات باقاعده به وسیله‌ی متد replace اینجا است که می‌توانیم به گروه‌های تطبیق خورده در رشته‌ی جایگزین رجوع کنیم. به عنوان مثال، فرض کنید که یک رشته‌ی بزرگ که حاوی نام افراد است در اختیار داریم، در هر خط یک نام وجود دارد و فرمت آن به شکل Lastname, Firstname می‌باشد. اگر بخواهیم ترتیب قرار‌گیری نام‌ها را عوض کرده و ویرگول بین آن را حذف کنیم، می‌توانیم از کد زیر استفاده کنیم:

console.log(
  "Liskov, Barbara\nMcCarthy, John\nWadler, Philip"
    .replace(/(\w+), (\w+)/g, "$2 $1"));
// → Barbara Liskov
//   John McCarthy
//   Philip Wadler

$1 و $2 در رشته‌ی جایگزین به گروه‌هایی که با پرانتز در الگو مشخص شده‌اند اشاره می‌کنند. $1 توسط متنی که با اولین گروه تطبیق یافته جایگزین می‌شود، $2 نیز با دومین گروه و الی آخر تا $9. تطبیق کلی را می‌توان با $& مورد ارجاع قرار داد.

می‌توان یک تابع را به جای رشته به عنوان آرگومان دوم متد replace ارسال کرد. برای هر جایگزینی، این تابع فراخوانی می‌شود درحالیکه دسته‌ی تطبیق خورده (همچنین تطبیق کامل) به عنوان آرگومان به آن ارسال می‌شود و مقداری که برمی‌گرداند در رشته‌ی جدید قرار می‌گیرد.

به مثال کوچک زیر توجه نمایید:

let s = "the cia and fbi";
console.log(s.replace(/\b(fbi|cia)\b/g,
            str => str.toUpperCase()));
// → the CIA and FBI

و مثالی جالب‌تر:

let stock = "1 lemon, 2 cabbages, and 101 eggs";
function minusOne(match, amount, unit) {
  amount = Number(amount) - 1;
  if (amount == 1) { // only one left, remove the 's'
    unit = unit.slice(0, unit.length - 1);
  } else if (amount == 0) {
    amount = "no";
  }
  return amount + " " + unit;
}
console.log(stock.replace(/(\d+) (\w+)/g, minusOne));
// → no lemon, 1 cabbage, and 100 eggs

این مثال رشته‌ای را می‌گیرد، تمامی دفعات تکرار یک عدد که بعد از آن یک کاراکتر کلمه (منظور کاراکتری از جنس حرف و عدد است) آمده باشد را پیدا می‌کند و رشته‌ای برمی گرداند که در آن هر تطبیق پیدا شده یک واحد کاهش یافته است.

گروه (\d+) به عنوان آرگومان amount در تابع استفاده شده است، و گروه (\w+) به unit اختصاص یافته است. این تابع amount را به یک عدد تبدیل می‌کند – این عمل همیشه درست کار خواهد کرد چرا که توسط \d+ تطبیق خورده است – و آن را در صورتی که فقط یک و صفر باقی مانده باشد، تغییراتی می‌دهد.

عملگر‌های حریصانه

می‌توان از متد replace برای نوشتن تابعی که همه‌ی توضیحات را از یک قطعه کد جاوااسکریپت حذف کند استفاده نمود. اولین تلاش ما برای این کار به شکل زیر است:

function stripComments(code) {
  return code.replace(/\/\/.*|\/\*[^]*\*\//g, "");
}
console.log(stripComments("1 + /* 2 */3"));
// → 1 + 3
console.log(stripComments("x = 10;// ten!"));
// → x = 10;
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1  1

قسمتی که قبل از عملگر یا (|) آمده است مطابق با دو کاراکتر اسلشی خواهد بود که می‌تواند بعد از آن‌ها هر کاراکتری غیر از کاراکتر‌های خط جدید بیاید. بخشی که مربوط به توضیحات چندخطه می‌باشد کمی پیچیده‌تر است. ما از [^] (به معنای هر کاراکتر که در یک مجموعه‌ی تهی از کاراکتر‌ها جا نمی‌گیرد) به عنوان روشی برای تطبیق همه‌ی کاراکتر‌ها استفاده کرده‌ایم. نمی‌توانیم فقط از یک نقطه (.) برای این منظور در اینجا استفاده کنیم چراکه بلاک‌های کامنت را می‌توان در چند خط نوشت و کاراکتر نقطه کاراکتر‌های خطوط جدید را تطبیق نمی‌دهد.

اما خروجی خط آخر به نظر می‌رسد که دارای اشتباه باشد. چرا؟

قسمت [^]* عبارت، همانطور که در قسمت عقب‌گرد توضیح دادم، در ابتدا تا آنجایی که می‌تواند تطبیق می‌دهد. اگر این کار منجر به این شود که بخش بعدی الگو شکست بخورد، تطبیق‌گر یک کاراکتر به عقب برگشته و از آن نقطه دوباره تلاش می‌کند. در مثال بالا، تطبیق‌گر ابتدا تلاش می‌کند تا کل رشته‌ی باقیمانده را تطبیق دهد سپس از آنجا به عقب برگردد. این موجب خواهد شد که یک نمونه از */ را بعد از اینکه چهار کاراکتر به عقب برمی‌گردد تطبیق دهد. این چیزی نیست که به دنبال آن بودیم – قصد ما این بود که یک توضیح را تطبیق دهیم، نه اینکه تا انتهای کد‌های برنامه را برای پیدا کردن پایان آخرین بلاک توضیحات پیمایش کنیم.

به خاطر این عملکرد، به عملگر‌های تکرار (+, *, ?, و {}) عملگرهای حریصانه می گوییم، به این معنا که تا جایی که می‌توانند تطبیق می‌دهند بعد به عقب برمی‌گردنند. اگر بعد از آن ها یک علامت سوال قرار دهید (+?, *?, ??, {}?)، دیگر حریص نخواهند بود و با حداقل تطبیق شروع می‌کنند، زمانی به تطبیق بیشتر می‌پردازند که الگوی باقیمانده با تطبیقی کوچکتر مطابقت نداشته باشد.

و این دقیقا آن چیزی است که در این مورد آن را می‌خواهیم. با تطبیق کوچکترین بازه‌هایی از کاراکتر‌ها به وسیله‌ی ستاره که مارا به یک */ برساند، ما فقط یک بلاک توضیحات را انتخاب کردیم و نه چیز بیشتری را.

function stripComments(code) {
  return code.replace(/\/\/.*|\/\*[^]*?\*\//g, "");
}
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1 + 1

زمانی که یک عملگر غیرحریصانه کارکرد بهتری برای مسئله دارد، اگر بدون دلیل و آگاهی از یک عملگر حریصانه استفاده کنید، ممکن است با باگ‌های زیادی در برنامه روبرو شوید. هنگام استفاده از یک عملگر تکرار، بهتر است ابتدا به سراغ نسخه‌ی غیر حریصانه‌ی آن بروید.

ساخت اشیاء RegExp به صورت پویا

در بعضی مواقع، ممکن است هنگام کدنویسی، الگوی مورد نیاز جهت تطبیق مشخص نباشد. فرض کنید که می‌خواهید به دنبال نام کاربر در یک متن بگردید و آن را توسط یک جفت کاراکتر خط زیرین محصور کنید تا بتوان آن ‌را شناسایی کرد. به دلیل اینکه فقط در هنگام اجرای برنامه نام مورد نظر مشخص می‌شود، نمی‌توان از روش استفاده از اسلش بهره برد.

اما می‌توانید یک رشته تولید کنید و از سازنده‌ی RegExp روی آن استفاده کنید. به مثال توجه کنید:

let name = "harry";
let text = "Harry is a suspicious character.";
let regexp = new RegExp("\\b(" + name + ")\\b", "gi");
console.log(text.replace(regexp, "_$1_"));
// → _Harry_ is a suspicious character.

هنگام نوشتن نشان‌گر‌های (مرز) \b ، باید از دو بک‌اسلش استفاده کنیم به این علت که آن‌ها را در یک رشته‌ی نرمال می‌نویسیم نه یک عبارت باقاعده که توسط اسلش محصور شده است. آرگومان دوم سازنده‌ی RegExp مربوط به گزینه‌های مربوط به عبارت باقاعده است – در این مثال، "gi" برای مشخص کردن سراسری بودن و غیرحساس بودن به حروف بزرگ و کوچک است.

اما چه می‌شود اگر نام کاربر مورد نظر "dea+hl[]rd" باشد که متعلق یک نوجوان خوره‌ی کامپیوتر است؟ این نام باعث می‌شود که یک عبارت باقاعده‌ی بی‌معنا تولید شود که منجر به تطبیق نام کاربر نمی‌شود.

راه حل این مشکل، اضافه کردن بک‌اسلش قبل از هر کاراکتری که معنای خاصی دارد است.

let name = "dea+hl[]rd";
let text = "This dea+hl[]rd guy is super annoying.";
let escaped = name.replace(/[\\[.+*?(){|^$]/g, "\\$&");
let regexp = new RegExp("\\b" + escaped + "\\b", "gi");
console.log(text.replace(regexp, "_$&_"));
// → This _dea+hl[]rd_ guy is super annoying.

متد search

متد indexOf که روی رشته‌ها کار می‌کرد را نمی‌توان با یک عبارت باقاعده فراخواند. اما متد دیگری به نام search وجود دارد که یک عبارت باقاعده را دریافت می‌کند. درست مانند indexOf، این متد نیز اولین خانه‌ی خروجی را به عبارتی که پیدا شد اختصاص می‌دهد و یا در صورت پیدا نکردن نتیجه، -1 را برمی‌گرداند.

console.log("  word".search(/\S/));
// → 2
console.log("    ".search(/\S/));
// → -1

متاسفانه، راهی برای مشخص کردن نقطه‌ی شروع برای تطبیق وجود ندارد (شبیه کاری که می‌توانیم با آرگومان دوم indexOf انجام دهیم) که در صورت وجود کاربرد داشت.

خاصیت lastIndex

متد exec نیز راهی مناسب برای شروع جستجو از یک موقعیت داده شده در یک رشته را پشتیبانی نمی‌کند. اما یک راه غیر سرراست برای این کار وجود دارد.

اشیائی که از نوع عبارت باقاعده هستند دارای یک سری خاصیت می‌باشند. یکی از این خاصیت‌ها source است، که رشته‌ای که عبارت از آن تولید شده است را نگهداری می‌کند. یک خاصیت دیگر، lastIndex است که در شرایط محدودی کنترل می‌کند که تطبیق بعدی از کجا شروع خواهد شد.

آن شرایط این است که عبارت باقاعده باید گزینه‌های سراسری (g) یا چسبنده (y) را فعال داشته باشد و تطبیق باید با متد ‌exec صورت پذیرد. باز هم یک راه حل کمتر گیج‌کننده می‌توانست این باشد که اجازه داده شود که یک آرگومان اضافی برای این کار به متد exec فرستاده می‌شود، اما گیج کنندگی، یکی از ویژگی‌های اساسی رابط عبارات باقاعده در جاوااسکریپت است.

let pattern = /y/g;
pattern.lastIndex = 3;
let match = pattern.exec("xyzzy");
console.log(match.index);
// → 4
console.log(pattern.lastIndex);
// → 5

اگر تطبیق با موفقیت انجام شد، فراخوانی exec به طور خودکار خاصیت lastIndex را به روز‌رسانی کرده تا به نقطه‌ی بعد از تطبیق اشاره کند. اگر تطبیقی پیدا نشود، lastIndex مقدار صفر را خواهد گرفت، که مقداری است که شیء در هنگام ایجاد یک عبارات باقاعده جدید نگهداری می‌کند.

تفاوت بین گزینه‌ی سراسری و چسبنده این است که در حالت فعال بودن گزینه‌ی چسبنده، زمانی تطبیق موفق خواهد بود که مستقیما از نقطه‌ی lastIndex شروع شود درحالیکه در حالت سراسری، جستجو رو به جلو انجام خواهد شد تا به موقعیتی برسد که یک تطبیق بتواند شروع شود.

let global = /abc/g;
console.log(global.exec("xyz abc"));
// → ["abc"]
let sticky = /abc/y;
console.log(sticky.exec("xyz abc"));
// → null

اگر از یک عبارت باقاعده‌ی مشترک برای چندین فراخوانی exec استفاده کنیم این به‌روز‌رسانی‌های خودکار خاصیت lastIndex می‌تواند مشکل‌ساز باشد. عبارت باقاعده‌ی شما ممکن است تصادفا از اندیسی شروع شود که از فراخوانی قبلی به جا مانده باشد.

let digit = /\d/g;
console.log(digit.exec("here it is: 1"));
// → ["1"]
console.log(digit.exec("and now: 1"));
// → null

یک اثر جالب توجه دیگر در صورت استفاده از گزینه‌ی سراسری این است که باعث می‌شود کارکرد متد match روی رشته‌ها، متفاوت باشد. زمانی که این متد با عبارتی سراسری فراخوانی شود، به جای اینکه آرایه‌ای شبیه چیزی که از exec برگردانده می‌شد تولید کند، متد match تمامی تطبیق‌های الگوی درون رشته را پیدا می‌کند و آرایه‌ای حاوی تمام رشته‌های تطبیق خورده تولید می‌کند.

console.log("Banana".match(/an/g));
// → ["an", "an"]

بنابراین با احتیاط سراغ عبارات باقاعده‌ی سراسری بروید. معمولا تنها مواردی که لازم است به سراغ آن‌ها بروید هنگامی است که به فراخوانی متد replace نیاز دارید و همچنین مواقعی که لازم است تا صراحتا از lastIndex استفاده کنید.

پیمایش تطبیق‌ها

یکی از کار‌های رایج این است که تمامی موارد رخداد یک الگو در رشته را در بدنه‌ی حلقه پیمایش کنیم به شکلی که شیء تطبیق داده شده در دسترس ما باشد. برای این‌کار می‌توانیم از متد‌های lastIndex و exec استفاده کنیم.

let input = "A string with 3 numbers in it... 42 and 88.";
let number = /\b\d+\b/g;
let match;
while (match = number.exec(input)) {
  console.log("Found", match[0], "at", match.index);
}
// → Found 3 at 14
//   Found 42 at 33
//   Found 88 at 40

این مثال از این واقعیت استفاده می‌کند که مقدار یک عبارت تخصیص (=)، همان مقدار انتساب داده شده است. بنابراین با استفاده از match = number.exec(input) به عنوان قسمت شرط دستور while، تطبیق را در شروع هر تکرار حلقه اجرا می‌کنیم و نتیجه‌ی آن را در یک متغیر ذخیره می‌کنیم، و هنگامی پیمایش حلقه را متوقف می‌کنیم که تطبیقی پیدا نشود.

تجزیه‌ی یک فایل ini

برای به پایان رساندن این فصل، به سراغ مسئله‌ای می‌رویم که به دست عبارات باقاعده حل می‌شود. فرض کنید که در حال نوشتن برنامه‌ای هستیم که به طور خودکار اطلاعاتی درباره‌ی دشمنانمان از سطح اینترنت جمع آوری می‌کند. (واقعا قرار نیست این برنامه را در اینجا بنویسیم، فقط بخشی را می‌نویسیم که فایل حاوی تنظیمات را می‌خواند. از این بابت متاسفم.) فایل تنظیمات به این شکل است:

searchengine=https://duckduckgo.com/?q=$1
spitefulness=9.7

; comments are preceded by a semicolon...
; each section concerns an individual enemy
[larry]
fullname=Larry Doe
type=kindergarten bully
website=http://www.geocities.com/CapeCanaveral/11451

[davaeorn]
fullname=Davaeorn
type=evil wizard
outputdir=/home/marijn/enemies/davaeorn

قوانین حاکم بر این فایل (که فرمتی بسیار رایج است و معمولا یک فایل INI نامیده می‌شود) به صورت زیر است:

وظیفه‌ی ما این است که رشته‌ای شبیه این را به یک شیء تبدیل کنیم که خاصیت‌هایش رشته‌های تنظیمات نوشته شده قبل از اولین بخش را نگهداری می‌کنند و زیر‌شیء‌هایش به بخش‌هایی تعلق دارند که هر زیر‌شیء تنظیمات یک بخش را در خود دارد.

به دلیل اینکه این فرمت باید خط به خط پردازش شود، تقسیم فایل به خطوط مجزا شروع خوبی به نظر می‌رسد. ما متد split را در فصل 4 دیدیم. بعضی سیستم عامل‌ها، به هر دلیلی، فقط از کاراکتر خط جدید برای جداسازی خطوط استفاده نمی‌کنند بلکه از یک کاراکتر بازگشت به ابتدای خط و بعد از آن کاراکتر خط جدید برای این کار استفاده می‌کنند ("\r\n"). با درنظر گرفتن اینکه می‌دانیم می‌توان به متد split، یک عبارات باقاعده ارسال کرد می‌توانیم جداسازی خطوط را با عبارت باقاعده ای شبیه /\r?\n/ انجام دهیم که باعث می‌شود هم "\n" و هم "\r\n" در نظر گرفته شود.

function parseINI(string) {
  // Start with an object to hold the top-level fields
  let result = {};
  let section = result;
  string.split(/\r?\n/).forEach(line => {
    let match;
    if (match = line.match(/^(\w+)=(.*)$/)) {
      section[match[1]] = match[2];
    } else if (match = line.match(/^\[(.*)\]$/)) {
      section = result[match[1]] = {};
    } else if (!/^\s*(;.*)?$/.test(line)) {
      throw new Error("Line '" + line + "' is not valid.");
    }
  });
  return result;
}

console.log(parseINI(`
name=Vasilis
[address]
city=Tessaloniki`));
// → {name: "Vasilis", address: {city: "Tessaloniki"}}

کد بالا به این صورت عمل می‌کند که خط به خط فایل را پردازش کرده و یک شیء می‌سازد. خاصیت‌های قسمت بالایی مستقیما درون شیء ذخیره می‌شوند، درحالیکه خاصیت‌هایی که در بخش‌ها قرار دارند به صورت جداگانه در شیئی مختص هر بخش قرار می‌گیرند. متغیر section به شیء بخش کنونی اشاره می‌کند.

دو نوع قابل توجه خط وجود دارد – سرتیتر‌های بخش یا خطوط خاصیت‌ها. زمانی که یک خط معرف یک خاصیت معمولی است، در بخش فعلی ذخیره می‌شود. زمانی که معرف یک سرتیتر بخش است، یک شیء جدید برای بخش مورد نظر ایجاد می‌شود و section به آن تخصیص می‌یابد.

توجه داشته باشید که استفاده‌ی مکرر از ^ و $ برای این است که مطمئن شویم عبارت تمام خط را تطبیق می‌دهد نه فقط بخشی از آن را. اگر از آن‌ها استفاده نشود، کد در اکثر مواقع کار می‌کند اما برای بعضی ورودی‌ها رفتار عجیبی از خود نشان دهد که ممکن است اشکال زدایی آن سخت باشد.

الگوی if (match = string.match(...)) شبیه به ترفندی است که از عبارت تخصیص به عنوان شرط while استفاده کردیم. اغلب اطمینان ندارید که فراخوانی match موفق خواهد شد، بنابراین می‌توانید فقط درون یک دستور if که آن را آزمایش می‌کند به نتیجه‌ی آن دسترسی داشته باشید. برای جلوگیری از شکستن زنجیره‌ی else if، نتیجه‌ی تطبیق را به متغیری اختصاص دادیم و بلافاصله آن تخصیص را به عنوان شرط دستور if استفاده کرده‌ایم.

اگر یک خط، سرتیتر بخش یا یک خاصیت نباشد، تابع با استفاده از عبارت /^\s*(;.*)?$/ بررسی می‌کند که آیا این خط توضیح است یا خطی خالی. متوجه نحوه‌ی کارکرد آن شدید؟ قسمتی که داخل پرانتز است توضیحات را تطبیق می‌دهد و علامت سوال ? اطمینان حاصل می‌کند که خطوطی که فقط فضای خالی هستند شناسایی شوند. اگر خطی با هیچکدام از اشکال قابل انتظار تطبیق نخورد، تابع یک استثنا تولید می‌کند.

کاراکترهای بین‌المللی

به دلیل اینکه پیاده‌سازی اولیه جاوااسکریپت بسیار ساده بوده است و این واقعیت که این شیوه‌ی ساده محور بعد‌ها به عنوان یک استاندارد رفتاری در نظر گرفته شد، عبارات باقاعده در جاوااسکریپت نسبتا برای کاراکتر‌های غیر انگلیسی، حرفی برای گفتن ندارند. به عنوان مثال، در عبارات باقاعده جاوااسکریپت، یک "کاراکتر کلمه" فقط شامل 26 حرف لاتین (حروف بزرگ و کوچک)، اعداد ده‌دهی، و به دلایلی کاراکتر خط زیرین می‌شود. چیز‌هایی مثل é یا β که قطعا کاراکتر کلمه محسوب می‌شوند توسط \w تطبیق نمی‌خورند (و با \W تطبیق می‌خورند، دسته‌ی کاراکتر‌های غیر کلمه).

به خاطر یک اتفاق نامعلوم در گذشته، \s (فضای خالی) این مشکل را ندارد و همه‌ی کاراکتر‌هایی که استاندارد یونیکد به عنوان فضای خالی درنظر می‌گیرد را شامل می‌شود، مثل کاراکتر‌هایی از قبیل نیم‌فاصله و جداکننده حروف صدادار در زبان مغولی.

مشکل دیگر این است که به طور پیش فرض عبارات باقاعده روی واحد‌های کد عمل می‌کنند؛ نه روی کاراکتر‌های واقعی؛ همانطور که در فصل 5 بحث شد. معنای آن این است که با کاراکتر‌هایی که از دو واحد کد تشکیل شده‌اند به شکل نامشخصی رفتار می‌شود.

console.log(/🍎{3}/.test("🍎🍎🍎"));
// → false
console.log(/<.>/.test("<🌹>"));
// → false
console.log(/<.>/u.test("<🌹>"));
// → true

مشکل اینجاست که 🍎 در خط اول به عنوان دو واحد کد شناخته می‌شود، و {3} فقط به واحد دوم اعمال می‌شود. به طور مشابه، عملگر نقطه فقط یک واحد کد را می‌شناسد نه دو واحدی که ایموجی گل رز را می‌سازند.

برای اینکه عبارت باقاعده این گونه کاراکتر‌ها را در نظر بگیرد باید گزینه‌ی u (یونیکد) را استفاده کنید. متاسفانه به صورت پیش‌فرض این اشکال وجود خواهد داشت چون تغییر آن ممکن است مشکلاتی را برای کد‌های نوشته شده از قبل که به این رفتار وابستگی دارند به وجود بیاورد.

اگرچه این قضیه به تازگی استاندارد شده است، و در هنگام نوشتن این کتاب، هنوز به طور گسترده از آن پشتیبانی نمی‌شود، می‌توان از \p در یک عبارت باقاعده (عبارتی که باید گزینه‌ی یونیکد را فعال داشته باشد) برای تطبیق همه‌ی کاراکتر‌هایی که استاندارد یونیکد برای آن‌ها خاصیتی در نظر گرفته است، استفاده کرد.

console.log(/\p{Script=Greek}/u.test("α"));
// → true
console.log(/\p{Script=Arabic}/u.test("α"));
// → false
console.log(/\p{Alphabetic}/u.test("α"));
// → true
console.log(/\p{Alphabetic}/u.test("!"));
// → false

یونیکد تعدادی خاصیت مفید تعریف می‌کند، اگرچه پیدا کردن خاصیتی که نیاز شما باشد ممکن است که همیشه ساده نباشد. می‌توانید از دستور \p{Property=Value} برای تطبیق هر کاراکتری که مقدار داده شده را برای آن خاصیت داشته باشد استفاده کنید. اگر نام خاصیت را همانطور که در \p{Name} می‌بینید حذف کنیم، نام آن یا به عنوان یک خاصیت دودویی مثل Alphabetic در نظر گرفته می‌شود یا یک دسته مثل Number.

خلاصه

عبارات باقاعده اشیائی هستند که الگو‌ها را در رشته‌ها نشان می‌دهند. این عبارات از زبانی مخصوص به خود برای بیان این الگو‌ها استفاده می‌کنند.

/abc/یک دنباله از کاراکترها
/[abc]/یک کاراکتر از یک مجموعه کاراکتر
/[^abc]/یک کاراکتر که در مجموعه‌ی مشخص شده نباشد
/[0-9]/یک کاراکتر که در یک بازه از کاراکترها قرار دارد
/x+/یک یا بیش از یک بار وقوع الگوی x
/x+?/یک یا بیش از یک بار وقوع به صورت غیر حریصانه
/x*/صفر یا بیش از صفر بار وقوع الگوی x
/x?/صفر یا یک بار وقوع
/x{2,4}/دو تا چهار بار وقوع
/(abc)/یک دسته یا گروه
/a|b|c/یکی از الگوهای متعدد
/\d/یک کاراکتر رقمی (عدد)
/\w/یک کاراکتر حرف-عددی (یک کاراکتر کلمه)
/\s/یک کاراکتر فضای خالی (هر نوعی)
/./هر کاراکتری به جز کاراکتر خط جدید
/\b/یک مرز کلمه
/^/شروع ورودی
/$/پایان ورودی

یک عبارت باقاعده دارای متدی به نام test است که رشته‌ی داده شده را جهت تطبیق با عبارت بررسی می‌کند. همچنین متدی به نام exec دارد که در صورت پیدا کردن تطبیق، آرایه‌ای تولید می‌کند که همه‌ی گروه‌های تطبیق خورده را در بر دارد. این آرایه دارای خاصیتی به نام index است که نقطه‌ی شروع تطبیق را مشخص می‌کند.

رشته‌ها دارای متدی به نام match می‌باشند که برای تطبیق آن‌ها با یک عبارات باقاعده استفاده می‌شود. متدی به نام search دارند که برای جستجوی یک عبارت استفاده می‌شود که تنها موقعیت شروع تطبیق یافته شده را برمی‌گرداند. متد replace متعلق به رشته‌ها می‌تواند تطبیق‌های پیدا شده برای یک الگو را با یک رشته یا تابع جایگزین کند.

عبارات باقاعده می‌توانند گزینه‌هایی هم داشته باشند که بعد از اسلش پایانی نوشته می‌شوند. گزینه‌ی i باعث می‌شود که تطبیق به بزرگی و کوچکی حروف حساس نباشد. گزینه‌ی g عبارت را سراسری می‌کند که علاوه بر نتایج دیگر، در متد replace باعث می‌شود که همه‌ی نمونه‌ها جایگزین شوند نه فقط اولین مورد. گزینه‌ی y باعث می‌شود که عبارت چسبنده شود، که معنای آن این است که به سمت جلو جستجو نخواهد کرد و بخشی از رشته را در هنگام جستجو برای تطبیق در نظر نمی‌گیرد. گزینه‌ی u حالت یونیکد را فعال می‌کند که مشکلات مربوط به کاراکتر‌هایی که دو واحد کد اشغال می‌کنند را برطرف می‌کند.

عبارت‌های باقاعده مانند چاقوی تیزی هستند که دسته‌ی نامناسبی دارند. بعضی از کار‌ها را به شدت ساده می‌کنند اما زمانی که به مسائل پیچیده اعمال می‌شوند می‌توانند به سرعت غیر قابل کنترل شوند. بخشی از فرهنگ صحیح استفاده از عبارات باقاعده این است که برای چیز‌هایی که به روشنی به وسیله‌ی آن‌ها قابل بیان نیستند به سراغشان نرویم.

تمرین‌ها

تقریبا غیر قابل اجتناب است که در حین انجام تمرین‌های این فصل، با دیدن بعضی از رفتار‌های پیچیده‌ی عبارات باقاعده، دچار سردرگمی و ناامیدی نشوید. گاهی اوقات بهتر است که عبارتتان را در ابزار‌های آنلاینی مثل https://debuggex.com وارد کنید تا ببینید تجسم عبارتتان با آنچه در نظر داشته‌اید ارتباط دارد یا خیر و با توجه به واکنش آن رشته‌های ورودی متفاوتی را آزمایش کنید.

گلف Regexp

گلف کد اصطلاحی است که برای تلاش نوشتن برنامه‌ای با حداقل کاراکتر استفاده می‌شود. به طور مشابه regexp golf، تمرین نوشتن کوتاهترین عبارت باقاعده‌ای است که برای تطبیق یک الگوی داده شده می‌توان نوشت و فقط همان الگو باید تطبیق بخورد.

برای هر یک از آیتم‌های زیر، یک عبارت باقاعده بنویسید و آزمایش کنید هر کدام از زیررشته‌های داده شده در آن وقوع دارد یا خیر. عبارت باقاعده‌ای که می‌نویسید باید فقط رشته‌هایی را تطبیق دهد که یکی از زیر رشته‌های داده شده را داشته باشند. نیازی نیست نگران مرز‌های کلمات باشید مگر اینکه به طور صریح ذکر شده باشد. وقتی عبارت باقاعده‌ی شما به طور صحیح کار کرد، ببینید می‌توانید آن را کوتاهتر بنویسید؟

  1. car و cat

  2. pop و prop

  3. ferret, ferry, و ferrari

  4. هر کلمه‌ای که با ious پایان پذیرد

  5. یک کاراکتر فضای خالی که بعد از نقطه، ویرگول، دونقطه، یا نقطه‌ویرگول بیاید

  6. کلمه‌ای که از شش حرف بیشتر باشد

  7. یک کلمه بدون داشتن حرف e (یا E)

به جدولی که در خلاصه فصل آمده است برای کمک گرفتن رجوع کنید. هر راه حل را با چندین رشته‌ی آزمایشی بررسی کنید.

// Fill in the regular expressions

verify(/.../,
       ["my car", "bad cats"],
       ["camper", "high art"]);

verify(/.../,
       ["pop culture", "mad props"],
       ["plop", "prrrop"]);

verify(/.../,
       ["ferret", "ferry", "ferrari"],
       ["ferrum", "transfer A"]);

verify(/.../,
       ["how delicious", "spacious room"],
       ["ruinous", "consciousness"]);

verify(/.../,
       ["bad punctuation ."],
       ["escape the period"]);

verify(/.../,
       ["hottentottententen"],
       ["no", "hotten totten tenten"]);

verify(/.../,
       ["red platypus", "wobbling nest"],
       ["earth bed", "learning ape", "BEET"]);


function verify(regexp, yes, no) {
  // Ignore unfinished exercises
  if (regexp.source == "...") return;
  for (let str of yes) if (!regexp.test(str)) {
    console.log(`Failure to match '${str}'`);
  }
  for (let str of no) if (regexp.test(str)) {
    console.log(`Unexpected match for '${str}'`);
  }
}

سبک نقل قول کردن

تصور کنید که یک داستان نوشته شده دارید و از علامت نقل قول تکی در طول کتاب برای مشخص کردن دیالوگ‌ها استفاده کرده‌اید. اکنون قصد دارید که همه‌ی علامت‌های تکی نقل قول را با علامت‌های جفتی عوض کنید و حواستان هم باشد که علامت‌های نقل قول تکی که در اختصار‌هایی مثل aren’t آمده‌اند را عوض نکنید.

به الگویی فکر کنید که این دو نوع نقل قول را تمییز دهد و از replace برای جایگزینی صحیح استفاده کنید.

let text = "'I'm the cook,' he said, 'it's my job.'";
// Change this call.
console.log(text.replace(/A/g, "B"));
// → "I'm the cook," he said, "it's my job."

روشن‌ترین راه حل برای این مسئله این است که فقط نقل‌قول‌هایی را جایگزین کنید که حداقل در یک سمت آن یک غیرکلمه قرار داشته باشد مثل /\W'|'\W/. اما همچنین لازم است تا شروع و پایان خط را هم در نظر داشته باشید.

علاوه بر این، باید اطمینان حاصل کنید که جایگزینی شامل کاراکترهایی که توسط \W تطبیق می‌خورند هم باشد تا از قلم نیفتند. این کار را می‌توان با قرار دادن آن‌ها درون پرانتز و استفاده از گروه‌هایشان در رشته‌ی جایگزینی ($1, $2) انجام داد. گروه‌هایی که تطبیق نمی خورند با چیزی جایگزین نمی‌شوند.

دوباره اعداد

عبارتی بنویسید که فقط اعداد سبک جاوااسکریپت را تطبیق دهد. عبارت باید علامت منفی یا مثبت را در جلوی عدد به صورت اختیاری پشتیبانی کند، همچنین نقطه‌ی ممیز و نماد توان - 5 5e-3 یا 1E10 - را دوباره با علامت اختیاری جلوی توان پشتیبانی کند. همچنین توجه داشته باشید که لازم نیست که بعد از نقطه‌ی ممیز حتما رقم بیاید اما نباید عدد فقط شامل یک نقطه‌ی تنها باشد. بنابراین.5 و 5. اعدادی معتبر در جاوااسکریپت محسوب می‌شوند اما یک نقطه‌ی تنها این طور نیست.

// Fill in this regular expression.
let number = /^...$/;

// Tests:
for (let str of ["1", "-1", "+15", "1.55", ".5", "5.",
                 "1.3e2", "1E-4", "1e+12"]) {
  if (!number.test(str)) {
    console.log(`Failed to match '${str}'`);
  }
}
for (let str of ["1a", "+-1", "1.2.3", "1+1", "1e4.5",
                 ".5.", "1f5", "."]) {
  if (number.test(str)) {
    console.log(`Incorrectly accepted '${str}'`);
  }
}

ابتدا، فراموش نکنید که بک‌اسلش را در جلوی نقطه قرار دهید.

تطبیق علامت اختیاری در جلوی یک عدد، همچنین جلوی یک توان، را می‌توان با استفاده از [+\-]? یا (\+|-|) انجام داد. (مثبت، منفی یا هیچی)

بخش پیچیده‌تر این تمرین این است که چه‌طور هر دوی "5." و ".5" را بدون تطبیق خوردن ".” تطبیق بزنید. برای این‌کار، یک راه خوب این است که از | برای جداسازی دو حالت استفاده شود - یک یا دو رقم که ممکن است با یک نقطه و صفر یا ارقام بیشتر ادامه یابد یا نقطه‌ای که به همراه یک را چندین رقم بیاید.

سرانجام، برای اینکه e را غیرحساس به بزرگی/کوچکی حروف داشته باشید، اضافه کردن گزینه‌ی i به انتهای عبارت باقاعده یا استفاده از [eE] مشکل را حل خواهد کرد.