রুবি ২.০ ২০১৩-এ যে অনেক নতুন ফিচার লाये 그중 যে একটি मुझे সবচেয়ে কম লাগলো সেটি হচ্ছে নতুন রেগুলার এক্সপ্রেশন ইঞ্জিন, Onigmo। রেগুলার এক্সপ্রেশন তো রেগুলার এক্সপ্রেশন—রুবি কীভাবে ইমপ্লিমেন্ট করে সেটি কেন আমি চিন্তা করব?
як 밝혀রকি, Onigmo regex ইঞ্জিনের হাতে কিছু মজার ট্রিক আছে যার মধ্যে রেগুলার এক্সপ্রেশনের ভেতরে কন্ডিশনাল ব্যবহার করার ক্ষমতা।
এই পোস্টে আমরা regex conditionals-এ গভীরে যাব, রুবির ইমপ্লিমেন্টেশনের কুইর্কস শিখব, এবং রুবির সীমাবদ্ধতা ওয়ার্কঅরাউন্ড করার কিছু ট্রিক আলোচনা করব। শুরু করা যাক!
গ্রুপ ও ক্যাপচারিং
রেগুলার এক্সপ্রেশনে কন্ডিশনাল বুঝতে হলে আপনাকে গ্রুপিং এবং ক্যাপচারিং বুঝতে হবে।
কল্পনা করুন আপনার কাছে ইউএস সিটির একটি লিস্ট আছে:
Fayetteville, AR
Seattle, WA
আপনি সিটি নাম এবং স্টেট আব্রিভিয়েশন আলাদা করতে চান। এক উপায় হচ্ছে একাধিক ম্যাচ করা:
PLACE = "Fayetteville, AR"
# সিটি: কমা ছাড়া যেকোনো ক্যারেক্টার ম্যাচ
PLACE.match(/[^,]+/) # => #<MatchData "Fayetteville">
# সেপারেটর: কমা এবং অপশনাল স্পেস ম্যাচ
PLACE.match(/, */) # => #<MatchData ", ">
# স্টেট: স্ট্রিংয়ের শেষে ২-লেটার কোড ম্যাচ
PLACE.match(/[A-Z]{2}$/) # => #<MatchData "AR">
এটি কাজ করে, কিন্তু খুব verbose। গ্রুপ ব্যবহার করে আপনি শুধু একটি রেগুলার এক্সপ্রেশনে সিটি এবং স্টেট দুটোই ক্যাপচার করতে পারেন।
চলুন উপরের রেগুলার এক্সপ্রেশনগুলো কম্বাইন করি, এবং প্রতিটি সেকশনকে পারেনথেসিস দিয়ে ঘেরে দিই। পারেনথেসিস হচ্ছে রেগুলার এক্সপ্রেশনে জিনিসগুলো গ্রুপ করার উপায়।
PLACE = "Fayetteville, AR"
m = PLACE.match(/([^,]+)(, *)([A-Z]{2})/)
# => #<MatchData "Fayetteville, AR" 1:"Fayetteville" 2:", " 3:"AR">
আপনি দেখতে পাচ্ছেন, উপরের এক্সপ্রেশন সিটি এবং স্টেট দুটোই ক্যাপচার করে। আপনি MatchData-কে অ্যারের মতো ট्रीট করে সেগুলো এক্সেস করতে পারেন:
m[1] # => "Fayetteville"
m[3] # => "AR"
গ্রুপিং-এর সাথে সমস্যা, যেভাবে উপরে করা হয়েছে, সেটি হচ্ছে ক্যাপচার্ড ডেটা অ্যারেতে যায়। যদি এর অ্যারেতে পজিশন পরিবর্তন হয়, তবে আপনাকে কোড আপডেট করতে হবে অথবা আপনি একটা বাগ ইন্ট্রোডুস করলেন।
উদাহরণস্বরূপ, আমরা সিদ্ধান্ত নিতে পারি ", " ক্যারেক্টারগুলো ক্যাপচার করা বোকামি। তাই আমরা সেই অংশের চারদিকের পারেনথেসিস সরিয়ে দিই:
m = PLACE.match(/([^,]+), *([A-Z]{2})/)
# => #<MatchData "Fayetteville, AR" 1:"Fayetteville" 2:"AR">
m[3] # => nil
কিন্তু এখন m[3] আর স্টেট ধারণ করে না—বাগ সিটি।
নেমড গ্রুপ
আপনি রেগুলার এক্সপ্রেশন গ্রুপগুলোকে অনেক বেশি সেম্যান্টিক করতে পারেন তাদের নাম দিয়ে। সিনট্যাক্স প্রায় একই যা আমরা 아까 করেছি। আমরা রেগেক্সকে পারেনথেসিসে ঘেরে দিই, এবং নাম স্পেসিফাই করি এইরকম:
/(?<groupname>regex)/
আমরা যদি আমাদের সিটি/স্টেট রেগুলার এক্সপ্রেশনে এটি প্রয়োগ করি, পাই:
m = PLACE.match(/(?<city>[^,]+), *(?<state>[A-Z]{2})/)
# => #<MatchData "Fayetteville, AR" city:"Fayetteville" state:"AR">
এবং আমরা ক্যাপচার্ড ডেটা এক্সেস করতে পারি MatchData-কে হ্যাশের মতো ট्रीট করে:
m[:city] # => "Fayetteville"
কন্ডিশনাল
রেগুলার এক্সপ্রেশনে কন্ডিশনালের ফর্ম হচ্ছে /(?(A)X|Y)/। এখানে কিছু ভ্যালিড ব্যবহার:
# যদি A true হয়, তবে এক্সপ্রেশন X ইভ্যালুয়েট করুন, অন্যথায় Y
/(?(A)X|Y)/
# যদি A true হয়, তবে X
/(?(A)X)/
# যদি A false হয়, তবে Y
/(?(A)|Y)/
আপনার কন্ডিশন, A-র জন্য দুটি সবচেয়ে কমন অপশন:
- একটি নামযুক্ত বা নম্বরযুক্ত গ্রুপ ক্যাপচার হয়েছে কিনা?
- একটি লুক-অরাউন্ড true ইভ্যালুয়েট হয় কিনা?
চলুন দেখি কীভাবে ব্যবহার করতে হয়:
একটি গ্রুপ ক্যাপচার হয়েছে কিনা?
একটি গ্রুপের উপস্থিতি চেক করতে, ?(n) সিনট্যাক্স ব্যবহার করুন, যেখানে n হচ্ছে একটি ইন্টিজার, বা গ্রুপের নাম <> বা '' দিয়ে ঘেরা।
# গ্রুপ নম্বর ১ ক্যাপচার হয়েছে কিনা?
/(?(1)foo|bar)/
# "mygroup" নামের গ্রুপ ক্যাপচার হয়েছে কিনা?
/(?(<mygroup>)foo|bar)/
উদাহরণ
কল্পনা করুন আপনি ইউএস টেলিফোন নম্বর পার্স করছেন। এই নম্বরগুলোর একটি তিন-ডিজিট এরিয়া কোড থাকে যা অপশনাল যদি না নম্বর ১ দিয়ে শুরু হয়।
1-800-555-1212 # বৈধ
800-555-1212 # বৈধ
555-1212 # বৈধ
1-555-1212 # অবৈধ!!
আমরা একটি কন্ডিশনাল ব্যবহার করে এরিয়া কোডকে শুধু তখন রিকোয়ার্ড করতে পারি যখন নম্বর ১ দিয়ে শুরু হয়।
# এই রেগুলার এক্সপ্রেশন জটিল দেখাচ্ছে, কিন্তু এটি সিম্পল পিস들 বানানো
# `^(1-)?` স্ট্রিং কি "1-"- দিয়ে শুরু হয়? যদি হয়, গ্রুপ ১-এ ক্যাপচার করুন
# `(?(1)` গ্রুপ वन-এ কী ক্যাপচার হয়েছে?
# `\d{3}-` যদি হয়, তিন ডিজিট এবং একটি ড্যাশের রিকোয়ার্ড ম্যাচ (এরিয়া কোড)
# `|(\d{3}-)?` যদি না হয়, তিন ডিজিট এবং ড্যাশের অপশনাল ম্যাচ (এরিয়া কোড)
# `\d{3}-\d{4}` ফোন নম্বরের বাকি অংশ ম্যাচ করুন, যা সবসময় রিকোয়ার্ড।
re = /^(1-)?(?(1)\d{3}-|(\d{3}-)?)\d{3}-\d{4}/
"1-800-555-1212".match(re) #=> #<MatchData "1-800-555-1212" 1:"1-" 2:nil>
"800-555-1212".match(re) #=> #<MatchData "800-555-1212" 1:nil 2:"800-">
"555-1212".match(re) #=> #<MatchData "555-1212" 1:nil 2:nil>
"1-555-1212".match(re) #=> nil
সীমাবদ্ধতা
গ্রুপ-ভিত্তিক কন্ডিশনাল ব্যবহার করার একটি সমস্যা হচ্ছে একটি গ্রুপ ম্যাচ করা সেই ক্যারেক্টারগুলোকে "কনজিউম" করে। সেই ক্যারেক্টারগুলো কন্ডিশনাল দ্বারা ব্যবহার করা যায় না।
উদাহরণস্বরূপ, নিচের কোড চেষ্টা করে ১০০ ম্যাচ করতে যদি "USD" টেক্সট থাকে, কিন্তু ফেলে:
"100USD".match(/(USD)(?(1)\d+)/) # nil
পার্ল এবং কিছু অন্যান্য ল্যাঙ্গুয়েজে, আপনি আপনার কন্ডিশনালে একটি লুক-অহেড স্টেটমেন্ট যোগ করতে পারেন। এটি আপনাকে স্ট্রিংয়ের যেকোনো জায়গা থেকে টেক্সটের ভিত্তিতে কন্ডিশনাল ট্রিগার করতে দেয়। কিন্তু রুবিতে এটি নেই, তাই আমাদের একটু ক্রিএটিভ হতে হবে।
লুক-অরাউন্ড
সৌভাগ্যtois, আমরা রুবির regex কন্ডিশনালের সীমাবদ্ধতা ওয়ার্কঅরাউন্ড করতে পারি লুক-অরাউন্ড এক্সপ্রেশন অপব্যবহার করে।
লুক-অরাউন্ড কী?
সাধারণত, রেগুলার এক্সপ্রেশন পার্সার আপনার স্ট্রিংয়ের শুরু থেকে শেষ পর্যন্ত যেতে থাকে ম্যাচ খুঁজে বের করার জন্য। এটি একটি ওয়ার্ড প্রসেসরে কার্সর বাম থেকে ডানে সরানোর মতো।
লুক-অহেড এবং লук-বিহাইন্ড এক্সপ্রেশন কিছুটা ভিন্নভাবে কাজ করে। এগুলো আপনাকে স্ট্রিং ইনস্পেক্ট করতে দেয় কোনো ক্যারেক্টার কনজিউম ছাড়া। সেগুলো হলে, কার্সর ঠিক সেটি জায়গায় থাকে যেখানে শুরুতে ছিল।
লুক-অরাউন্ডে মাস্টার होनेর জন্য Rexegg-এর গাইড দেখুন।
সিনট্যাক্স এইরকম:
| টাইপ | সিনট্যাক্স | উদাহরণ |
|---|---|---|
| লুক অহেড | (?=query) |
\d+(?= dollars) "100 dollars"-এ 100 ম্যাচ করে |
| নেগেটিভ লুক অহেড | (?!query) |
\d+(?! dollars) 100 ম্যাচ করে যদি এর পেছনে "dollars" না থাকে |
| লুক বিহাইন্ড | (?<=query) |
(?<=lucky )\d "lucky 7"-এ 7 ম্যাচ করে |
| নেগেটিভ লুক বিহাইন্ড | (?<!query) |
(?<!furious )\d "lucky 7"-এ 7 ম্যাচ করে |
লুক-অরাউন্ড দুর্নীত করে কন্ডিশনাল উন্নত
আমাদের কন্ডিশনালে, আমরা শুধু তাদের অস্তিত্ব কোয়েরি করতে পারি যেসব গ্রুপ ইতিমধ্যে সেট হয়েছে। সাধারণত, এর মানে গ্রুপের কন্টেন্ট কনজিউম হয়েছে এবং কন্ডিশনালের জন্য উপলব্ধ নেই।
কিন্তু আপনি একটি লুক-অহেড ব্যবহার করে একটি গ্রুপ সেট করতে পারেন কোনো ক্যারেক্টার কনজিউম ছাড়া! মাইন্ড ব্লো হয়েছে কিনা?
ইতিমধ্যে যে কোডটি কাজ করতো না সেটি মনে করুন?
"100USD".match(/(USD)(?(1)\d+)/) # nil
আমরা যদি এটিকে মডিফাই করি গ্রুপ ক্যাপচার করার জন্য একটি লুক-অহেডে, এটি হঠাৎ কাজ করছে:
"100USD".match(/(?=.*(USD))(?(1)\d+)/)
# => #<MatchData "100" 1:"USD">
আসুন কোয়েরিটি ব্রেকডাউন করি এবং দেখি কী হচ্ছে:
(?=.*(USD))লুক-অহেড ব্যবহার করে, "USD" টেক্সট স্ক্যান করুন এবং গ্রুপ ১-এ ক্যাপচার করুন(?(1)যদি গ্রুপ ১ অস্তিত্ব থাকে\d+তবে এক বা অধিক নম্বর ম্যাচ করুন
খুব চমৎকার, কি না?