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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9befa4c64115384c05f950d6f3c4e73ddf4e3e364d88408cbbe75358924ecfde
4
- data.tar.gz: 1e93bbf0bff87dacb53e57b0be7c3e457d955d38a39d0144dbbd0124f6c0def1
3
+ metadata.gz: bc5cd28c86059cae0603624ace5e07a070875268b942adf798d664755e028e91
4
+ data.tar.gz: 5cc96a1439b056f18e53cb2eb8af749ee2be05c74fcae55bd1aa31fb694a2016
5
5
  SHA512:
6
- metadata.gz: 0336bf8d47213dbc2b5a8811671968c42d62025338d6872f9d88a95a7976b753c09bc49eecab612b6a7e48819792c835126786f7f1626db6e47b5139a9273625
7
- data.tar.gz: 3a307cfb11ad03e23526497a853e3411175c2e50a1893d6bca4e7b2addbf1a8e4a0af52d5e9f1561daa18af514989c09e92bf278ca78169d69d3040eaff70ab7
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
- def initialize(records)
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
- @records.each do |record|
47
- csv << @columns.map { |col| col.extract(record) }
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module CsvBuilder
5
- VERSION = '0.1.4'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  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.1.4
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-03-31 00:00:00.000000000 Z
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://github.com/philiprehberger/rb-csv-builder
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://github.com/philiprehberger/rb-csv-builder
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