dbf2/dbf.rb
# $Id: dbf.rb 376 2006-02-12 22:02:54Z aamine $
#
# Copyright (c) 2005,2006 yrock
#
# This program is free software.
# You can distribute/modify this program under the terms of the Ruby License.
#
# 2006-02-11 refactored by Minero Aoki
module DBF
class PackedStruct
class << PackedStruct
def define(&block)
c = Class.new(self)
def c.inherited(subclass)
proto = @prototypes
subclass.instance_eval {
@prototypes = proto
}
end
c.module_eval(&block)
c
end
def char(name)
define_field name, 'A', 1
end
def byte(name)
define_field name, 'C', 1
end
def int16LE(name)
define_field name, 'v', 2
end
def int32LE(name)
define_field name, 'V', 4
end
def string(n, name)
define_field name, "Z#{n}", n
end
private
def define_field(name, template, size)
(@prototypes ||= []).push FieldPrototype.new(name, template, size)
define_accessor name
end
def define_accessor(name)
module_eval(<<-End, __FILE__, __LINE__ + 1)
def #{name}
self['#{name}']
end
def #{name}=(val)
self['#{name}'] = val
end
End
end
end
class FieldPrototype
def initialize(name, template, size)
@name = name
@template = template
@size = size
end
attr_reader :name
attr_reader :size
def read(f)
parse(f.read(@size))
end
def parse(s)
s.unpack(@template)[0]
end
def serialize(val)
[val].pack(@template)
end
end
def PackedStruct.size
@prototypes.map {|proto| proto.size }.inject(0) {|sum, s| sum + s }
end
def PackedStruct.names
@prototypes.map {|proto| proto.name }
end
def PackedStruct.prototypes
@prototypes
end
def PackedStruct.read(f)
new(* @prototypes.map {|proto| proto.read(f) })
end
def initialize(*vals)
@alist = self.class.names.zip(vals)
end
def inspect
"\#<#{self.class} #{@alist.map {|n,v| "#{n}=#{v.inspect}" }.join(' ')}>"
end
def [](name)
k, v = @alist.assoc(name.to_s.intern)
raise ArgumentError, "no such field: #{name}" unless k
v
end
def []=(name, val)
a = @alist.assoc(name.to_s.intern)
raise ArgumentError, "no such field: #{name}" unless a
a[1] = val
end
def serialize
self.class.prototypes.zip(@alist.map {|_, val| val })\
.map {|proto, val| proto.serialize(val) }.join('')
end
end
DBF_VERSION = 3
# dBASE IV 2.0 file header leading block
#
# - filesize = header_size + (record_size * n_records)
# - header_size = HeaderLead.size + (Field.size * n_Fields) + 1
#
HeaderLead = PackedStruct.define {
byte :magic # MSSSmVVV (M: dBASE III+/IV memo file,
# S: SQL table,
# m: dBASE IV memo file,
# V: format version)
byte :_year # last-modifield year
byte :month # last-modifield month
byte :date # last-modifield date
int32LE :n_records # a number of records
int16LE :header_size # byte-size of whole header
int16LE :record_size # byte-size of a record
string 2, :reserved1
byte :in_transaction
byte :encrypted
string 12, :reserved2
byte :mdx # 0x01: MDX; 0x0: no MDX
byte :langid # language driver ID
string 2, :reserved3
}
class HeaderLead # reopen
def HeaderLead.create
now = Time.now
new(DBF_VERSION, now.year, now.month, now.day,
0, size() + 1, 0,
"", 0, 0, "", 0, 0, "")
end
def version
magic() | 0b111
end
def year
1900 + _year()
end
def year=(y)
self._year = y - 1900
end
def last_modified
Time.local(year(), month(), date())
end
def last_modified=(t)
self.year = t.year
self.month = t.month
self.date = t.day
end
def n_fields
(header_size() - self.class.size() - 1) / Field.size
end
end
class FormatError < StandardError; end
Field = PackedStruct.define {
string 11, :name
char :type
string 4, :reserved1
byte :size
byte :decimal
string 2, :reserved2
byte :workingID
string 10, :reserved3
byte :mdx # 0x0: MDX; 0x1: no MDX
}
class Field # reopen
# filled by field_type
TYPE_TO_CLASS = {}
class << self
def field_type(ch)
@type = ch
TYPE_TO_CLASS[ch] = self
end
alias newobj new
def new(name, type, *args)
return newobj(name, @type, *args) if self < Field
c = TYPE_TO_CLASS[type] or
raise FormatError, "illegal type: #{type.inspect}"
c.newobj(name, type, *args)
end
end
def initialize(*args)
super(*args)
@value = nil
end
attr_accessor :value
def inspect
"\#<#{self.class} #{@alist.map {|n,v| "#{n}=#{v.inspect}" }.join(' ')} value=#{@value.inspect}>"
end
alias serialize_schema serialize
end
class NumericField < Field
field_type 'N'
def NumericField.new2(name, size, dec)
new(name, @type, "", size, dec, "", 0, "", 0x0)
end
def string_field?
false
end
def serialize_value
# Î㡧sprintf("%8.3f", ...)
sprintf("%#{size()}.#{decimal()}f", @value)
end
def load_value(f)
@value = f.read(size()).to_f
end
def load_default_value
@value = 0.0
end
end
class FloatField < NumericField
field_type 'F'
end
class StringField < Field
field_type 'C'
def StringField.new2(name, size)
new(name, @type, "", size, 0, "", 0, "", 0x0)
end
def string_field?
true
end
def serialize_value
sprintf("%-#{size()}s", @value)
end
def load_value(f)
@value = f.read(size())
end
def load_default_value
@value = ""
end
end
class RecordSet
def RecordSet.open(path, mode = 'r')
recset = new(path, mode)
if block_given?
begin
return yield(recset)
ensure
recset.close
end
else
recset
end
end
def initialize(path, mode = 'r')
@mode = mode
@idx = 0 # record index (beginning with 0)
@current = nil
case mode
when 'r'
@f = File.open(path, 'rb+')
@lead = HeaderLead.read(@f)
@header_modified = false
@fields = (0 ... @lead.n_fields).to_a.map { Field.read(@f) }
set_current_record 0
when 'c'
@f = File.open(path, 'wb+')
@lead = HeaderLead.create
@header_modified = true
@fields = []
else
raise ArgumentError, "invalid open mode: #{mode.inspect}"
end
end
def close
if @mode == "c"
save_header
seek @lead.n_records
@f.write EOF
end
@f.close
end
def size
@lead.n_records
end
#
# Database Schema
#
attr_reader :fields
def field(name)
@fields.detect {|f| f.name == name } or
raise ArgumentError, "no such field: #{name.inspect}"
end
def add_numeric_field(name, size, dec)
add_field NumericField.new2(name, size, dec)
end
def add_float_field(name, size, dec)
add_field FloatField.new2(name, size, dec)
end
def add_string_field(name, size)
add_field StringField.new2(name, size)
end
def add_field(f)
@fields.push f
schema_modified
end
#
# Record Pointer, Read/Write
#
def eof?
@idx >= @lead.n_records
end
def empty?
@lead.n_records == 0
end
def current
return nil if empty?
return nil if eof?
_current()
end
def first
return nil if empty?
set_current_record 0
self
end
def next
return nil if eof?
set_current_record @idx + 1
self
end
def prev
return nil if @idx == 0
set_current_record @idx - 1
self
end
def each_record
until eof?
yield current()
self.next
end
end
alias each each_record
def append
set_current_record @lead.n_records
@fields.each do |field|
field.load_default_value
end
if block_given?
yield _current()
update
else
_current()
end
end
def update
save_record
if eof?
@lead.n_records += 1
header_modified
end
end
private
EOH = "\x0d" # End Of Header
EOF = "\x1a" # End Of File
REC_ALIVE = " "
REC_REMOVED = "*"
def _current
@current ||= Record.new(@fields)
end
def schema_modified
@current = nil
@lead.header_size = HeaderLead.size +
(Field.size * @fields.size) +
EOH.size
@lead.record_size = REC_ALIVE.size +
@fields.inject(0) {|sum, f| sum + f.size }
header_modified
end
def header_modified
@header_modified = true
end
def header_modified?
@header_modified
end
def set_current_record(idx)
@idx = idx
seek @idx
return if empty?
return if eof?
load_record
end
def seek(idx)
@f.seek pos(@idx), File::SEEK_SET
end
# 0 =< idx <= @lead.n_records
def pos(idx)
@lead.header_size + @lead.record_size * idx
end
def save_header
return unless header_modified?
@f.seek 0, File::SEEK_SET
@lead.last_modified = Time.now
@f.write @lead.serialize
@fields.each do |field|
@f.write field.serialize_schema
end
@f.write EOH
@header_modified = false
end
def save_record
@f.write REC_ALIVE
@fields.each do |field|
@f.write field.serialize_value
end
end
def load_record
@f.read REC_ALIVE.size # discard ALIVE/REMOVED mark
@fields.each do |field|
field.load_value @f
end
end
end
class Record
def initialize(fields)
@fields = fields
fields.each do |f|
define_accessor f.name
end
end
def define_accessor(name)
instance_eval(<<-End, __FILE__, __LINE__ + 1)
def #{name}
self["#{name}"]
end
def #{name}=(val)
self["#{name}"] = val
end
End
end
private :define_accessor
def inspect
"\#<#{self.class} #{@fields.map {|f| "#{f.name}(#{f.type})=#{f.value.inspect}" }.join(' ')}>"
end
def field(name)
@fields.detect {|f| f.name == name } or
raise ArgumentError, "no such field: #{name.inspect}"
end
attr_reader :fields
def [](name)
field(name).value
end
def []=(name, val)
field(name).value = val
end
def names
@fields.map {|f| f.name }
end
def values
@fields.map {|f| f.value }
end
end
end