Chapter 9عبارات باقاعده

Some people, when confronted with a problem, think ‘I know, I’ll use regular expressions.’ Now they have two problems.

Jamie Zawinski

Yuan-Ma said, ‘When you cut against the grain of the wood, much strength is needed. When you program against the grain of the problem, much code is needed.’

Master Yuan-Ma, The Book of Programming
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

برای مشخص کرد یک بازه از کاراکترها می توان درون براکت‌ها از یک کاراکتر (‍-) بین دو کاراکتر استفاده کرد که ترتیب کاراکترها توسط کد یونیکد آن‌ها مشخص می شود. کاراکترهای ۰ تا ۹ کنار هم و در بازه‌ی یونیکد (کدهای 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

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

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

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

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

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

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) انجام داد. گروه‌هایی که تطبیق نمی خورند با چیزی جایگزین نمی شوند.

دوباره اعداد

عبارتی بنویسید که فقط اعداد سبک جاوااسکریپت را تطبیق دهد. عبارت باید علامت منفی یا مثبت را در جلوی عدد به صورت اختیاری پشتیبانی کند، همچنین نقطه‌ی ممیز و نماد توان – 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] مشکل را حل خواهد کرد.