Skip to content

Instantly share code, notes, and snippets.

@JoshCheek
Created March 18, 2010 20:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JoshCheek/336838 to your computer and use it in GitHub Desktop.
Save JoshCheek/336838 to your computer and use it in GitHub Desktop.
# got inspired to play with files based on this post http://www.ruby-forum.com/topic/206208
# define the constants our MailRecord uses
class MailRecord
LINE_WIDTH = 9 # all lines have this size
LINES_PER_RECORD = 5 # title , id , to , from , blankline
RECORD_WIDTH = LINE_WIDTH * LINES_PER_RECORD
end
# our MailRecord is composed of lots of fixed-width records.
# we define how to deal with an individual record here
class MailRecord
class Record
attr_accessor :title , :id , :from , :to , :offset
# initialize a new MailRecord::Record
def initialize(file)
self.file = file # store the file for writing
self.offset = file.pos # store the offset so we know where to write
self.title = file.readline.strip
self.id = file.readline.split('=').last.to_i
self.from = file.readline.split('=').last.strip
self.to = file.readline.split('=').last.strip
end
# write the record back to the file
def write
file.seek offset , IO::SEEK_SET
file.puts self.to_s
end
# the record as a string (will be the same as it would be in the file, this allows us to directly write it)
# sprintf allows us to use a format string, and make it look the way we want
# the "% ... s" tells it that we will be putting a string into there
# the dash to the right of the percent tells it to left justify the results
# by interpolating LINE_WIDTH, we tell it how many characters in width we want it to be
# we subtract 1 from the LINE_WIDTH because we need to add a newline to the end
# so if LINE_WIDTH is 9, then that becomes %-8s\n" meaning that we want to put a left justified
# string with 8 characters into there. We multiply it by 5, to get "%-8s\n%-8s\n%-8s\n%-8s\n%-8s\n"
# which will have slots for 5 lines.
#
# Then we pass in the five strings we want to use: the title, id, from, to, and blankline strings.
# But we need to be careful not to exceed the maximum width and corrupt our file, so we validate each
# one to make sure it is within the 8 chars
def to_s
sprintf "%-#{LINE_WIDTH-1}s\n"*5 ,
validate(title) ,
validate("id=#{id}") ,
validate("from=#{from}") ,
validate("to=#{to}") ,
validate("")
end
private
attr_accessor :file
# raise an error if the string is too large to fit in the file (we don't want to mismatch the line lengths
# because we calculate all the things by offsets, so if a line length was incorrect, then we would end up jumping
# to the wrong location, which would mess everything up.)
def validate(str)
raise 'string exceeds the maximum size' if str.size > LINE_WIDTH-1
str
end
end
end
# the mail record class itself, gives us the functionality to interface with our file
class MailRecord
# create a new MailRecord
def initialize(file)
self.file = file
yield self # let the user interact through this block so that we can make sure the file gets closed
end
# returns the given record (note this is based on location in file, not id)
# will store it and return the same record if queried again, or will pull from file if not seen before
def []( record_number )
records[record_number] ||= get_from_file(record_number)
end
# a nice syntax to access the objects through, see the example
def self.execute( filename , &block )
file = File.open filename , 'r+' # open the file for reading and writing
MailRecord.new file , &block
nil
ensure
file.close # need to make sure the file gets closed or things can get messed up
end
# gives us all of these methods http://ruby-doc.org/core/classes/Enumerable.html
# just pulls each record until it hits the end of the file
include Enumerable
def each
i = 0
loop do
record = get_from_file(i)
yield record
i += 1
end
rescue EOFError
end
private
# For integrity, user should only interact with instance variables through methods we define
attr_accessor :file
# a hash of records we have pulled, stored in memory
def records
@records ||= Hash.new
end
# retrieves the record from the file
def get_from_file( record_number )
offset = MailRecord.record_offset record_number
file.seek( offset , IO::SEEK_SET )
Record.new file
end
# remember that we begin counting at zero not one
# so record_offset(2) will be the third record, not the second
def self.record_offset( record_number )
record_number * RECORD_WIDTH
end
end
# ----- setup for example -----
# create the file and populate it so that we have something to experiment on
filename = 'Mail.dat'
File.open filename , 'w' do |file|
file.puts DATA.read # pulls data from after __END__ and writes it to the file
end
# show you what it looks like before we mess with it, as a reference
display_file = lambda do |text|
puts text
File.readlines(filename).each { |line| puts line.inspect } # show inspected version so the fixed width is clear
end
display_file.call "#{filename} before we do anything:"
# ----- example of how to use -----
# open the file, get the first record, change it's title, change who it was sent to, and write it back to the file
MailRecord.execute filename do |records|
# change a record
record = records[1]
record.to = 'jill'
record.title = 'Hi love!'
record.write
# use an iterator to pull all the titles
titles = records.map { |record| record.title.inspect }
puts "\n\nthe titles are: #{titles.join ', '}"
# display the record whose id is 416
puts "\n\nThe record with the id of 416 looks like"
puts records.find { |record| record.id == 416 }
# use an iterator to double all of the ids
records.each do |record|
record.id *= 2
record.write
end
end
# show you what it looks like after we messed with it
display_file.call "\n\n\n#{filename} after we are done:"
__END__
hi there
id=12
from=me
to=you
hey, pal
id=94
from=you
to=me
pay up!
id=416
from=us
to=them
pay up!
id=416
from=us
to=them
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment