Fix File.Stream Enumerable.count for files without trailing newline (#15147)
The optimized count implementation was counting newline characters instead of actual lines, returning incorrect results for files that don't end with a newline (which is common for source code files). For example, a file with content "line1\nline2\nline3" (3 lines) was returning 2 instead of 3. The fix tracks the last byte read and adds 1 at EOF if the file has content but doesn't end with a newline.
This commit is contained in:
@@ -119,7 +119,7 @@ defmodule File.Stream do
|
||||
|
||||
counter = fn device ->
|
||||
device = skip_bom_and_offset(device, raw, modes)
|
||||
count_lines(device, path, pattern, read_function(stream), 0)
|
||||
count_lines(device, path, pattern, read_function(stream), 0, :empty)
|
||||
end
|
||||
|
||||
{:ok, open!(stream, modes, counter)}
|
||||
@@ -229,21 +229,28 @@ defmodule File.Stream do
|
||||
for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode
|
||||
end
|
||||
|
||||
defp count_lines(device, path, pattern, read, count) do
|
||||
defp count_lines(device, path, pattern, read, count, last_byte) do
|
||||
case read.(device) do
|
||||
data when is_binary(data) and byte_size(data) > 0 ->
|
||||
newlines = length(:binary.matches(data, pattern))
|
||||
last = :binary.last(data)
|
||||
count_lines(device, path, pattern, read, count + newlines, last)
|
||||
|
||||
data when is_binary(data) ->
|
||||
count_lines(device, path, pattern, read, count + count_lines(data, pattern))
|
||||
count_lines(device, path, pattern, read, count, last_byte)
|
||||
|
||||
:eof ->
|
||||
count
|
||||
case last_byte do
|
||||
:empty -> 0
|
||||
?\n -> count
|
||||
_ -> count + 1
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
raise File.Error, reason: reason, action: "stream", path: path
|
||||
end
|
||||
end
|
||||
|
||||
defp count_lines(data, pattern), do: length(:binary.matches(data, pattern))
|
||||
|
||||
defp read_function(%{raw: true}), do: &IO.binread(&1, @read_ahead_size)
|
||||
defp read_function(%{raw: false}), do: &IO.read(&1, @read_ahead_size)
|
||||
end
|
||||
|
||||
@@ -65,6 +65,34 @@ defmodule File.StreamTest do
|
||||
assert Enum.count(stream) == 2
|
||||
end
|
||||
|
||||
test "counts lines without trailing newline" do
|
||||
no_trailing = tmp_path("no_trailing.txt")
|
||||
single_line = tmp_path("single_line.txt")
|
||||
empty_file = tmp_path("empty.txt")
|
||||
|
||||
try do
|
||||
File.write!(no_trailing, "line1\nline2\nline3")
|
||||
File.write!(single_line, "hello")
|
||||
File.write!(empty_file, "")
|
||||
|
||||
# 3 lines, no trailing newline
|
||||
stream = stream!(@node, no_trailing)
|
||||
assert Enum.count(stream) == 3
|
||||
|
||||
# 1 line, no newline at all
|
||||
stream = stream!(@node, single_line)
|
||||
assert Enum.count(stream) == 1
|
||||
|
||||
# empty file
|
||||
stream = stream!(@node, empty_file)
|
||||
assert Enum.count(stream) == 0
|
||||
after
|
||||
File.rm(no_trailing)
|
||||
File.rm(single_line)
|
||||
File.rm(empty_file)
|
||||
end
|
||||
end
|
||||
|
||||
test "reads and writes lines" do
|
||||
src = fixture_path("file.txt")
|
||||
dest = tmp_path("tmp_test.txt")
|
||||
|
||||
Reference in New Issue
Block a user