Skip to content

Commit

Permalink
adding feature to output CSV files
Browse files Browse the repository at this point in the history
  • Loading branch information
tilo committed Jun 29, 2024
1 parent 07160c1 commit 628bf6d
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ Style/SlicingWithRange:
Style/SpecialGlobalVars: # DANGER: unsafe rule!!
Enabled: false

Style/StringConcatenation:
Enabled: false

Style/StringLiterals:
Enabled: false
EnforcedStyle: double_quotes
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@

# SmarterCSV 1.x Change Log

## 1.11.0
* added feature to output CSV files ([issue #44](https://github.com/tilo/smarter_csv/issues/44))

## 1.10.3 (2024-03-10)
* fixed issue when frozen options are handed in (thanks to Daniel Pepper)
* cleaned-up rspec tests (thanks to Daniel Pepper)
Expand Down
1 change: 1 addition & 0 deletions lib/smarter_csv.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require "smarter_csv/headers"
require "smarter_csv/hash_transformations"
require "smarter_csv/parse"
require "smarter_csv/generator"

# load the C-extension:
case RUBY_ENGINE
Expand Down
56 changes: 56 additions & 0 deletions lib/smarter_csv/generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module SmarterCSV
#
# Generate CSV files from batches of array_of_hashes data
# - automatically generates the header on-the-fly
# - automatically quotes fields containing the col_sep
#
# Optionally headers can be passed-in via the options,
# If any new headers are fund in the data, they will be appended to the headers.
#
class Generator
def initialize(file_path, options = {})
@options = options
@headers = options[:headers] || []
@col_sep = options[:col_sep] || ','
@force_quotes = options[:force_quotes]
@map_headers = options[:map_headers] || {}
@file = File.open(file_path, 'w+')
end

def append(array_of_hashes)
array_of_hashes.each do |hash|
hash_keys = hash.keys
new_keys = hash_keys - @headers
@headers.concat(new_keys)

# Reorder the hash to match the current headers order and fill missing fields
ordered_row = @headers.map { |header| hash[header] || '' }

@file.puts ordered_row.map { |value| escape_csv_field(value) }.join(@col_sep)
end
end

def finalize
# Map headers if :map_headers option is provided
mapped_headers = @headers.map { |header| @map_headers[header] || header }

# Rewind to the beginning of the file to write the headers
@file.rewind
@file.write(mapped_headers.join(@col_sep) + "\n")
@file.flush # Ensure all data is written to the file
@file.close
end

private

def escape_csv_field(field)
if @force_quotes || field.to_s.include?(@col_sep)
"\"#{field}\""
else
field.to_s
end
end
end
end
93 changes: 93 additions & 0 deletions spec/smarter_csv/generator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# frozen_string_literal: true

RSpec.describe SmarterCSV::Generator do
let(:file_path) { 'test_output.csv' }

after(:each) do
File.delete(file_path) if File.exist?(file_path)
end

context 'when headers are given in advance' do
let(:options) { { headers: %w[name age city] } }
let(:data_batches) do
[
[
{ name: 'John', age: 30, city: 'New York' },
{ name: 'Jane', age: 25, country: 'USA' }
],
[
{ name: 'Mike', age: 35, city: 'Chicago', state: 'IL' }
]
]
end

it 'writes the given headers and data correctly' do
generator = SmarterCSV::Generator.new(file_path, options)
data_batches.each { |batch| generator.append(batch) }
generator.finalize

output = File.read(file_path)
expect(output).to include("name,age,city\n")
expect(output).to include("John,30,New York\n")
expect(output).to include("Jane,25,\n")
expect(output).to include("Mike,35,Chicago\n")
end
end

context 'when headers are automatically discovered' do
let(:data_batches) do
[
[
{ name: 'John', age: 30, city: 'New York' },
{ name: 'Jane', age: 25, country: 'USA' }
],
[
{ name: 'Mike', age: 35, city: 'Chicago', state: 'IL' }
]
]
end

it 'writes the discovered headers and data correctly' do
generator = SmarterCSV::Generator.new(file_path)
data_batches.each { |batch| generator.append(batch) }
generator.finalize

output = File.read(file_path)
expect(output).to include("name,age,city,country,state\n")
expect(output).to include("John,30,New York,,\n")
expect(output).to include("Jane,25,,USA,\n")
expect(output).to include("Mike,35,Chicago,,IL\n")
end
end

context 'when headers are mapped' do
let(:options) do
{
map_headers: { name: 'Full Name', age: 'Age', city: 'City', country: 'Country', state: 'State' }
}
end
let(:data_batches) do
[
[
{ name: 'John', age: 30, city: 'New York' },
{ name: 'Jane', age: 25, country: 'USA' }
],
[
{ name: 'Mike', age: 35, city: 'Chicago', state: 'IL' }
]
]
end

it 'writes the mapped headers and data correctly' do
generator = SmarterCSV::Generator.new(file_path, options)
data_batches.each { |batch| generator.append(batch) }
generator.finalize

output = File.read(file_path)
expect(output).to include("Full Name,Age,City,Country,State\n")
expect(output).to include("John,30,New York,,\n")
expect(output).to include("Jane,25,,USA,\n")
expect(output).to include("Mike,35,Chicago,,IL\n")
end
end
end

0 comments on commit 628bf6d

Please sign in to comment.