diff --git a/spec/std/string_spec.cr b/spec/std/string_spec.cr index 0dc744f39659..67b64f7525f0 100644 --- a/spec/std/string_spec.cr +++ b/spec/std/string_spec.cr @@ -2700,8 +2700,26 @@ describe "String" do lines.should eq(["foo\n", "\n", "bar\r\n", "baz\r\n"]) end + it "gets each_line with remove_empty = true" do + lines = [] of String + "\nfoo\n\nbar\r\nbaz\n\n".each_line(remove_empty: true) do |line| + lines << line + end.should be_nil + lines.should eq(["foo", "bar", "baz"]) + end + + it "gets each_line with remove_empty = true and chomp = false" do + lines = [] of String + "\nfoo\n\nbar\r\n\r\nbaz".each_line(remove_empty: true, chomp: false) do |line| + lines << line + end.should be_nil + lines.should eq(["foo\n", "bar\r\n", "baz"]) + end + it_iterates "#each_line", ["foo", "bar", "baz"], "foo\nbar\r\nbaz\r\n".each_line it_iterates "#each_line(chomp: false)", ["foo\n", "bar\r\n", "baz\r\n"], "foo\nbar\r\nbaz\r\n".each_line(chomp: false) + it_iterates "#each_line(remove_empty: true)", ["foo", "bar", "baz"], "\nfoo\n\nbar\r\n\r\nbaz".each_line(remove_empty: true) + it_iterates "#each_line(remove_empty: true, chomp: false)", ["foo\n", "bar\r\n", "baz"], "\nfoo\n\nbar\r\n\r\nbaz".each_line(remove_empty: true, chomp: false) it_iterates "#each_codepoint", [97, 98, 9731], "ab☃".each_codepoint diff --git a/src/string.cr b/src/string.cr index 99a26e55fcf1..4545764da2e4 100644 --- a/src/string.cr +++ b/src/string.cr @@ -4395,6 +4395,8 @@ class String # "hello\nworld\r\n".each_line(chomp: false) { } # yields "hello\n", "world\r\n" # ``` # + # If *remove_empty* is `true`, any empty lines are removed from the result. + # # A trailing line feed is not considered starting a final, empty line. The # empty string does not contain any lines. # @@ -4405,13 +4407,19 @@ class String # ``` # # * `#lines` returns an array of lines - def each_line(chomp : Bool = true, & : String ->) : Nil + def each_line(chomp : Bool = true, remove_empty : Bool = false, & : String ->) : Nil return if empty? offset = 0 while byte_index = byte_index('\n'.ord.to_u8, offset) count = byte_index - offset + 1 + + if remove_empty && (byte_index == offset || (byte_index == offset + 1 && to_unsafe[offset] === '\r')) + offset = byte_index + 1 + next + end + if chomp count -= 1 if offset + count > 0 && to_unsafe[offset + count - 1] === '\r' @@ -4429,8 +4437,8 @@ class String end # Returns an `Iterator` which yields each line of this string (see `String#each_line`). - def each_line(chomp = true) - LineIterator.new(self, chomp) + def each_line(chomp = true, *, remove_empty : Bool = false) + LineIterator.new(self, chomp, remove_empty) end # Converts camelcase boundaries to underscores. @@ -5699,7 +5707,7 @@ class String private class LineIterator include Iterator(String) - def initialize(@string : String, @chomp : Bool) + def initialize(@string : String, @chomp : Bool, @remove_empty : Bool) @offset = 0 @end = false end @@ -5710,6 +5718,12 @@ class String byte_index = @string.byte_index('\n'.ord.to_u8, @offset) if byte_index count = byte_index - @offset + 1 + + if @remove_empty && (byte_index == @offset || (byte_index == @offset + 1 && @string.to_unsafe[@offset] === '\r')) + @offset = byte_index + 1 + return self.next + end + if @chomp count -= 1 if @offset + count > 0 && @string.to_unsafe[@offset + count - 1] === '\r'