lib/keisan/string_and_group_parser.rb
module Keisan
class StringAndGroupParser
class Portion
attr_reader :start_index, :end_index
def initialize(start_index)
@start_index = start_index
end
end
class StringPortion < Portion
attr_reader :string, :escaped_string
def initialize(expression, start_index)
super(start_index)
@string = expression[start_index]
@escaped_string = expression[start_index]
@end_index = start_index + 1
while @end_index < expression.size
if expression[@end_index] == quote_type
@string << quote_type
@escaped_string << quote_type
@end_index += 1
# Successfully parsed the string
return
end
n, c = get_potentially_escaped_next_character(expression, @end_index)
@escaped_string << c
@end_index += n
end
raise Keisan::Exceptions::TokenizingError.new("Tokenizing error, no closing quote #{quote_type}")
end
def size
string.size
end
def to_s
string
end
private
# Returns number of processed input characters, and the output character
# If a sequence like '\"' is encountered, the first backslash escapes the
# second double-quote, and the two characters will act as a one double-quote
# character.
def get_potentially_escaped_next_character(expression, index)
@string << expression[index]
if expression[index] == "\\" && index + 1 < expression.size
@string << expression[index + 1]
return [2, escaped_character(expression[index + 1])]
else
return [1, expression[index]]
end
end
def quote_type
@string[0]
end
def escaped_character(character)
case character
when "\\", '"', "'"
character
when "a"
"\a"
when "b"
"\b"
when "r"
"\r"
when "n"
"\n"
when "s"
"\s"
when "t"
"\t"
else
raise Keisan::Exceptions::TokenizingError.new("Tokenizing error, unknown escape character: \"\\#{character}\"")
end
end
end
class GroupPortion < Portion
attr_reader :opening_brace, :closing_brace ,:portions, :size
OPENING_TO_CLOSING_BRACE = {
"(" => ")",
"{" => "}",
"[" => "]",
}
def initialize(expression, start_index)
super(start_index)
case expression[start_index]
when OPEN_GROUP_REGEX
@opening_brace = expression[start_index]
else
raise Keisan::Exceptions::TokenizingError.new("Internal error, GroupPortion did not start with brace")
end
@closing_brace = OPENING_TO_CLOSING_BRACE[opening_brace]
parser = StringAndGroupParser.new(expression, start_index: start_index + 1, ending_character: closing_brace)
@portions = parser.portions
@size = parser.size + 2
if start_index + size > expression.size || expression[start_index + size - 1] != closing_brace
raise Keisan::Exceptions::TokenizingError.new("Tokenizing error, group with opening brace #{opening_brace} did not have closing brace")
end
end
def to_s
opening_brace + portions.map(&:to_s).join + closing_brace
end
end
class OtherPortion < Portion
attr_reader :string
def initialize(expression, start_index)
super(start_index)
case expression[start_index]
when STRING_CHARACTER_REGEX, OPEN_GROUP_REGEX, CLOSED_GROUP_REGEX
raise Keisan::Exceptions::TokenizingError.new("Internal error, OtherPortion should not have string/braces at start")
else
index = start_index + 1
end
while index < expression.size
case expression[index]
when STRING_CHARACTER_REGEX, OPEN_GROUP_REGEX, CLOSED_GROUP_REGEX, COMMENT_CHARACTER_REGEX
break
else
index += 1
end
end
@end_index = index
@string = expression[start_index...end_index]
end
def size
string.size
end
def to_s
string
end
end
class CommentPortion < Portion
attr_reader :string
def initialize(expression, start_index)
super(start_index)
if expression[start_index] != '#'
raise Keisan::Exceptions::TokenizingError.new("Comment should start with '#'")
else
index = start_index + 1
end
while index < expression.size
break if expression[index] == "\n"
index += 1
end
@end_index = index
@string = expression[start_index...end_index]
end
def size
string.size
end
def to_s
string
end
end
# An ordered array of "portions", which
attr_reader :portions, :size
COMMENT_CHARACTER_REGEX = /[#]/
STRING_CHARACTER_REGEX = /["']/
OPEN_GROUP_REGEX = /[\(\{\[]/
CLOSED_GROUP_REGEX = /[\)\}\]]/
# Ending character is used as a second ending condition besides expression size
def initialize(expression, start_index: 0, ending_character: nil)
index = start_index
@portions = []
while index < expression.size && (ending_character.nil? || expression[index] != ending_character)
case expression[index]
when STRING_CHARACTER_REGEX
portion = StringPortion.new(expression, index)
index = portion.end_index
@portions << portion
when OPEN_GROUP_REGEX
portion = GroupPortion.new(expression, index)
index += portion.size
@portions << portion
when CLOSED_GROUP_REGEX
raise Keisan::Exceptions::TokenizingError.new("Tokenizing error, unexpected closing brace #{expression[start_index]}")
when COMMENT_CHARACTER_REGEX
portion = CommentPortion.new(expression, index)
index += portion.size
@portions << portion
else
portion = OtherPortion.new(expression, index)
index += portion.size
@portions << portion
end
end
@size = index - start_index
end
def to_s
portions.map(&:to_s).join
end
end
end