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