smarter_csv 1.13.1 → 1.14.1

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: 2e785bafb4281cfadba23ef7db89a50a1a22f642ca4dd03cd2e97323a8cfa761
4
- data.tar.gz: 2d3b7f87540a0982582859fc804cb50088d9b4906aa75765fa3bf8d4ac535a23
3
+ metadata.gz: c4619533008fc05b02a009b1409d1368d56245584baf52134511b45ff505f011
4
+ data.tar.gz: 051fa13106a0dfe41486accfee972a07c7d80670a65dd1066f07dfc600489be3
5
5
  SHA512:
6
- metadata.gz: 0d3e04d0ec26174a179d42f577c412483956e4ecf8af0e3b4b27234ed52027b984e26afbadfdb8fc1ee95f77780c6856f824dc1ca6e8eb91a87e7a81d46d40ed
7
- data.tar.gz: 379e3a0daa40afb0158fb546cf33fbb33d52eea77847f587349e69e4f9c6f51233b9953d60c5c3cf0a06e4a30115174cbefb81f42f9c5b2ac495d22a1fa7efa1
6
+ metadata.gz: 5ee274a0a485e87f356940eeaf5e68d6af3d83dd72cd5199eca9729f6c760d0528ac2db8931c2d741521edb69251a4ff3d0c4d458f449a824f2b8110e7ad3a51
7
+ data.tar.gz: 87ac821280845041514a0418b94389296e6b03faf5c5fded8e2b0a0840015c03c477ad57d6a8268316b110adffb295882c81b7876c6f9f76a444531faed3c431
data/.rubocop.yml CHANGED
@@ -58,6 +58,9 @@ Style/ClassMethods:
58
58
  Style/ConditionalAssignment:
59
59
  Enabled: false
60
60
 
61
+ Style/CommentAnnotation:
62
+ Enabled: false
63
+
61
64
  Style/CommentedKeyword:
62
65
  Enabled: false
63
66
 
@@ -76,6 +79,9 @@ Style/Encoding:
76
79
  Style/EvalWithLocation:
77
80
  Enabled: false
78
81
 
82
+ Style/EvenOdd:
83
+ Enabled: false
84
+
79
85
  Style/FormatString:
80
86
  Enabled: false
81
87
 
@@ -94,9 +100,15 @@ Style/IfUnlessModifier:
94
100
  Style/InverseMethods:
95
101
  Enabled: false
96
102
 
103
+ Style/Lambda:
104
+ Enabled: false
105
+
97
106
  Style/NestedTernaryOperator:
98
107
  Enabled: false
99
108
 
109
+ Style/OptionalBooleanParameter:
110
+ Enabled: false
111
+
100
112
  Style/PreferredHashMethods:
101
113
  Enabled: false
102
114
 
@@ -115,6 +127,9 @@ Style/SafeNavigation:
115
127
  Style/SlicingWithRange:
116
128
  Enabled: false
117
129
 
130
+ Style/SoleNestedConditional:
131
+ Enabled: false
132
+
118
133
  Style/SpecialGlobalVars: # DANGER: unsafe rule!!
119
134
  Enabled: false
120
135
 
@@ -135,8 +150,13 @@ Style/SymbolArray:
135
150
  Style/SymbolProc: # old Ruby versions can't do this
136
151
  Enabled: false
137
152
 
153
+ Style/TrailingCommaInArrayLiteral:
154
+ Enabled: false
155
+ EnforcedStyleForMultiline: consistent_comma
156
+
138
157
  Style/TrailingCommaInHashLiteral:
139
158
  Enabled: false
159
+ EnforcedStyleForMultiline: consistent_comma
140
160
 
141
161
  Style/TrailingUnderscoreVariable:
142
162
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,6 +1,14 @@
1
1
 
2
2
  # SmarterCSV 1.x Change Log
3
3
 
4
+ ## 1.14.1 (2025-04-09)
5
+ * bugfix: empty hash results in a blank line ([issue 299](https://github.com/tilo/smarter_csv/issues/299))
6
+ * bugfix: automatically quote problematic headers ([issue #300](https://github.com/tilo/smarter_csv/issues/300))
7
+ * new option: `quote_headers` allows to explicitly quote all headers
8
+
9
+ ## 1.14.0 (2025-04-07)
10
+ * adding advanced configuration options for writing CSV files. ([issue 297](https://github.com/tilo/smarter_csv/issues/297) thanks to Robert Reiz, [issue 296](https://github.com/tilo/smarter_csv/issues/296))
11
+
4
12
  ## 1.13.1 (2024-12-12)
5
13
  * fix bug with SmarterCSV.generate with `force_quotes: true` ([issue 294](https://github.com/tilo/smarter_csv/issues/294))
6
14
 
data/CONTRIBUTORS.md CHANGED
@@ -58,3 +58,4 @@ A Big Thank you to everyone who filed issues, sent comments, and who contributed
58
58
  * [Simon Rentzke](https://github.com/simonrentzke)
59
59
  * [Randall B](https://github.com/randall-coding)
60
60
  * [Matthew Kennedy](https://github.com/MattKitmanLabs)
61
+ * [Robert Reiz](https://github.com/reiz)
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  # SmarterCSV
3
3
 
4
- [![codecov](https://codecov.io/gh/tilo/smarter_csv/branch/main/graph/badge.svg?token=1L7OD80182)](https://codecov.io/gh/tilo/smarter_csv) ![Gem Version](https://img.shields.io/gem/v/smarter_csv) [View on RubyGems](https://rubygems.org/gems/smarter_csv) [View on RubyToolbox](https://www.ruby-toolbox.com/search?q=smarter_csv)
4
+ ![Gem Version](https://img.shields.io/gem/v/smarter_csv) [![codecov](https://codecov.io/gh/tilo/smarter_csv/branch/main/graph/badge.svg?token=1L7OD80182)](https://codecov.io/gh/tilo/smarter_csv) [View on RubyGems](https://rubygems.org/gems/smarter_csv) [View on RubyToolbox](https://www.ruby-toolbox.com/search?q=smarter_csv)
5
5
 
6
6
  SmarterCSV provides a convenient interface for reading and writing CSV files and data.
7
7
 
@@ -35,7 +35,8 @@ Or install it yourself as:
35
35
  # Documentation
36
36
 
37
37
  * [Introduction](docs/_introduction.md)
38
- * [The Basic API](docs/basic_api.md)
38
+ * [The Basic Read API](docs/basic_read_api.md)
39
+ * [The Basic Write API](docs/basic_write_api.md)
39
40
  * [Batch Processing](./docs/batch_processing.md)
40
41
  * [Configuration Options](docs/options.md)
41
42
  * [Row and Column Separators](docs/row_col_sep.md)
@@ -45,10 +46,11 @@ Or install it yourself as:
45
46
  * [Value Converters](docs/value_converters.md)
46
47
 
47
48
  # Articles
48
- * [Parsing CSV Files in Ruby with SmarterCSV](https://tilo-sloboda.medium.com/parsing-csv-files-in-ruby-with-smartercsv-6ce66fb6cf38)
49
- * [Processing 1.4 Million CSV Records in Ruby, fast ](https://lcx.wien/blog/processing-14-million-csv-records-in-ruby/)
50
- * [Faster Parsing CSV with Parallel Processing](http://xjlin0.github.io/tech/2015/05/25/faster-parsing-csv-with-parallel-processing) by [Jack lin](https://github.com/xjlin0/)
51
- * [The original post](http://www.unixgods.org/Ruby/process_csv_as_hashes.html) that started SmarterCSV
49
+ * [Parsing CSV Files in Ruby with SmarterCSV](https://tilo-sloboda.medium.com/parsing-csv-files-in-ruby-with-smartercsv-6ce66fb6cf38)
50
+ * [Processing 1.4 Million CSV Records in Ruby, fast ](https://lcx.wien/blog/processing-14-million-csv-records-in-ruby/)
51
+ * [Faster Parsing CSV with Parallel Processing](http://xjlin0.github.io/tech/2015/05/25/faster-parsing-csv-with-parallel-processing) by [Jack lin](https://github.com/xjlin0/)
52
+ * The original [Stackoverflow Question](https://stackoverflow.com/questions/7788618/update-mongodb-with-array-from-csv-join-table/7788746#7788746) that inspired SmarterCSV
53
+ * [The original post](http://www.unixgods.org/Ruby/process_csv_as_hashes.html) for SmarterCSV
52
54
 
53
55
  # [ChangeLog](./CHANGELOG.md)
54
56
 
@@ -2,7 +2,8 @@
2
2
  ### Contents
3
3
 
4
4
  * [**Introduction**](./_introduction.md)
5
- * [The Basic API](./basic_api.md)
5
+ * [The Basic Read API](./basic_read_api.md)
6
+ * [The Basic Write API](./basic_write_api.md)
6
7
  * [Batch Processing](././batch_processing.md)
7
8
  * [Configuration Options](./options.md)
8
9
  * [Row and Column Separators](./row_col_sep.md)
@@ -53,4 +54,4 @@ The CSV processing also needed to be robust against variations in the input data
53
54
  (planned feature)
54
55
 
55
56
  ---------------
56
- PREVIOUS [README](../README.md) | NEXT: [The Basic API](./basic_api.md)
57
+ PREVIOUS [README](../README.md) | NEXT: [The Basic Read API](./basic_read_api.md)
@@ -2,7 +2,8 @@
2
2
  ### Contents
3
3
 
4
4
  * [Introduction](./_introduction.md)
5
- * [**The Basic API**](./basic_api.md)
5
+ * [**The Basic Read API**](./basic_read_api.md)
6
+ * [The Basic Write API](./basic_write_api.md)
6
7
  * [Batch Processing](././batch_processing.md)
7
8
  * [Configuration Options](./options.md)
8
9
  * [Row and Column Separators](./row_col_sep.md)
@@ -70,46 +71,6 @@ It cal also be used with a block:
70
71
  This allows you access to the internal state of the `reader` instance after processing.
71
72
 
72
73
 
73
- ## Interface for Writing CSV
74
-
75
- To generate a CSV file, we use the `<<` operator to append new data to the file.
76
-
77
- The input operator for adding data to a CSV file `<<` can handle single hashes, array-of-hashes, or array-of-arrays-of-hashes, and can be called one or multiple times for each file.
78
-
79
- One smart feature of writing CSV data is the discovery of headers.
80
-
81
- If you have hashes of data, where each hash can have different keys, the `SmarterCSV::Reader` automatically discovers the superset of keys as the headers of the CSV file. This can be disabled by either providing one of the options `headers`, `map_headers`, or `discover_headers: false`.
82
-
83
-
84
- ### Simplified Interface
85
-
86
- The simplified interface takes a block:
87
-
88
- ```
89
- SmarterCSV.generate(filename, options) do |csv_writer|
90
-
91
- MyModel.find_in_batches(batch_size: 100) do |batch|
92
- batch.pluck(:name, :description, :instructor).each do |record|
93
- csv_writer << record
94
- end
95
- end
96
-
97
- end
98
- ```
99
-
100
- ### Full Interface
101
-
102
- ```
103
- writer = SmarterCSV::Writer.new(file_path, options)
104
-
105
- MyModel.find_in_batches(batch_size: 100) do |batch|
106
- batch.pluck(:name, :description, :instructor).each do |record|
107
- csv_writer << record
108
- end
109
-
110
- writer.finalize
111
- ```
112
-
113
74
  ## Rescue from Exceptions
114
75
 
115
76
  While SmarterCSV uses sensible defaults to process the most common CSV files, it will raise exceptions if it can not auto-detect `col_sep`, `row_sep`, or if it encounters other problems. Therefore please rescue from `SmarterCSV::Error`, and handle outliers according to your requirements.
@@ -154,4 +115,4 @@ $ hexdump -C spec/fixtures/bom_test_feff.csv
154
115
  ```
155
116
 
156
117
  ----------------
157
- PREVIOUS: [Introduction](./_introduction.md) | NEXT: [Batch Processing](./batch_processing.md)
118
+ PREVIOUS: [Introduction](./_introduction.md) | NEXT: [The Basic Write API](./basic_write_api.md)
@@ -0,0 +1,160 @@
1
+
2
+ ### Contents
3
+
4
+ * [Introduction](./_introduction.md)
5
+ * [The Basic Read API](./basic_read_api.md)
6
+ * [**The Basic Write API**](./basic_write_api.md)
7
+ * [Batch Processing](././batch_processing.md)
8
+ * [Configuration Options](./options.md)
9
+ * [Row and Column Separators](./row_col_sep.md)
10
+ * [Header Transformations](./header_transformations.md)
11
+ * [Header Validations](./header_validations.md)
12
+ * [Data Transformations](./data_transformations.md)
13
+ * [Value Converters](./value_converters.md)
14
+
15
+ --------------
16
+
17
+ # SmarterCSV Basic Write API
18
+
19
+ Let's explore the basic API for writing CSV files. There is a simplified API (backwards conpatible with previous SmarterCSV versions) and the full API, which allows you to access the internal state of the writer instance after processing.
20
+
21
+ ## Writing CSV Files
22
+
23
+ To generate a CSV file, we use the `<<` operator to append new data to the file.
24
+
25
+ The input operator for adding data to a CSV file `<<` can handle single hashes, array-of-hashes, or array-of-arrays-of-hashes, and can be called one or multiple times in order to create a file.
26
+
27
+ ### Auto-Discovery of Headers
28
+
29
+ By default, the `SmarterCSV::Writer` discovers all keys that are present in the input data, and as they become know, appends them to the CSV headers. This ensures that all data will be included in the output CSV file.
30
+
31
+ If you want to customize the output file, or only include select headers, check the section about Advanced Features below.
32
+
33
+ ### Auto-Quoting of Problematic Values
34
+
35
+ CSV files use some special characters that are important for the CSV format to function:
36
+ * @row_sep : typically `\n` the carriage return
37
+ * @col_sep : typically `,` the comma
38
+ * @quote_char : typically `"` the double-quote
39
+
40
+ When your data for a given field in a CSV row contains either of these characters, we need to prevent them to break the CSV file format.
41
+
42
+ `SmarterCSV::Writer` automatically detects if a field contains either of these three characters. If a field contains the `@quote_char`, it will be prefixed by another `@qoute_char` as per CSV conventions.
43
+ In either case the corresponding field will be put in double-quotes.
44
+
45
+
46
+ ### Simplified Interface
47
+
48
+ The simplified interface takes a block:
49
+
50
+ ```
51
+ SmarterCSV.generate(filename, options) do |csv_writer|
52
+
53
+ MyModel.find_in_batches(batch_size: 100) do |batch|
54
+ batch.pluck(:name, :description, :instructor).each do |record|
55
+ csv_writer << record
56
+ end
57
+ end
58
+
59
+ end
60
+ ```
61
+
62
+ ### Full Interface
63
+
64
+ ```
65
+ csv_writer = SmarterCSV::Writer.new(file_path, options)
66
+
67
+ MyModel.find_in_batches(batch_size: 100) do |batch|
68
+ batch.pluck(:name, :description, :instructor).each do |record|
69
+ csv_writer << record
70
+ end
71
+
72
+ csv_writer.finalize
73
+ ```
74
+
75
+ ## Advanced Features: Customizing the Output Format
76
+
77
+ You can customize the output format through different features.
78
+
79
+ In the options, you can pass-in either of these parameters to customize your output format.
80
+ * `headers`, which limits the CSV headers to just the specified list.
81
+ * `map_header`, which maps a given list of Hash keys to custom strings, and limits the CSV headers to just those.
82
+ * `value_converters`, which specifies a hash with more advanced value transformations.
83
+
84
+ ### Limited Headers
85
+
86
+ You can use the `headers` option to limit the CSV headers to only a sub-set of Hash keys from your data.
87
+ This will switch-off the automatic detection of headers, and limit the CSV output file to only the CSV headers you provide in this option.
88
+
89
+
90
+ ### Mapping Headers
91
+
92
+ Similar to the `headers` option, you can define `map_headers` in order to rename a given set of Hash keys to some custom strings in order to rename them in the CSV header. This will switch-off the automatic detection of headers.
93
+
94
+
95
+ ### Per Key Value Converters
96
+
97
+
98
+ Using per-key value converters, you can control how specific hash keys in your data are converted in the output.
99
+
100
+ Example 1:
101
+
102
+ ```
103
+ options = {
104
+ value_converters: {
105
+ active: ->(v) { !!v ? 'YES' : 'NO' },
106
+ }
107
+ }
108
+ ```
109
+
110
+ This maps the boolean value of the hash key `:active` into strings `"YES"`, `"NO"`.
111
+
112
+ Example 2:
113
+
114
+ ```
115
+ options = {
116
+ value_converters: {
117
+ active: ->(v) { !!v ? '✅' : '❌' },
118
+ balance: ->(v) do
119
+ case v
120
+ when Float
121
+ '$%.2f' % v.round(2)
122
+ when Integer
123
+ "$#{v}"
124
+ else
125
+ v.to_s
126
+ end
127
+ end,
128
+ }
129
+ }
130
+ ```
131
+
132
+ This maps the hash key `:balance` to a string. Floats are rounded and displayed with 2 decimals and prefixed by `$`. Integers are prefixed by `$`.
133
+ The boolean value of the key `:active` is mapped into an emoji.
134
+
135
+ ### Global Value Converters
136
+
137
+ You can also use the special keyword `:_all` to define transformations that are applied to each field of the CSV file.
138
+
139
+ ```
140
+ options = {
141
+ value_converters: {
142
+ disable_auto_quoting: true, # ⚠️ Important: turn off auto-quoting because we're messing with it below
143
+ active: ->(v) { !!v ? 'YES' : 'NO' },
144
+ _all: ->(_k, v) { v.is_a?(String) ? "\"#{v}\"" : v } # only double-quote string fields
145
+ }
146
+ }
147
+ ```
148
+
149
+ Using the `:_all` keyword, you can set up rules to convert all hash keys. This is applied after all per-key conversions are made.
150
+
151
+ This example puts double-quotes around all String-value data, but leaves other types unchanged.
152
+
153
+ Note that when you're customizing putting quote-chars around fields, you need to `disable_auto_quoting`.
154
+
155
+ ## More Examples
156
+
157
+ Check out the [RSpec tests](../spec/smarter_csv/writer_spec.rb) for more examples.
158
+
159
+ ----------------
160
+ PREVIOUS: [The Basic Read API](./basic_read_api.md) | NEXT: [Batch Processing](./batch_processing.md)
@@ -2,7 +2,8 @@
2
2
  ### Contents
3
3
 
4
4
  * [Introduction](./_introduction.md)
5
- * [The Basic API](./basic_api.md)
5
+ * [The Basic Read API](./basic_read_api.md)
6
+ * [The Basic Write API](./basic_write_api.md)
6
7
  * [**Batch Processing**](././batch_processing.md)
7
8
  * [Configuration Options](./options.md)
8
9
  * [Row and Column Separators](./row_col_sep.md)
@@ -65,4 +66,4 @@ and how the `process` method returns the number of chunks when called with a blo
65
66
  ```
66
67
 
67
68
  ----------------
68
- PREVIOUS: [The Basic API](./basic_api.md) | NEXT: [Configuration Options](./options.md)
69
+ PREVIOUS: [The Basic Write API](./basic_write_api.md) | NEXT: [Configuration Options](./options.md)
@@ -2,7 +2,8 @@
2
2
  ### Contents
3
3
 
4
4
  * [Introduction](./_introduction.md)
5
- * [The Basic API](./basic_api.md)
5
+ * [The Basic Read API](./basic_read_api.md)
6
+ * [The Basic Write API](./basic_write_api.md)
6
7
  * [Batch Processing](././batch_processing.md)
7
8
  * [Configuration Options](./options.md)
8
9
  * [Row and Column Separators](./row_col_sep.md)
data/docs/examples.md CHANGED
@@ -2,7 +2,8 @@
2
2
  ### Contents
3
3
 
4
4
  * [Introduction](./_introduction.md)
5
- * [The Basic API](./basic_api.md)
5
+ * [The Basic Read API](./basic_read_api.md)
6
+ * [The Basic Write API](./basic_write_api.md)
6
7
  * [Batch Processing](././batch_processing.md)
7
8
  * [Configuration Options](./options.md)
8
9
  * [Row and Column Separators](./row_col_sep.md)
@@ -2,7 +2,8 @@
2
2
  ### Contents
3
3
 
4
4
  * [Introduction](./_introduction.md)
5
- * [The Basic API](./basic_api.md)
5
+ * [The Basic Read API](./basic_read_api.md)
6
+ * [The Basic Write API](./basic_write_api.md)
6
7
  * [Batch Processing](././batch_processing.md)
7
8
  * [Configuration Options](./options.md)
8
9
  * [Row and Column Separators](./row_col_sep.md)
@@ -2,7 +2,8 @@
2
2
  ### Contents
3
3
 
4
4
  * [Introduction](./_introduction.md)
5
- * [The Basic API](./basic_api.md)
5
+ * [The Basic Read API](./basic_read_api.md)
6
+ * [The Basic Write API](./basic_write_api.md)
6
7
  * [Batch Processing](././batch_processing.md)
7
8
  * [Configuration Options](./options.md)
8
9
  * [Row and Column Separators](./row_col_sep.md)
data/docs/options.md CHANGED
@@ -2,7 +2,8 @@
2
2
  ### Contents
3
3
 
4
4
  * [Introduction](./_introduction.md)
5
- * [The Basic API](./basic_api.md)
5
+ * [The Basic Read API](./basic_read_api.md)
6
+ * [The Basic Write API](./basic_write_api.md)
6
7
  * [Batch Processing](././batch_processing.md)
7
8
  * [**Configuration Options**](./options.md)
8
9
  * [Row and Column Separators](./row_col_sep.md)
@@ -20,14 +21,18 @@
20
21
  | Option | Default | Explanation |
21
22
  ---------------------------------------------------------------------------------------------------------------------------------
22
23
  | :row_sep | $/ | Separates rows; Defaults to your OS row separator. `/n` on UNIX, `/r/n` oon Windows |
23
- | :col_sep | "," | Separates each value in a row |
24
- | :quote_char | '"' | |
24
+ | :col_sep | "," | Separates each value in a row |
25
+ | :quote_char | '"' | To quote CSV fields. |
25
26
  | :force_quotes | false | Forces each individual value to be quoted |
26
- | :discover_headers | true | Automatically detects all keys in the input before writing the header |
27
- | | | This can be disabled by providing `headers` or `map_headers` options. |
28
27
  | :headers | [] | You can provide the specific list of keys from the input you'd like to be used as headers in the CSV file |
28
+ | | | ⚠️ This disables automatic header detection! |
29
29
  | :map_headers | {} | Similar to `headers`, but also maps each desired key to a user-specified value that is uesd as the header. |
30
- |
30
+ | | | ⚠️ This disables automatic header detection! |
31
+ | :discover_headers | true | Automatically detects all keys in the input before writing the header |
32
+ | | | Do not manually set this to `false`. ⚠️ |
33
+ | | | But you can set this to `true` when using `map_headers` option. |
34
+ | :disable_auto_quoting | false | To manually disable auto-quoting of special characters. ⚠️ Be careful with this! |
35
+
31
36
 
32
37
  ## CSV Reading
33
38
 
data/docs/row_col_sep.md CHANGED
@@ -2,7 +2,8 @@
2
2
  ### Contents
3
3
 
4
4
  * [Introduction](./_introduction.md)
5
- * [The Basic API](./basic_api.md)
5
+ * [The Basic Read API](./basic_read_api.md)
6
+ * [The Basic Write API](./basic_write_api.md)
6
7
  * [Batch Processing](././batch_processing.md)
7
8
  * [Configuration Options](./options.md)
8
9
  * [**Row and Column Separators**](./row_col_sep.md)
@@ -2,7 +2,8 @@
2
2
  ### Contents
3
3
 
4
4
  * [Introduction](./_introduction.md)
5
- * [The Basic API](./basic_api.md)
5
+ * [The Basic Read API](./basic_read_api.md)
6
+ * [The Basic Write API](./basic_write_api.md)
6
7
  * [Batch Processing](././batch_processing.md)
7
8
  * [Configuration Options](./options.md)
8
9
  * [Row and Column Separators](./row_col_sep.md)
@@ -13,7 +14,7 @@
13
14
 
14
15
  --------------
15
16
 
16
- # Using Value Converters
17
+ # Using Value Converters for Reading CSV
17
18
 
18
19
  Value Converters allow you to do custom transformations specific rows, to help you massage the data so it fits the expectations of your down-stream process, such as creating a DB record.
19
20
 
@@ -88,7 +88,9 @@ module SmarterCSV
88
88
 
89
89
  # Check for unclosed quotes at the end of the line
90
90
  if in_quotes
91
+ # :nocov:
91
92
  raise MalformedCSV, "Unclosed quoted field detected in line: #{line}"
93
+ # :nocov:
92
94
  end
93
95
 
94
96
  # Process the remaining field
@@ -108,7 +110,7 @@ module SmarterCSV
108
110
  end
109
111
 
110
112
  # Replace double quotes with a single quote
111
- field.gsub!("#{quote * 2}", quote)
113
+ field.gsub!((quote * 2).to_s, quote)
112
114
 
113
115
  field
114
116
  end
@@ -112,7 +112,9 @@ module SmarterCSV
112
112
  raise MalformedCSV, "Unclosed quoted field detected in multiline data"
113
113
  else
114
114
  # Quotes are balanced; proceed without raising an error.
115
+ # :nocov:
115
116
  break
117
+ # :nocov:
116
118
  end
117
119
  end
118
120
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SmarterCSV
4
- VERSION = "1.13.1"
4
+ VERSION = "1.14.1"
5
5
  end
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'tempfile'
4
+
3
5
  module SmarterCSV
4
6
  #
5
7
  # Generate CSV files
6
8
  #
7
9
  # Create an instance of the Writer class with the filename and options.
8
- # call `<<` one or mulltiple times to append data to the file.
10
+ # call `<<` one or multiple times to append data to the file.
9
11
  # call `finalize` to save the file.
10
12
  #
11
13
  # The `<<` method can take different arguments:
12
- # * a signle Hash
14
+ # * a single Hash
13
15
  # * an array of Hashes
14
16
  # * nested arrays of arrays of Hashes
15
17
  #
@@ -27,8 +29,10 @@ module SmarterCSV
27
29
  # quote_char : defaults to "
28
30
  # discover_headers : defaults to true
29
31
  # headers : defaults to []
32
+ # quote_headers: defaults to false
30
33
  # force_quotes: defaults to false
31
34
  # map_headers: defaults to {}, can be a hash of key -> value mappings
35
+ # value_converters: optional hash of key -> lambda to control serialization
32
36
 
33
37
  # IMPORTANT NOTES:
34
38
  # * Data hashes could contain strings or symbols as keys.
@@ -41,30 +45,33 @@ module SmarterCSV
41
45
  def initialize(file_path, options = {})
42
46
  @options = options
43
47
 
44
- @row_sep = options[:row_sep] || $/ # Defaults to system's row separator. RFC4180 "\r\n"
48
+ @row_sep = options[:row_sep] || $/
45
49
  @col_sep = options[:col_sep] || ','
46
50
  @quote_char = options[:quote_char] || '"'
47
51
  @force_quotes = options[:force_quotes] == true
48
- @discover_headers = true # defaults to true
52
+ @quote_headers = options[:quote_headers] == true
53
+ @disable_auto_quoting = options[:disable_auto_quoting] == true
54
+ @value_converters = options[:value_converters] || {}
55
+ @map_all_keys = @value_converters.has_key?(:_all)
56
+ @mapped_keys = @value_converters.keys - [:_all]
57
+
58
+ @discover_headers = true
49
59
  if options.has_key?(:discover_headers)
50
- # passing in the option overrides the default behavior
51
60
  @discover_headers = options[:discover_headers] == true
52
61
  else
53
- # disable discover_headers when headers are given explicitly
54
62
  @discover_headers = !(options.has_key?(:map_headers) || options.has_key?(:headers))
55
63
  end
56
- @headers = [] # start with empty headers
57
- @headers = options[:headers] if options.has_key?(:headers) # unless explicitly given
64
+
65
+ @headers = []
66
+ @headers = options[:headers] if options.has_key?(:headers)
58
67
  @headers = options[:map_headers].keys if options.has_key?(:map_headers) && !options.has_key?(:headers)
59
68
  @map_headers = options[:map_headers] || {}
60
69
 
61
70
  @output_file = File.open(file_path, 'w+')
62
- # hidden state:
63
71
  @temp_file = Tempfile.new('tempfile', '/tmp')
64
72
  @quote_regex = Regexp.union(@col_sep, @row_sep, @quote_char)
65
73
  end
66
74
 
67
- # this can be called many times in order to append lines to the csv file
68
75
  def <<(data)
69
76
  case data
70
77
  when Hash
@@ -74,17 +81,19 @@ module SmarterCSV
74
81
  when NilClass
75
82
  # ignore
76
83
  else
84
+ # :nocov:
77
85
  raise InvalidInputData, "Invalid data type: #{data.class}. Must be a Hash or an Array."
86
+ # :nocov:
78
87
  end
79
88
  end
80
89
 
81
90
  def finalize
82
- # Map headers if :map_headers option is provided
83
91
  mapped_headers = @headers.map { |header| @map_headers[header] || header }
84
- mapped_headers = mapped_headers.map{|x| escape_csv_field(x)} if @force_quotes
92
+ force_quotes = @quote_headers || @force_quotes
93
+ mapped_headers = mapped_headers.map { |x| escape_csv_field(x, force_quotes) }
85
94
 
86
95
  @temp_file.rewind
87
- @output_file.write(mapped_headers.join(@col_sep) + @row_sep)
96
+ @output_file.write(mapped_headers.join(@col_sep) + @row_sep) unless mapped_headers.empty?
88
97
  @output_file.write(@temp_file.read)
89
98
  @output_file.flush
90
99
  @output_file.close
@@ -100,17 +109,43 @@ module SmarterCSV
100
109
  @headers.concat(new_keys)
101
110
  end
102
111
 
103
- # Reorder the hash to match the current headers order and fill missing fields
104
- ordered_row = @headers.map { |header| hash[header] || '' }
112
+ # Reorder the hash to match the current headers order and fill + map missing keys
113
+ ordered_row = @headers.map do |header|
114
+ has_header = hash.key?(header)
115
+ value = has_header ? hash[header] : '' # default to empty value
116
+
117
+ # first map individual keys
118
+ value = map_value(header, value) if @mapped_keys.include?(header)
119
+
120
+ # then apply general mapping rules
121
+ value = map_all_values(header, value) if @map_all_keys
122
+
123
+ escape_csv_field(value, @force_quotes) # for backwards compatibility
124
+ end
105
125
 
106
- @temp_file.write ordered_row.map { |value| escape_csv_field(value) }.join(@col_sep) + @row_sep
126
+ @temp_file.write(ordered_row.join(@col_sep) + @row_sep) unless ordered_row.empty?
107
127
  end
108
128
 
109
- def escape_csv_field(field)
110
- if @force_quotes || field.to_s.match(@quote_regex)
111
- "\"#{field}\""
129
+ def map_value(key, value)
130
+ @value_converters[key].call(value)
131
+ end
132
+
133
+ def map_all_values(key, value)
134
+ @value_converters[:_all].call(key, value)
135
+ end
136
+
137
+ def escape_csv_field(field, force_quotes = false)
138
+ str = field.to_s
139
+ return str if @disable_auto_quoting
140
+
141
+ # double-quote fields if we force that, or if the field contains the comma, new-line, or quote character
142
+ contains_special_char = str.to_s.match(@quote_regex)
143
+ if force_quotes || contains_special_char
144
+ str = str.gsub(@quote_char, @quote_char * 2) if contains_special_char # escape double-quote
145
+
146
+ "\"#{str}\""
112
147
  else
113
- field.to_s
148
+ str
114
149
  end
115
150
  end
116
151
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smarter_csv
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.1
4
+ version: 1.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tilo Sloboda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-12 00:00:00.000000000 Z
11
+ date: 2025-04-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: awesome_print
@@ -117,7 +117,8 @@ files:
117
117
  - Rakefile
118
118
  - TO_DO_v2.md
119
119
  - docs/_introduction.md
120
- - docs/basic_api.md
120
+ - docs/basic_read_api.md
121
+ - docs/basic_write_api.md
121
122
  - docs/batch_processing.md
122
123
  - docs/data_transformations.md
123
124
  - docs/examples.md