কবুল করছি: আমি রেগুলার এক্সপ্রেশন (regex) নিয়ে কাজ করা বিশেষ পছন্দ করি না। আমি সবসময়ই এটা ব্যবহার করি, কিন্তু /^foo.*$/ চেয়ে জটিল যেকোনো কিছু আমাকে থেমে চিন্তা করতে বাধ্য করে। নিশ্চিতভাবে কিছু মানুষ রyek্সpressions like \A(?=\w{6,10}\z)(?=[^a-z]*[a-z])(?=(?:[^A-Z]*[A-Z]){3}) এক নজরে বুঝতে পারেন, কিন্তু আমার কাছে এটা কয়েক মিনিট গুগলিং এবং দুঃখের বিষয়। রুবি পড়ার থেকে এর পার্থক্য বড়।
আপনি যদি কৌতুহলী হন, উপরের উদাহরণটি regex lookaheads-এ関する একটি আর্টিকেল থেকে নেওয়া।
পরিস্থিতি
Honeybadger-এ আমি বর্তমানে আমাদের সার্চ UI উন্নত করছি। অনেক সার্চ সিস্টেমের মত, আমাদেরও একটি সিম্পল কুয়েরি ল্যাঙ্গুয়েজ রয়েছে। আমার পরিবর্তনগুলো করার আগে, আপনি যদি কাস্টম ডেট রেঞ্জের জন্য সার্চ করতে চান, তবে আপনাকে ম্যানুয়ালি এমন একটি কুয়েরি টাইপ করতে হতো:
occurred:[2017-06-12T16:10:00Z TO 2017-06-12T17:10:00Z]
যন্ত্রণা!
নতুন সার্চ UI-তে, আমরা ডিটেক্ট করতে চাই আপনি কখন ডেট-রিলেটেড কুয়েরি টাইপ करना শুরু করলেন এবং একটি হেল্পফুল ডেটপিকার পপ আপ করব। এবং অবশ্যই, ডেটপিকার শুধু শুরু। آخرে আমরা কনটেক্সট-সেনসিটিভ হিন্টিংকে আরও انواع সার্চ টার্ম কভার করতে এক্সপ্যান্ড করব। কিছু উদাহরণ:
assigned:jane@email.com context.user.id=100 resolved:false ignored:false occurred:[ params.article.title:"Starr's parser post" foo:'ba
আমাকে এই স্ট্রিংগুলো টোকেনাইজ করতে হবে এমনভাবে যে:
- হোয়াইটস্পেস টোকেন আলাদা করে, কেবল '' বা "" বা [] দিয়ে ঘেরা থাকলে না
- আনকোটেড হোয়াইটস্পেস নিজের টোকেন হবে
- আমি
tokens.join("")রান করে ইনপুট স্ট্রিংটি এক্সাক্টলি রিক্রিয়েট করতে পারব
উদাহরণস্বরূপ:
tokenize(%[params.article.title:"Starr's parser post" foo:'ba]) => ["params.article.title:\"Starr's parser post\"", " ", "foo:'ba"]
রেগুলার এক্সপ্রেশন ব্যবহার
আমার প্রথম ধারণা ছিল একটি ক্যাপচারিং রেগুলার এক্সপ্রেশন ব্যবহার করে একটি বৈধ টোকেন কেমন দেখাবে সেটি ডিফাইন করা, তারপর String#split ব্যবহার করে স্ট্রিংকে টোকেনগুলোর মধ্যে ভাগ করা। এটা আসলে একটি কুল ট্রিক:
# রyek্সএক্সপ্রেশনের প্যারেনথেসিস মানে সেপারেটরটিও অ্যারেতে যোগ হবে
"foo bar baz".split(/(foo|bar|baz)/) => ["", "foo", " ", "bar", " ", "baz"]
এটা আশা দায়ক দেখাচ্ছিল, வိယрд এমপটি স্ট্রিংস থাকা সত্ত্বেও। কিন্তু আমার রিয়েল-ওয়ার্ল্ড রyek্সপ্রেশন অনেক জটিল। আমার প্রথম ড্রাফ্ট দেখতে এরকম ছিল:
/ (
# ক্যাপচার গ্রুপ যাতে split ম্যাচিং এবং নন-ম্যাচিং স্ট্রিং দুটোই ইনক্লুড করে
(?:
# কির প্রথম ক্যারেক্টার, যা
(?!\s)[^:\s"'\[]{1}
# ..যেকোনো বৈধ "কি" ক্যারেক্টার যেটি হোয়াইটস্পেস দ্বারা প্রসেড করা নেই
|^[^:\s"'\[]{0,1}
# ..অথবা লাইনের শুরুতে যেকোনো বৈধ "কি" ক্যারেক্টার
)
[^:\s"'\[]* # "কি" এর বাকি ক্যারেক্টারগুলো
: # একটি কলন
(?:
# "ভ্যালু" ক্যারেক্টারগুলো, যারা
'[^']+'
# ..সিঙ্গেল কোট দিয়ে ঘেরা যেকোনো কিছু
| "[^"]+"
# ..অথবা ডবল কোট দিয়ে ঘেরা যেকোনো কিছু
| \[\S+\sTO\s\S+\]
# ..অথবা [x TO y] এর মত যেকোনো কিছু
| [^\s"'\[]+
# ..অথবা হোয়াইটস্পেস বা স্পেশাল ক্যারেক্টার না থাকা যেকোনো স্ট্রিং
)
) /xi
এটার সাথে কাজ করলে আমার ভয়াবহ লাগল। প্রতিবার আমি একটি এজ কেস পেতাম, রyek্সপ্রেশন আপডেট করতে হতো, যা এটাকে আরও জটিল করে দিতো। উপরন্তু, এটি জাভাস্ক্রিপ্টেও কাজ করতে হতো, তাই নেগেটিভ লুকবিহাইন্ডের মত কিছু ফিচার উপলব্ধ ছিল না।
...প্রায় এই সময়ে আমাকে এই সবকারের অবমাননাগুলো ধাক্কা দিল। আমি যে রyek্সপ্রেশন অ্যাপ্রোচটি ব্যবহার করছিলাম, সেটি স্ক্র্যাচ থেকে একটি সিম্পল পারসার লিখার চেয়ে অনেক বেশি জটিল।
একটি পারসারের অ্যানাটমি
আমি এক্সপার্ট নই, কিন্তু সিম্পল পারসারগুলো সিম্পল। তারা শুধু এই কাজগুলো করে:
- একটি স্ট্রিংয়ে ক্যারেক্টার বাই ক্যারেক্টার এগিয়ে
- প্রতিটি ক্যারেক্টারকে একটি বাফারে অ্যাপেন্ড করা
- যখন একটি টোকেন-সেপারেটিং কন্ডিশন مواجه হয়, বাফারটিকে একটি অ্যারেতে সেভ করে খালি করা।
এটি জানলে, আমরা একটি সিম্পল পারসার সেটআপ করতে পারি যা স্ট্রিংকে হোয়াইটস্পেস দিয়ে স্প্লিট করে। এটি মোটামুটি "foo bar".split(/(\s+)/)-এর সমতুল্য।
class Parser
WHITESPACE = /\s/
NON_WHITESPACE = /\S/
def initialize
@buffer = []
@output = []
end
def parse(text)
text.each_char do |c|
case c
when WHITESPACE
flush if previous.match(NON_WHITESPACE)
@buffer << c
else
flush if previous.match(WHITESPACE)
@buffer << c
end
end
flush
@output
end
protected
def flush
if @buffer.any?
@output << @buffer.join("")
@buffer = []
end
end
def previous
@buffer.last || ""
end
end
puts Parser.new().parse("foo bar baz").inspect
# আউটপুট ["foo", " ", "bar", " ", "baz"]
এটা আমার চাওয়ার দিকে একটি ধাপ, কিন্তু এতে কোট এবং ব্র্যাকেটের সাপোর্ট নেই। সৌভাগ্যবশত, সেটি যোগ করতে কয়েক লাইন কোডই লাগবে:
def parse(text)
surround = nil
text.each_char do |c|
case c
when WHITESPACE
flush if previous.match(NON_WHITESPACE) && !surround
@buffer << c
when '"', "'"
@buffer << c if !surround
surround = c
elsif surround == c
flush
surround = nil
end
when "["
@buffer << c
surround = c if !surround
when "]"
@buffer << c
if surround == "["
flush
surround = nil
end
else
flush() if previous().match(WHITESPACE) && !surround
@buffer << c
end
end
flush
@output
end
এই কোডটি আমার রyek্সপ্রেশন-ভিত্তিক অ্যাপ্রোচের চেয়ে একটু বড় হলেও, এটি অনেক বেশি স্পষ্ট।
শেষ চিন্তা
হয়তো এমন একটি রyek্সপ্রেশন আছে যা আমার ইউজ কেসের সাথে ফাইন কাজ করত। ইতিহাস যেন গাইড, সেটি নিশ্চিতভাবে যথেষ্ট সিম্পল যাতে আমাকে মুর্খ সাবিত করতে পারে। :)
তবে আমি এই ছোট পারসারটি লিখতে খুব উপভোগ করলাম। এটি আমাকে রyek্সপ্রেশন অ্যাপ্রোচের রুট থেকে বের করল। আর একটু বোনাস হিসেবে, ফলাফল কোডে আমার আত্মবিশ্বাস রyek্সপ্রেশন-ভিত্তিক জটিল কোডের চেয়ে অনেক বেশি।