Files
mruby-mruby/mrblib/string.rb
T
Ryan Lopopolo 0f516bbe1d Add paragraph mode to String#each_line in mrblib
mruby/mruby#4511 demonstrated an infinite loop in `String#each_line` when
given an empty string separator. In MRI, an empty separator places
String#each_line in paragraph mode, where the String is separated on
successive runs of newlines. In paragraph mode, the String
`"abc\n\n\ndef\nxyz"` is split into `["abc\n\n\n", "def\nxyz"]`.

This commit makes the String#each_line implementation as close to
ruby/spec compliant as possible given the limitations of mruby core.
With this patch, the following specs fail for `String#each_line`:

- uses `$/` as the separator when none is given (can be fixed by
  aliasing and redefining the method to use $/ as the default value
  of separator in mruby-io)
- when no block is given returned Enumerator size should return nil
  (`Enumerator#size` is not supported on mruby)
- tries to convert the separator to a string using to_str (`String#to_str`
  is not implemented on mruby)

This patch has similar memory consumption compared to the prior
implementation and is takes 4x the time the prior implementation takes to
execute:

```console
/usr/bin/time -l ./bin/mruby -e '("aaa\n\nbbbbb\n\n\n\n\ncccc" * 100000).each_line("\n") { }';
```
2019-06-22 12:31:01 +01:00

292 lines
7.3 KiB
Ruby

##
# String
#
# ISO 15.2.10
class String
# ISO 15.2.10.3
include Comparable
##
# Calls the given block for each line
# and pass the respective line.
#
# ISO 15.2.10.5.15
def each_line(separator = "\n", getline_args = nil)
return to_enum(:each_line, separator, getline_args) unless block_given?
if separator.nil?
yield self
return self
end
raise TypeError unless separator.is_a?(String)
start = 0
pointer = 0
string = dup
if separator.empty?
matched_newlines = 0
while pointer < string.length
c = string[pointer]
if c == "\n"
matched_newlines += 1
elsif matched_newlines > 1 && self.class == String
yield string[start...pointer]
matched_newlines = 0
start = pointer
elsif matched_newlines > 1
yield self.class.new(string[start...pointer])
matched_newlines = 0
start = pointer
else
matched_newlines = 0
end
pointer += 1
end
else
matched_length = 0
separator_length = separator.length
while pointer < string.length
c = string[pointer]
pointer += 1
matched_length += 1 if c == separator[matched_length]
next unless matched_length == separator_length
if self.class == String
yield string[start...pointer]
else
yield self.class.new(string[start...pointer])
end
matched_length = 0
start = pointer
end
end
return self if start == string.length
if self.class == String
yield string[start..-1]
else
yield self.class.new(string[start..-1])
end
self
end
# private method for gsub/sub
def __sub_replace(pre, m, post)
s = ""
i = 0
while j = index("\\", i)
break if j == length-1
t = case self[j+1]
when "\\"
"\\"
when "`"
pre
when "&", "0"
m
when "'"
post
when "1", "2", "3", "4", "5", "6", "7", "8", "9"
""
else
self[j, 2]
end
s += self[i, j-i] + t
i = j + 2
end
s + self[i, length-i]
end
##
# Replace all matches of +pattern+ with +replacement+.
# Call block (if given) for each match and replace
# +pattern+ with the value of the block. Return the
# final value.
#
# ISO 15.2.10.5.18
def gsub(*args, &block)
return to_enum(:gsub, *args) if args.length == 1 && !block
raise ArgumentError, "wrong number of arguments" unless (1..2).include?(args.length)
pattern, replace = *args
plen = pattern.length
if args.length == 2 && block
block = nil
end
if !replace.nil? || !block
replace.__to_str
end
offset = 0
result = []
while found = index(pattern, offset)
result << self[offset, found - offset]
offset = found + plen
result << if block
block.call(pattern).to_s
else
replace.__sub_replace(self[0, found], pattern, self[offset..-1] || "")
end
if plen == 0
result << self[offset, 1]
offset += 1
end
end
result << self[offset..-1] if offset < length
result.join
end
##
# Replace all matches of +pattern+ with +replacement+.
# Call block (if given) for each match and replace
# +pattern+ with the value of the block. Modify
# +self+ with the final value.
#
# ISO 15.2.10.5.19
def gsub!(*args, &block)
raise FrozenError, "can't modify frozen String" if frozen?
return to_enum(:gsub!, *args) if args.length == 1 && !block
str = self.gsub(*args, &block)
return nil unless self.index(args[0])
self.replace(str)
end
# ##
# # Calls the given block for each match of +pattern+
# # If no block is given return an array with all
# # matches of +pattern+.
# #
# # ISO 15.2.10.5.32
# def scan(pattern, &block)
# # TODO: String#scan is not implemented yet
# end
##
# Replace only the first match of +pattern+ with
# +replacement+. Call block (if given) for each
# match and replace +pattern+ with the value of the
# block. Return the final value.
#
# ISO 15.2.10.5.36
def sub(*args, &block)
unless (1..2).include?(args.length)
raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 2)"
end
pattern, replace = *args
pattern.__to_str
if args.length == 2 && block
block = nil
end
unless block
replace.__to_str
end
result = []
this = dup
found = index(pattern)
return this unless found
result << this[0, found]
offset = found + pattern.length
result << if block
block.call(pattern).to_s
else
replace.__sub_replace(this[0, found], pattern, this[offset..-1] || "")
end
result << this[offset..-1] if offset < length
result.join
end
##
# Replace only the first match of +pattern+ with
# +replacement+. Call block (if given) for each
# match and replace +pattern+ with the value of the
# block. Modify +self+ with the final value.
#
# ISO 15.2.10.5.37
def sub!(*args, &block)
raise FrozenError, "can't modify frozen String" if frozen?
str = self.sub(*args, &block)
return nil unless self.index(args[0])
self.replace(str)
end
##
# Call the given block for each byte of +self+.
def each_byte(&block)
return to_enum(:each_byte, &block) unless block
bytes = self.bytes
pos = 0
while pos < bytes.size
block.call(bytes[pos])
pos += 1
end
self
end
##
# Modify +self+ by replacing the content of +self+.
# The portion of the string affected is determined using the same criteria as +String#[]+.
def []=(*args)
anum = args.size
if anum == 2
pos, value = args[0], args[1].__to_str
case pos
when String
posnum = self.index(pos)
if posnum
b = self[0, posnum]
a = self[(posnum + pos.length)..-1]
self.replace([b, value, a].join(''))
else
raise IndexError, "string not matched"
end
when Range
head = pos.begin
tail = pos.end
tail += self.length if tail < 0
unless pos.exclude_end?
tail += 1
end
return self[head, tail-head]=value
else
pos = pos.__to_int
pos += self.length if pos < 0
if pos < 0 || pos > self.length
raise IndexError, "index #{args[0]} out of string"
end
b = self[0, pos]
a = self[pos + 1..-1]
self.replace([b, value, a].join(''))
end
return value
elsif anum == 3
pos, len, value = args[0].__to_int, args[1].__to_int, args[2].__to_str
pos += self.length if pos < 0
if pos < 0 || pos > self.length
raise IndexError, "index #{args[0]} out of string"
end
if len < 0
raise IndexError, "negative length #{len}"
end
b = self[0, pos]
a = self[pos + len..-1]
self.replace([b, value, a].join(''))
return value
else
raise ArgumentError, "wrong number of arguments (#{anum} for 2..3)"
end
end
# those two methods requires Regexp that is optional in mruby
##
# ISO 15.2.10.5.3
#def =~(re)
# re =~ self
#end
##
# ISO 15.2.10.5.27
#def match(re, &block)
# re.match(self, &block)
#end
end