Class: Elif

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/elif.rb

Overview

A File-like object for reading lines from a disk file in reverse order. See Elif::new and Elif#gets for details. All other methods are just interface conveniences.

Based on Perl’s File::ReadBackwards module, by Uri Guttman.

Constant Summary collapse

VERSION =

The version of the installed library.

"0.1.0".freeze
MAX_READ_SIZE =

The size of the reads we will use to add to the line buffer.

1 << 10

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args) ⇒ Elif

The first half of the Elif algorithm (to read file lines in reverse order). This creates a new Elif object, shifts the read pointer to the end of the file, and prepares a buffer to hold read lines until they can be returned. This method also sets the @read_size to the remainer of File#size and MAX_READ_SIZE for the first read.

Technically args are delegated straight to File#new, but you must open the File object for reading for it to work with this algorithm.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/elif.rb', line 63

def initialize(*args)
  # Delegate to File::new and move to the end of the file.
  @file = File.new(*args)
  @file.seek(0, IO::SEEK_END)
  
  # Record where we are.
  @current_pos = @file.pos
  
  # Get the size of the next of the first read, the dangling bit of the file.
  @read_size = @file.pos % MAX_READ_SIZE
  @read_size = MAX_READ_SIZE if @read_size.zero?
  
  # A buffer to hold lines read, but not yet returned.
  @line_buffer = Array.new
end

Class Method Details

.foreach(name, sep_string = $/) ⇒ Object

Works just line File::foreach, save that the lines come in reverse order.



23
24
25
26
27
28
29
# File 'lib/elif.rb', line 23

def self.foreach(name, sep_string = $/)
  open(name) do |file|
    while line = file.gets(sep_string)
      yield line
    end
  end
end

.open(*args) ⇒ Object

Works just line File::open.



32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/elif.rb', line 32

def self.open(*args)
  file = new(*args)
  if block_given?
    begin
      yield file
    ensure
      file.close
    end
  else
    file
  end
end

.readlines(name, sep_string = $/) ⇒ Object

Works just line File::readlines, save that line Array will be in reverse order.



49
50
51
# File 'lib/elif.rb', line 49

def self.readlines(name, sep_string = $/)
  open(name) { |file| file.readlines(sep_string) }
end

Instance Method Details

#closeObject

Works just line File#close.



151
152
153
# File 'lib/elif.rb', line 151

def close
  @file.close
end

#each(sep_string = $/) ⇒ Object Also known as: each_line

Works just line File#each, save that the lines come in reverse order.



125
126
127
128
129
# File 'lib/elif.rb', line 125

def each(sep_string = $/)
  while line = gets(sep_string)
    yield line
  end
end

#gets(sep_string = $/) ⇒ Object

The second half on the Elif algorthim (see Elif::new). This method returns the next line of the File, working from the end to the beginning in reverse line order.

It works by moving the file pointer backwords MAX_READ_SIZE at a time, storing seen lines in @line_buffer. Once the buffer contains at least two lines (ensuring we have seen on full line) or the file pointer reaches the head of the File, the last line from the buffer is returned.

When the buffer is exhausted, this will throw nil (from the empty Array).



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/elif.rb', line 90

def gets(sep_string = $/)
  # 
  # If we have more than one line in the buffer or we have reached the
  # beginning of the file, send the last line in the buffer to the caller.  
  # (This may be +nil+, if the buffer has been exhausted.)
  # 
  return @line_buffer.pop if @line_buffer.size > 2 or @current_pos.zero?
  
  # 
  # If we made it this far, we need to read more data to try and find the 
  # beginning of a line or the beginning of the file.  Move the file pointer
  # back a step, to give us new bytes to read.
  # 
  @current_pos -= @read_size
  @file.seek(@current_pos, IO::SEEK_SET)
  
  # 
  # Read more bytes and prepend them to the first (likely partial) line in the
  # buffer.
  # 
  @line_buffer[0] = "#{@file.read(@read_size)}#{@line_buffer[0]}"
  @read_size      = MAX_READ_SIZE  # Set a size for the next read.
  
  # 
  # Divide the first line of the buffer based on +sep_string+ and #flatten!
  # those new lines into the buffer.
  # 
  @line_buffer[0] = @line_buffer[0].scan(/.*?#{Regexp.escape(sep_string)}|.+/)
  @line_buffer.flatten!
  
  # We have move data now, so try again to read a line...
  gets(sep_string)
end

#readline(sep_string = $/) ⇒ Object

Works just line File#readline, save that the lines come in reverse order.



134
135
136
# File 'lib/elif.rb', line 134

def readline(sep_string = $/)
  gets(sep_string) || raise(EOFError, "end of file reached")
end

#readlines(sep_string = $/) ⇒ Object

Works just line File#readlines, save that line Array will be in reverse order.



142
143
144
145
146
147
148
# File 'lib/elif.rb', line 142

def readlines(sep_string = $/)
  lines = Array.new
  while line = gets(sep_string)
    lines << line
  end
  lines
end