philiprehberger-csv_builder 0.3.0 → 0.5.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: bc5cd28c86059cae0603624ace5e07a070875268b942adf798d664755e028e91
4
- data.tar.gz: 5cc96a1439b056f18e53cb2eb8af749ee2be05c74fcae55bd1aa31fb694a2016
3
+ metadata.gz: 921263332f87142c3cfcdc4483e601a9ab51ce54cd53481988cc75b3b46eb7ed
4
+ data.tar.gz: fa347e0127c702eca2a1c4104ca1b5eada5c43458660884c9b6f9028753a8f80
5
5
  SHA512:
6
- metadata.gz: 867447e6eb9146acaa103b6167683745ca589f2d5cc59e78de72b9cf8191e956e6f639a2cc313b7c2d6e8249bb841e0ee3f3ced92e61fef4f18d8aba4ab2afbe
7
- data.tar.gz: 9464cab4f2cc762d16458a066a2268dff54bbfb72a897e9188837e5b34a78b490b9cf11018d63cb5ba43ffb10880465bbd792dd1cd405d038e158e85585dcabf
6
+ metadata.gz: a9fcc0e96415c5b1d5b55e257963c48881ec879809b652ae7604138d524df50264577650e60bb239cf6b838b5fbae2bd03e6b76d5d6cdb0b3225528dd6a5d9a6
7
+ data.tar.gz: c7a65b052c99facd38baee4f16f2cca0bbd36ae5906ce6ca1302da36797c6b1b585066e096b1aec8432dacbd8d1167ab23f9cb522123e4253c3088b8643c14b4
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-04-11
11
+
12
+ ### Added
13
+ - UTF-8 BOM support via `bom: true` option for Excel-compatible output
14
+ - Custom output encoding via `encoding:` option
15
+
16
+ ### Fixed
17
+ - Bug report template now requires Ruby version and gem version fields
18
+ - Feature request template now includes "Alternatives considered" field
19
+
20
+ ## [0.4.0] - 2026-04-10
21
+
22
+ ### Added
23
+ - `Builder#footer(&block)` appends a computed summary row after all data rows
24
+ - `Builder#limit(n)` caps output to N rows
25
+ - `Builder#offset(n)` skips the first N filtered/sorted records
26
+
10
27
  ## [0.3.0] - 2026-04-09
11
28
 
12
29
  ### Added
@@ -57,3 +74,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
57
74
  - File output via `to_file`
58
75
  - Support for hash records with symbol and string keys
59
76
  - Proper CSV escaping for values with commas and quotes
77
+
78
+ [0.5.0]: https://github.com/philiprehberger/rb-csv-builder/releases/tag/v0.5.0
79
+ [0.4.0]: https://github.com/philiprehberger/rb-csv-builder/releases/tag/v0.4.0
80
+ [0.3.0]: https://github.com/philiprehberger/rb-csv-builder/releases/tag/v0.3.0
81
+ [0.2.0]: https://github.com/philiprehberger/rb-csv-builder/releases/tag/v0.2.0
82
+ [0.1.5]: https://github.com/philiprehberger/rb-csv-builder/releases/tag/v0.1.5
83
+ [0.1.4]: https://github.com/philiprehberger/rb-csv-builder/releases/tag/v0.1.4
84
+ [0.1.3]: https://github.com/philiprehberger/rb-csv-builder/releases/tag/v0.1.3
85
+ [0.1.2]: https://github.com/philiprehberger/rb-csv-builder/releases/tag/v0.1.2
86
+ [0.1.1]: https://github.com/philiprehberger/rb-csv-builder/releases/tag/v0.1.1
87
+ [0.1.0]: https://github.com/philiprehberger/rb-csv-builder/releases/tag/v0.1.0
data/README.md CHANGED
@@ -173,6 +173,51 @@ builder = Philiprehberger::CsvBuilder.build(records) do
173
173
  end
174
174
  ```
175
175
 
176
+ ### Footer Row
177
+
178
+ Append a computed summary row after all data rows:
179
+
180
+ ```ruby
181
+ builder = Philiprehberger::CsvBuilder.build(records) do
182
+ column :name
183
+ column(:amount) { |r| r[:amount] }
184
+ footer { |recs| ['Total', recs.sum { |r| r[:amount] }] }
185
+ end
186
+ ```
187
+
188
+ ### Pagination
189
+
190
+ ```ruby
191
+ builder = Philiprehberger::CsvBuilder.build(records) do
192
+ column :name
193
+ offset 10 # skip first 10 records
194
+ limit 25 # output at most 25 rows
195
+ end
196
+ ```
197
+
198
+ `offset` and `limit` are applied after filtering and sorting.
199
+
200
+ ### Excel-Compatible Output (BOM)
201
+
202
+ Prepend a UTF-8 BOM so Excel opens the CSV with correct encoding:
203
+
204
+ ```ruby
205
+ builder = Philiprehberger::CsvBuilder.build(records, bom: true) do
206
+ column :name
207
+ column :email
208
+ end
209
+
210
+ builder.to_file('export.csv')
211
+ ```
212
+
213
+ ### Custom Encoding
214
+
215
+ ```ruby
216
+ builder = Philiprehberger::CsvBuilder.build(records, encoding: 'ISO-8859-1') do
217
+ column :name
218
+ end
219
+ ```
220
+
176
221
  ### Streaming
177
222
 
178
223
  ```ruby
@@ -208,10 +253,13 @@ builder.headers # => ["name", "email"]
208
253
 
209
254
  | Method | Description |
210
255
  |--------|-------------|
211
- | `CsvBuilder.build(records, delimiter:, quote_char:, &block)` | Build a CSV using the column DSL |
256
+ | `CsvBuilder.build(records, delimiter:, quote_char:, bom:, encoding:, &block)` | Build a CSV using the column DSL |
212
257
  | `Builder#column(name, header:, &block)` | Define a column with optional alias and transform |
213
258
  | `Builder#filter(&block)` | Filter records (block returns true to include) |
214
259
  | `Builder#sort_by(direction:, &block)` | Sort records by block key (`:asc` or `:desc`) |
260
+ | `Builder#footer(&block)` | Append a computed footer row (block receives filtered records) |
261
+ | `Builder#limit(n)` | Cap output to N rows |
262
+ | `Builder#offset(n)` | Skip first N filtered/sorted records |
215
263
  | `Builder#row_number(header:)` | Add auto-incrementing row number column |
216
264
  | `Builder#to_csv` | Generate CSV as a string |
217
265
  | `Builder#to_file(path)` | Write CSV to a file |
@@ -16,7 +16,9 @@ module Philiprehberger
16
16
  # @param records [Array] the source records
17
17
  # @param delimiter [String] the column separator (default: ",")
18
18
  # @param quote_char [String] the quote character (default: '"')
19
- def initialize(records, delimiter: ',', quote_char: '"')
19
+ # @param bom [Boolean] prepend UTF-8 BOM (default: false)
20
+ # @param encoding [String] output encoding name (default: "UTF-8")
21
+ def initialize(records, delimiter: ',', quote_char: '"', bom: false, encoding: 'UTF-8')
20
22
  @records = records
21
23
  @columns = []
22
24
  @filters = []
@@ -25,6 +27,11 @@ module Philiprehberger
25
27
  @quote_char = quote_char
26
28
  @sort_by = nil
27
29
  @sort_direction = :asc
30
+ @limit_count = nil
31
+ @offset_count = nil
32
+ @footer_block = nil
33
+ @bom = bom
34
+ @encoding = encoding
28
35
  end
29
36
 
30
37
  # Sort records before CSV output
@@ -36,13 +43,42 @@ module Philiprehberger
36
43
  # @raise [Error] if direction is not :asc or :desc
37
44
  def sort_by(direction: :asc, &block)
38
45
  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)
46
+ raise Error, "direction must be :asc or :desc (got #{direction.inspect})" unless %i[asc
47
+ desc].include?(direction)
40
48
 
41
49
  @sort_by = block
42
50
  @sort_direction = direction
43
51
  self
44
52
  end
45
53
 
54
+ # Limit the number of output rows
55
+ #
56
+ # @param n [Integer] maximum rows
57
+ # @return [self]
58
+ def limit(n)
59
+ @limit_count = n
60
+ self
61
+ end
62
+
63
+ # Skip the first N filtered/sorted records
64
+ #
65
+ # @param n [Integer] number of rows to skip
66
+ # @return [self]
67
+ def offset(n)
68
+ @offset_count = n
69
+ self
70
+ end
71
+
72
+ # Append a computed footer row after all data rows
73
+ #
74
+ # @yield [Array] filtered records
75
+ # @yieldreturn [Array] footer row values
76
+ # @return [self]
77
+ def footer(&block)
78
+ @footer_block = block
79
+ self
80
+ end
81
+
46
82
  # Define a column
47
83
  #
48
84
  # @param name [Symbol, String] the column name
@@ -94,6 +130,8 @@ module Philiprehberger
94
130
  result = result.sort_by(&@sort_by)
95
131
  result = result.reverse if @sort_direction == :desc
96
132
  end
133
+ result = result.drop(@offset_count) if @offset_count
134
+ result = result.first(@limit_count) if @limit_count
97
135
  result
98
136
  end
99
137
 
@@ -101,12 +139,16 @@ module Philiprehberger
101
139
  #
102
140
  # @return [String]
103
141
  def to_csv
104
- CSV.generate(**csv_options) do |csv|
142
+ recs = filtered_records
143
+ csv_string = CSV.generate(**csv_options) do |csv|
105
144
  csv << headers
106
- filtered_records.each_with_index do |record, index|
145
+ recs.each_with_index do |record, index|
107
146
  csv << build_row(record, index)
108
147
  end
148
+ csv << @footer_block.call(recs) if @footer_block
109
149
  end
150
+ csv_string = csv_string.encode(@encoding) unless @encoding == 'UTF-8'
151
+ @bom ? "\xEF\xBB\xBF#{csv_string}" : csv_string
110
152
  end
111
153
 
112
154
  # Write the CSV to a file
@@ -114,7 +156,7 @@ module Philiprehberger
114
156
  # @param path [String] the output file path
115
157
  # @return [void]
116
158
  def to_file(path)
117
- File.write(path, to_csv)
159
+ File.binwrite(path, to_csv)
118
160
  end
119
161
 
120
162
  # Stream CSV to any IO object
@@ -122,11 +164,14 @@ module Philiprehberger
122
164
  # @param io [IO, StringIO] the IO object to write to
123
165
  # @return [void]
124
166
  def to_io(io)
167
+ io.write("\xEF\xBB\xBF") if @bom
168
+ recs = filtered_records
125
169
  csv = CSV.new(io, **csv_options)
126
170
  csv << headers
127
- filtered_records.each_with_index do |record, index|
171
+ recs.each_with_index do |record, index|
128
172
  csv << build_row(record, index)
129
173
  end
174
+ csv << @footer_block.call(recs) if @footer_block
130
175
  end
131
176
 
132
177
  private
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module CsvBuilder
5
- VERSION = '0.3.0'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end
@@ -13,14 +13,20 @@ module Philiprehberger
13
13
  # @param records [Array] the source records
14
14
  # @param delimiter [String] the column separator (default: ",")
15
15
  # @param quote_char [String] the quote character (default: '"')
16
+ # @param bom [Boolean] prepend UTF-8 BOM for Excel compatibility (default: false)
17
+ # @param encoding [String] output encoding name (default: "UTF-8")
16
18
  # @yield [builder] the builder instance for defining columns
17
19
  # @yieldparam builder [Builder]
18
20
  # @return [Builder] the configured builder
19
21
  # @raise [Error] if no block is given
20
- def self.build(records, delimiter: ',', quote_char: '"', &block)
22
+ def self.build(records, delimiter: ',', quote_char: '"', bom: false, encoding: 'UTF-8', &block)
21
23
  raise Error, 'A block is required' unless block
22
24
 
23
- builder = Builder.new(records, delimiter: delimiter, quote_char: quote_char)
25
+ builder = Builder.new(
26
+ records,
27
+ delimiter: delimiter, quote_char: quote_char,
28
+ bom: bom, encoding: encoding
29
+ )
24
30
  builder.instance_eval(&block)
25
31
  builder
26
32
  end
metadata CHANGED
@@ -1,17 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-csv_builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.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-04-09 00:00:00.000000000 Z
11
+ date: 2026-04-11 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Build CSV files from record collections using a declarative DSL with
14
- column definitions, custom transforms, and file output support.
14
+ column definitions, custom transforms, filtering, sorting, pagination via limit/offset,
15
+ computed footer rows, row numbers, streaming output, and custom delimiters.
15
16
  email:
16
17
  - me@philiprehberger.com
17
18
  executables: []