Skip to content

Commit

Permalink
column gets version and encoding from table
Browse files Browse the repository at this point in the history
  • Loading branch information
infused committed Apr 19, 2014
1 parent e5700ff commit 36065c0
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 49 deletions.
45 changes: 29 additions & 16 deletions lib/dbf/column/base.rb
Expand Up @@ -7,19 +7,25 @@ class LengthError < StandardError; end
class NameError < StandardError; end

class Base
attr_reader :name, :type, :length, :decimal
attr_reader :table, :name, :type, :length, :decimal

# Initialize a new DBF::Column
#
# @param [String] name
# @param [String] type
# @param [Fixnum] length
# @param [Fixnum] decimal
def initialize(name, type, length, decimal, version, encoding=nil)
@name, @type, @length, @decimal, @version, @encoding = clean(name), type, length, decimal, version, encoding
def initialize(table, name, type, length, decimal)
@table = table
@name = clean(name)
@type = type
@length = length
@decimal = decimal
@version = table.version
@encoding = table.encoding

raise LengthError, "field length must be greater than 0" unless length > 0
raise NameError, "column name cannot be empty" if @name.length == 0
raise NameError, "column name cannot be empty" if @name.empty?
end

# Cast value to native type
Expand All @@ -40,6 +46,9 @@ def type_cast(value)
end
end

# Returns true if the column is a memo
#
# @return [Boolean]
def memo?
@memo ||= type == 'M'
end
Expand All @@ -51,16 +60,20 @@ def schema_definition
"\"#{underscored_name}\", #{schema_data_type}\n"
end

def self.underscore_name(string)
string.gsub(/::/, '/').
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
gsub(/([a-z\d])([A-Z])/,'\1_\2').
tr('-', '_').
downcase
end

# Underscored name
#
# This is the column name converted to underscore format.
# For example, MyColumn will be returned as my_column.
#
# @return [String]
def underscored_name
@underscored_name ||= self.class.underscore_name(name)
@underscored_name ||= begin
name.gsub(/::/, '/').
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
gsub(/([a-z\d])([A-Z])/,'\1_\2').
tr('-', '_').
downcase
end
end

private
Expand Down Expand Up @@ -95,10 +108,10 @@ def boolean(value) #nodoc
end

def encode_string(value) #nodoc
if @encoding
if String.new.respond_to?(:encoding)
if @encoding && table.supports_encoding?
if table.supports_string_encoding?
value.force_encoding(@encoding).encode(Encoding.default_external, :undef => :replace, :invalid => :replace)
else
elsif table.supports_iconv?
Iconv.conv('UTF-8', @encoding, value)
end
else
Expand Down
14 changes: 9 additions & 5 deletions lib/dbf/table.rb
Expand Up @@ -64,7 +64,7 @@ def initialize(data, memo = nil, encoding = nil)
begin
@data = open_data(data)
@data.rewind
@header = Header.new(@data.read(DBF_HEADER_SIZE), supports_encoding? || supports_iconv?)
@header = Header.new(@data.read(DBF_HEADER_SIZE), supports_encoding?)
@encoding = encoding || header.encoding
@memo = open_memo(data, memo)
rescue StandardError => error
Expand Down Expand Up @@ -227,18 +227,23 @@ def columns
@data.seek(DBF_HEADER_SIZE)
columns = []
while !["\0", "\r"].include?(first_byte = @data.read(1))
column_data = first_byte + @data.read(31)
column_data = first_byte + @data.read(DBF_HEADER_SIZE - 1)
name, type, length, decimal = column_data.unpack('a10 x a x4 C2')
name.strip!
if length > 0
columns << column_class.new(name.strip, type, length, decimal, version, encoding)
columns << column_class.new(self, name, type, length, decimal)
end
end
columns
end
end

def supports_encoding?
String.new.respond_to?(:encoding)
supports_string_encoding? || supports_iconv?
end

def supports_string_encoding?
''.respond_to?(:encoding)
end

def supports_iconv?
Expand Down Expand Up @@ -324,7 +329,6 @@ def seek_to_record(index) #nodoc

def csv_class #nodoc
@csv_class ||= CSV.const_defined?(:Reader) ? FCSV : CSV

end
end

Expand Down
54 changes: 28 additions & 26 deletions spec/dbf/column_spec.rb
Expand Up @@ -3,8 +3,10 @@
require "spec_helper"

describe DBF::Column::Dbase do
let(:table) { DBF::Table.new fixture_path('dbase_30.dbf')}

context "when initialized" do
let(:column) { DBF::Column::Dbase.new "ColumnName", "N", 1, 0, "30" }
let(:column) { DBF::Column::Dbase.new table, "ColumnName", "N", 1, 0 }

it "sets :name accessor" do
expect(column.name).to eq "ColumnName"
Expand All @@ -24,19 +26,19 @@

describe 'with length of 0' do
it 'raises DBF::Column::LengthError' do
expect { DBF::Column::Dbase.new "ColumnName", "N", 0, 0, "30" }.to raise_error(DBF::Column::LengthError)
expect { DBF::Column::Dbase.new table, "ColumnName", "N", 0, 0 }.to raise_error(DBF::Column::LengthError)
end
end

describe 'with length less than 0' do
it 'raises DBF::Column::LengthError' do
expect { DBF::Column::Dbase.new "ColumnName", "N", -1, 0, "30" }.to raise_error(DBF::Column::LengthError)
expect { DBF::Column::Dbase.new table, "ColumnName", "N", -1, 0 }.to raise_error(DBF::Column::LengthError)
end
end

describe 'with empty column name' do
it 'raises DBF::Column::NameError' do
expect { DBF::Column::Dbase.new "\xFF\xFC", "N", 1, 0, "30" }.to raise_error(DBF::Column::NameError)
expect { DBF::Column::Dbase.new table, "\xFF\xFC", "N", 1, 0 }.to raise_error(DBF::Column::NameError)
end
end
end
Expand All @@ -46,7 +48,7 @@
context 'and 0 decimals' do
it 'casts value to Fixnum' do
value = '135'
column = DBF::Column::Dbase.new "ColumnName", "N", 3, 0, "30"
column = DBF::Column::Dbase.new table, "ColumnName", "N", 3, 0
expect(column.type_cast(value)).to be_a(Fixnum)
expect(column.type_cast(value)).to eq 135
end
Expand All @@ -55,7 +57,7 @@
context 'and more than 0 decimals' do
it 'casts value to Float' do
value = '13.5'
column = DBF::Column::Dbase.new "ColumnName", "N", 2, 1, "30"
column = DBF::Column::Dbase.new table, "ColumnName", "N", 2, 1
expect(column.type_cast(value)).to be_a(Float)
expect(column.type_cast(value)).to eq 13.5
end
Expand All @@ -65,7 +67,7 @@
context 'with type F (float)' do
it 'casts value to Float' do
value = '135'
column = DBF::Column::Dbase.new "ColumnName", "F", 3, 0, "30"
column = DBF::Column::Dbase.new table, "ColumnName", "F", 3, 0
expect(column.type_cast(value)).to be_a(Float)
expect(column.type_cast(value)).to eq 135.0
end
Expand All @@ -74,13 +76,13 @@
context 'with type I (integer)' do
it "casts value to Fixnum" do
value = "\203\171\001\000"
column = DBF::Column::Dbase.new "ColumnName", "I", 3, 0, "30"
column = DBF::Column::Dbase.new table, "ColumnName", "I", 3, 0
expect(column.type_cast(value)).to eq 96643
end
end

context 'with type L (logical/boolean)' do
let(:column) { DBF::Column::Dbase.new "ColumnName", "L", 1, 0, "30" }
let(:column) { DBF::Column::Dbase.new table, "ColumnName", "L", 1, 0 }

it "casts 'y' to true" do
expect(column.type_cast('y')).to eq true
Expand All @@ -96,7 +98,7 @@
end

context 'with type T (datetime)' do
let(:column) { DBF::Column::Dbase.new "ColumnName", "T", 16, 0, "30" }
let(:column) { DBF::Column::Dbase.new table, "ColumnName", "T", 16, 0 }

context 'with valid datetime' do
it "casts to DateTime" do
Expand Down Expand Up @@ -124,7 +126,7 @@
end

context 'with type D (date)' do
let(:column) { DBF::Column::Dbase.new "ColumnName", "D", 8, 0, "30" }
let(:column) { DBF::Column::Dbase.new table, "ColumnName", "D", 8, 0 }

context 'with valid date' do
it "casts to Date" do
Expand All @@ -141,19 +143,19 @@

context 'with type M (memo)' do
it "casts to string" do
column = DBF::Column::Dbase.new "ColumnName", "M", 3, 0, "30"
column = DBF::Column::Dbase.new table, "ColumnName", "M", 3, 0
expect(column.type_cast('abc')).to be_a(String)
end

it 'casts nil to nil' do
column = DBF::Column::Dbase.new "ColumnName", "M", 3, 0, "30"
column = DBF::Column::Dbase.new table, "ColumnName", "M", 3, 0
expect(column.type_cast(nil)).to be_nil
end
end
end

context 'with type Y (currency)' do
let(:column) { DBF::Column::Dbase.new "ColumnName", "Y", 8, 4, "31" }
let(:column) { DBF::Column::Dbase.new table, "ColumnName", "Y", 8, 4 }

it 'casts to float' do
expect(column.type_cast(" \xBF\x02\x00\x00\x00\x00\x00")).to eq 18.0
Expand All @@ -163,7 +165,7 @@
context "#schema_definition" do
context 'with type N (number)' do
it "outputs an integer column" do
column = DBF::Column::Dbase.new "ColumnName", "N", 1, 0, "30"
column = DBF::Column::Dbase.new table, "ColumnName", "N", 1, 0
expect(column.schema_definition).to eq "\"column_name\", :integer\n"
end
end
Expand All @@ -172,65 +174,65 @@
context "with Foxpro dbf" do
context "when decimal is greater than 0" do
it "outputs an float column" do
column = DBF::Column::Dbase.new "ColumnName", "B", 1, 2, "f5"
column = DBF::Column::Dbase.new table, "ColumnName", "B", 1, 2
expect(column.schema_definition).to eq "\"column_name\", :float\n"
end
end

context "when decimal is 0" do
it "outputs an integer column" do
column = DBF::Column::Dbase.new "ColumnName", "B", 1, 0, "f5"
column = DBF::Column::Dbase.new table, "ColumnName", "B", 1, 0
expect(column.schema_definition).to eq "\"column_name\", :integer\n"
end
end
end
end

it "defines a float colmn if type is (N)umber with more than 0 decimals" do
column = DBF::Column::Dbase.new "ColumnName", "N", 1, 2, "30"
column = DBF::Column::Dbase.new table, "ColumnName", "N", 1, 2
expect(column.schema_definition).to eq "\"column_name\", :float\n"
end

it "defines a date column if type is (D)ate" do
column = DBF::Column::Dbase.new "ColumnName", "D", 8, 0, "30"
column = DBF::Column::Dbase.new table, "ColumnName", "D", 8, 0
expect(column.schema_definition).to eq "\"column_name\", :date\n"
end

it "defines a datetime column if type is (D)ate" do
column = DBF::Column::Dbase.new "ColumnName", "T", 16, 0, "30"
column = DBF::Column::Dbase.new table, "ColumnName", "T", 16, 0
expect(column.schema_definition).to eq "\"column_name\", :datetime\n"
end

it "defines a boolean column if type is (L)ogical" do
column = DBF::Column::Dbase.new "ColumnName", "L", 1, 0, "30"
column = DBF::Column::Dbase.new table, "ColumnName", "L", 1, 0
expect(column.schema_definition).to eq "\"column_name\", :boolean\n"
end

it "defines a text column if type is (M)emo" do
column = DBF::Column::Dbase.new "ColumnName", "M", 1, 0, "30"
column = DBF::Column::Dbase.new table, "ColumnName", "M", 1, 0
expect(column.schema_definition).to eq "\"column_name\", :text\n"
end

it "defines a string column with length for any other data types" do
column = DBF::Column::Dbase.new "ColumnName", "X", 20, 0, "30"
column = DBF::Column::Dbase.new table, "ColumnName", "X", 20, 0
expect(column.schema_definition).to eq "\"column_name\", :string, :limit => 20\n"
end
end

context "#name" do
it "contains only ASCII characters" do
column = DBF::Column::Dbase.new "--\x1F-\x68\x65\x6C\x6C\x6F world-\x80--", "N", 1, 0, "30"
column = DBF::Column::Dbase.new table, "--\x1F-\x68\x65\x6C\x6C\x6F world-\x80--", "N", 1, 0
expect(column.name).to eq "---hello world---"
end

it "is truncated at the null character" do
column = DBF::Column::Dbase.new "--\x1F-\x68\x65\x6C\x6C\x6F \x00 world-\x80--", "N", 1, 0, "30"
column = DBF::Column::Dbase.new table, "--\x1F-\x68\x65\x6C\x6C\x6F \x00 world-\x80--", "N", 1, 0
expect(column.name).to eq "---hello "
end
end

context '#decode_date' do
let(:column) { DBF::Column::Dbase.new "ColumnName", "N", 1, 0, "30" }
let(:column) { DBF::Column::Dbase.new table, "ColumnName", "N", 1, 0 }

it 'is nil if value is blank' do
expect(column.send(:decode_date, '')).to be_nil
Expand Down
4 changes: 2 additions & 2 deletions spec/dbf/record_spec.rb
Expand Up @@ -67,7 +67,7 @@
let(:record) { table.find(0) }

it 'should automatically encodes to default system encoding' do
if table.supports_encoding?
if table.supports_string_encoding?
expect(record.name.encoding).to eq Encoding.default_external
expect(record.name.encode("UTF-8").unpack("H4")).to eq ["d0b0"] # russian a
end
Expand All @@ -79,7 +79,7 @@
let(:record) { table.find(0) }

it 'should transcode from manually specified encoding to default system encoding' do
if table.supports_encoding?
if table.supports_string_encoding?
expect(record.name.encoding).to eq Encoding.default_external
expect(record.name.encode("UTF-8").unpack("H4")).to eq ["d180"] # russian а encoded in cp1251 and read as if it was encoded in cp866
end
Expand Down

0 comments on commit 36065c0

Please sign in to comment.