philiprehberger-csv_builder 0.1.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +128 -2
- data/lib/philiprehberger/csv_builder/builder.rb +96 -7
- data/lib/philiprehberger/csv_builder/column.rb +4 -2
- data/lib/philiprehberger/csv_builder/version.rb +1 -1
- data/lib/philiprehberger/csv_builder.rb +4 -2
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bc5cd28c86059cae0603624ace5e07a070875268b942adf798d664755e028e91
|
|
4
|
+
data.tar.gz: 5cc96a1439b056f18e53cb2eb8af749ee2be05c74fcae55bd1aa31fb694a2016
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 867447e6eb9146acaa103b6167683745ca589f2d5cc59e78de72b9cf8191e956e6f639a2cc313b7c2d6e8249bb841e0ee3f3ced92e61fef4f18d8aba4ab2afbe
|
|
7
|
+
data.tar.gz: 9464cab4f2cc762d16458a066a2268dff54bbfb72a897e9188837e5b34a78b490b9cf11018d63cb5ba43ffb10880465bbd792dd1cd405d038e158e85585dcabf
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-04-09
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Record sorting via `sort_by` DSL method with `:asc`/`:desc` direction support
|
|
14
|
+
|
|
15
|
+
## [0.2.0] - 2026-04-03
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Custom CSV delimiters via `delimiter:` option
|
|
19
|
+
- Custom quote character via `quote_char:` option
|
|
20
|
+
- Column header aliasing via `header:` option on columns
|
|
21
|
+
- Record filtering via `filter`
|
|
22
|
+
- Auto-incrementing row numbers via `row_number`
|
|
23
|
+
- Streaming output via `to_io`
|
|
24
|
+
|
|
25
|
+
## [0.1.5] - 2026-03-31
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Add GitHub issue templates, dependabot config, and PR template
|
|
29
|
+
|
|
10
30
|
## [0.1.4] - 2026-03-31
|
|
11
31
|
|
|
12
32
|
### Changed
|
data/README.md
CHANGED
|
@@ -71,6 +71,128 @@ end
|
|
|
71
71
|
builder.to_file('output.csv')
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
### Custom Delimiters
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
builder = Philiprehberger::CsvBuilder.build(records, delimiter: "\t") do
|
|
78
|
+
column :name
|
|
79
|
+
column :email
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
puts builder.to_csv
|
|
83
|
+
# name email
|
|
84
|
+
# Alice alice@example.com
|
|
85
|
+
# Bob bob@example.com
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
You can also set a custom quote character:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
builder = Philiprehberger::CsvBuilder.build(records, quote_char: "'") do
|
|
92
|
+
column :name
|
|
93
|
+
column :email
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Column Aliases
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
builder = Philiprehberger::CsvBuilder.build(records) do
|
|
101
|
+
column :name, header: 'Full Name'
|
|
102
|
+
column :email, header: 'Email Address'
|
|
103
|
+
column(:status, header: 'Active?') { |r| r[:active] ? 'Yes' : 'No' }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
puts builder.to_csv
|
|
107
|
+
# Full Name,Email Address,Active?
|
|
108
|
+
# Alice,alice@example.com,Yes
|
|
109
|
+
# Bob,bob@example.com,No
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Filtering
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
builder = Philiprehberger::CsvBuilder.build(records) do
|
|
116
|
+
column :name
|
|
117
|
+
column :email
|
|
118
|
+
filter { |r| r[:active] }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
puts builder.to_csv
|
|
122
|
+
# name,email
|
|
123
|
+
# Alice,alice@example.com
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Multiple filters are combined with AND logic:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
builder = Philiprehberger::CsvBuilder.build(records) do
|
|
130
|
+
column :name
|
|
131
|
+
filter { |r| r[:active] }
|
|
132
|
+
filter { |r| r[:name].start_with?('A') }
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Row Numbers
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
builder = Philiprehberger::CsvBuilder.build(records) do
|
|
140
|
+
column :name
|
|
141
|
+
column :email
|
|
142
|
+
row_number
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
puts builder.to_csv
|
|
146
|
+
# #,name,email
|
|
147
|
+
# 1,Alice,alice@example.com
|
|
148
|
+
# 2,Bob,bob@example.com
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Customize the header label:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
row_number(header: 'Row')
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Sorting
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
builder = Philiprehberger::CsvBuilder.build(records) do
|
|
161
|
+
column :name
|
|
162
|
+
column :email
|
|
163
|
+
sort_by { |r| r[:name] }
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Sort descending:
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
builder = Philiprehberger::CsvBuilder.build(records) do
|
|
171
|
+
column :name
|
|
172
|
+
sort_by(direction: :desc) { |r| r[:name] }
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Streaming
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
File.open('output.csv', 'w') do |file|
|
|
180
|
+
builder = Philiprehberger::CsvBuilder.build(records) do
|
|
181
|
+
column :name
|
|
182
|
+
column :email
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
builder.to_io(file)
|
|
186
|
+
end
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Works with any IO object, including `StringIO`:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
io = StringIO.new
|
|
193
|
+
builder.to_io(io)
|
|
194
|
+
```
|
|
195
|
+
|
|
74
196
|
### Headers
|
|
75
197
|
|
|
76
198
|
```ruby
|
|
@@ -86,10 +208,14 @@ builder.headers # => ["name", "email"]
|
|
|
86
208
|
|
|
87
209
|
| Method | Description |
|
|
88
210
|
|--------|-------------|
|
|
89
|
-
| `CsvBuilder.build(records, &block)` | Build a CSV using the column DSL |
|
|
90
|
-
| `Builder#column(name, &block)` | Define a column with optional transform |
|
|
211
|
+
| `CsvBuilder.build(records, delimiter:, quote_char:, &block)` | Build a CSV using the column DSL |
|
|
212
|
+
| `Builder#column(name, header:, &block)` | Define a column with optional alias and transform |
|
|
213
|
+
| `Builder#filter(&block)` | Filter records (block returns true to include) |
|
|
214
|
+
| `Builder#sort_by(direction:, &block)` | Sort records by block key (`:asc` or `:desc`) |
|
|
215
|
+
| `Builder#row_number(header:)` | Add auto-incrementing row number column |
|
|
91
216
|
| `Builder#to_csv` | Generate CSV as a string |
|
|
92
217
|
| `Builder#to_file(path)` | Write CSV to a file |
|
|
218
|
+
| `Builder#to_io(io)` | Stream CSV to any IO object |
|
|
93
219
|
| `Builder#headers` | Return column header names |
|
|
94
220
|
|
|
95
221
|
## Development
|
|
@@ -14,19 +14,63 @@ module Philiprehberger
|
|
|
14
14
|
attr_reader :records
|
|
15
15
|
|
|
16
16
|
# @param records [Array] the source records
|
|
17
|
-
|
|
17
|
+
# @param delimiter [String] the column separator (default: ",")
|
|
18
|
+
# @param quote_char [String] the quote character (default: '"')
|
|
19
|
+
def initialize(records, delimiter: ',', quote_char: '"')
|
|
18
20
|
@records = records
|
|
19
21
|
@columns = []
|
|
22
|
+
@filters = []
|
|
23
|
+
@row_number_header = nil
|
|
24
|
+
@delimiter = delimiter
|
|
25
|
+
@quote_char = quote_char
|
|
26
|
+
@sort_by = nil
|
|
27
|
+
@sort_direction = :asc
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Sort records before CSV output
|
|
31
|
+
#
|
|
32
|
+
# @param direction [Symbol] :asc (default) or :desc
|
|
33
|
+
# @yield [record] block returning the sort key
|
|
34
|
+
# @yieldparam record [Object] the source record
|
|
35
|
+
# @return [self]
|
|
36
|
+
# @raise [Error] if direction is not :asc or :desc
|
|
37
|
+
def sort_by(direction: :asc, &block)
|
|
38
|
+
raise Error, 'A block is required for sort_by' unless block
|
|
39
|
+
raise Error, "direction must be :asc or :desc (got #{direction.inspect})" unless %i[asc desc].include?(direction)
|
|
40
|
+
|
|
41
|
+
@sort_by = block
|
|
42
|
+
@sort_direction = direction
|
|
43
|
+
self
|
|
20
44
|
end
|
|
21
45
|
|
|
22
46
|
# Define a column
|
|
23
47
|
#
|
|
24
48
|
# @param name [Symbol, String] the column name
|
|
49
|
+
# @param header [String, nil] optional custom header label
|
|
25
50
|
# @yield [record] optional block to transform the value
|
|
26
51
|
# @yieldparam record [Object] the source record
|
|
27
52
|
# @return [self]
|
|
28
|
-
def column(name, &)
|
|
29
|
-
@columns << Column.new(name, &)
|
|
53
|
+
def column(name, header: nil, &block)
|
|
54
|
+
@columns << Column.new(name, header: header, &block)
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Add a filter to exclude records
|
|
59
|
+
#
|
|
60
|
+
# @yield [record] block that returns true to include the record
|
|
61
|
+
# @yieldparam record [Object] the source record
|
|
62
|
+
# @return [self]
|
|
63
|
+
def filter(&block)
|
|
64
|
+
@filters << block
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Add an auto-incrementing row number as the first column
|
|
69
|
+
#
|
|
70
|
+
# @param header [String] the header label for the row number column
|
|
71
|
+
# @return [self]
|
|
72
|
+
def row_number(header: '#')
|
|
73
|
+
@row_number_header = header
|
|
30
74
|
self
|
|
31
75
|
end
|
|
32
76
|
|
|
@@ -34,17 +78,33 @@ module Philiprehberger
|
|
|
34
78
|
#
|
|
35
79
|
# @return [Array<String>]
|
|
36
80
|
def headers
|
|
37
|
-
@columns.map(&:header)
|
|
81
|
+
base = @columns.map(&:header)
|
|
82
|
+
@row_number_header ? [@row_number_header] + base : base
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Return the filtered records
|
|
86
|
+
#
|
|
87
|
+
# @return [Array]
|
|
88
|
+
def filtered_records
|
|
89
|
+
result = @records
|
|
90
|
+
@filters.each do |f|
|
|
91
|
+
result = result.select(&f)
|
|
92
|
+
end
|
|
93
|
+
if @sort_by
|
|
94
|
+
result = result.sort_by(&@sort_by)
|
|
95
|
+
result = result.reverse if @sort_direction == :desc
|
|
96
|
+
end
|
|
97
|
+
result
|
|
38
98
|
end
|
|
39
99
|
|
|
40
100
|
# Generate the CSV as a string
|
|
41
101
|
#
|
|
42
102
|
# @return [String]
|
|
43
103
|
def to_csv
|
|
44
|
-
CSV.generate do |csv|
|
|
104
|
+
CSV.generate(**csv_options) do |csv|
|
|
45
105
|
csv << headers
|
|
46
|
-
|
|
47
|
-
csv <<
|
|
106
|
+
filtered_records.each_with_index do |record, index|
|
|
107
|
+
csv << build_row(record, index)
|
|
48
108
|
end
|
|
49
109
|
end
|
|
50
110
|
end
|
|
@@ -56,6 +116,35 @@ module Philiprehberger
|
|
|
56
116
|
def to_file(path)
|
|
57
117
|
File.write(path, to_csv)
|
|
58
118
|
end
|
|
119
|
+
|
|
120
|
+
# Stream CSV to any IO object
|
|
121
|
+
#
|
|
122
|
+
# @param io [IO, StringIO] the IO object to write to
|
|
123
|
+
# @return [void]
|
|
124
|
+
def to_io(io)
|
|
125
|
+
csv = CSV.new(io, **csv_options)
|
|
126
|
+
csv << headers
|
|
127
|
+
filtered_records.each_with_index do |record, index|
|
|
128
|
+
csv << build_row(record, index)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# @return [Hash] CSV library options
|
|
135
|
+
def csv_options
|
|
136
|
+
{ col_sep: @delimiter, quote_char: @quote_char }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Build a single row array for the given record
|
|
140
|
+
#
|
|
141
|
+
# @param record [Object] the source record
|
|
142
|
+
# @param index [Integer] zero-based row index
|
|
143
|
+
# @return [Array]
|
|
144
|
+
def build_row(record, index)
|
|
145
|
+
row = @columns.map { |col| col.extract(record) }
|
|
146
|
+
@row_number_header ? [index + 1] + row : row
|
|
147
|
+
end
|
|
59
148
|
end
|
|
60
149
|
end
|
|
61
150
|
end
|
|
@@ -11,9 +11,11 @@ module Philiprehberger
|
|
|
11
11
|
attr_reader :transform
|
|
12
12
|
|
|
13
13
|
# @param name [Symbol, String] the column name (also used as the record key)
|
|
14
|
+
# @param header [String, nil] optional custom header label
|
|
14
15
|
# @param transform [Proc, nil] optional block to transform the value
|
|
15
|
-
def initialize(name, &transform)
|
|
16
|
+
def initialize(name, header: nil, &transform)
|
|
16
17
|
@name = name.to_sym
|
|
18
|
+
@custom_header = header
|
|
17
19
|
@transform = block_given? ? transform : nil
|
|
18
20
|
end
|
|
19
21
|
|
|
@@ -37,7 +39,7 @@ module Philiprehberger
|
|
|
37
39
|
#
|
|
38
40
|
# @return [String]
|
|
39
41
|
def header
|
|
40
|
-
@name.to_s
|
|
42
|
+
@custom_header || @name.to_s
|
|
41
43
|
end
|
|
42
44
|
end
|
|
43
45
|
end
|
|
@@ -11,14 +11,16 @@ module Philiprehberger
|
|
|
11
11
|
# Build a CSV from records using a declarative DSL
|
|
12
12
|
#
|
|
13
13
|
# @param records [Array] the source records
|
|
14
|
+
# @param delimiter [String] the column separator (default: ",")
|
|
15
|
+
# @param quote_char [String] the quote character (default: '"')
|
|
14
16
|
# @yield [builder] the builder instance for defining columns
|
|
15
17
|
# @yieldparam builder [Builder]
|
|
16
18
|
# @return [Builder] the configured builder
|
|
17
19
|
# @raise [Error] if no block is given
|
|
18
|
-
def self.build(records, &block)
|
|
20
|
+
def self.build(records, delimiter: ',', quote_char: '"', &block)
|
|
19
21
|
raise Error, 'A block is required' unless block
|
|
20
22
|
|
|
21
|
-
builder = Builder.new(records)
|
|
23
|
+
builder = Builder.new(records, delimiter: delimiter, quote_char: quote_char)
|
|
22
24
|
builder.instance_eval(&block)
|
|
23
25
|
builder
|
|
24
26
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-csv_builder
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-04-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Build CSV files from record collections using a declarative DSL with
|
|
14
14
|
column definitions, custom transforms, and file output support.
|
|
@@ -25,11 +25,11 @@ files:
|
|
|
25
25
|
- lib/philiprehberger/csv_builder/builder.rb
|
|
26
26
|
- lib/philiprehberger/csv_builder/column.rb
|
|
27
27
|
- lib/philiprehberger/csv_builder/version.rb
|
|
28
|
-
homepage: https://
|
|
28
|
+
homepage: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-csv_builder
|
|
29
29
|
licenses:
|
|
30
30
|
- MIT
|
|
31
31
|
metadata:
|
|
32
|
-
homepage_uri: https://
|
|
32
|
+
homepage_uri: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-csv_builder
|
|
33
33
|
source_code_uri: https://github.com/philiprehberger/rb-csv-builder
|
|
34
34
|
changelog_uri: https://github.com/philiprehberger/rb-csv-builder/blob/main/CHANGELOG.md
|
|
35
35
|
bug_tracker_uri: https://github.com/philiprehberger/rb-csv-builder/issues
|