স্বীকারোক্তির সময়:আমি রেগুলার এক্সপ্রেশন নিয়ে কাজ করতে বিশেষভাবে পছন্দ করি না। যদিও আমি সেগুলি সব সময় ব্যবহার করি, /^foo.*$/
এর চেয়ে জটিল কিছু আমাকে থামতে এবং চিন্তা করতে হবে। যদিও আমি নিশ্চিত এমন কিছু লোক আছে যারা \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:[email protected] 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
ব্যবহার করুন স্ট্রিংকে টোকেনে বিভক্ত করতে। এটি একটি সুন্দর কৌশল, আসলে:
# The parens in the regexp mean that the separator is added to the array
"foo bar baz".split(/(foo|bar|baz)/)
=> ["", "foo", " ", "bar", " ", "baz"]
অদ্ভুত খালি স্ট্রিং সত্ত্বেও এটি প্রাথমিকভাবে প্রতিশ্রুতিশীল লাগছিল। কিন্তু আমার বাস্তব-বিশ্বের নিয়মিত অভিব্যক্তি অনেক বেশি জটিল ছিল। আমার প্রথম খসড়াটি এইরকম ছিল:
/
( # Capture group is so split will include matching and non-matching strings
(?: # The first character of the key, which is
(?!\s)[^:\s"'\[]{1} # ..any valid "key" char not preceeded by whitespace
|^[^:\s"'\[]{0,1} # ..or any valid "key" char at beginning of line
)
[^:\s"'\[]* # The rest of the "key" chars
: # a colon
(?: # The "value" chars, which are
'[^']+' # ..anything surrounded by single quotes
| "[^"]+" # ..or anything surrounded by double quotes
| \[\S+\sTO\s\S+\] # ..or anything like [x TO y]
| [^\s"'\[]+ # ..or any string not containing whitespace or special chars
)
)
/xi
এটির সাথে কাজ করা আমাকে একটি ডুবন্ত অনুভূতি দিয়েছে। প্রতিবার যখন আমি একটি এজ কেস পেয়েছি তখন আমাকে নিয়মিত এক্সপ্রেশনটি সংশোধন করতে হবে, এটিকে আরও জটিল করে তুলবে। উপরন্তু, এটি জাভাস্ক্রিপ্টের পাশাপাশি রুবিতে কাজ করার প্রয়োজন ছিল, তাই নেতিবাচক লুকবিহাইন্ডের মতো কিছু বৈশিষ্ট্য উপলব্ধ ছিল না।
...এই সময়েই এই সবের অযৌক্তিকতা আমাকে তাড়িত করেছিল। আমি যে নিয়মিত অভিব্যক্তি পদ্ধতি ব্যবহার করছিলাম তা স্ক্র্যাচ থেকে একটি সাধারণ পার্সার লেখার চেয়ে অনেক বেশি জটিল ছিল।
পার্সারের শারীরস্থান
আমি কোন বিশেষজ্ঞ নই, কিন্তু সহজ পার্সাররা সহজ। তারা যা করে তা হল:
- একটি স্ট্রিং দিয়ে ধাপে ধাপে, অক্ষর অনুসারে অক্ষর
- প্রতিটি অক্ষর একটি বাফারে যুক্ত করুন
- যখন একটি টোকেন-বিভাজক অবস্থার সম্মুখীন হয়, বাফারটিকে একটি অ্যারেতে সংরক্ষণ করুন এবং এটি খালি করুন।
এটি জেনে, আমরা একটি সাধারণ পার্সার সেট আপ করতে পারি যা হোয়াইটস্পেস দ্বারা স্ট্রিংগুলিকে বিভক্ত করে। এটি মোটামুটি "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
# Outputs ["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
এই কোডটি আমার রেগুলার-এক্সপ্রেশন-ভিত্তিক পদ্ধতির চেয়ে একটু দীর্ঘ কিন্তু অনেক বেশি সোজা।
বিচ্ছেদ চিন্তা
সম্ভবত সেখানে একটি নিয়মিত অভিব্যক্তি আছে যা আমার ব্যবহারের ক্ষেত্রে সূক্ষ্ম কাজ করবে। ইতিহাস যদি কোন পথপ্রদর্শক হয়, তাহলে আমাকে বোকা বানানোর জন্য এটি সম্ভবত যথেষ্ট সহজ। :)
কিন্তু আমি সত্যিই এই সামান্য পার্সার লেখার সুযোগ উপভোগ করেছি. এটি আমাকে রেজেক্স পদ্ধতির সাথে যে রুটটির মধ্যে ছিলাম তা থেকে বেরিয়ে এসেছিল। একটি চমৎকার বোনাস হিসাবে, আমি ফলাফলের কোডে অনেক বেশি আত্মবিশ্বাসী, যতটা না জটিল রেগুলার এক্সপ্রেশনের উপর ভিত্তি করে তৈরি করা হয়।