dbf/dbf.rb
=begin
dbf.rb
05-11-21
=end
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# DBFheader
# ヘッダ先導部
#
# インターフェイス
# version, date1, date2, date3, numrec, headerbytes, recordbytes, reserve
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
class DBFheader
def initialize
@version = nil # バージョンなど
@date1 = nil # 最終更新日(年)
@date2 = nil # 最終更新日(月)
@date3 = nil # 最終更新日(日)
@numrec = nil # レコード数
@headerbytes = nil # ヘッダのバイト数
@recordbytes = nil # レコードのバイト数
@reserve = nil # 予約領域など
end
attr_accessor :version, :date1, :date2, :date3, :numrec, :headerbytes, :recordbytes, :reserve
end
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# DBFfield
# フィールド要素
#
# インターフェイス
# fieldname, fieldtype, fieldsize, decimal, value
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
class DBFfield
def initialize
@fieldname = "" #フィールド名
@fieldtype = "" #フィールド型
@fieldsize = 0 #フィールド長
@decimal = 0 #フィールド小数部長
@value = nil #フィールド値
end
attr_accessor :fieldname, :fieldtype, :fieldsize, :decimal, :value
end
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# DBFfields
# フィールド記述配列
#
# インターフェイス
# add, fieldname, item, numfields
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
class DBFfields
def initialize
@fieldarray = [] #フィールド要素の配列
@fieldhash = {} #フィールド名でアクセスするためのハッシュ配列
end
#-----------------------------------------------------------------------
# add
#
# フィールド定義を引数に、フィールド配列にフィールド要素を追加する
#
# 引数
# fname:フィールド名
# ftype:型
# fsize:フィールド長
# dec:小数部長
# 戻り値
# なし
#-----------------------------------------------------------------------
def add(fname, ftype, fsize, dec)
@field = DBFfield.new
@field.fieldname = fname
@field.fieldtype = ftype
@field.fieldsize = fsize
@field.decimal = dec
@fieldarray.push(@field)
@fieldhash[@field.fieldname] = @fieldarray.size - 1 #フィールド名に対応するフィールド番号を取得する
end
#-----------------------------------------------------------------------
# fieldname
#
# 概要
# フィールド番号を引数に、フィールド名を返す
#
# 引数
# num:フィールド番号
# 戻り値
# フィールド名
#-----------------------------------------------------------------------
def fieldname(num)
@fieldarray[num].fieldname
end
#-----------------------------------------------------------------------
# item
# フィールドへのアクセス
#
# 概要
# フィールド名を引数に、フィールド記述配列の要素を返す
#
# 引数
# フィールド名
# 戻り値
# フィールド要素
#-----------------------------------------------------------------------
def item(fname)
@fieldarray[@fieldhash[fname]]
end
#-----------------------------------------------------------------------
# numfields
#
# 概要
# フィールド記述配列の要素数を返す
#
# 引数
# なし
# 戻り値
# フィールド数
#-----------------------------------------------------------------------
def numfields
@fieldarray.size
end
end # class DBFfields
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# DBFrecordset
#
# 概要
# データベースファイルは、以下の3つの部分からなる
# 1 ヘッダ先導部
# 2 フィールド記述部
# 3 データレコード部
#
# インターフェイス
# addfield, dbfopen, close, eof, movefirst, movenext, addnew, update
# numfields, fieldname, fieldspec, fields
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
class DBFrecordset
def initialize
@hdlead = DBFheader.new #ヘッダ先導部
@fields = DBFfields.new #フィールド記述配列
@dbfeof = FALSE #EOF
@dbfbof = FALSE #BOF
@headerset = FALSE # ファイルにヘッダ部が書き込まれているか
@currentrecno = -1 # 0から始まるレコード番号
@numrecords = 0 # 1から始まるレコード数
@headerlen = 0 # ヘッダ長
@recordlen = 0 # レコード長
end
#-----------------------------------------------------------------------
# addfield
#
# 引数
# fname:フィールド名
# ftype:型
# fsize:フィールド長
# dec:小数部長
# 戻り値
# なし
#-----------------------------------------------------------------------
def addfield(fname, ftype, fsize, dec)
@fields.add(fname, ftype, fsize, dec)
end
#-----------------------------------------------------------------------
# dbfopen
#
# 概要
# 読込みモードでは、ヘッダを読み込んでフィールド配列にセットする
# 書込みモードでは、オープンするだけで何もしない
#
# 引数
# filename:ファイル名
# openmode:オープンモード "r"=読込み、c=新規作成
# 戻り値
# なし
#-----------------------------------------------------------------------
def dbfopen(filename, openmode)
if openmode != "r" and openmode != "c" then
p "オプションのオープンモード [" + openmode + "] が不正です"
exit
end
@openmode = openmode
# 読み込みモード
if openmode == "r" then
@fp = open(filename, "rb+")
if @fp != nil then
# (dbfヘッダを読み込んでも、いまのところ使うめどはないが)
#1 ヘッダ先導部
@hdlead.version = @fp.read(1)
@hdlead.date1 = @fp.read(1)
@hdlead.date2 = @fp.read(1)
@hdlead.date3 = @fp.read(1)
@hdlead.numrec = @fp.read(4)
@hdlead.headerbytes = @fp.read(2)
@hdlead.recordbytes = @fp.read(2)
@hdlead.reserve = @fp.read(20)
@numrecords = @hdlead.numrec.unpack("l").pop # - 1
@headerlen = @hdlead.headerbytes.unpack("s").pop
@recordlen = @hdlead.recordbytes.unpack("s").pop
#2 フィールド記述部
numfields = (@hdlead.headerbytes.unpack("s").pop - 1) / 32 - 1
count = 0
while count < numfields do
hdldfieldname = @fp.read(11) # フィールドの後ろに詰まっている\000をカットする
hdldfieldtype = @fp.read(1)
hdldreserve1 = @fp.read(4)
hdldfieldsize = @fp.read(1)
hdlddecimal = @fp.read(1)
hdldreserve2 = @fp.read(14)
@fields.add(hdldfieldname.scan(/^[^\000]+/).pop, hdldfieldtype, hdldfieldsize.unpack("C").pop, hdlddecimal.unpack("C").pop)
count += 1
end #while count < numfields do
@headerset = TRUE # ヘッダ部を確認した
else
p "infile open fail"
end
# 新規作成モード
else
@fp = open(filename, "wb+")
end
end #def dbfopen(filename, openmode)
#-----------------------------------------------------------------------
# eof
#-----------------------------------------------------------------------
def eof
@dbfeof
end
#-----------------------------------------------------------------------
# close
# 新規作成モードの場合、ヘッダを書込む
#-----------------------------------------------------------------------
def close
if @openmode == "c" then
putheader
# ファイルの終端マーク(Chr(26)、&H1A、&O32)を書き込む
@fp.seek(0 + @headerlen + @recordlen * @numrecords, File::SEEK_SET)
@fp.write("\x1a")
end
if @fp != nil then
@fp.close
end
end
#-----------------------------------------------------------------------
# movefirst
# ポインタを最初にセットして1レコード読込む
#
# ファイルの読み書き位置は、レコード番号を元に指定する
# ファイル終端にコード(&H1A ?)があるため、レコード数を知ってないとeofを捉えられない
#-----------------------------------------------------------------------
def movefirst
if @headerset == FALSE then
return FALSE
end
@currentrecno = 0
moverecord(@currentrecno)
readrecord
end
#-----------------------------------------------------------------------
# movenext
# レコードポインタ1進め、レコードを読込む
#-----------------------------------------------------------------------
def movenext
@currentrecno += 1
moverecord(@currentrecno)
if @currentrecno >= @numrecords then
@currentrecno = @numrecords - 1
else
readrecord
end
end
#-----------------------------------------------------------------------
# addnew
# 読み書きポインタを最後のレコードの後ろに動かす
# 空のファイルに対する最初のレコード追加の場合、ヘッダ部を書き込んでから読み書きポインタをセットする
#-----------------------------------------------------------------------
def addnew
if @headerset == FALSE then
putheader
end
@currentrecno = @numrecords
@fp.seek(0 + @headerlen + @recordlen * @currentrecno, File::SEEK_SET)
count = 0
while count < (@fields.numfields) do
typechar = @fields.item(@fields.fieldname(count)).fieldtype
if typechar == "N" or typechar == "F" then
@fields.item(@fields.fieldname(count)).value = 0.0
elsif typechar == "C" then
@fields.item(@fields.fieldname(count)).value = ""
else
p "illegal type"
end
count += 1
end
end # while count < (@fields.numfields) do
#-----------------------------------------------------------------------
# update
#-----------------------------------------------------------------------
def update
writerecord
if @currentrecno == @numrecords then # + 1
@numrecords += 1
end
end
#-----------------------------------------------------------------------
# numfields
#-----------------------------------------------------------------------
def numfields
@fields.numfields
end
#-----------------------------------------------------------------------
# fieldname
#-----------------------------------------------------------------------
def fieldname(num)
@fields.fieldname(num)
end
#-----------------------------------------------------------------------
# fieldspec
# フィールド記述配列へのインターフェイス
# 紛らわしいが、次のfieldsでなく、こちらが@fieldsオブジェクトを返す
# さしあたり、使わない
#-----------------------------------------------------------------------
def fieldspec
@fields
end
#-----------------------------------------------------------------------
# fields
# フィールド要素へのインターフェイス
# 紛らわしいが、@fieldオブジェクトを返すのではなく、itemを返す
#-----------------------------------------------------------------------
def fields(fname)
@fields.item(fname)
end
#-----------------------------------------------------------------------
# putheader
#-----------------------------------------------------------------------
def putheader
@fp.seek(0, File::SEEK_SET)
#1 ヘッダ先導部
@fp.write("\003") # バージョンなど
@fp.write([Time.now.strftime("%y").to_i].pack("c")) # 最終更新日(年)
@fp.write([Time.now.strftime("%m").to_i].pack("c")) # 最終更新日(月)
@fp.write([Time.now.strftime("%d").to_i].pack("c")) # 最終更新日(日)
@fp.write([@numrecords].pack("l")) # レコード数
@headerlen = ((@fields.numfields + 1) * 32 + 1) # ヘッダのバイト数 ヘッダ先導部(32バイト)+Σフィールド記述部(32バイト) +1 ヘッダの終わりに1バイト付く
@fp.write([@headerlen].pack("s"))
count = 0
@recordlen = 1
while count < (@fields.numfields) do
@recordlen += @fields.item(@fields.fieldname(count)).fieldsize
count += 1
end
@fp.write([@recordlen].pack("s")) # レコードのバイト数 Σ((フィールド長)+1) レコードの先頭に削除フィールドが付く
@fp.write("\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000") # 予約領域など 20バイト出力
#2 フィールド記述部
count = 0
while count < (@fields.numfields) do
#フィールド名 (DBF ファイルの定義では11バイト)
fieldnamelen = 0
fieldnamearr = @fields.fieldname(count).split(//)
while fieldnamelen < 11 do
if fieldnamearr.size > 0 then
@fp.write(fieldnamearr.shift)
else
@fp.write("\000")
end
fieldnamelen += 1
end
@fp.write(@fields.item(@fields.fieldname(count)).fieldtype) # フィールド型 N,F:数値型、C:文字型
@fp.write("\000\000\000\000") # 予約領域(4バイト)
@fp.write([@fields.item(@fields.fieldname(count)).fieldsize.to_i].pack("C")) # フィールド長
@fp.write([@fields.item(@fields.fieldname(count)).decimal.to_i].pack("C")) # 小数部の長さ
@fp.write("\000\000\000\000\000\000\000\000\000\000\000\000\000\000") # 予約領域など(14バイト)
count += 1
end # while count < (@fields.numfields) do
#2 ヘッダ部(フィールド記述部)の終わりマーク(&H0D)
@fp.write("\x0d")
@headerset = TRUE # ヘッダ部を書き込んだ
end
#-----------------------------------------------------------------------
# writerecord
#-----------------------------------------------------------------------
def writerecord
# 1バイト空白を出力(dbfファイル仕様の削除マーク)
@fp.write(" ")
count = 0
while count < (@fields.numfields) do
typechar = @fields.item(@fields.fieldname(count)).fieldtype
if typechar == "N" or typechar == "F" then
# 例:@fp.printf("%8.3f", value)
@fp.printf("%" + @fields.item(@fields.fieldname(count)).fieldsize.to_s + "." + @fields.item(@fields.fieldname(count)).decimal.to_s + "f", @fields.item(@fields.fieldname(count)).value)
elsif typechar == "C" then
# 例:@fp.printf("%-8s", value)
@fp.printf("%-" + @fields.item(@fields.fieldname(count)).fieldsize.to_s + "s", @fields.item(@fields.fieldname(count)).value)
else
p "illegal type"
end
count += 1
end # while count < (@fields.numfields) do
end
#-----------------------------------------------------------------------
# readrecord
#-----------------------------------------------------------------------
def readrecord
# 1バイト読み捨てる(dbfファイル仕様の削除マーク)
@fp.read(1)
count = 0
while count < (@fields.numfields) do
typechar = @fields.item(@fields.fieldname(count)).fieldtype
if typechar == "N" or typechar == "F" then
@fields.item(@fields.fieldname(count)).value = @fp.read(@fields.item(@fields.fieldname(count)).fieldsize).to_f
elsif typechar == "C" then
@fields.item(@fields.fieldname(count)).value = @fp.read(@fields.item(@fields.fieldname(count)).fieldsize)
else
p "illegal type"
end
count += 1
end # while count < (@fields.numfields) do
end
#-----------------------------------------------------------------------
# moverecord
# ポインタを引数のレコード番号にセットする
#
# 引数
# recno:レコード番号
# 戻り値
# なし
#-----------------------------------------------------------------------
def moverecord(recno)
if recno >= @numrecords then
@dbfeof = TRUE
elsif recno < 0 then
@dbfbof = TRUE
else
@dbfeof = FALSE
@dbfbof = FALSE
# 読み書きの開始位置にセット ヘッダ部の最後に1バイトのコード(&H0D)がある
# バイト位置は0から始まる。レコード番号は0から始まる。
@fp.seek(0 + @headerlen + @recordlen * recno, File::SEEK_SET)
end
end
#-----------------------------------------------------------------------
# 呼び出し制限
#-----------------------------------------------------------------------
protected :putheader, :writerecord, :readrecord, :moverecord
end # class DBFrecordset