-
-
Notifications
You must be signed in to change notification settings - Fork 190
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
156 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |