Created
May 27, 2023 16:33
-
-
Save okuramasafumi/099f3b9590eef0a6a1b55d4ff78b582a to your computer and use it in GitHub Desktop.
Pure Ruby implementation of StringScanner
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class MyStringScanner | |
class ScanError < StandardError; end | |
attr_reader :string, :captures | |
attr_accessor :pos | |
def initialize(string, _obsolete = false, fixed_anchor: false) | |
@string = string | |
@fixed_anchor = fixed_anchor | |
@pos = 0 | |
@previous_pos = 0 | |
end | |
def fixed_anchor? | |
!!@fixed_anchor | |
end | |
def string=(string) | |
@string = string | |
@pos = 0 | |
end | |
def <<(string) | |
@string << string | |
end | |
alias concat << | |
def scan(pattern) | |
scan_full(pattern, true, true) | |
end | |
def scan_until(pattern) | |
match = yield_if_match(pattern, affect_matches: true, reset_if_fail: true) do |md| | |
@previous_pos = @pos | |
@pos += md.offset(0).last | |
@string[@previous_pos...@pos] | |
end | |
end | |
def scan_full(pattern, advance_pointer_p, return_string_p) | |
match = yield_if_match(pattern, match_from_current: true, reset_if_fail: true) do |md| | |
@previous_pos = @pos | |
@pos += md.match_length(0) if advance_pointer_p | |
match = md.match(0) | |
if return_string_p | |
match.to_s | |
else | |
match.size | |
end | |
end | |
end | |
def skip(pattern) | |
yield_if_match(pattern, affect_matches: false, match_from_current: true) do |md| | |
length = md.match_length(0) | |
@pos += length | |
length | |
end | |
end | |
def skip_until(pattern) | |
yield_if_match(pattern, affect_matches: false) do |md| | |
advanced = md.offset(0).last | |
@pos += advanced | |
advanced | |
end | |
end | |
def exist?(pattern) | |
raise TypeError if pattern.is_a?(String) | |
yield_if_match(pattern, affect_matches: false) do |md| | |
md.offset(0).first + 1 | |
end | |
end | |
def match?(pattern) | |
yield_if_match(pattern, match_from_current: true) do |md| | |
md.match(0).size | |
end | |
end | |
def check(pattern) | |
yield_if_match(pattern, match_from_current: true) do |md| | |
md.match(0).to_s | |
end | |
end | |
def check_until(pattern) | |
yield_if_match(pattern) do |md| | |
rest[..md.offset(0).last - 1] | |
end | |
end | |
def search_full(pattern, advance_pointer_p, return_string_p) | |
yield_if_match(pattern, affect_matches: false) do |md| | |
last_index = md.offset(0).last | |
r = rest | |
@pos += last_index if advance_pointer_p | |
if return_string_p | |
r[...last_index] | |
else | |
last_index | |
end | |
end | |
end | |
private def yield_if_match(pattern, affect_matches: true, match_from_current: false, reset_if_fail: false) | |
return nil if rest.nil? | |
md = rest.match(pattern) | |
if md && (!match_from_current || md.offset(0).first == 0) | |
@last_md = md | |
yield(md) | |
else | |
@last_md = nil if affect_matches | |
if reset_if_fail | |
@captures = nil | |
@previous_pos = nil | |
end | |
nil | |
end | |
end | |
def bol? | |
@pos == 0 || @string[@pos - 1] == ?\n | |
end | |
alias beginning_of_line? bol? | |
def eos? | |
@pos >= string.length | |
end | |
def get_byte | |
byte = @string.bytes[@pos] | |
char = byte&.chr&.force_encoding(@string.encoding) | |
get_char(char) | |
end | |
def getch | |
char = @string[@pos] | |
get_char(char) | |
end | |
private def get_char(char) | |
@last_md = rest[0].match(char) rescue nil | |
@pos += 1 | |
char | |
end | |
def peek(len) | |
@string[@pos, len] | |
end | |
def terminate | |
@pos = @string.length | |
@last_md = nil | |
end | |
def reset | |
@pos = 0 | |
end | |
def unscan | |
raise ScanError if @previous_pos.nil? | |
@pos = @previous_pos | |
end | |
def [](n) | |
case n | |
when Integer | |
last_matches[n] | |
when String, Symbol | |
value = named_captures[n.to_s] | |
value.nil? ? raise(IndexError) : value | |
end | |
end | |
def matched | |
last_matches.last | |
end | |
def matched_size | |
matched&.size | |
end | |
def size | |
last_matches.size | |
end | |
def pre_match | |
@last_md.nil? ? nil : @string[...@pos - 1] | |
end | |
def post_match | |
@last_md.nil? ? nil : @string[@pos..] | |
end | |
def captures | |
@last_md&.captures | |
end | |
def named_captures | |
@last_md&.named_captures | |
end | |
def rest | |
@string[@pos..] | |
end | |
def rest_size | |
rest.size | |
end | |
def values_at(*args) | |
return nil if captures.nil? | |
args.map do |i| | |
if i.zero? | |
last_matches[0] | |
else | |
i.negative? ? captures[i] : captures[i - 1] | |
end | |
end | |
end | |
def charpos | |
@pos | |
end | |
private | |
def last_matches | |
@last_md&.to_a || [] | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment