{{quote {author: "جیمی زاوینسکی", chapter: true}
بعضیها، وقتی با مشکلی روبرو میشوند، فکر میکنند "خب، راه حل را میدانم، استفاده از عبارات باقاعده. " ولی حالا با دو مشکل روبرو هستند.
quote}}
{{index "Zawinski, Jamie"}}
{{if interactive
{{quote {author: "استاد یوانما", title: "کتاب برنامهنویسی", chapter: true}
یوانما گفت، 'وقتی چوب را برخلاف جهت الیافش برش میدهید، نیروی بیشتری نیاز دارید. و هنگامی که بر خلاف روش صحیح حل مسئله، برنامهنویسی میکنید، به کد بیشتری نیاز دارید.'
quote}}
if}}
{{figure {url: "img/chapter_picture_9.jpg", alt: "A railroad diagram", chapter: "square-framed"}}}
{{index evolution, adoption, integration}}
ابزارها و تکنیکهای برنامهنویسی در طول زمان به شکلی نامنظم و تکاملی حفظ میشوند و گسترش مییابند. اینطور نیست که همیشه آنهایی که درخشان یا خوب هستند برنده شوند؛ بلکه تکنیکها و ابزارهایی باقی میمانند که در یک حوزهی مناسب به اندازهی کافی خوب عمل میکنند یا این ویژگی را دارند که با تکنولوژی موفق دیگری به خوبی یکپارچه و تلفیق میشوند.
{{index "domain-specific language"}}
در این فصل، دربارهی یکی از این ابزارهای موفق، ((عبارات باقاعده))، صحبت خواهم کرد. عبارات باقاعده روشی برای توصیف ((الگوها)) در دادههای متنی (رشتهای) میباشند. این عبارات، زبانی کوچک و مجزا را تشکیل میدهند که بخشی از زبان جاوااسکریپت و خیلی زبانها و سیستمهای دیگر محسوب میشوند.
{{index [interface, design]}}
عبارات باقاعده، به طور همزمان هم خیلی بیقواره و هم فوقالعاده کاربردی هستند. قواعد دستوری آنها رمزگونه و رابط برنامهنویسی آنها در جاوااسکریپت کمی نچسب است. اما ابزار بسیار قدرتمندی برای پردازش و وارسی رشتهها محسوب میشوند. درک صحیح عبارات باقاعده، شما را به برنامهنویس بهتری تبدیل میکند.
{{index ["regular expression", creation], "RegExp class", "literal expression", "slash character"}}
یک عبارت باقاعده یک نوع شیء است. میتوان آن را هم با سازندهی RegExp
و هم به طور مستقیم با قرار دادن یک الگو بین دو کاراکتر اسلش (/
) ایجاد نمود.
let re1 = new RegExp("abc");
let re2 = /abc/;
هر دوی عبارتهای باقاعدهی بالا نمایانگر یک ((الگو)) میباشند: کاراکتر a که بعد از آن b و بعد c می آید.
{{index ["backslash character", "in regular expressions"], "RegExp class"}}
زمانی که از سازندهی RegExp
استفاده میشود، الگو به صورت رشتهی معمولی نوشته میشود؛ بنابراین قوانین معمول برای کاراکتر بکاسلش برقرار است.
{{index ["regular expression", escaping], [escaping, "in regexps"], "slash character"}}
در روش دوم که در آن الگو بین دو کاراکتر اسلش ظاهر میشود، تفسیر بک اسلش کمی متفاوت است. اول اینکه، به دلیل اینکه کاراکتر اسلش نشان دهندهی پایان الگو است، باید یک بک اسلش را قبل از اسلشی که میخواهیم به عنوان بخشی از الگو تفسیر شود قرار دهیم. افزون بر آن، بک اسلشهایی که بخشی از کدکاراکترهای خاص (مانند \n
) محسوب نمیشوند، بر خلاف حالت رشتهای، حفظ شده و باعث تغییر در معنای الگو خواهند شد. بعضی کاراکترها مثل علامت سوال یا مثبت، معانی خاصی در عبارات باقاعده دارند و اگر قرار است نمایانگر کاراکتر خودشان باشند، باید قبلشان یک بک اسلش قرار داده شود.
let eighteenPlus = /eighteen\+/;
{{index matching, "test method", ["regular expression", methods]}}
اشیاء عبارات باقاعده دارای تعدادی متد میباشند. سادهترین آنها متد test
است. اگر به این متد یک رشته ارسال کنید، با برگرداندن یک مقدار بولی، به شما خواهد گفت که آیا در رشتهی داده شده، نمونهای مطابق الگوی عبارت باقاعده، وجود دارد یا خیر.
console.log(/abc/.test("abcde"));
// → true
console.log(/abc/.test("abxde"));
// → false
{{index pattern}}
اگر در ((عبارات باقاعده)) هیچ کاراکتر خاصی استفاده نشود، آن عبارت معادل همان دنبالهی کاراکترها میباشد. اگر abc در هر جای رشتهای که مورد آزمایش قرار دادهایم قرار گرفته باشد (نه فقط در شروع رشته)، متد test
مقدار true
را تولید میکند.
{{index "regular expression", "indexOf method"}}
فهمیدن اینکه آیا یک رشته حاوی abc هست یا خیر را میتوان به خوبی با متد indexOf
نیز انجام داد. عبارات باقاعده به ما امکان تولید ((الگوهای)) پیچیدهتری را میدهند.
فرض کنید قصد داریم همه ((اعداد)) را شناسایی کنیم. در یک عبارت باقاعده، قرار دادن یک ((مجموعه)) کاراکتر درون براکت باعث میشود که آن بخش از عبارت با هر کاراکتری که بین براکتها آمده است تطبیق یابد.
هر دوی عبارتهای زیر همهی رشتههایی که دارای رقم هستند را شامل میشود:
console.log(/[0123456789]/.test("in 1992"));
// → true
console.log(/[0-9]/.test("in 1992"));
// → true
{{index "hyphen character"}}
برای مشخص کردن یک بازه از کاراکترها میتوان درون براکتها از یک کاراکتر (-
) بین دو کاراکتر استفاده کرد که ترتیب کاراکترها توسط کد یونیکد آنها مشخص میشود. کاراکترهای 0 تا 9 کنار هم و در بازهی یونیکد (کدهای 48 تا 57) قرار دارند بنابراین [0-9]
همهی آنها را پوشش داده و هر رقمی را شامل میشود.
{{index [whitespace, matching], "alphanumeric character", "period character"}}
برای بعضی از گروههای کاراکتری روش کوتاهتری هم از پیش تعریف شده است. اعداد یکی از آنها هستند: مثلا \d
معنایی مشابه [0-9]
دارد.
{{index "newline character", [whitespace, matching]}}
{{table {cols: [1, 5]}}}
| \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
{{index ["backslash character", "in regular expressions"]}}
ظاهر عبارت بالا خیلی بیقواره است، درست است؟ نیمی از آن بکاسلش است که الگو را بیش از حد شلوغ کرده و تشخیص معنای آن را سخت نموده است. در ادامه با نسخهای از آن که کمی بهبود یافته است آشنا خواهیم شد.
{{index [escaping, "in regexps"], "regular expression", set}}
این کدهای بکاسلش را همچنین میتوان درون براکت استفاده کرد. به عنوان مثال، [\d.]
به معنای یک رقم یا یک کاراکتر نقطه است. اما خود نقطه وقتی داخل براکت قرار میگیرد معنای خاصش را از دست میدهد. این قضیه برای دیگر کاراکترهای خاص مثل +
هم برقرار است.
{{index "square brackets", inversion, "caret character"}}
برای معکوس کردن یک مجموعهی کاراکتر – به این معنا که شما قصد دارید هر کاراکتری بجز آنهایی که در مجموعه مشخص شدهاند را بیان کنید – میتوانید از یک کاراکتر (^
) بعد از براکت شروع بازه استفاده کنید.
let notBinary = /[^01]/;
console.log(notBinary.test("1100100010100110"));
// → false
console.log(notBinary.test("1100100010200110"));
// → true
{{index ["regular expression", repetition]}}
میدانیم که چگونه یک عدد یا رقم را شناسایی کنیم. چه باید کرد اگر بخواهیم که یک عدد کامل – دنبالهای از یک یا بیشتر رقم – را هدف قرار بدهیم؟
{{index "plus character", repetition, "+ operator"}}
زمانی که یک علامت مثبت (+
) را بعد از چیزی در یک عبارت باقاعده قرار میدهید، این علامت نشان میدهد که آن عنصر ممکن است یک بار یا بیشتر تکرار شود. بنابراین، /\d+/
به معنای مطابقت عبارت با تعداد یک یا بیشتر از کاراکترهای عددی خواهد بود.
console.log(/'\d+'/.test("'123'"));
// → true
console.log(/'\d+'/.test("''"));
// → false
console.log(/'\d*'/.test("'123'"));
// → true
console.log(/'\d*'/.test("''"));
// → true
{{index "* operator", asterisk}}
کاراکتر ستاره (*
) معنای مشابهی دارد با این تفاوت که به الگو اجازه میدهد تا صفر بار تکرار (نبودن کاراکتر) را هم شامل شود. اگر بعد از چیزی کاراکتر ستاره قرار گیرد، باعث میشود که الگو همیشه چیزی برای مطابقت پیدا کند – در صورتی که نتواند متنی برای مطابقت پیدا کند، با نبود آن عنصر مطابقت خواهد داد.
{{index "British English", "American English", "question mark"}}
استفاده از علامت سوال (?) در یک الگو به معنای ((اختیاری)) بودن است، یعنی ممکن است که آن عنصر نباشد یا یک بار حاضر باشد. در مثال پیش رو، کاراکتر u اختیاری است و میتواند باشد و در صورت نبودن هم الگو صدق خواهد کرد.
let neighbor = /neighbou?r/;
console.log(neighbor.test("neighbour"));
// → true
console.log(neighbor.test("neighbor"));
// → true
{{index repetition, [braces, "in regular expression"]}}
برای مشخص کردن این موضوع که یک الگو باید به تعداد دقیقی رخ دهد، میتوانید از کروشه استفاده کنید؛ به عنوان مثال، قرار دادن {4}
بعد از یک عنصر، باعث میشود که الگو انتظار داشته باشد آن عنصر دقیقا 4 مرتبه رخ داده باشد. همچنین میتوان یک بازه را نیز مشخص نمود: {2,4}
به این معنا است که این عنصر باید حداقل دو مرتبه و حداکثر چهار مرتبه رخ دهد.
{{id date_regexp_counted}}
اینجا نسخهی دیگر از الگوی تشخیص تاریخ و زمان را داریم که امکان تشخیص روز، ماه و ساعت به هر دو فرمت تک رقمی و دو رقمی را دارد. همچنین درک این الگو کمی راحتتر از الگوی پیشین است.
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,}
به معنای پنج یا بیشتر میباشد.
{{index ["regular expression", grouping], grouping, [parentheses, "in regular expressions"]}}
برای استفاده از یک عملگر مانند *
یا +
روی بیش از یک عنصر در آنِ واحد، باید از پرانتز استفاده کنید. از دید عملگرهایی که بعد از عبارتهای داخل پرانتز قرار میگیرند، هر عبارت محصور بین پرانتز به عنوان یک عنصر در نظر گرفته میشود.
let cartoonCrying = /boo+(hoo+)+/i;
console.log(cartoonCrying.test("Boohoooohoohooo"));
// → true
{{index crying}}
کاراکترهای +
اول و دوم فقط به o دوم از boo و hoo اعمال میشوند. کاراکتر + سوم به کل گروه (hoo+)
اعمال میشود و یک یا بیش از یک بار تکرار آن الگو را شامل میشود.
{{index "case sensitivity", capitalization, ["regular expression", flags]}}
کاراکتر i
که در انتهای عبارت مثال آمده است باعث میشود که عبارت باقاعده به بزرگی و کوچکی حروف حساس نباشد، یعنی کاراکتر B بزرگ هم در رشتهی ورودی تطبیق خواهد خورد، با وجود اینکه الگو خودش به حروف کوچک نوشته شده است.
{{index ["regular expression", grouping], "exec method", [array, "RegExp match"]}}
متد test
سادهترین راهی است که برای تطبیق یک عبارت باقاعده استفاده میشود. این متد فقط تطبیق و عدم تطبیق عبارت را مشخص میکند و دیگر هیچ. عبارات باقاعده همچنین متدی به نام exec
(به معنای اجرا) دارند که در صورت نبود تطبیق، مقدار null
را برمی گرداند و در صورت وجود تطبیق، شیئی شامل اطلاعاتی راجع به آن تولید میکند.
let match = /\d+/.exec("one two 100");
console.log(match);
// → ["100"]
console.log(match.index);
// → 8
{{index "index property", [string, indexing]}}
شیئی که از یک متد exec
برگردانده میشود خاصیتی به نام index
دارد که نقطه شروع تطبیق پیدا شده را در رشته به ما مینشان میدهد. علاوه بر آن، این شیء شبیه به (و در واقع یک) آرایهای از رشتهها است، که عنصر اولش رشتهای است که با الگو مطابقت داشته است – در مثال قبل، دنبالهای از ((اعداد)) که به دنبال آن بودیم.
{{index [string, methods], "match method"}}
مقدارهای رشتهای متدی به نام match
دارند که به شکل مشابهی عمل میکند.
console.log("one two 100".match(/\d+/));
// → ["100"]
{{index grouping, "capture group", "exec method"}}
زمانی که یک عبارت باقاعده شامل زیرعبارتهایی باشد که با پرانتز گروهبندی شدهاند، متنهایی که با آن گروهها مطابقت دارند نیز درون یک آرایه نمایش داده خواهد شد. تطبیق کامل همیشه در همان عنصر اول است. عنصر بعدی آرایه متعلق به بخشی است که توسط اولین گروه تطبیق یافته است (گروهی که پرانتز شروعش در عبارت اول آمده است)، سپس گروه دوم و الی آخر.
let quotedText = /'([^']*)'/;
console.log(quotedText.exec("she said 'hello'"));
// → ["'hello'", "hello"]
{{index "capture group"}}
زمانی که برای یک گروه تطبیقی در رشته پیدا نمیشود (به عنوان مثال، زمانی که بعد از گروه علامت سوال قرار گرفته باشد) موقعیت آن در آرایهی خروجی به صورت undefined
خواهد بود. به طور مشابه، اگر یک گروه چندین تطبیق داشته باشد، فقط آخرین آنها در آرایه قرار خواهد گرفت.
console.log(/bad(ly)?/.exec("bad"));
// → ["bad", undefined]
console.log(/(\d)+/.exec("123"));
// → ["123", "3"]
{{index "exec method", ["regular expression", methods], extraction}}
از قابلیت گروهها میتوان برای استخراج قسمتهای یک رشته استفاده کرد. به عنوان مثال، زمانی که فقط بودن یک تاریخ در یک رشته برای ما مهم نیست و قصد داریم تا آن را از دل آن استخراج کرده و شیئی حاوی آن بسازیم، میتوانیم با استفاده از پرانتز در الگوی ارقام، به طور مستقیم آن را در نتیجهی exec
مجزا کنیم.
اما ابتدا، یک فاصلهی کوتاه بگیریم و کمی در رابطه با روش از پیش تعریف شده برای نمایش مقادیر زمان و تاریخ در جاوااسکریپت صحبت کنیم.
{{index constructor, "Date class"}}
جاوااسکریپت کلاس استانداردی برای نمایش تاریخها – یا به عبارتی نقاطی در زمان – دارد. این کلاس Date
نامیده میشود. اگر با new
یک کلاس تاریخ ایجاد کنید، زمان و تاریخ فعلی را خواهید گرفت.
console.log(new Date());
// → Mon Nov 13 2017 16:19:11 GMT+0100 (CET)
{{index "Date class"}}
همچنین میتوانید یک شیء برای یک تاریخ مشخص ایجاد کنید.
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)
{{index "zero-based counting", [interface, design]}}
جاوااسکریپت از قراردادی استفاده میکند که در آن ماهها از صفر شروع میشوند (بنابراین ماه دسامبر برابر 11 خواهد شد)، اما روزها از یک شروع میشوند. این به نظر گیجکننده و احمقانه میرسد. پس دقت داشته باشید.
چهار آرگومان آخر (hours، minutes، seconds و milliseconds) اختیاری هستند و اگر مشخص نشوند با صفر مقداردهی میشوند.
{{index "getTime method"}}
برچسبهای ثبت زمان (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)
{{index "Date.now function", "Date class"}}
اگر به تابع سازندهی Date
یک آرگومان ارسال نمایید، این آرگومان به عنوان همان شمارش هزارم ثانیهها تفسیر میشود. میتوانید تعداد هزام ثانیههای لحظهی کنونی را با ایجاد یک شیء جدید Date
و فراخوانی متد getTime
روی آن یا با فراخوانی تابع Date.now
بدست بیاورید.
{{index "getFullYear method", "getMonth method", "getDate method", "getHours method", "getMinutes method", "getSeconds method", "getYear method"}}
اشیاء Date متدهایی مانند getFullYear
، getMonth
، getDate
، getHours
، getMinutes،
و getSeconds
را فراهم میکنند که بتوان اجزای یک تاریخ را به وسیلهی آنها استخراج کرد. در کنار متد getFullYear
، متدی به نام getYear
نیز وجود دارد، که سال را با کسر از 1900 تولید میکند (مثل 98
یا 119
) که تقریبا کاربردی ندارد.
{{index "capture group", "getDate method", [parentheses, "in regular expressions"]}}
با قراردادن پرانتز دور بخشهای عبارتی که به آن نیاز داریم، میتوانیم شیء تاریخ را از یک رشته ایجاد کنیم.
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)
{{index destructuring, "underscore character"}}
کاراکتر خط زیرین (_
) که در مثال به عنوان یک متغیر استفاده شده است، در اینجا استفادهای ندارد و فقط برای عبور از خانهی اول آرایهی تولیدی exec
استفاده شده است.
{{index matching, ["regular expression", boundary]}}
متاسفانه، متد getDate
همچنین تاریخهای غلطی مانند 00-1-3000 را از رشتهی "100-1-30000"
استخراج میکند. یک تطبیق ممکن است در هرجای رشته رخ بدهد، بنابراین در این مورد، از کاراکتر دوم این رشته شروع میشود و در کاراکتر یکی مانده به پایان، تمام میشود.
{{index boundary, "caret character", "dollar sign"}}
اگر بخواهیم تطبیق شامل کل رشته باشد، باید با استفاده از نشانگرهای ^
و $
این کار را انجام دهیم. کاراکتر ^
، شروع رشتهی ورودی را مشخص میکند، در حالیکه کاراکتر $
، این کار را برای پایان انجام میدهد. بنابراین /^\d+$/
رشتهای را تطبیق خواهد داد که کلا دارای یک یا بیش از یک رقم باشد، /^!/
شامل همهی رشتههایی میشود که با یک علامت تعجب شروع شده باشند، و /x^/
هیچ رشتهای را شامل نخواهد شد (نمیتوان یک کاراکتر x را قبل از کاراکتر شروع یک رشته تصور کرد).
{{index "word boundary", "word character"}}
اگر، از سوی دیگر، بخواهیم مطمئن شویم که تاریخ مورد نظر در مرزهای یک کلمه شروع و پایان مییابد، میتوانیم از نشانگر \b
استفاده کنیم. یک مرز کلمه میتواند شروع یا پایان یک رشته یا هر نقطهای در رشته باشد که یک کارکتر از نوع کلمه (حرف الفبا یا رقم مثل \w
) در یک سمت داشته باشد و یک کاراکتر غیرکلمهای در سمت دیگر داشته باشد.
console.log(/cat/.test("concatenate"));
// → true
console.log(/\bcat\b/.test("concatenate"));
// → false
{{index matching}}
توجه داشته باشید که یک نشانگر تعیین مرز (حدود) خود کاراکتری را تطبیق نمیدهد. این نشانگر فقط باعث میشود که عبارت باقاعده فقط زمانی تطبیق بخورد که یک شرط مشخص در نقطهای که نشانگر در الگو قرار گرفته برقرار باشد.
{{index branching, ["regular expression", alternatives], "farm example"}}
فرض کنید بخواهیم بدانیم که در یک رشتهی متنی عددی وجود دارد که بعد از آن یکی از کلمههای 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
{{index [parentheses, "in regular expressions"]}}
میتوان با استفاده از پرانتز بخشهایی از الگو که عملگر پایپ روی آنها اعمال میشود را محدود کرد، و نیز میتوان چندین عملگر پایپ را کنار هم قرار داد تا امکان انتخاب بین بیش از دو جایگزین را فراهم نمود.
{{index ["regular expression", matching], [matching, algorithm], "search problem"}}
از نظر مفهومی، زمانی که از متد exec
یا test
استفاده میکنید، موتور عبارت باقاعده به دنبال تطبیقی در رشتهی شما میگردد و سعی دارد این کار را با تطبیق دادن عبارت از ابتدای رشته انجام دهد، سپس از کاراکتر دوم، و همین طور ادامه میدهد تا اینکه تطبیقی پیدا کند یا به انتهای رشتهی داده شده برسد. در پایان رشته، یا اولین تطبیق ممکن را برمیگرداند یا جستجو با شکست روبرو میشود.
{{index ["regular expression", matching], [matching, algorithm]}}
موتور جاوااسکریپت برای انجام تطبیق، با عبارت باقاعده مانند یک نمودار جریان برخورد میکند. نمودار پایین برای عبارت مربوط به مثال حیوانات است:
{{figure {url: "img/re_pigchickens.svg", alt: "Visualization of /\b\d+ (pig|cow|chicken)s?\b/"}}}
{{index traversal}}
عبارت ما موفق به تطبیق خواهد شد اگر بتوانیم مسیری از سمت چپ نمودار به سمت راست آن بیابیم. موقعیت فعلی را در رشته حفظ میکنیم، و هر بار که به سمت یک مستطیل حرکت میکنیم، مطمئن میشویم که بخشی از رشته که بعد از موقعیت فعلی ما قرار دارد با آن مستطیل تطبیق دارد.
بنابراین اگر سعی کنیم که رشتهی "the 3 pigs"
را از موقعیت 4 تطبیق دهیم، پیشروی ما در نمودار چیزی شبیه به زیر میشود:
-
در موقعیت 4، یک مرز واژه وجود دارد، پس باید از اولین مستطیل عبور کنیم.
-
هنوز در موقعیت 4 هستیم، یک عدد میبینیم، پس میتوان از مستطیل بعدی نیز عبور کرد.
-
در موقعیت 5، یک مسیر به مستطیل دوم (رقم) بر میگردد، در حالیکه مسیر دیگر به سمت مستطیلی میرود که یک کاراکتر فضای خالی را نگه میدارد. در اینجا یک فضای خالی وجود دارد، نه یک رقم، پس باید از مسیر دوم برویم.
-
اکنون در موقعیت 6 (شروع رشتهی pigs) قرار داریم و در شاخهی سهراهی نمودار. cow و chiken را اینجا نمیبینیم اما pig را میبینیم پس به سراغ آن شاخه میرویم.
-
در موقعیت 9، بعد از شاخهی سه راهی، یک مسیر مستطیل s را نادیده میگیرد و مستقیما به مرز واژهی نهایی میرود، درحالیکه مسیر دیگر یک s را تطبیق میدهد. در اینجا ما یک کاراکتر s داریم نه یک مرز کلمه، پس به سراغ مستطیل s میرویم.
-
در موقعیت 10 (پایان رشته) قرار گرفتهایم و تنها میتوانیم یک مرز کلمه را تطبیق دهیم. پایان رشته به معنای یک مرز کلمه است؛ پس به سراغ آخرین مستطیل میرویم و با موفقیت این رشته را تطبیق میدهیم.
{{id backtracking}}
{{index ["regular expression", backtracking], "binary number", "decimal number", "hexadecimal number", "flow diagram", [matching, algorithm], backtracking}}
عبارت باقاعدهی /\b([01]+b|[\da-f]+h|\d+)\b/
یکی از اعداد زیر را تطبیق میدهد: یک عدد دودویی که بعد از آن یک b آمده باشد، یک عدد هگزادسیمال (عددی در مبنای 16 که دارای حروف a تا f است که برای اعداد 10 تا 15 استفاده میشوند) که بعد از آن یک h قرار گرفته، یا یک عدد دهدهی معمولی که هیچ پسوندی ندارد. نمودار زیر مربوط به این عبارت است:
{{figure {url: "img/re_number.svg", alt: "Visualization of /\b([01]+b|\d+|[\da-f]+h)\b/"}}}
{{index branching}}
در زمان تطبیق این عبارت، اغلب اینگونه میشود که علی رغم اینکه ممکن است ورودی دارای عدد دودویی نباشد، اما شاخهی بالایی (دودویی) انتخاب میشود. در زمان تطبیق رشتهی "103"
به عنوان مثال، فقط زمانی متوجه میشویم که در شاخهی اشتباهی قرار داریم که به کاراکتر 3 برسیم. رشته با عبارت تطبیق دارد اما نه لزوما با شاخهای که در حال حاضر در آن قرار گرفتهایم.
{{index backtracking, "search problem"}}
بنابراین تطبیقدهنده عقبگرد انجام میدهد. هنگام ورود به یک شاخه، موقعیت کنونی خودش را به خاطر میسپارد (در اینجا، در ابتدای رشته، درست قبل از اولین مستطیل مرز (محدوده) در نمودار) با این کار میتواند به عقب برگردد و اگر شاخهی فعلی جواب نداد به سراغ شاخهی دیگری برود. برای رشتهی "103"
بعد از مواجه با کاراکتر 3، به سراغ شاخهی اعداد هگزادسیمال میرود، که نتیجهای نخواهد داشت به این دلیل که بعد از عدد، هیج کاراکتر h ای وجود ندارد. بنابراین به سراغ شاخهی عدد دهدهی میرود. این شاخه انتخاب درستی است و یک تطبیق در پایان گزارش داده میشود.
{{index [matching, algorithm]}}
تطبیقگر به محض اینکه یک تطبیق کامل پیدا میکند متوقف میشود. معنای این کار این است که اگر چندین شاخهی بالقوه برای تطبیق یک رشته موجود باشد، فقط اولین شاخه (به ترتیبی که شاخه در عبارت منظم قرار گرفته است) استفاده میشود.
عقبگرد همچنین برای عملگرهای تکرار مثل +
و *
نیز اتفاق می افتد. اگر الگوی /^.*x/
را روی رشتهی "abcxe"
تطبیق دهید، قسمت .*
، ابتدا سعی میکند که تمام رشته را مصرف کند. موتور سپس متوجه میشود که نیاز به یک x دارد تا بتواند الگو را تطبیق دهد. چون هیچ x ای قبل از پایان رشته وجود ندارد، عملگر *
سعی میکند تا یک کاراکتر کمتر را تطبیق دهد. اما تطبیقگر، x را بعد از abcx
نیز پیدا نمیکند بنابراین عقبگرد دوباره اتفاق میافتد که موجب میشود عملگر ستاره فقط abc
را تطبیق دهد. اکنون یک x درست جایی که لازمش دارد پیدا میکند و آن را به عنوان یک تطبیق موفق از موقعیت 0 تا 4 گزارش میدهد.
{{index performance, complexity}}
میتوان عبارات باقاعدهای نوشت که در آنها تعداد زیادی عقبگرد انجام شود. این مشکل زمانی رخ میدهد که یک الگو میتواند یک ورودی را به شیوههای زیاد و متفاوتی تطبیق دهد. به عنوان مثال، اگر هنگام نوشتن یک عبارت باقاعده برای یک عدد دودویی حواسمان نباشد، ممکن است تصادفا چیزی شبیه /([01]+)+b/
بنویسیم.
{{figure {url: "img/re_slow.svg", alt: "Visualization of /([01]+)+b/",width: "6cm"}}}
{{index "inner loop", [nesting, "in regexps"]}}
اگر این الگو سعی کند که سریهای بلندی از صفر و یکها را بدون کاراکتر پایانی b تطبیق دهد، تطبیقگر ابتدا سراغ حلقهی درونی میرود تا اینکه تمامی اعداد تمام شوند. سپس متوجه میشود که کاراکتر b وجود ندارد، بنابراین یک مکان (موقعیت) عقبگرد میکند، یک بار به سراغ حلقهی بیرونی میرود و نتیجهای نمیگیرد، دوباره برای خروج از حلقهی درونی عقبگرد انجام میدهد. یعنی مقدار کار انجام شده به ازای هر کاراکتر دو برابر میشود. حتی برای چند دوجین کاراکتر، عمل تطبیق در واقع برای همیشه طول خواهد کشید.
{{index "replace method", "regular expression"}}
مقادیر رشتهای دارای متدی به نام replace
هستند که میتوان از آن برای جایگزینی بخشی از رشته با رشتهای دیگر استفاده کرد.
console.log("papa".replace("p", "m"));
// → mapa
{{index ["regular expression", flags], ["regular expression", global]}}
آرگومان اول این متد همچنین میتواند یک عبارت باقاعده باشد، که در این صورت، اولین تطبیق پیدا شده توسط عبارت باقاعده، با رشتهی مورد نظر جایگزین میشود. زمانی که گزینهی g
(سراسری) به عبارت باقاعده اضافه شود، به جای جایگزینی اولین مورد، تمامی تطبیقهای پیداشده در رشته، جایگزین خواهند شد.
console.log("Borobudur".replace(/[ou]/, "a"));
// → Barobudur
console.log("Borobudur".replace(/[ou]/g, "a"));
// → Barabadar
{{index [interface, design], argument}}
بهتر به نظر میرسید اگر گزینهی انتخاب بین جایگزینی همهی تطبیقها یا یک تطبیق، به شکل یک آرگومان مجزا برای متد replace
تعریف میشد یا اینکه متدی متفاوت برای آن در نظر گرفته میشد؛ مانند replaceAll
. اما از بد روزگار، این گزینه وابسته به خاصیتی در عبارت باقاعده میباشد.
{{index grouping, "capture group", "dollar sign", "replace method", ["regular expression", grouping]}}
قدرت اصلی استفاده از عبارات باقاعده به وسیلهی متد 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
. تطبیق کلی را میتوان با $&
مورد ارجاع قرار داد.
{{index [function, "higher-order"], grouping, "capture group"}}
میتوان یک تابع را به جای رشته به عنوان آرگومان دوم متد 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+
تطبیق خورده است – و آن را در صورتی که فقط یک و صفر باقی مانده باشد، تغییراتی میدهد.
{{index greed, "regular expression"}}
میتوان از متد 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
{{index "period character", "slash character", "newline character", "empty set", "block comment", "line comment"}}
قسمتی که قبل از عملگر یا (|) آمده است مطابق با دو کاراکتر اسلشی خواهد بود که میتواند بعد از آنها هر کاراکتری غیر از کاراکترهای خط جدید بیاید. بخشی که مربوط به توضیحات چندخطه میباشد کمی پیچیدهتر است. ما از [^]
(به معنای هر کاراکتر که در یک مجموعهی تهی از کاراکترها جا نمیگیرد) به عنوان روشی برای تطبیق همهی کاراکترها استفاده کردهایم. نمیتوانیم فقط از یک نقطه (.) برای این منظور در اینجا استفاده کنیم چراکه بلاکهای کامنت را میتوان در چند خط نوشت و کاراکتر نقطه کاراکترهای خطوط جدید را تطبیق نمیدهد.
اما خروجی خط آخر به نظر میرسد که دارای اشتباه باشد. چرا؟
{{index backtracking, greed, "regular expression"}}
قسمت [^]*
عبارت، همانطور که در قسمت عقبگرد توضیح دادم، در ابتدا تا آنجایی که میتواند تطبیق میدهد. اگر این کار منجر به این شود که بخش بعدی الگو شکست بخورد، تطبیقگر یک کاراکتر به عقب برگشته و از آن نقطه دوباره تلاش میکند. در مثال بالا، تطبیقگر ابتدا تلاش میکند تا کل رشتهی باقیمانده را تطبیق دهد سپس از آنجا به عقب برگردد. این موجب خواهد شد که یک نمونه از */
را بعد از اینکه چهار کاراکتر به عقب برمیگردد تطبیق دهد. این چیزی نیست که به دنبال آن بودیم – قصد ما این بود که یک توضیح را تطبیق دهیم، نه اینکه تا انتهای کدهای برنامه را برای پیدا کردن پایان آخرین بلاک توضیحات پیمایش کنیم.
به خاطر این عملکرد، به عملگرهای تکرار (+
, *
, ?
, و {}
) عملگرهای حریصانه می گوییم، به این معنا که تا جایی که میتوانند تطبیق میدهند بعد به عقب برمیگردنند. اگر بعد از آن ها یک علامت سوال قرار دهید (+?
,
*?
, ??
, {}?
)، دیگر حریص نخواهند بود و با حداقل تطبیق شروع میکنند، زمانی به تطبیق بیشتر میپردازند که الگوی باقیمانده با تطبیقی کوچکتر مطابقت نداشته باشد.
و این دقیقا آن چیزی است که در این مورد آن را میخواهیم. با تطبیق کوچکترین بازههایی از کاراکترها به وسیلهی ستاره که مارا به یک */
برساند، ما فقط یک بلاک توضیحات را انتخاب کردیم و نه چیز بیشتری را.
function stripComments(code) {
return code.replace(/\/\/.*|\/\*[^]*?\*\//g, "");
}
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1 + 1
زمانی که یک عملگر غیرحریصانه کارکرد بهتری برای مسئله دارد، اگر بدون دلیل و آگاهی از یک عملگر حریصانه استفاده کنید، ممکن است با باگهای زیادی در برنامه روبرو شوید. هنگام استفاده از یک عملگر تکرار، بهتر است ابتدا به سراغ نسخهی غیر حریصانهی آن بروید.
{{index ["regular expression", creation], "underscore character", "RegExp class"}}
در بعضی مواقع، ممکن است هنگام کدنویسی، الگوی مورد نیاز جهت تطبیق مشخص نباشد. فرض کنید که میخواهید به دنبال نام کاربر در یک متن بگردید و آن را توسط یک جفت کاراکتر خط زیرین محصور کنید تا بتوان آن را شناسایی کرد. به دلیل اینکه فقط در هنگام اجرای برنامه نام مورد نظر مشخص میشود، نمیتوان از روش استفاده از اسلش بهره برد.
اما میتوانید یک رشته تولید کنید و از سازندهی 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.
{{index ["regular expression", flags], ["backslash character", "in regular expressions"]}}
هنگام نوشتن نشانگرهای (مرز) \b
، باید از دو بکاسلش استفاده کنیم به این علت که آنها را در یک رشتهی نرمال مینویسیم نه یک عبارت باقاعده که توسط اسلش محصور شده است. آرگومان دوم سازندهی RegExp
مربوط به گزینههای مربوط به عبارت باقاعده است – در این مثال، "gi"
برای مشخص کردن سراسری بودن و غیرحساس بودن به حروف بزرگ و کوچک است.
اما چه میشود اگر نام کاربر مورد نظر "dea+hl[]rd"
باشد که متعلق یک نوجوان خورهی کامپیوتر است؟ این نام باعث میشود که یک عبارت باقاعدهی بیمعنا تولید شود که منجر به تطبیق نام کاربر نمیشود.
{{index ["backslash character", "in regular expressions"], [escaping, "in regexps"], ["regular expression", escaping]}}
راه حل این مشکل، اضافه کردن بکاسلش قبل از هر کاراکتری که معنای خاصی دارد است.
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.
{{index ["regular expression", methods], "indexOf method", "search method"}}
متد indexOf
که روی رشتهها کار میکرد را نمیتوان با یک عبارت باقاعده فراخواند. اما متد دیگری به نام search
وجود دارد که یک عبارت باقاعده را دریافت میکند. درست مانند indexOf
، این متد نیز اولین خانهی خروجی را به عبارتی که پیدا شد اختصاص میدهد و یا در صورت پیدا نکردن نتیجه، -1 را برمیگرداند.
console.log(" word".search(/\S/));
// → 2
console.log(" ".search(/\S/));
// → -1
متاسفانه، راهی برای مشخص کردن نقطهی شروع برای تطبیق وجود ندارد (شبیه کاری که میتوانیم با آرگومان دوم indexOf
انجام دهیم) که در صورت وجود کاربرد داشت.
{{index "exec method", "regular expression"}}
متد exec
نیز راهی مناسب برای شروع جستجو از یک موقعیت داده شده در یک رشته را پشتیبانی نمیکند. اما یک راه غیر سرراست برای این کار وجود دارد.
{{index ["regular expression", matching], matching, "source property", "lastIndex property"}}
اشیائی که از نوع عبارت باقاعده هستند دارای یک سری خاصیت میباشند. یکی از این خاصیتها source
است، که رشتهای که عبارت از آن تولید شده است را نگهداری میکند. یک خاصیت دیگر، lastIndex
است که در شرایط محدودی کنترل میکند که تطبیق بعدی از کجا شروع خواهد شد.
{{index [interface, design], "exec method", ["regular expression", global]}}
آن شرایط این است که عبارت باقاعده باید گزینههای سراسری (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
{{index "side effect", "lastIndex property"}}
اگر تطبیق با موفقیت انجام شد، فراخوانی 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
{{index bug}}
اگر از یک عبارت باقاعدهی مشترک برای چندین فراخوانی exec
استفاده کنیم این بهروزرسانیهای خودکار خاصیت lastIndex
میتواند مشکلساز باشد. عبارت باقاعدهی شما ممکن است تصادفا از اندیسی شروع شود که از فراخوانی قبلی به جا مانده باشد.
let digit = /\d/g;
console.log(digit.exec("here it is: 1"));
// → ["1"]
console.log(digit.exec("and now: 1"));
// → null
{{index ["regular expression", global], "match method"}}
یک اثر جالب توجه دیگر در صورت استفاده از گزینهی سراسری این است که باعث میشود کارکرد متد match
روی رشتهها، متفاوت باشد. زمانی که این متد با عبارتی سراسری فراخوانی شود، به جای اینکه آرایهای شبیه چیزی که از exec
برگردانده میشد تولید کند، متد match
تمامی تطبیقهای الگوی درون رشته را پیدا میکند و آرایهای حاوی تمام رشتههای تطبیق خورده تولید میکند.
console.log("Banana".match(/an/g));
// → ["an", "an"]
بنابراین با احتیاط سراغ عبارات باقاعدهی سراسری بروید. معمولا تنها مواردی که لازم است به سراغ آنها بروید هنگامی است که به فراخوانی متد replace
نیاز دارید و همچنین مواقعی که لازم است تا صراحتا از lastIndex
استفاده کنید.
{{index "lastIndex property", "exec method", loop}}
یکی از کارهای رایج این است که تمامی موارد رخداد یک الگو در رشته را در بدنهی حلقه پیمایش کنیم به شکلی که شیء تطبیق داده شده در دسترس ما باشد. برای اینکار میتوانیم از متدهای 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
{{index "while loop", ["= operator", "as expression"], [binding, "as state"]}}
این مثال از این واقعیت استفاده میکند که مقدار یک عبارت تخصیص (=
)، همان مقدار انتساب داده شده است. بنابراین با استفاده از match = number.exec(input)
به عنوان قسمت شرط دستور while
، تطبیق را در شروع هر تکرار حلقه اجرا میکنیم و نتیجهی آن را در یک متغیر ذخیره میکنیم، و هنگامی پیمایش حلقه را متوقف میکنیم که تطبیقی پیدا نشود.
{{id ini}}
{{index comment, "file format", "enemies example", "INI file"}}
برای به پایان رساندن این فصل، به سراغ مسئلهای میرویم که به دست عبارات باقاعده حل میشود. فرض کنید که در حال نوشتن برنامهای هستیم که به طور خودکار اطلاعاتی دربارهی دشمنانمان از سطح اینترنت جمع آوری میکند. (واقعا قرار نیست این برنامه را در اینجا بنویسیم، فقط بخشی را مینویسیم که فایل حاوی تنظیمات را میخواند. از این بابت متاسفم.) فایل تنظیمات به این شکل است:
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
{{index grammar}}
قوانین حاکم بر این فایل (که فرمتی بسیار رایج است و معمولا یک فایل INI نامیده میشود) به صورت زیر است:
-
خطوط خالی و خطهایی که با نقطهویرگول شروع میشوند صرف نظر میشوند.
-
خطوطی که بین
[
و]
محصور هستند یک بخش جدید را شروع میکنند. -
خطوطی که حاوی یک شناسهی عددی-حرفی هستند که بعد از آن کاراکتر
=
میآید، یک گزینه به تنظیمات بخش فعلی اضافه میکنند. -
هر چیز دیگری غیر از موارد بالا نامعتبر شناخته میشود.
وظیفهی ما این است که رشتهای شبیه این را به یک شیء تبدیل کنیم که خاصیتهایش رشتههای تنظیمات نوشته شده قبل از اولین بخش را نگهداری میکنند و زیرشیءهایش به بخشهایی تعلق دارند که هر زیرشیء تنظیمات یک بخش را در خود دارد.
{{index "carriage return", "line break", "newline character"}}
به دلیل اینکه این فرمت باید خط به خط پردازش شود، تقسیم فایل به خطوط مجزا شروع خوبی به نظر میرسد. ما متد split
را در فصل ? دیدیم. بعضی سیستم عاملها، به هر دلیلی، فقط از کاراکتر خط جدید برای جداسازی خطوط استفاده نمیکنند بلکه از یک کاراکتر بازگشت به ابتدای خط و بعد از آن کاراکتر خط جدید برای این کار استفاده میکنند ("\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"}}
{{index "parseINI function", parsing}}
کد بالا به این صورت عمل میکند که خط به خط فایل را پردازش کرده و یک شیء میسازد. خاصیتهای قسمت بالایی مستقیما درون شیء ذخیره میشوند، درحالیکه خاصیتهایی که در بخشها قرار دارند به صورت جداگانه در شیئی مختص هر بخش قرار میگیرند. متغیر section
به شیء بخش کنونی اشاره میکند.
دو نوع قابل توجه خط وجود دارد – سرتیترهای بخش یا خطوط خاصیتها. زمانی که یک خط معرف یک خاصیت معمولی است، در بخش فعلی ذخیره میشود. زمانی که معرف یک سرتیتر بخش است، یک شیء جدید برای بخش مورد نظر ایجاد میشود و section
به آن تخصیص مییابد.
{{index "caret character", "dollar sign", boundary}}
توجه داشته باشید که استفادهی مکرر از ^
و $
برای این است که مطمئن شویم عبارت تمام خط را تطبیق میدهد نه فقط بخشی از آن را. اگر از آنها استفاده نشود، کد در اکثر مواقع کار میکند اما برای بعضی ورودیها رفتار عجیبی از خود نشان دهد که ممکن است اشکال زدایی آن سخت باشد.
{{index "if keyword", assignment, ["= operator", "as expression"]}}
الگوی if (match = string.match(...))
شبیه به ترفندی است که از عبارت تخصیص به عنوان شرط while
استفاده کردیم. اغلب اطمینان ندارید که فراخوانی match
موفق خواهد شد، بنابراین میتوانید فقط درون یک دستور if که آن را آزمایش میکند به نتیجهی آن دسترسی داشته باشید. برای جلوگیری از شکستن زنجیرهی else if
، نتیجهی تطبیق را به متغیری اختصاص دادیم و بلافاصله آن تخصیص را به عنوان شرط دستور if
استفاده کردهایم.
{{index [parentheses, "in regular expressions"]}}
اگر یک خط، سرتیتر بخش یا یک خاصیت نباشد، تابع با استفاده از عبارت /^\s*(;.*)?$/
بررسی میکند که آیا این خط توضیح است یا خطی خالی. متوجه نحوهی کارکرد آن شدید؟ قسمتی که داخل پرانتز است توضیحات را تطبیق میدهد و علامت سوال ?
اطمینان حاصل میکند که خطوطی که فقط فضای خالی هستند شناسایی شوند. اگر خطی با هیچکدام از اشکال قابل انتظار تطبیق نخورد، تابع یک استثنا تولید میکند.
{{index internationalization, Unicode, ["regular expression", internationalization]}}
به دلیل اینکه پیادهسازی اولیه جاوااسکریپت بسیار ساده بوده است و این واقعیت که این شیوهی ساده محور بعدها به عنوان یک استاندارد رفتاری در نظر گرفته شد، عبارات باقاعده در جاوااسکریپت نسبتا برای کاراکترهای غیر انگلیسی، حرفی برای گفتن ندارند. به عنوان مثال، در عبارات باقاعده جاوااسکریپت، یک "کاراکتر کلمه" فقط شامل 26 حرف لاتین (حروف بزرگ و کوچک)، اعداد دهدهی، و به دلایلی کاراکتر خط زیرین میشود. چیزهایی مثل é یا β که قطعا کاراکتر کلمه محسوب میشوند توسط \w
تطبیق نمیخورند (و با \W
تطبیق میخورند، دستهی کاراکترهای غیر کلمه).
{{index [whitespace, matching]}}
به خاطر یک اتفاق نامعلوم در گذشته، \s
(فضای خالی) این مشکل را ندارد و همهی کاراکترهایی که استاندارد یونیکد به عنوان فضای خالی درنظر میگیرد را شامل میشود، مثل کاراکترهایی از قبیل نیمفاصله و جداکننده حروف صدادار در زبان مغولی.
مشکل دیگر این است که به طور پیش فرض عبارات باقاعده روی واحدهای کد عمل میکنند؛ نه روی کاراکترهای واقعی؛ همانطور که در فصل ? بحث شد. معنای آن این است که با کاراکترهایی که از دو واحد کد تشکیل شدهاند به شکل نامشخصی رفتار میشود.
console.log(/🍎{3}/.test("🍎🍎🍎"));
// → false
console.log(/<.>/.test("<🌹>"));
// → false
console.log(/<.>/u.test("<🌹>"));
// → true
مشکل اینجاست که 🍎 در خط اول به عنوان دو واحد کد شناخته میشود، و {3}
فقط به واحد دوم اعمال میشود. به طور مشابه، عملگر نقطه فقط یک واحد کد را میشناسد نه دو واحدی که ایموجی گل رز را میسازند.
برای اینکه عبارت باقاعده این گونه کاراکترها را در نظر بگیرد باید گزینهی u
(یونیکد) را استفاده کنید. متاسفانه به صورت پیشفرض این اشکال وجود خواهد داشت چون تغییر آن ممکن است مشکلاتی را برای کدهای نوشته شده از قبل که به این رفتار وابستگی دارند به وجود بیاورد.
{{index "character category", [Unicode, property]}}
اگرچه این قضیه به تازگی استاندارد شده است، و در هنگام نوشتن این کتاب، هنوز به طور گسترده از آن پشتیبانی نمیشود، میتوان از \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
.
{{id summary_regexp}}
عبارات باقاعده اشیائی هستند که الگوها را در رشتهها نشان میدهند. این عبارات از زبانی مخصوص به خود برای بیان این الگوها استفاده میکنند.
{{table {cols: [1, 5]}}}
| /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
حالت یونیکد را فعال میکند که مشکلات مربوط به کاراکترهایی که دو واحد کد اشغال میکنند را برطرف میکند.
عبارتهای باقاعده مانند چاقوی تیزی هستند که دستهی نامناسبی دارند. بعضی از کارها را به شدت ساده میکنند اما زمانی که به مسائل پیچیده اعمال میشوند میتوانند به سرعت غیر قابل کنترل شوند. بخشی از فرهنگ صحیح استفاده از عبارات باقاعده این است که برای چیزهایی که به روشنی به وسیلهی آنها قابل بیان نیستند به سراغشان نرویم.
{{index debugging, bug}}
تقریبا غیر قابل اجتناب است که در حین انجام تمرینهای این فصل، با دیدن بعضی از رفتارهای پیچیدهی عبارات باقاعده، دچار سردرگمی و ناامیدی نشوید. گاهی اوقات بهتر است که عبارتتان را در ابزارهای آنلاینی مثل https://debuggex.com وارد کنید تا ببینید تجسم عبارتتان با آنچه در نظر داشتهاید ارتباط دارد یا خیر و با توجه به واکنش آن رشتههای ورودی متفاوتی را آزمایش کنید.
{{index "program size", "code golf", "regexp golf (exercise)"}}
گلف کد اصطلاحی است که برای تلاش نوشتن برنامهای با حداقل کاراکتر استفاده میشود. به طور مشابه regexp golf، تمرین نوشتن کوتاهترین عبارت باقاعدهای است که برای تطبیق یک الگوی داده شده میتوان نوشت و فقط همان الگو باید تطبیق بخورد.
{{index boundary, matching}}
برای هر یک از آیتمهای زیر، یک عبارت باقاعده بنویسید و آزمایش کنید هر کدام از زیررشتههای داده شده در آن وقوع دارد یا خیر. عبارت باقاعدهای که مینویسید باید فقط رشتههایی را تطبیق دهد که یکی از زیر رشتههای داده شده را داشته باشند. نیازی نیست نگران مرزهای کلمات باشید مگر اینکه به طور صریح ذکر شده باشد. وقتی عبارت باقاعدهی شما به طور صحیح کار کرد، ببینید میتوانید آن را کوتاهتر بنویسید؟
- car و cat
- pop و prop
- ferret, ferry, و ferrari
- هر کلمهای که با ious پایان پذیرد
- یک کاراکتر فضای خالی که بعد از نقطه، ویرگول، دونقطه، یا نقطهویرگول بیاید
- کلمهای که از شش حرف بیشتر باشد
- یک کلمه بدون داشتن حرف e (یا E)
به جدولی که در خلاصه فصل آمده است برای کمک گرفتن رجوع کنید. هر راه حل را با چندین رشتهی آزمایشی بررسی کنید.
{{if interactive
// 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}'`);
}
}
if}}
{{index "quoting style (exercise)", "single-quote character", "double-quote character"}}
تصور کنید که یک داستان نوشته شده دارید و از علامت نقل قول تکی در طول کتاب برای مشخص کردن دیالوگها استفاده کردهاید. اکنون قصد دارید که همهی علامتهای تکی نقل قول را با علامتهای جفتی عوض کنید و حواستان هم باشد که علامتهای نقل قول تکی که در اختصارهایی مثل aren’t آمدهاند را عوض نکنید.
{{index "replace method"}}
به الگویی فکر کنید که این دو نوع نقل قول را تمییز دهد و از replace
برای جایگزینی صحیح استفاده کنید.
{{if interactive
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."
if}}
{{hint
{{index "quoting style (exercise)", boundary}}
روشنترین راه حل برای این مسئله این است که فقط نقلقولهایی را جایگزین کنید که حداقل در یک سمت آن یک غیرکلمه قرار داشته باشد مثل /\W'|'\W/
. اما همچنین لازم است تا شروع و پایان خط را هم در نظر داشته باشید.
{{index grouping, "replace method", [parentheses, "in regular expressions"]}}
علاوه بر این، باید اطمینان حاصل کنید که جایگزینی شامل کاراکترهایی که توسط \W
تطبیق میخورند هم باشد تا از قلم نیفتند. این کار را میتوان با قرار دادن آنها درون پرانتز و استفاده از گروههایشان در رشتهی جایگزینی ($1
, $2
) انجام داد. گروههایی که تطبیق نمی خورند با چیزی جایگزین نمیشوند.
hint}}
{{index sign, "fractional number", [syntax, number], minus, "plus character", exponent, "scientific notation", "period character"}}
عبارتی بنویسید که فقط اعداد سبک جاوااسکریپت را تطبیق دهد. عبارت باید علامت منفی یا مثبت را در جلوی عدد به صورت اختیاری پشتیبانی کند، همچنین نقطهی ممیز و نماد توان - 5 5e-3
یا 1E10
- را دوباره با علامت اختیاری جلوی توان پشتیبانی کند. همچنین توجه داشته باشید که لازم نیست که بعد از نقطهی ممیز حتما رقم بیاید اما نباید عدد فقط شامل یک نقطهی تنها باشد. بنابراین.5
و 5.
اعدادی معتبر در جاوااسکریپت محسوب میشوند اما یک نقطهی تنها این طور نیست.
{{if interactive
// 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}'`);
}
}
if}}
{{hint
{{index ["regular expression", escaping], ["backslash character", "in regular expressions"]}}
ابتدا، فراموش نکنید که بکاسلش را در جلوی نقطه قرار دهید.
تطبیق علامت اختیاری در جلوی یک عدد، همچنین جلوی یک توان، را میتوان با استفاده از
[+\-]?
یا (\+|-|)
انجام داد. (مثبت، منفی یا هیچی)
{{index "pipe character"}}
بخش پیچیدهتر این تمرین این است که چهطور هر دوی "5."
و ".5"
را بدون تطبیق خوردن "." تطبیق بزنید. برای اینکار، یک راه خوب این است که از | برای جداسازی دو حالت استفاده شود - یک یا دو رقم که ممکن است با یک نقطه و صفر یا ارقام بیشتر ادامه یابد یا نقطهای که به همراه یک را چندین رقم بیاید.
{{index exponent, "case sensitivity", ["regular expression", flags]}}
سرانجام، برای اینکه e را غیرحساس به بزرگی/کوچکی حروف داشته باشید، اضافه کردن گزینهی i
به انتهای عبارت باقاعده یا استفاده از [eE]
مشکل را حل خواهد کرد.
hint}}