zlight_csv 0.5.1 → 0.6.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.
data/lib/zlight_csv.rb CHANGED
@@ -1,238 +1,120 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'zlight_csv/version'
4
+ require_relative 'zlight_csv/errors'
5
+ require_relative 'zlight_csv/native'
4
6
 
5
- # Load the prebuilt native extension for this Ruby version
6
- begin
7
- ruby_version = RUBY_VERSION.match(/(\d+\.\d+)/)[1]
8
- require_relative "zlight_csv/#{ruby_version}/zlight_csv"
9
- rescue LoadError
10
- require_relative 'zlight_csv/zlight_csv'
11
- end
12
-
13
- # ZLight is a high-performance CSV parser for Ruby, powered by Rust.
14
- #
15
- # It provides a simple, Ruby CSV-compatible API for parsing CSV files
16
- # up to 40x faster than the standard library.
17
- #
18
- # @example Basic parsing with headers
19
- # result = ZLight.parse("name,age\nAlice,30\nBob,25")
20
- # # => [{:name=>"Alice", :age=>"30"}, {:name=>"Bob", :age=>"25"}]
7
+ # ZLight is a CSV parser and writer for Ruby, implemented in Rust.
21
8
  #
22
- # @example Parsing with numeric conversion
23
- # result = ZLight.parse("name,age\nAlice,30", converters: :numeric)
24
- # # => [{:name=>"Alice", :age=>30}]
9
+ # The public API is deliberately small. Four of the methods below are defined
10
+ # in the native extension and four are defined here in Ruby as conveniences on
11
+ # top of them; which is which is noted on each.
25
12
  #
26
- # @example Parsing without headers
27
- # result = ZLight.parse("Alice,30\nBob,25", headers: false)
28
- # # => [["Alice", "30"], ["Bob", "25"]]
13
+ # @example Parse a string
14
+ # ZLight.parse("name,age\nAlice,30") # => [{name: "Alice", age: "30"}]
29
15
  #
30
- # @example Reading from a file
31
- # result = ZLight.read("path/to/file.csv", headers: true)
16
+ # @example Parse a file, converting numbers
17
+ # ZLight.read("users.csv", converters: :numeric)
32
18
  #
33
- # @example Streaming large files
34
- # ZLight.stream_file("large.csv") do |reader|
35
- # reader.each { |row| process(row) }
36
- # end
19
+ # @example Stream a large file in constant memory
20
+ # ZLight.open("huge.csv") { |reader| reader.each { |row| process(row) } }
37
21
  #
38
- # @example Lazy iteration with Enumerator
39
- # reader = ZLight.stream_file("large.csv")
40
- # reader.lazy.select { |row| row[:age] > 30 }.first(10)
41
- # reader.close
22
+ # @example Write a file
23
+ # ZLight.write("out.csv", [{ name: "Alice", age: 30 }])
42
24
  #
43
25
  # @see https://github.com/codebyisaad/zlight
26
+ # @see file:README.md#differences-from-stdlib-csv Differences from stdlib CSV
44
27
  module ZLight
45
- # Base error class for all ZLight errors
46
- class Error < StandardError; end
47
-
48
- # Raised when CSV parsing fails due to malformed data
49
- class ParseError < Error; end
50
-
51
- # Raised when CSV contains invalid UTF-8 encoding
52
- class EncodingError < Error; end
53
-
54
- # Raised when attempting to use a closed stream reader
55
- class StreamClosedError < Error; end
56
-
57
- # Parses a CSV string and returns an array of rows.
58
- #
59
- # This method is implemented as a native Rust extension for maximum performance.
60
- #
61
- # @param csv_string [String] The CSV data to parse
62
- # @param headers [Boolean] Treat first row as headers (default: true)
63
- # @param converters [Symbol, nil] Set to :numeric for auto-conversion of integers/floats
64
- # @param col_sep [String] Column separator character (default: ",")
65
- # @param quote_char [String] Quote character for escaping (default: '"')
66
- # @param flexible [Boolean] Allow variable-length records (default: true)
67
- #
68
- # @return [Array<Hash>] When headers: true, returns array of Hashes with Symbol keys
69
- # @return [Array<Array>] When headers: false, returns array of Arrays
70
- #
71
- # @raise [ArgumentError] If options are invalid
72
- # @raise [ZLight::ParseError] If CSV data is malformed
73
- # @raise [ZLight::EncodingError] If headers contain invalid UTF-8
74
- #
75
- # @example Parse with headers (default)
76
- # ZLight.parse("name,age\nAlice,30")
77
- # # => [{:name=>"Alice", :age=>"30"}]
78
- #
79
- # @example Parse without headers
80
- # ZLight.parse("Alice,30", headers: false)
81
- # # => [["Alice", "30"]]
82
- #
83
- # @example Parse with numeric conversion
84
- # ZLight.parse("name,age\nAlice,30", converters: :numeric)
85
- # # => [{:name=>"Alice", :age=>30}]
86
- #
87
- # @example Parse TSV (tab-separated values)
88
- # ZLight.parse("name\tage\nAlice\t30", col_sep: "\t")
89
- # # => [{:name=>"Alice", :age=>"30"}]
90
- #
91
- # @note The parse method is defined in the Rust native extension.
92
- # See ext/zlight_csv/src/lib.rs for implementation.
93
-
94
- # Generates a CSV string from an array of rows.
95
- #
96
- # This method is implemented as a native Rust extension for maximum performance.
97
- #
98
- # @param rows [Array<Hash>, Array<Array>] The data to convert to CSV
99
- # @param headers [Boolean] Write header row for hash input (default: true)
100
- # @param col_sep [String] Column separator character (default: ",")
101
- # @param quote_char [String] Quote character for escaping (default: '"')
102
- # @param force_quotes [Boolean] Force quoting all fields (default: false)
103
- #
104
- # @return [String] The generated CSV string
105
- #
106
- # @raise [ArgumentError] If options are invalid or rows format is mixed
107
- #
108
- # @example Generate CSV from array of hashes
109
- # ZLight.generate([{name: "Alice", age: 30}, {name: "Bob", age: 25}])
110
- # # => "name,age\nAlice,30\nBob,25\n"
111
- #
112
- # @example Generate CSV from array of arrays (no headers)
113
- # ZLight.generate([["Alice", 30], ["Bob", 25]])
114
- # # => "Alice,30\nBob,25\n"
115
- #
116
- # @example Generate without headers
117
- # ZLight.generate([{name: "Alice"}], headers: false)
118
- # # => "Alice\n"
119
- #
120
- # @example Generate TSV
121
- # ZLight.generate([{a: 1, b: 2}], col_sep: "\t")
122
- # # => "a\tb\n1\t2\n"
123
- #
124
- # @note The generate method is defined in the Rust native extension.
125
- # See ext/zlight_csv/src/writer.rs for implementation.
28
+ class << self
29
+ # @!method parse(csv_string, headers: true, converters: nil, col_sep: ",", quote_char: "\"", flexible: true)
30
+ # Parses a CSV string into an Array of rows.
31
+ #
32
+ # Defined in the native extension; see ext/zlight_csv/src/read/eager.rs.
33
+ # Reads Ruby's string buffer directly without copying it.
34
+ #
35
+ # @param csv_string [String] the CSV data
36
+ # @param headers [Boolean] treat the first row as headers, returning
37
+ # Hashes with Symbol keys; when false, rows are Arrays
38
+ # @param converters [Symbol, nil] `:numeric` converts integers and floats
39
+ # @param col_sep [String] single-byte column separator
40
+ # @param quote_char [String] single-byte quote character
41
+ # @param flexible [Boolean] allow rows with differing field counts
42
+ # @return [Array<Hash>, Array<Array>] one element per row
43
+ # @raise [ZLight::ParseError] if the CSV is malformed
44
+ # @raise [ZLight::EncodingError] if a header is not valid UTF-8
45
+ # @raise [ArgumentError] if an option is invalid
126
46
 
127
- # StreamReader is defined in the Rust extension.
128
- # It provides lazy/streaming CSV parsing for large files.
129
- #
130
- # @see ZLight.stream
131
- # @see ZLight.stream_file
47
+ # @!method generate(rows, headers: true, col_sep: ",", quote_char: "\"", force_quotes: false)
48
+ # Builds a CSV string from an Array of rows.
49
+ #
50
+ # Defined in the native extension; see ext/zlight_csv/src/write/mod.rs.
51
+ # Accepts an Array of Hashes or an Array of Arrays, not a mix of both.
52
+ # For Hashes the column order comes from the keys of the first one.
53
+ #
54
+ # @param rows [Array<Hash>, Array<Array>] the rows to write
55
+ # @param headers [Boolean] emit a header row (Hash input only)
56
+ # @param col_sep [String] single-byte column separator
57
+ # @param quote_char [String] single-byte quote character
58
+ # @param force_quotes [Boolean] quote every field, not only those needing it
59
+ # @return [String] the generated CSV
60
+ # @raise [ArgumentError] if an option is invalid or the row types are mixed
132
61
 
133
- class << self
134
- # Reads and parses a CSV file from disk.
62
+ # @!method stream(csv_string, **options)
63
+ # Returns a {ZLight::StreamReader} over a CSV string.
135
64
  #
136
- # Convenience method that reads the file contents and passes them to {.parse}.
137
- # The file is read with UTF-8 encoding.
65
+ # Defined in the native extension; see ext/zlight_csv/src/read/stream.rs.
66
+ # Takes the same options as {.parse}.
138
67
  #
139
- # @param path [String] Path to the CSV file
140
- # @param headers [Boolean] Treat first row as headers (default: true)
141
- # @param converters [Symbol, nil] Set to :numeric for auto-conversion
142
- # @param col_sep [String] Column separator character (default: ",")
143
- # @param quote_char [String] Quote character for escaping (default: '"')
144
- # @param flexible [Boolean] Allow variable-length records (default: true)
145
- #
146
- # @return [Array<Hash>] When headers: true, returns array of Hashes with Symbol keys
147
- # @return [Array<Array>] When headers: false, returns array of Arrays
148
- #
149
- # @raise [Errno::ENOENT] If the file does not exist
150
- # @raise [ArgumentError] If options are invalid
151
- # @raise [ZLight::ParseError] If CSV data is malformed
68
+ # @return [ZLight::StreamReader] a single-pass reader
69
+
70
+ # @!method stream_file(path, **options)
71
+ # Returns a {ZLight::StreamReader} over a file.
152
72
  #
153
- # @example Read a CSV file with headers
154
- # ZLight.read("users.csv")
155
- # # => [{:name=>"Alice", :age=>"30"}, ...]
73
+ # Defined in the native extension. The file is opened immediately, so a
74
+ # missing or unreadable path raises here rather than on the first read.
75
+ # Prefer {.open}, which closes the reader for you.
156
76
  #
157
- # @example Read with numeric conversion
158
- # ZLight.read("users.csv", converters: :numeric)
159
- # # => [{:name=>"Alice", :age=>30}, ...]
77
+ # @return [ZLight::StreamReader] a single-pass reader
78
+ # @raise [IOError] if the file does not exist or cannot be read
79
+
80
+ # Reads and parses a CSV file.
160
81
  #
161
- # @example Read a TSV file
162
- # ZLight.read("data.tsv", col_sep: "\t")
82
+ # Convenience wrapper: reads the file as UTF-8 and hands it to {.parse},
83
+ # so the whole file is held in memory. Use {.open} for large files.
163
84
  #
85
+ # @param path [String] path to the CSV file
86
+ # @param options [Hash] any option accepted by {.parse}
87
+ # @return [Array<Hash>, Array<Array>] one element per row
88
+ # @raise [Errno::ENOENT] if the file does not exist
164
89
  # @see .parse
165
90
  def read(path, **options)
166
91
  parse(File.read(path, encoding: 'UTF-8'), **options)
167
92
  end
168
93
 
169
- # Writes CSV data to a file.
170
- #
171
- # Convenience method that generates CSV and writes to disk.
172
- # The file is written with UTF-8 encoding.
173
- #
174
- # @param path [String] Path to the output CSV file
175
- # @param rows [Array<Hash>, Array<Array>] The data to write
176
- # @param headers [Boolean] Write header row for hash input (default: true)
177
- # @param col_sep [String] Column separator character (default: ",")
178
- # @param quote_char [String] Quote character for escaping (default: '"')
179
- # @param force_quotes [Boolean] Force quoting all fields (default: false)
180
- #
181
- # @return [Integer] Number of bytes written
182
- #
183
- # @raise [Errno::ENOENT] If the directory does not exist
184
- # @raise [Errno::EACCES] If the file is not writable
185
- # @raise [ArgumentError] If options are invalid
94
+ # Writes rows to a CSV file.
186
95
  #
187
- # @example Write hashes to CSV
188
- # ZLight.write("users.csv", [{name: "Alice", age: 30}])
189
- #
190
- # @example Write arrays to CSV
191
- # ZLight.write("data.csv", [["a", "b"], [1, 2]])
192
- #
193
- # @example Write TSV
194
- # ZLight.write("data.tsv", rows, col_sep: "\t")
96
+ # Convenience wrapper around {.generate}. The CSV is built in memory before
97
+ # being written, so this is not suited to output larger than RAM.
195
98
  #
99
+ # @param path [String] path to write to
100
+ # @param rows [Array<Hash>, Array<Array>] the rows to write
101
+ # @param options [Hash] any option accepted by {.generate}
102
+ # @return [Integer] bytes written
103
+ # @raise [Errno::ENOENT] if the directory does not exist
196
104
  # @see .generate
197
105
  def write(path, rows, **options)
198
106
  File.write(path, generate(rows, **options), encoding: 'UTF-8')
199
107
  end
200
108
 
201
- # Iterates over each row in the CSV string.
202
- #
203
- # When called with a block, yields each row and returns nil.
204
- # When called without a block, returns an Enumerator for lazy iteration.
205
- #
206
- # @param csv_string [String] The CSV data to parse
207
- # @param headers [Boolean] Treat first row as headers (default: true)
208
- # @param converters [Symbol, nil] Set to :numeric for auto-conversion
209
- # @param col_sep [String] Column separator character (default: ",")
210
- # @param quote_char [String] Quote character for escaping (default: '"')
211
- # @param flexible [Boolean] Allow variable-length records (default: true)
109
+ # Iterates over the rows of a CSV string.
212
110
  #
213
- # @yield [row] Yields each row to the block
214
- # @yieldparam row [Hash, Array] A single row (Hash with headers, Array without)
215
- #
216
- # @return [nil] When a block is given
217
- # @return [Enumerator] When no block is given, for lazy iteration
218
- #
219
- # @example Iterate with a block
220
- # ZLight.foreach("name,age\nAlice,30\nBob,25") do |row|
221
- # puts row[:name]
222
- # end
223
- # # Output:
224
- # # Alice
225
- # # Bob
226
- #
227
- # @example Use as Enumerator
228
- # names = ZLight.foreach("name,age\nAlice,30").map { |row| row[:name] }
229
- # # => ["Alice"]
230
- #
231
- # @example Chain with Enumerable methods
232
- # adults = ZLight.foreach(csv_data, converters: :numeric)
233
- # .select { |row| row[:age] >= 18 }
234
- # .map { |row| row[:name] }
111
+ # Note that unlike `CSV.foreach`, this takes a **string, not a path**, and
112
+ # parses the whole input before yielding. For lazy iteration use {.open}.
235
113
  #
114
+ # @param csv_string [String] the CSV data
115
+ # @param options [Hash] any option accepted by {.parse}
116
+ # @yieldparam row [Hash, Array] each row in turn
117
+ # @return [nil, Enumerator] an Enumerator when no block is given
236
118
  # @see .parse
237
119
  def foreach(csv_string, **options, &)
238
120
  return to_enum(:foreach, csv_string, **options) unless block_given?
@@ -240,66 +122,62 @@ module ZLight
240
122
  parse(csv_string, **options).each(&)
241
123
  end
242
124
 
243
- # Creates a streaming reader for a CSV file with automatic resource management.
244
- #
245
- # This is the most memory-efficient way to process large CSV files.
246
- # Rows are read one at a time directly from disk.
247
- #
248
- # When called with a block, automatically closes the reader after
249
- # the block completes (even if an exception is raised).
250
- #
251
- # @param path [String] Path to the CSV file
252
- # @param headers [Boolean] Treat first row as headers (default: true)
253
- # @param converters [Symbol, nil] Set to :numeric for auto-conversion
254
- # @param col_sep [String] Column separator character (default: ",")
255
- # @param quote_char [String] Quote character (default: '"')
256
- # @param flexible [Boolean] Allow variable-length records (default: true)
257
- #
258
- # @yield [reader] If a block is given, yields the reader and auto-closes
259
- # @yieldparam reader [ZLight::StreamReader] The streaming reader
260
- #
261
- # @return [ZLight::StreamReader] The streaming reader (if no block)
262
- # @return [Object] The block's return value (if block given)
263
- #
264
- # @raise [IOError] If the file does not exist or cannot be read
265
- # @raise [ZLight::StreamClosedError] If a row is read after the reader
266
- # has been closed
267
- #
268
- # @example Process a large file row by row
269
- # ZLight.open("users.csv") do |reader|
270
- # reader.each do |row|
271
- # User.create!(name: row[:name], email: row[:email])
272
- # end
273
- # end
274
- #
275
- # @example Get first 100 matching rows lazily
276
- # ZLight.open("data.csv", converters: :numeric) do |reader|
277
- # reader.lazy
278
- # .select { |row| row[:score] > 90 }
279
- # .first(100)
280
- # end
281
- #
282
- # @example Manual resource management
283
- # reader = ZLight.open("large.csv")
284
- # begin
285
- # reader.each { |row| process(row) }
286
- # ensure
287
- # reader.close
125
+ # Streams a CSV file, closing the reader afterwards.
126
+ #
127
+ # The memory-efficient way to process a large file: rows are read one at a
128
+ # time rather than materialised up front. With a block the reader is closed
129
+ # when the block exits, including on an exception.
130
+ #
131
+ # @param path [String] path to the CSV file
132
+ # @param options [Hash] any option accepted by {.parse}
133
+ # @yieldparam reader [ZLight::StreamReader] the reader
134
+ # @return [Object, ZLight::StreamReader] the block's value, or the reader
135
+ # itself when no block is given, in which case you must close it
136
+ # @raise [IOError] if the file does not exist or cannot be read
137
+ #
138
+ # @example Stop early without reading the rest of the file
139
+ # ZLight.open("huge.csv", converters: :numeric) do |reader|
140
+ # reader.lazy.select { |row| row[:score] > 90 }.first(100)
288
141
  # end
289
142
  #
290
143
  # @see ZLight::StreamReader
291
144
  def open(path, **options)
292
145
  reader = stream_file(path, **options)
146
+ return reader unless block_given?
293
147
 
294
- if block_given?
295
- begin
296
- yield reader
297
- ensure
298
- reader.close unless reader.closed?
299
- end
300
- else
301
- reader
148
+ begin
149
+ yield reader
150
+ ensure
151
+ reader.close unless reader.closed?
302
152
  end
303
153
  end
304
154
  end
155
+
156
+ # A single-pass reader over a CSV source, yielding one row at a time.
157
+ #
158
+ # Defined in the native extension; see ext/zlight_csv/src/read/stream.rs.
159
+ # Obtained from {ZLight.stream}, {ZLight.stream_file} or {ZLight.open}.
160
+ #
161
+ # Includes `Enumerable`, so `map`, `select`, `find` and `lazy` all work — but
162
+ # the reader is **single-pass**: each row is consumed as it is read, so a
163
+ # second enumeration yields nothing and there is no rewind.
164
+ #
165
+ # @!method next_row
166
+ # @return [Hash, Array, nil] the next row, or nil once exhausted
167
+ # @raise [ZLight::StreamClosedError] if the reader has been closed
168
+ # @!method each
169
+ # @yieldparam row [Hash, Array] each row in turn
170
+ # @return [self, Enumerator] an Enumerator when no block is given
171
+ # @!method headers
172
+ # @return [Array<Symbol>, nil] the header symbols, or nil without headers
173
+ # @!method close
174
+ # Releases the underlying file or buffer. Idempotent.
175
+ # @return [nil]
176
+ # @!method closed?
177
+ # @return [Boolean]
178
+ # @!method eof?
179
+ # @return [Boolean] true once exhausted or closed
180
+ # Reopened solely to attach the documentation above; the class itself and
181
+ # all of its methods come from the extension.
182
+ class StreamReader; end # rubocop:disable Lint/EmptyClass
305
183
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zlight_csv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zaidan Chaudhary
@@ -40,20 +40,22 @@ extensions:
40
40
  - ext/zlight_csv/extconf.rb
41
41
  extra_rdoc_files: []
42
42
  files:
43
+ - ARCHITECTURE.md
43
44
  - CHANGELOG.md
44
45
  - LICENSE
45
46
  - README.md
46
47
  - VERSION
47
48
  - ext/zlight_csv/Cargo.toml
48
49
  - ext/zlight_csv/extconf.rb
49
- - ext/zlight_csv/src/converter.rs
50
+ - ext/zlight_csv/src/convert.rs
50
51
  - ext/zlight_csv/src/error.rs
51
52
  - ext/zlight_csv/src/lib.rs
52
53
  - ext/zlight_csv/src/options.rs
53
- - ext/zlight_csv/src/parser.rs
54
- - ext/zlight_csv/src/stream.rs
55
- - ext/zlight_csv/src/writer.rs
54
+ - ext/zlight_csv/src/reader.rs
55
+ - ext/zlight_csv/src/row.rs
56
56
  - lib/zlight_csv.rb
57
+ - lib/zlight_csv/errors.rb
58
+ - lib/zlight_csv/native.rb
57
59
  - lib/zlight_csv/version.rb
58
60
  homepage: https://github.com/codebyisaad/zlight
59
61
  licenses:
@@ -1,133 +0,0 @@
1
- use magnus::{encoding::Index, prelude::*, Ruby, Value};
2
-
3
- /// Converts a byte slice field to a Ruby string value tagged with `encoding`.
4
- ///
5
- /// Fields are labelled with the encoding of the original input string (or UTF-8
6
- /// for file streams) so that parsed values match Ruby's stdlib CSV behavior and
7
- /// round-trip cleanly through the writer.
8
- #[inline(always)]
9
- pub fn field_to_string(ruby: &Ruby, field: &[u8], encoding: Index) -> Value {
10
- ruby.enc_str_new(field, encoding).as_value()
11
- }
12
-
13
- /// Fast check if a byte slice looks like it could be a float.
14
- /// Scans for '.', 'e', or 'E' in a single pass.
15
- #[inline(always)]
16
- fn looks_like_float(bytes: &[u8]) -> bool {
17
- bytes.iter().any(|&b| b == b'.' || b == b'e' || b == b'E')
18
- }
19
-
20
- /// Trim ASCII whitespace from both ends without allocation.
21
- #[inline(always)]
22
- fn trim_ascii(bytes: &[u8]) -> &[u8] {
23
- let start = bytes.iter().position(|&b| !b.is_ascii_whitespace()).unwrap_or(bytes.len());
24
- let end = bytes.iter().rposition(|&b| !b.is_ascii_whitespace()).map_or(start, |i| i + 1);
25
- &bytes[start..end]
26
- }
27
-
28
- /// Outcome of the integer fast path.
29
- enum IntScan {
30
- /// Parsed exactly into an i64.
31
- Fits(i64),
32
- /// A valid decimal integer, but too large in magnitude for an i64.
33
- TooLarge,
34
- /// Not a decimal integer at all.
35
- NotAnInteger,
36
- }
37
-
38
- /// Fast path: scan an optionally signed run of ASCII digits without full UTF-8
39
- /// validation.
40
- ///
41
- /// Values outside i64 are reported as `TooLarge` rather than rejected, so the
42
- /// caller can fall back to an exact Ruby Integer instead of an inexact float.
43
- #[inline(always)]
44
- fn scan_int(bytes: &[u8]) -> IntScan {
45
- if bytes.is_empty() {
46
- return IntScan::NotAnInteger;
47
- }
48
-
49
- let digits = match bytes[0] {
50
- b'-' | b'+' => &bytes[1..],
51
- _ => bytes,
52
- };
53
- let negative = bytes[0] == b'-';
54
-
55
- if digits.is_empty() {
56
- return IntScan::NotAnInteger;
57
- }
58
-
59
- let mut magnitude: i64 = 0;
60
- let mut overflowed = false;
61
-
62
- for &b in digits {
63
- if !b.is_ascii_digit() {
64
- return IntScan::NotAnInteger;
65
- }
66
- if overflowed {
67
- continue;
68
- }
69
- match magnitude
70
- .checked_mul(10)
71
- .and_then(|m| m.checked_add((b - b'0') as i64))
72
- {
73
- Some(next) => magnitude = next,
74
- // Keep scanning to confirm the rest is digits too, but remember
75
- // that the value needs Ruby's arbitrary-precision Integer.
76
- None => overflowed = true,
77
- }
78
- }
79
-
80
- if overflowed {
81
- IntScan::TooLarge
82
- } else if negative {
83
- IntScan::Fits(-magnitude)
84
- } else {
85
- IntScan::Fits(magnitude)
86
- }
87
- }
88
-
89
- /// Builds an exact Ruby Integer from a decimal string too large for an i64.
90
- ///
91
- /// Ruby Integers are arbitrary precision, so no digits are lost. `scan_int` has
92
- /// already established that the slice is ASCII digits with an optional sign,
93
- /// which is why `String#to_i` is safe here.
94
- #[inline]
95
- fn big_integer(ruby: &Ruby, digits: &[u8]) -> Option<Value> {
96
- let text = std::str::from_utf8(digits).ok()?;
97
- ruby.str_new(text).funcall("to_i", ()).ok()
98
- }
99
-
100
- /// Attempts to parse a byte slice as a numeric value (integer or float).
101
- #[inline]
102
- fn try_parse_numeric(ruby: &Ruby, field: &[u8]) -> Option<Value> {
103
- let trimmed = trim_ascii(field);
104
- if trimmed.is_empty() {
105
- return None;
106
- }
107
-
108
- // Fast path: try integer parsing without UTF-8 conversion.
109
- if !looks_like_float(trimmed) {
110
- match scan_int(trimmed) {
111
- IntScan::Fits(i) => return Some(ruby.integer_from_i64(i).as_value()),
112
- IntScan::TooLarge => return big_integer(ruby, trimmed),
113
- IntScan::NotAnInteger => {}
114
- }
115
- }
116
-
117
- // Slow path: need UTF-8 for float parsing.
118
- let text = std::str::from_utf8(trimmed).ok()?;
119
- text.parse::<f64>()
120
- .ok()
121
- .map(|f| ruby.float_from_f64(f).as_value())
122
- }
123
-
124
- /// Converts a byte slice field to an appropriate Ruby value.
125
- /// If `convert_numeric` is true, attempts to parse as number first.
126
- #[inline(always)]
127
- pub fn field_to_value(ruby: &Ruby, field: &[u8], convert_numeric: bool, encoding: Index) -> Value {
128
- if !convert_numeric {
129
- return field_to_string(ruby, field, encoding);
130
- }
131
-
132
- try_parse_numeric(ruby, field).unwrap_or_else(|| field_to_string(ruby, field, encoding))
133
- }