This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
Copyright (C) 2005-2016 James Edward Gray II. All rights reserved.
|
||||
Copyright (C) 2007-2017 Yukihiro Matsumoto. All rights reserved.
|
||||
Copyright (C) 2017 SHIBATA Hiroshi. All rights reserved.
|
||||
Copyright (C) 2017 Olivier Lacan. All rights reserved.
|
||||
Copyright (C) 2017 Espartaco Palma. All rights reserved.
|
||||
Copyright (C) 2017 Marcus Stollsteimer. All rights reserved.
|
||||
Copyright (C) 2017 pavel. All rights reserved.
|
||||
Copyright (C) 2017-2018 Steven Daniels. All rights reserved.
|
||||
Copyright (C) 2018 Tomohiro Ogoke. All rights reserved.
|
||||
Copyright (C) 2018 Kouhei Sutou. All rights reserved.
|
||||
Copyright (C) 2018 Mitsutaka Mimura. All rights reserved.
|
||||
Copyright (C) 2018 Vladislav. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
||||
@@ -0,0 +1,965 @@
|
||||
# News
|
||||
|
||||
## 3.3.2 - 2024-12-21
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a parse bug with a quoted line with `col_sep` and an empty
|
||||
line. This was introduced in 3.3.1.
|
||||
* GH-324
|
||||
* Reported by stoodfarback
|
||||
|
||||
### Thanks
|
||||
|
||||
* stoodfarback
|
||||
|
||||
## 3.3.1 - 2024-12-15
|
||||
|
||||
### Improvements
|
||||
|
||||
* `CSV.open`: Changed to detect BOM by default. Note that this isn't
|
||||
enabled on Windows because Ruby may have a bug. See also:
|
||||
https://bugs.ruby-lang.org/issues/20526
|
||||
* GH-301
|
||||
* Reported by Junichi Ito
|
||||
|
||||
* Improved performance.
|
||||
* GH-311
|
||||
* GH-312
|
||||
* Patch by Vladimir Kochnev
|
||||
|
||||
* `CSV.open`: Added support for `StringIO` as an input.
|
||||
* GH-300
|
||||
* GH-302
|
||||
* Patch by Marcelo
|
||||
|
||||
* Added a built-in time converter. You can use it by `converters:
|
||||
:time`.
|
||||
* GH-313
|
||||
* Patch by Bart de Water
|
||||
|
||||
* Added `CSV::TSV` for tab-separated values.
|
||||
* GH-272
|
||||
* GH-319
|
||||
* Reported by kojix2
|
||||
* Patch by Jas
|
||||
|
||||
### Thanks
|
||||
|
||||
* Junichi Ito
|
||||
|
||||
* Vladimir Kochnev
|
||||
|
||||
* Marcelo
|
||||
|
||||
* Bart de Water
|
||||
|
||||
* kojix2
|
||||
|
||||
* Jas
|
||||
|
||||
## 3.3.0 - 2024-03-22
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a regression parse bug in 3.2.9 that parsing with
|
||||
`:skip_lines` may cause wrong result.
|
||||
|
||||
## 3.2.9 - 2024-03-22
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a parse bug that wrong result may be happen when:
|
||||
|
||||
* `:skip_lines` is used
|
||||
* `:row_separator` is `"\r\n"`
|
||||
* There is a line that includes `\n` as a column value
|
||||
|
||||
Reported by Ryo Tsukamoto.
|
||||
|
||||
GH-296
|
||||
|
||||
### Thanks
|
||||
|
||||
* Ryo Tsukamoto
|
||||
|
||||
## 3.2.8 - 2023-11-08
|
||||
|
||||
### Improvements
|
||||
|
||||
* Added `CSV::InvalidEncodingError`.
|
||||
|
||||
Patch by Kosuke Shibata.
|
||||
|
||||
GH-287
|
||||
|
||||
### Thanks
|
||||
|
||||
* Kosuke Shibata
|
||||
|
||||
## 3.2.7 - 2023-06-26
|
||||
|
||||
### Improvements
|
||||
|
||||
* Removed an unused internal variable.
|
||||
[GH-273](https://github.com/ruby/csv/issues/273)
|
||||
[Patch by Mau Magnaguagno]
|
||||
|
||||
* Changed to use `https://` instead of `http://` in documents.
|
||||
[GH-274](https://github.com/ruby/csv/issues/274)
|
||||
[Patch by Vivek Bharath Akupatni]
|
||||
|
||||
* Added prefix to a helper module in test.
|
||||
[GH-278](https://github.com/ruby/csv/issues/278)
|
||||
[Patch by Luke Gruber]
|
||||
|
||||
* Added a documentation for `liberal_parsing: {backslash_quotes: true}`.
|
||||
[GH-280](https://github.com/ruby/csv/issues/280)
|
||||
[Patch by Mark Schneider]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a wrong execution result in documents.
|
||||
[GH-276](https://github.com/ruby/csv/issues/276)
|
||||
[Patch by Yuki Tsujimoto]
|
||||
|
||||
* Fixed a bug that the same line is used multiple times.
|
||||
[GH-279](https://github.com/ruby/csv/issues/279)
|
||||
[Reported by Gabriel Nagy]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Mau Magnaguagno
|
||||
|
||||
* Vivek Bharath Akupatni
|
||||
|
||||
* Yuki Tsujimoto
|
||||
|
||||
* Luke Gruber
|
||||
|
||||
* Mark Schneider
|
||||
|
||||
* Gabriel Nagy
|
||||
|
||||
## 3.2.6 - 2022-12-08
|
||||
|
||||
### Improvements
|
||||
|
||||
* `CSV#read` consumes the same lines with other methods like
|
||||
`CSV#shift`.
|
||||
[[GitHub#258](https://github.com/ruby/csv/issues/258)]
|
||||
[Reported by Lhoussaine Ghallou]
|
||||
|
||||
* All `Enumerable` based methods consume the same lines with other
|
||||
methods. This may have a performance penalty.
|
||||
[[GitHub#260](https://github.com/ruby/csv/issues/260)]
|
||||
[Reported by Lhoussaine Ghallou]
|
||||
|
||||
* Simplify some implementations.
|
||||
[[GitHub#262](https://github.com/ruby/csv/pull/262)]
|
||||
[[GitHub#263](https://github.com/ruby/csv/pull/263)]
|
||||
[Patch by Mau Magnaguagno]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed `CSV.generate_lines` document.
|
||||
[[GitHub#257](https://github.com/ruby/csv/pull/257)]
|
||||
[Patch by Sampat Badhe]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Sampat Badhe
|
||||
|
||||
* Lhoussaine Ghallou
|
||||
|
||||
* Mau Magnaguagno
|
||||
|
||||
## 3.2.5 - 2022-08-26
|
||||
|
||||
### Improvements
|
||||
|
||||
* Added `CSV.generate_lines`.
|
||||
[[GitHub#255](https://github.com/ruby/csv/issues/255)]
|
||||
[Reported by OKURA Masafumi]
|
||||
[[GitHub#256](https://github.com/ruby/csv/pull/256)]
|
||||
[Patch by Eriko Sugiyama]
|
||||
|
||||
### Thanks
|
||||
|
||||
* OKURA Masafumi
|
||||
|
||||
* Eriko Sugiyama
|
||||
|
||||
## 3.2.4 - 2022-08-22
|
||||
|
||||
### Improvements
|
||||
|
||||
* Cleaned up internal implementations.
|
||||
[[GitHub#249](https://github.com/ruby/csv/pull/249)]
|
||||
[[GitHub#250](https://github.com/ruby/csv/pull/250)]
|
||||
[[GitHub#251](https://github.com/ruby/csv/pull/251)]
|
||||
[Patch by Mau Magnaguagno]
|
||||
|
||||
* Added support for RFC 3339 style time.
|
||||
[[GitHub#248](https://github.com/ruby/csv/pull/248)]
|
||||
[Patch by Thierry Lambert]
|
||||
|
||||
* Added support for transcoding String CSV. Syntax is
|
||||
`from-encoding:to-encoding`.
|
||||
[[GitHub#254](https://github.com/ruby/csv/issues/254)]
|
||||
[Reported by Richard Stueven]
|
||||
|
||||
* Added quoted information to `CSV::FieldInfo`.
|
||||
[[GitHub#254](https://github.com/ruby/csv/pull/253)]
|
||||
[Reported by Hirokazu SUZUKI]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a link in documents.
|
||||
[[GitHub#244](https://github.com/ruby/csv/pull/244)]
|
||||
[Patch by Peter Zhu]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Peter Zhu
|
||||
|
||||
* Mau Magnaguagno
|
||||
|
||||
* Thierry Lambert
|
||||
|
||||
* Richard Stueven
|
||||
|
||||
* Hirokazu SUZUKI
|
||||
|
||||
## 3.2.3 - 2022-04-09
|
||||
|
||||
### Improvements
|
||||
|
||||
* Added contents summary to `CSV::Table#inspect`.
|
||||
[GitHub#229][Patch by Eriko Sugiyama]
|
||||
[GitHub#235][Patch by Sampat Badhe]
|
||||
|
||||
* Suppressed `$INPUT_RECORD_SEPARATOR` deprecation warning by
|
||||
`Warning.warn`.
|
||||
[GitHub#233][Reported by Jean byroot Boussier]
|
||||
|
||||
* Improved error message for liberal parsing with quoted values.
|
||||
[GitHub#231][Patch by Nikolay Rys]
|
||||
|
||||
* Fixed typos in documentation.
|
||||
[GitHub#236][Patch by Sampat Badhe]
|
||||
|
||||
* Added `:max_field_size` option and deprecated `:field_size_limit` option.
|
||||
[GitHub#238][Reported by Dan Buettner]
|
||||
|
||||
* Added `:symbol_raw` to built-in header converters.
|
||||
[GitHub#237][Reported by taki]
|
||||
[GitHub#239][Patch by Eriko Sugiyama]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a bug that some texts may be dropped unexpectedly.
|
||||
[Bug #18245][ruby-core:105587][Reported by Hassan Abdul Rehman]
|
||||
|
||||
* Fixed a bug that `:field_size_limit` doesn't work with not complex row.
|
||||
[GitHub#238][Reported by Dan Buettner]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Hassan Abdul Rehman
|
||||
|
||||
* Eriko Sugiyama
|
||||
|
||||
* Jean byroot Boussier
|
||||
|
||||
* Nikolay Rys
|
||||
|
||||
* Sampat Badhe
|
||||
|
||||
* Dan Buettner
|
||||
|
||||
* taki
|
||||
|
||||
## 3.2.2 - 2021-12-24
|
||||
|
||||
### Improvements
|
||||
|
||||
* Added a validation for invalid option combination.
|
||||
[GitHub#225][Patch by adamroyjones]
|
||||
|
||||
* Improved documentation for developers.
|
||||
[GitHub#227][Patch by Eriko Sugiyama]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a bug that all of `ARGF` contents may not be consumed.
|
||||
[GitHub#228][Reported by Rafael Navaza]
|
||||
|
||||
### Thanks
|
||||
|
||||
* adamroyjones
|
||||
|
||||
* Eriko Sugiyama
|
||||
|
||||
* Rafael Navaza
|
||||
|
||||
## 3.2.1 - 2021-10-23
|
||||
|
||||
### Improvements
|
||||
|
||||
* doc: Fixed wrong class name.
|
||||
[GitHub#217][Patch by Vince]
|
||||
|
||||
* Changed to always use `"\n"` for the default row separator on Ruby
|
||||
3.0 or later because `$INPUT_RECORD_SEPARATOR` was deprecated
|
||||
since Ruby 3.0.
|
||||
|
||||
* Added support for Ractor.
|
||||
[GitHub#218][Patch by rm155]
|
||||
|
||||
* Users who want to use the built-in converters in non-main
|
||||
Ractors need to call `Ractor.make_shareable(CSV::Converters)`
|
||||
and/or `Ractor.make_shareable(CSV::HeaderConverters)` before
|
||||
creating non-main Ractors.
|
||||
|
||||
### Thanks
|
||||
|
||||
* Vince
|
||||
|
||||
* Joakim Antman
|
||||
|
||||
* rm155
|
||||
|
||||
## 3.2.0 - 2021-06-06
|
||||
|
||||
### Improvements
|
||||
|
||||
* `CSV.open`: Added support for `:newline` option.
|
||||
[GitHub#198][Patch by Nobuyoshi Nakada]
|
||||
|
||||
* `CSV::Table#each`: Added support for column mode with duplicated
|
||||
headers.
|
||||
[GitHub#206][Reported by Yaroslav Berezovskiy]
|
||||
|
||||
* `Object#CSV`: Added support for Ruby 3.0.
|
||||
|
||||
* `CSV::Row`: Added support for pattern matching.
|
||||
[GitHub#207][Patch by Kevin Newton]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed typos in documentation.
|
||||
[GitHub#196][GitHub#205][Patch by Sampat Badhe]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Sampat Badhe
|
||||
|
||||
* Nobuyoshi Nakada
|
||||
|
||||
* Yaroslav Berezovskiy
|
||||
|
||||
* Kevin Newton
|
||||
|
||||
## 3.1.9 - 2020-11-23
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a compatibility bug that the line to be processed by
|
||||
`skip_lines:` has a row separator.
|
||||
[GitHub#194][Reported by Josef Šimánek]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Josef Šimánek
|
||||
|
||||
## 3.1.8 - 2020-11-18
|
||||
|
||||
### Improvements
|
||||
|
||||
* Improved documentation.
|
||||
[Patch by Burdette Lamar]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Burdette Lamar
|
||||
|
||||
## 3.1.7 - 2020-08-04
|
||||
|
||||
### Improvements
|
||||
|
||||
* Improved document.
|
||||
[GitHub#158][GitHub#160][GitHub#161]
|
||||
[Patch by Burdette Lamar]
|
||||
|
||||
* Updated required Ruby version to 2.5.0 or later.
|
||||
[GitHub#159]
|
||||
[Patch by Gabriel Nagy]
|
||||
|
||||
* Removed stringio 0.1.3 or later dependency.
|
||||
|
||||
### Thanks
|
||||
|
||||
* Burdette Lamar
|
||||
|
||||
* Gabriel Nagy
|
||||
|
||||
## 3.1.6 - 2020-07-20
|
||||
|
||||
### Improvements
|
||||
|
||||
* Improved document.
|
||||
[GitHub#127][GitHub#135][GitHub#136][GitHub#137][GitHub#139][GitHub#140]
|
||||
[GitHub#141][GitHub#142][GitHub#143][GitHub#145][GitHub#146][GitHub#148]
|
||||
[GitHub#148][GitHub#151][GitHub#152][GitHub#154][GitHub#155][GitHub#157]
|
||||
[Patch by Burdette Lamar]
|
||||
|
||||
* `CSV.open`: Added support for `undef: :replace`.
|
||||
[GitHub#129][Patch by Koichi ITO]
|
||||
|
||||
* `CSV.open`: Added support for `invalid: :replace`.
|
||||
[GitHub#129][Patch by Koichi ITO]
|
||||
|
||||
* Don't run quotable check for invalid encoding field values.
|
||||
[GitHub#131][Patch by Koichi ITO]
|
||||
|
||||
* Added support for specifying the target indexes and names to
|
||||
`force_quotes:`.
|
||||
[GitHub#153][Reported by Aleksandr]
|
||||
|
||||
* `CSV.generate`: Changed to use the encoding of the first non-ASCII
|
||||
field rather than the encoding of ASCII only field.
|
||||
|
||||
* Changed to require the stringio gem 0.1.3 or later.
|
||||
|
||||
### Thanks
|
||||
|
||||
* Burdette Lamar
|
||||
|
||||
* Koichi ITO
|
||||
|
||||
* Aleksandr
|
||||
|
||||
## 3.1.5 - 2020-05-18
|
||||
|
||||
### Improvements
|
||||
|
||||
* Improved document.
|
||||
[GitHub#124][Patch by Burdette Lamar]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Added missing document files.
|
||||
[GitHub#125][Reported by joast]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Burdette Lamar
|
||||
|
||||
* joast
|
||||
|
||||
## 3.1.4 - 2020-05-17
|
||||
|
||||
### Improvements
|
||||
|
||||
* Improved document.
|
||||
[GitHub#122][Patch by Burdette Lamar]
|
||||
|
||||
* Stopped to dropping stack trace for exception caused by
|
||||
`CSV.parse_line`.
|
||||
[GitHub#120][Reported by Kyle d'Oliveira]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a bug that `:write_nil_value` or `:write_empty_value` don't
|
||||
work with non `String` objects.
|
||||
[GitHub#123][Reported by asm256]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Burdette Lamar
|
||||
|
||||
* asm256
|
||||
|
||||
* Kyle d'Oliveira
|
||||
|
||||
## 3.1.3 - 2020-05-09
|
||||
|
||||
### Improvements
|
||||
|
||||
* `CSV::Row#dup`: Copied deeply.
|
||||
[GitHub#108][Patch by Jim Kane]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a infinite loop bug for zero length match `skip_lines`.
|
||||
[GitHub#110][Patch by Mike MacDonald]
|
||||
|
||||
* `CSV.generate`: Fixed a bug that encoding isn't set correctly.
|
||||
[GitHub#110][Patch by Seiei Miyagi]
|
||||
|
||||
* Fixed document for the `:strip` option.
|
||||
[GitHub#114][Patch by TOMITA Masahiro]
|
||||
|
||||
* Fixed a parse bug when split charcter exists in middle of column
|
||||
value.
|
||||
[GitHub#115][Reported by TOMITA Masahiro]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Jim Kane
|
||||
|
||||
* Mike MacDonald
|
||||
|
||||
* Seiei Miyagi
|
||||
|
||||
* TOMITA Masahiro
|
||||
|
||||
## 3.1.2 - 2019-10-12
|
||||
|
||||
### Improvements
|
||||
|
||||
* Added `:col_sep` check.
|
||||
[GitHub#94][Reported by Florent Beaurain]
|
||||
|
||||
* Suppressed warnings.
|
||||
[GitHub#96][Patch by Nobuyoshi Nakada]
|
||||
|
||||
* Improved documentation.
|
||||
[GitHub#101][GitHub#102][Patch by Vitor Oliveira]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a typo in documentation.
|
||||
[GitHub#95][Patch by Yuji Yaginuma]
|
||||
|
||||
* Fixed a multibyte character handling bug.
|
||||
[GitHub#97][Patch by koshigoe]
|
||||
|
||||
* Fixed typos in documentation.
|
||||
[GitHub#100][Patch by Vitor Oliveira]
|
||||
|
||||
* Fixed a bug that seeked `StringIO` isn't accepted.
|
||||
[GitHub#98][Patch by MATSUMOTO Katsuyoshi]
|
||||
|
||||
* Fixed a bug that `CSV.generate_line` doesn't work with
|
||||
`Encoding.default_internal`.
|
||||
[GitHub#105][Reported by David Rodríguez]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Florent Beaurain
|
||||
|
||||
* Yuji Yaginuma
|
||||
|
||||
* Nobuyoshi Nakada
|
||||
|
||||
* koshigoe
|
||||
|
||||
* Vitor Oliveira
|
||||
|
||||
* MATSUMOTO Katsuyoshi
|
||||
|
||||
* David Rodríguez
|
||||
|
||||
## 3.1.1 - 2019-04-26
|
||||
|
||||
### Improvements
|
||||
|
||||
* Added documentation for `strip` option.
|
||||
[GitHub#88][Patch by hayashiyoshino]
|
||||
|
||||
* Added documentation for `write_converters`, `write_nil_value` and
|
||||
`write_empty_value` options.
|
||||
[GitHub#87][Patch by Masafumi Koba]
|
||||
|
||||
* Added documentation for `quote_empty` option.
|
||||
[GitHub#89][Patch by kawa\_tech]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a bug that `strip; true` removes a newline.
|
||||
|
||||
### Thanks
|
||||
|
||||
* hayashiyoshino
|
||||
|
||||
* Masafumi Koba
|
||||
|
||||
* kawa\_tech
|
||||
|
||||
## 3.1.0 - 2019-04-17
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a backward incompatibility bug that `CSV#eof?` may raises an
|
||||
error.
|
||||
[GitHub#86][Reported by krororo]
|
||||
|
||||
### Thanks
|
||||
|
||||
* krororo
|
||||
|
||||
## 3.0.9 - 2019-04-15
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a test for Windows.
|
||||
|
||||
## 3.0.8 - 2019-04-11
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a bug that `strip: String` doesn't work.
|
||||
|
||||
## 3.0.7 - 2019-04-08
|
||||
|
||||
### Improvements
|
||||
|
||||
* Improve parse performance 1.5x by introducing loose parser.
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix performance regression in 3.0.5.
|
||||
|
||||
* Fix a bug that `CSV#line` returns wrong value when you
|
||||
use `quote_char: nil`.
|
||||
|
||||
## 3.0.6 - 2019-03-30
|
||||
|
||||
### Improvements
|
||||
|
||||
* `CSV.foreach`: Added support for `mode`.
|
||||
|
||||
## 3.0.5 - 2019-03-24
|
||||
|
||||
### Improvements
|
||||
|
||||
* Added `:liberal_parsing => {backslash_quote: true}` option.
|
||||
[GitHub#74][Patch by 284km]
|
||||
|
||||
* Added `:write_converters` option.
|
||||
[GitHub#73][Patch by Danillo Souza]
|
||||
|
||||
* Added `:write_nil_value` option.
|
||||
|
||||
* Added `:write_empty_value` option.
|
||||
|
||||
* Improved invalid byte line number detection.
|
||||
[GitHub#78][Patch by Alyssa Ross]
|
||||
|
||||
* Added `quote_char: nil` optimization.
|
||||
[GitHub#79][Patch by 284km]
|
||||
|
||||
* Improved error message.
|
||||
[GitHub#81][Patch by Andrés Torres]
|
||||
|
||||
* Improved IO-like implementation for `StringIO` data.
|
||||
[GitHub#80][Patch by Genadi Samokovarov]
|
||||
|
||||
* Added `:strip` option.
|
||||
[GitHub#58]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a compatibility bug that `CSV#each` doesn't care `CSV#shift`.
|
||||
[GitHub#76][Patch by Alyssa Ross]
|
||||
|
||||
* Fixed a compatibility bug that `CSV#eof?` doesn't care `CSV#each`
|
||||
and `CSV#shift`.
|
||||
[GitHub#77][Reported by Chi Leung]
|
||||
|
||||
* Fixed a compatibility bug that invalid line isn't ignored.
|
||||
[GitHub#82][Reported by krororo]
|
||||
|
||||
* Fixed a bug that `:skip_lines` doesn't work with multibyte characters data.
|
||||
[GitHub#83][Reported by ff2248]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Alyssa Ross
|
||||
|
||||
* 284km
|
||||
|
||||
* Chi Leung
|
||||
|
||||
* Danillo Souza
|
||||
|
||||
* Andrés Torres
|
||||
|
||||
* Genadi Samokovarov
|
||||
|
||||
* krororo
|
||||
|
||||
* ff2248
|
||||
|
||||
## 3.0.4 - 2019-01-25
|
||||
|
||||
### Improvements
|
||||
|
||||
* Removed duplicated `CSV::Row#include?` implementations.
|
||||
[GitHub#69][Patch by Max Schwenk]
|
||||
|
||||
* Removed duplicated `CSV::Row#header?` implementations.
|
||||
[GitHub#70][Patch by Max Schwenk]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a typo in document.
|
||||
[GitHub#72][Patch by Artur Beljajev]
|
||||
|
||||
* Fixed a compatibility bug when row headers are changed.
|
||||
[GitHub#71][Reported by tomoyuki kosaka]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Max Schwenk
|
||||
|
||||
* Artur Beljajev
|
||||
|
||||
* tomoyuki kosaka
|
||||
|
||||
## 3.0.3 - 2019-01-12
|
||||
|
||||
### Improvements
|
||||
|
||||
* Migrated benchmark tool to benchmark-driver from benchmark-ips.
|
||||
[GitHub#57][Patch by 284km]
|
||||
|
||||
* Added `liberal_parsing: {double_quote_outside_quote: true}` parse
|
||||
option.
|
||||
[GitHub#66][Reported by Watson]
|
||||
|
||||
* Added `quote_empty:` write option.
|
||||
[GitHub#35][Reported by Dave Myron]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a compatibility bug that `CSV.generate` always return
|
||||
`ASCII-8BIT` encoding string.
|
||||
[GitHub#63][Patch by Watson]
|
||||
|
||||
* Fixed a compatibility bug that `CSV.parse("", headers: true)`
|
||||
doesn't return `CSV::Table`.
|
||||
[GitHub#64][Reported by Watson][Patch by 284km]
|
||||
|
||||
* Fixed a compatibility bug that multiple-characters column
|
||||
separator doesn't work.
|
||||
[GitHub#67][Reported by Jesse Reiss]
|
||||
|
||||
* Fixed a compatibility bug that double `#each` parse twice.
|
||||
[GitHub#68][Reported by Max Schwenk]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Watson
|
||||
|
||||
* 284km
|
||||
|
||||
* Jesse Reiss
|
||||
|
||||
* Dave Myron
|
||||
|
||||
* Max Schwenk
|
||||
|
||||
## 3.0.2 - 2018-12-23
|
||||
|
||||
### Improvements
|
||||
|
||||
* Changed to use strscan in parser.
|
||||
[GitHub#52][Patch by 284km]
|
||||
|
||||
* Improves CSV write performance.
|
||||
3.0.2 will be about 2 times faster than 3.0.1.
|
||||
|
||||
* Improves CSV parse performance for complex case.
|
||||
3.0.2 will be about 2 times faster than 3.0.1.
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a parse error bug for new line only input with `headers` option.
|
||||
[GitHub#53][Reported by Chris Beer]
|
||||
|
||||
* Fixed some typos in document.
|
||||
[GitHub#54][Patch by Victor Shepelev]
|
||||
|
||||
### Thanks
|
||||
|
||||
* 284km
|
||||
|
||||
* Chris Beer
|
||||
|
||||
* Victor Shepelev
|
||||
|
||||
## 3.0.1 - 2018-12-07
|
||||
|
||||
### Improvements
|
||||
|
||||
* Added a test.
|
||||
[GitHub#38][Patch by 284km]
|
||||
|
||||
* `CSV::Row#dup`: Changed to duplicate internal data.
|
||||
[GitHub#39][Reported by André Guimarães Sakata]
|
||||
|
||||
* Documented `:nil_value` and `:empty_value` options.
|
||||
[GitHub#41][Patch by OwlWorks]
|
||||
|
||||
* Added support for separator detection for non-seekable inputs.
|
||||
[GitHub#45][Patch by Ilmari Karonen]
|
||||
|
||||
* Removed needless code.
|
||||
[GitHub#48][Patch by Espartaco Palma]
|
||||
|
||||
* Added support for parsing header only CSV with `headers: true`.
|
||||
[GitHub#47][Patch by Kazuma Shibasaka]
|
||||
|
||||
* Added support for coverage report in CI.
|
||||
[GitHub#48][Patch by Espartaco Palma]
|
||||
|
||||
* Improved auto CR row separator detection.
|
||||
[GitHub#51][Reported by Yuki Kurihara]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a typo in document.
|
||||
[GitHub#40][Patch by Marcus Stollsteimer]
|
||||
|
||||
### Thanks
|
||||
|
||||
* 284km
|
||||
|
||||
* André Guimarães Sakata
|
||||
|
||||
* Marcus Stollsteimer
|
||||
|
||||
* OwlWorks
|
||||
|
||||
* Ilmari Karonen
|
||||
|
||||
* Espartaco Palma
|
||||
|
||||
* Kazuma Shibasaka
|
||||
|
||||
* Yuki Kurihara
|
||||
|
||||
## 3.0.0 - 2018-06-06
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed a bug that header isn't returned for empty row.
|
||||
[GitHub#37][Patch by Grace Lee]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Grace Lee
|
||||
|
||||
## 1.0.2 - 2018-05-03
|
||||
|
||||
### Improvements
|
||||
|
||||
* Split file for CSV::VERSION
|
||||
|
||||
* Code cleanup: Split csv.rb into a more manageable structure
|
||||
[GitHub#19][Patch by Espartaco Palma]
|
||||
[GitHub#20][Patch by Steven Daniels]
|
||||
|
||||
* Use CSV::MalformedCSVError for invalid encoding line
|
||||
[GitHub#26][Reported by deepj]
|
||||
|
||||
* Support implicit Row <-> Array conversion
|
||||
[Bug #10013][ruby-core:63582][Reported by Dawid Janczak]
|
||||
|
||||
* Update class docs
|
||||
[GitHub#32][Patch by zverok]
|
||||
|
||||
* Add `Row#each_pair`
|
||||
[GitHub#33][Patch by zverok]
|
||||
|
||||
* Improve CSV performance
|
||||
[GitHub#30][Patch by Watson]
|
||||
|
||||
* Add :nil_value and :empty_value option
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix a bug that "bom|utf-8" doesn't work
|
||||
[GitHub#23][Reported by Pavel Lobashov]
|
||||
|
||||
* `CSV::Row#to_h`, `#to_hash`: uses the same value as `Row#[]`
|
||||
[Bug #14482][Reported by tomoya ishida]
|
||||
|
||||
* Make row separator detection more robust
|
||||
[GitHub#25][Reported by deepj]
|
||||
|
||||
* Fix a bug that too much separator when col_sep is `" "`
|
||||
[Bug #8784][ruby-core:63582][Reported by Sylvain Laperche]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Espartaco Palma
|
||||
|
||||
* Steven Daniels
|
||||
|
||||
* deepj
|
||||
|
||||
* Dawid Janczak
|
||||
|
||||
* zverok
|
||||
|
||||
* Watson
|
||||
|
||||
* Pavel Lobashov
|
||||
|
||||
* tomoya ishida
|
||||
|
||||
* Sylvain Laperche
|
||||
|
||||
* Ryunosuke Sato
|
||||
|
||||
## 1.0.1 - 2018-02-09
|
||||
|
||||
### Improvements
|
||||
|
||||
* `CSV::Table#delete`: Added bulk delete support. You can delete
|
||||
multiple rows and columns at once.
|
||||
[GitHub#4][Patch by Vladislav]
|
||||
|
||||
* Updated Gem description.
|
||||
[GitHub#11][Patch by Marcus Stollsteimer]
|
||||
|
||||
* Code cleanup.
|
||||
[GitHub#12][Patch by Marcus Stollsteimer]
|
||||
[GitHub#14][Patch by Steven Daniels]
|
||||
[GitHub#18][Patch by takkanm]
|
||||
|
||||
* `CSV::Table#dig`: Added.
|
||||
[GitHub#15][Patch by Tomohiro Ogoke]
|
||||
|
||||
* `CSV::Row#dig`: Added.
|
||||
[GitHub#15][Patch by Tomohiro Ogoke]
|
||||
|
||||
* Added ISO 8601 support to date time converter.
|
||||
[GitHub#16]
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fixed wrong `CSV::VERSION`.
|
||||
[GitHub#10][Reported by Marcus Stollsteimer]
|
||||
|
||||
* `CSV.generate`: Fixed a regression bug that `String` argument is
|
||||
ignored.
|
||||
[GitHub#13][Patch by pavel]
|
||||
|
||||
### Thanks
|
||||
|
||||
* Vladislav
|
||||
|
||||
* Marcus Stollsteimer
|
||||
|
||||
* Steven Daniels
|
||||
|
||||
* takkanm
|
||||
|
||||
* Tomohiro Ogoke
|
||||
|
||||
* pavel
|
||||
@@ -0,0 +1,55 @@
|
||||
# CSV
|
||||
|
||||
This library provides a complete interface to CSV files and data. It offers tools to enable you to read and write to and from Strings or IO objects, as needed.
|
||||
|
||||
## Installation
|
||||
|
||||
Add this line to your application's Gemfile:
|
||||
|
||||
```ruby
|
||||
gem 'csv'
|
||||
```
|
||||
|
||||
And then execute:
|
||||
|
||||
$ bundle
|
||||
|
||||
Or install it yourself as:
|
||||
|
||||
$ gem install csv
|
||||
|
||||
## Usage
|
||||
|
||||
```ruby
|
||||
require "csv"
|
||||
|
||||
CSV.foreach("path/to/file.csv") do |row|
|
||||
# use row here...
|
||||
end
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [API](https://ruby.github.io/csv/): all classes, methods, and constants.
|
||||
- [Recipes](https://ruby.github.io/csv/doc/csv/recipes/recipes_rdoc.html): specific code for specific tasks.
|
||||
|
||||
## Development
|
||||
|
||||
After checking out the repo, run `ruby run-test.rb` to check if your changes can pass the test.
|
||||
|
||||
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
||||
|
||||
## Contributing
|
||||
|
||||
Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/csv.
|
||||
|
||||
### NOTE: About RuboCop
|
||||
|
||||
We don't use RuboCop because we can manage our coding style by ourselves. We want to accept small fluctuations in our coding style because we use Ruby.
|
||||
Please do not submit issues and PRs that aim to introduce RuboCop in this repository.
|
||||
|
||||
## License
|
||||
|
||||
The gem is available as open source under the terms of the [2-Clause BSD License](https://opensource.org/licenses/BSD-2-Clause).
|
||||
|
||||
See LICENSE.txt for details.
|
||||
@@ -0,0 +1,5 @@
|
||||
* Argument +io+ should be an IO object that is:
|
||||
* Open for reading; on return, the IO object will be closed.
|
||||
* Positioned at the beginning.
|
||||
To position at the end, for appending, use method CSV.generate.
|
||||
For any other positioning, pass a preset \StringIO object instead.
|
||||
@@ -0,0 +1,57 @@
|
||||
====== Option +col_sep+
|
||||
|
||||
Specifies the \String column separator to be used
|
||||
for both parsing and generating.
|
||||
The \String will be transcoded into the data's \Encoding before use.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:col_sep) # => "," (comma)
|
||||
|
||||
Using the default (comma):
|
||||
str = CSV.generate do |csv|
|
||||
csv << [:foo, 0]
|
||||
csv << [:bar, 1]
|
||||
csv << [:baz, 2]
|
||||
end
|
||||
str # => "foo,0\nbar,1\nbaz,2\n"
|
||||
ary = CSV.parse(str)
|
||||
ary # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
Using +:+ (colon):
|
||||
col_sep = ':'
|
||||
str = CSV.generate(col_sep: col_sep) do |csv|
|
||||
csv << [:foo, 0]
|
||||
csv << [:bar, 1]
|
||||
csv << [:baz, 2]
|
||||
end
|
||||
str # => "foo:0\nbar:1\nbaz:2\n"
|
||||
ary = CSV.parse(str, col_sep: col_sep)
|
||||
ary # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
Using +::+ (two colons):
|
||||
col_sep = '::'
|
||||
str = CSV.generate(col_sep: col_sep) do |csv|
|
||||
csv << [:foo, 0]
|
||||
csv << [:bar, 1]
|
||||
csv << [:baz, 2]
|
||||
end
|
||||
str # => "foo::0\nbar::1\nbaz::2\n"
|
||||
ary = CSV.parse(str, col_sep: col_sep)
|
||||
ary # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
Using <tt>''</tt> (empty string):
|
||||
col_sep = ''
|
||||
str = CSV.generate(col_sep: col_sep) do |csv|
|
||||
csv << [:foo, 0]
|
||||
csv << [:bar, 1]
|
||||
csv << [:baz, 2]
|
||||
end
|
||||
str # => "foo0\nbar1\nbaz2\n"
|
||||
|
||||
---
|
||||
|
||||
Raises an exception if parsing with the empty \String:
|
||||
col_sep = ''
|
||||
# Raises ArgumentError (:col_sep must be 1 or more characters: "")
|
||||
CSV.parse("foo0\nbar1\nbaz2\n", col_sep: col_sep)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
====== Option +quote_char+
|
||||
|
||||
Specifies the character (\String of length 1) used used to quote fields
|
||||
in both parsing and generating.
|
||||
This String will be transcoded into the data's \Encoding before use.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:quote_char) # => "\"" (double quote)
|
||||
|
||||
This is useful for an application that incorrectly uses <tt>'</tt> (single-quote)
|
||||
to quote fields, instead of the correct <tt>"</tt> (double-quote).
|
||||
|
||||
Using the default (double quote):
|
||||
str = CSV.generate do |csv|
|
||||
csv << ['foo', 0]
|
||||
csv << ["'bar'", 1]
|
||||
csv << ['"baz"', 2]
|
||||
end
|
||||
str # => "foo,0\n'bar',1\n\"\"\"baz\"\"\",2\n"
|
||||
ary = CSV.parse(str)
|
||||
ary # => [["foo", "0"], ["'bar'", "1"], ["\"baz\"", "2"]]
|
||||
|
||||
Using <tt>'</tt> (single-quote):
|
||||
quote_char = "'"
|
||||
str = CSV.generate(quote_char: quote_char) do |csv|
|
||||
csv << ['foo', 0]
|
||||
csv << ["'bar'", 1]
|
||||
csv << ['"baz"', 2]
|
||||
end
|
||||
str # => "foo,0\n'''bar''',1\n\"baz\",2\n"
|
||||
ary = CSV.parse(str, quote_char: quote_char)
|
||||
ary # => [["foo", "0"], ["'bar'", "1"], ["\"baz\"", "2"]]
|
||||
|
||||
---
|
||||
|
||||
Raises an exception if the \String length is greater than 1:
|
||||
# Raises ArgumentError (:quote_char has to be nil or a single character String)
|
||||
CSV.new('', quote_char: 'xx')
|
||||
|
||||
Raises an exception if the value is not a \String:
|
||||
# Raises ArgumentError (:quote_char has to be nil or a single character String)
|
||||
CSV.new('', quote_char: :foo)
|
||||
@@ -0,0 +1,91 @@
|
||||
====== Option +row_sep+
|
||||
|
||||
Specifies the row separator, a \String or the \Symbol <tt>:auto</tt> (see below),
|
||||
to be used for both parsing and generating.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:row_sep) # => :auto
|
||||
|
||||
---
|
||||
|
||||
When +row_sep+ is a \String, that \String becomes the row separator.
|
||||
The String will be transcoded into the data's Encoding before use.
|
||||
|
||||
Using <tt>"\n"</tt>:
|
||||
row_sep = "\n"
|
||||
str = CSV.generate(row_sep: row_sep) do |csv|
|
||||
csv << [:foo, 0]
|
||||
csv << [:bar, 1]
|
||||
csv << [:baz, 2]
|
||||
end
|
||||
str # => "foo,0\nbar,1\nbaz,2\n"
|
||||
ary = CSV.parse(str)
|
||||
ary # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
Using <tt>|</tt> (pipe):
|
||||
row_sep = '|'
|
||||
str = CSV.generate(row_sep: row_sep) do |csv|
|
||||
csv << [:foo, 0]
|
||||
csv << [:bar, 1]
|
||||
csv << [:baz, 2]
|
||||
end
|
||||
str # => "foo,0|bar,1|baz,2|"
|
||||
ary = CSV.parse(str, row_sep: row_sep)
|
||||
ary # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
Using <tt>--</tt> (two hyphens):
|
||||
row_sep = '--'
|
||||
str = CSV.generate(row_sep: row_sep) do |csv|
|
||||
csv << [:foo, 0]
|
||||
csv << [:bar, 1]
|
||||
csv << [:baz, 2]
|
||||
end
|
||||
str # => "foo,0--bar,1--baz,2--"
|
||||
ary = CSV.parse(str, row_sep: row_sep)
|
||||
ary # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
Using <tt>''</tt> (empty string):
|
||||
row_sep = ''
|
||||
str = CSV.generate(row_sep: row_sep) do |csv|
|
||||
csv << [:foo, 0]
|
||||
csv << [:bar, 1]
|
||||
csv << [:baz, 2]
|
||||
end
|
||||
str # => "foo,0bar,1baz,2"
|
||||
ary = CSV.parse(str, row_sep: row_sep)
|
||||
ary # => [["foo", "0bar", "1baz", "2"]]
|
||||
|
||||
---
|
||||
|
||||
When +row_sep+ is the \Symbol +:auto+ (the default),
|
||||
generating uses <tt>"\n"</tt> as the row separator:
|
||||
str = CSV.generate do |csv|
|
||||
csv << [:foo, 0]
|
||||
csv << [:bar, 1]
|
||||
csv << [:baz, 2]
|
||||
end
|
||||
str # => "foo,0\nbar,1\nbaz,2\n"
|
||||
|
||||
Parsing, on the other hand, invokes auto-discovery of the row separator.
|
||||
|
||||
Auto-discovery reads ahead in the data looking for the next <tt>\r\n</tt>, +\n+, or +\r+ sequence.
|
||||
The sequence will be selected even if it occurs in a quoted field,
|
||||
assuming that you would have the same line endings there.
|
||||
|
||||
Example:
|
||||
str = CSV.generate do |csv|
|
||||
csv << [:foo, 0]
|
||||
csv << [:bar, 1]
|
||||
csv << [:baz, 2]
|
||||
end
|
||||
str # => "foo,0\nbar,1\nbaz,2\n"
|
||||
ary = CSV.parse(str)
|
||||
ary # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
The default <tt>$INPUT_RECORD_SEPARATOR</tt> (<tt>$/</tt>) is used
|
||||
if any of the following is true:
|
||||
* None of those sequences is found.
|
||||
* Data is +ARGF+, +STDIN+, +STDOUT+, or +STDERR+.
|
||||
* The stream is only available for output.
|
||||
|
||||
Obviously, discovery takes a little time. Set manually if speed is important. Also note that IO objects should be opened in binary mode on Windows if this feature will be used as the line-ending translation can cause problems with resetting the document position to where it was before the read ahead.
|
||||
@@ -0,0 +1,17 @@
|
||||
====== Option +force_quotes+
|
||||
|
||||
Specifies the boolean that determines whether each output field is to be double-quoted.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:force_quotes) # => false
|
||||
|
||||
For examples in this section:
|
||||
ary = ['foo', 0, nil]
|
||||
|
||||
Using the default, +false+:
|
||||
str = CSV.generate_line(ary)
|
||||
str # => "foo,0,\n"
|
||||
|
||||
Using +true+:
|
||||
str = CSV.generate_line(ary, force_quotes: true)
|
||||
str # => "\"foo\",\"0\",\"\"\n"
|
||||
@@ -0,0 +1,12 @@
|
||||
====== Option +quote_empty+
|
||||
|
||||
Specifies the boolean that determines whether an empty value is to be double-quoted.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:quote_empty) # => true
|
||||
|
||||
With the default +true+:
|
||||
CSV.generate_line(['"', ""]) # => "\"\"\"\",\"\"\n"
|
||||
|
||||
With +false+:
|
||||
CSV.generate_line(['"', ""], quote_empty: false) # => "\"\"\"\",\n"
|
||||
@@ -0,0 +1,25 @@
|
||||
====== Option +write_converters+
|
||||
|
||||
Specifies converters to be used in generating fields.
|
||||
See {Write Converters}[#class-CSV-label-Write+Converters]
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:write_converters) # => nil
|
||||
|
||||
With no write converter:
|
||||
str = CSV.generate_line(["\na\n", "\tb\t", " c "])
|
||||
str # => "\"\na\n\",\tb\t, c \n"
|
||||
|
||||
With a write converter:
|
||||
strip_converter = proc {|field| field.strip }
|
||||
str = CSV.generate_line(["\na\n", "\tb\t", " c "], write_converters: strip_converter)
|
||||
str # => "a,b,c\n"
|
||||
|
||||
With two write converters (called in order):
|
||||
upcase_converter = proc {|field| field.upcase }
|
||||
downcase_converter = proc {|field| field.downcase }
|
||||
write_converters = [upcase_converter, downcase_converter]
|
||||
str = CSV.generate_line(['a', 'b', 'c'], write_converters: write_converters)
|
||||
str # => "a,b,c\n"
|
||||
|
||||
See also {Write Converters}[#class-CSV-label-Write+Converters]
|
||||
@@ -0,0 +1,15 @@
|
||||
====== Option +write_empty_value+
|
||||
|
||||
Specifies the object that is to be substituted for each field
|
||||
that has an empty \String.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:write_empty_value) # => ""
|
||||
|
||||
Without the option:
|
||||
str = CSV.generate_line(['a', '', 'c', ''])
|
||||
str # => "a,\"\",c,\"\"\n"
|
||||
|
||||
With the option:
|
||||
str = CSV.generate_line(['a', '', 'c', ''], write_empty_value: "x")
|
||||
str # => "a,x,c,x\n"
|
||||
@@ -0,0 +1,29 @@
|
||||
====== Option +write_headers+
|
||||
|
||||
Specifies the boolean that determines whether a header row is included in the output;
|
||||
ignored if there are no headers.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:write_headers) # => nil
|
||||
|
||||
Without +write_headers+:
|
||||
file_path = 't.csv'
|
||||
CSV.open(file_path,'w',
|
||||
:headers => ['Name','Value']
|
||||
) do |csv|
|
||||
csv << ['foo', '0']
|
||||
end
|
||||
CSV.open(file_path) do |csv|
|
||||
csv.shift
|
||||
end # => ["foo", "0"]
|
||||
|
||||
With +write_headers+":
|
||||
CSV.open(file_path,'w',
|
||||
:write_headers => true,
|
||||
:headers => ['Name','Value']
|
||||
) do |csv|
|
||||
csv << ['foo', '0']
|
||||
end
|
||||
CSV.open(file_path) do |csv|
|
||||
csv.shift
|
||||
end # => ["Name", "Value"]
|
||||
@@ -0,0 +1,14 @@
|
||||
====== Option +write_nil_value+
|
||||
|
||||
Specifies the object that is to be substituted for each +nil+-valued field.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:write_nil_value) # => nil
|
||||
|
||||
Without the option:
|
||||
str = CSV.generate_line(['a', nil, 'c', nil])
|
||||
str # => "a,,c,\n"
|
||||
|
||||
With the option:
|
||||
str = CSV.generate_line(['a', nil, 'c', nil], write_nil_value: "x")
|
||||
str # => "a,x,c,x\n"
|
||||
@@ -0,0 +1,46 @@
|
||||
====== Option +converters+
|
||||
|
||||
Specifies converters to be used in parsing fields.
|
||||
See {Field Converters}[#class-CSV-label-Field+Converters]
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:converters) # => nil
|
||||
|
||||
The value may be a field converter name
|
||||
(see {Stored Converters}[#class-CSV-label-Stored+Converters]):
|
||||
str = '1,2,3'
|
||||
# Without a converter
|
||||
array = CSV.parse_line(str)
|
||||
array # => ["1", "2", "3"]
|
||||
# With built-in converter :integer
|
||||
array = CSV.parse_line(str, converters: :integer)
|
||||
array # => [1, 2, 3]
|
||||
|
||||
The value may be a converter list
|
||||
(see {Converter Lists}[#class-CSV-label-Converter+Lists]):
|
||||
str = '1,3.14159'
|
||||
# Without converters
|
||||
array = CSV.parse_line(str)
|
||||
array # => ["1", "3.14159"]
|
||||
# With built-in converters
|
||||
array = CSV.parse_line(str, converters: [:integer, :float])
|
||||
array # => [1, 3.14159]
|
||||
|
||||
The value may be a \Proc custom converter:
|
||||
(see {Custom Field Converters}[#class-CSV-label-Custom+Field+Converters]):
|
||||
str = ' foo , bar , baz '
|
||||
# Without a converter
|
||||
array = CSV.parse_line(str)
|
||||
array # => [" foo ", " bar ", " baz "]
|
||||
# With a custom converter
|
||||
array = CSV.parse_line(str, converters: proc {|field| field.strip })
|
||||
array # => ["foo", "bar", "baz"]
|
||||
|
||||
See also {Custom Field Converters}[#class-CSV-label-Custom+Field+Converters]
|
||||
|
||||
---
|
||||
|
||||
Raises an exception if the converter is not a converter name or a \Proc:
|
||||
str = 'foo,0'
|
||||
# Raises NoMethodError (undefined method `arity' for nil:NilClass)
|
||||
CSV.parse(str, converters: :foo)
|
||||
@@ -0,0 +1,13 @@
|
||||
====== Option +empty_value+
|
||||
|
||||
Specifies the object that is to be substituted
|
||||
for each field that has an empty \String.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:empty_value) # => "" (empty string)
|
||||
|
||||
With the default, <tt>""</tt>:
|
||||
CSV.parse_line('a,"",b,"",c') # => ["a", "", "b", "", "c"]
|
||||
|
||||
With a different object:
|
||||
CSV.parse_line('a,"",b,"",c', empty_value: 'x') # => ["a", "x", "b", "x", "c"]
|
||||
@@ -0,0 +1,39 @@
|
||||
====== Option +field_size_limit+
|
||||
|
||||
Specifies the \Integer field size limit.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:field_size_limit) # => nil
|
||||
|
||||
This is a maximum size CSV will read ahead looking for the closing quote for a field.
|
||||
(In truth, it reads to the first line ending beyond this size.)
|
||||
If a quote cannot be found within the limit CSV will raise a MalformedCSVError,
|
||||
assuming the data is faulty.
|
||||
You can use this limit to prevent what are effectively DoS attacks on the parser.
|
||||
However, this limit can cause a legitimate parse to fail;
|
||||
therefore the default value is +nil+ (no limit).
|
||||
|
||||
For the examples in this section:
|
||||
str = <<~EOT
|
||||
"a","b"
|
||||
"
|
||||
2345
|
||||
",""
|
||||
EOT
|
||||
str # => "\"a\",\"b\"\n\"\n2345\n\",\"\"\n"
|
||||
|
||||
Using the default +nil+:
|
||||
ary = CSV.parse(str)
|
||||
ary # => [["a", "b"], ["\n2345\n", ""]]
|
||||
|
||||
Using <tt>50</tt>:
|
||||
field_size_limit = 50
|
||||
ary = CSV.parse(str, field_size_limit: field_size_limit)
|
||||
ary # => [["a", "b"], ["\n2345\n", ""]]
|
||||
|
||||
---
|
||||
|
||||
Raises an exception if a field is too long:
|
||||
big_str = "123456789\n" * 1024
|
||||
# Raises CSV::MalformedCSVError (Field size exceeded in line 1.)
|
||||
CSV.parse('valid,fields,"' + big_str + '"', field_size_limit: 2048)
|
||||
@@ -0,0 +1,43 @@
|
||||
====== Option +header_converters+
|
||||
|
||||
Specifies converters to be used in parsing headers.
|
||||
See {Header Converters}[#class-CSV-label-Header+Converters]
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:header_converters) # => nil
|
||||
|
||||
Identical in functionality to option {converters}[#class-CSV-label-Option+converters]
|
||||
except that:
|
||||
- The converters apply only to the header row.
|
||||
- The built-in header converters are +:downcase+ and +:symbol+.
|
||||
|
||||
This section assumes prior execution of:
|
||||
str = <<-EOT
|
||||
Name,Value
|
||||
foo,0
|
||||
bar,1
|
||||
baz,2
|
||||
EOT
|
||||
# With no header converter
|
||||
table = CSV.parse(str, headers: true)
|
||||
table.headers # => ["Name", "Value"]
|
||||
|
||||
The value may be a header converter name
|
||||
(see {Stored Converters}[#class-CSV-label-Stored+Converters]):
|
||||
table = CSV.parse(str, headers: true, header_converters: :downcase)
|
||||
table.headers # => ["name", "value"]
|
||||
|
||||
The value may be a converter list
|
||||
(see {Converter Lists}[#class-CSV-label-Converter+Lists]):
|
||||
header_converters = [:downcase, :symbol]
|
||||
table = CSV.parse(str, headers: true, header_converters: header_converters)
|
||||
table.headers # => [:name, :value]
|
||||
|
||||
The value may be a \Proc custom converter
|
||||
(see {Custom Header Converters}[#class-CSV-label-Custom+Header+Converters]):
|
||||
upcase_converter = proc {|field| field.upcase }
|
||||
table = CSV.parse(str, headers: true, header_converters: upcase_converter)
|
||||
table.headers # => ["NAME", "VALUE"]
|
||||
|
||||
See also {Custom Header Converters}[#class-CSV-label-Custom+Header+Converters]
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
====== Option +headers+
|
||||
|
||||
Specifies a boolean, \Symbol, \Array, or \String to be used
|
||||
to define column headers.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:headers) # => false
|
||||
|
||||
---
|
||||
|
||||
Without +headers+:
|
||||
str = <<-EOT
|
||||
Name,Count
|
||||
foo,0
|
||||
bar,1
|
||||
bax,2
|
||||
EOT
|
||||
csv = CSV.new(str)
|
||||
csv # => #<CSV io_type:StringIO encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
|
||||
csv.headers # => nil
|
||||
csv.shift # => ["Name", "Count"]
|
||||
|
||||
---
|
||||
|
||||
If set to +true+ or the \Symbol +:first_row+,
|
||||
the first row of the data is treated as a row of headers:
|
||||
str = <<-EOT
|
||||
Name,Count
|
||||
foo,0
|
||||
bar,1
|
||||
bax,2
|
||||
EOT
|
||||
csv = CSV.new(str, headers: true)
|
||||
csv # => #<CSV io_type:StringIO encoding:UTF-8 lineno:2 col_sep:"," row_sep:"\n" quote_char:"\"" headers:["Name", "Count"]>
|
||||
csv.headers # => ["Name", "Count"]
|
||||
csv.shift # => #<CSV::Row "Name":"bar" "Count":"1">
|
||||
|
||||
---
|
||||
|
||||
If set to an \Array, the \Array elements are treated as headers:
|
||||
str = <<-EOT
|
||||
foo,0
|
||||
bar,1
|
||||
bax,2
|
||||
EOT
|
||||
csv = CSV.new(str, headers: ['Name', 'Count'])
|
||||
csv
|
||||
csv.headers # => ["Name", "Count"]
|
||||
csv.shift # => #<CSV::Row "Name":"bar" "Count":"1">
|
||||
|
||||
---
|
||||
|
||||
If set to a \String +str+, method <tt>CSV::parse_line(str, options)</tt> is called
|
||||
with the current +options+, and the returned \Array is treated as headers:
|
||||
str = <<-EOT
|
||||
foo,0
|
||||
bar,1
|
||||
bax,2
|
||||
EOT
|
||||
csv = CSV.new(str, headers: 'Name,Count')
|
||||
csv
|
||||
csv.headers # => ["Name", "Count"]
|
||||
csv.shift # => #<CSV::Row "Name":"bar" "Count":"1">
|
||||
@@ -0,0 +1,38 @@
|
||||
====== Option +liberal_parsing+
|
||||
|
||||
Specifies the boolean or hash value that determines whether
|
||||
CSV will attempt to parse input not conformant with RFC 4180,
|
||||
such as double quotes in unquoted fields.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:liberal_parsing) # => false
|
||||
|
||||
For the next two examples:
|
||||
str = 'is,this "three, or four",fields'
|
||||
|
||||
Without +liberal_parsing+:
|
||||
# Raises CSV::MalformedCSVError (Illegal quoting in str 1.)
|
||||
CSV.parse_line(str)
|
||||
|
||||
With +liberal_parsing+:
|
||||
ary = CSV.parse_line(str, liberal_parsing: true)
|
||||
ary # => ["is", "this \"three", " or four\"", "fields"]
|
||||
|
||||
Use the +backslash_quote+ sub-option to parse values that use
|
||||
a backslash to escape a double-quote character. This
|
||||
causes the parser to treat <code>\"</code> as if it were
|
||||
<code>""</code>.
|
||||
|
||||
For the next two examples:
|
||||
str = 'Show,"Harry \"Handcuff\" Houdini, the one and only","Tampa Theater"'
|
||||
|
||||
With +liberal_parsing+, but without the +backslash_quote+ sub-option:
|
||||
# Incorrect interpretation of backslash; incorrectly interprets the quoted comma as a field separator.
|
||||
ary = CSV.parse_line(str, liberal_parsing: true)
|
||||
ary # => ["Show", "\"Harry \\\"Handcuff\\\" Houdini", " the one and only\"", "Tampa Theater"]
|
||||
puts ary[1] # => "Harry \"Handcuff\" Houdini
|
||||
|
||||
With +liberal_parsing+ and its +backslash_quote+ sub-option:
|
||||
ary = CSV.parse_line(str, liberal_parsing: { backslash_quote: true })
|
||||
ary # => ["Show", "Harry \"Handcuff\" Houdini, the one and only", "Tampa Theater"]
|
||||
puts ary[1] # => Harry "Handcuff" Houdini, the one and only
|
||||
@@ -0,0 +1,12 @@
|
||||
====== Option +nil_value+
|
||||
|
||||
Specifies the object that is to be substituted for each null (no-text) field.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:nil_value) # => nil
|
||||
|
||||
With the default, +nil+:
|
||||
CSV.parse_line('a,,b,,c') # => ["a", nil, "b", nil, "c"]
|
||||
|
||||
With a different object:
|
||||
CSV.parse_line('a,,b,,c', nil_value: 0) # => ["a", 0, "b", 0, "c"]
|
||||
@@ -0,0 +1,22 @@
|
||||
====== Option +return_headers+
|
||||
|
||||
Specifies the boolean that determines whether method #shift
|
||||
returns or ignores the header row.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:return_headers) # => false
|
||||
|
||||
Examples:
|
||||
str = <<-EOT
|
||||
Name,Count
|
||||
foo,0
|
||||
bar,1
|
||||
bax,2
|
||||
EOT
|
||||
# Without return_headers first row is str.
|
||||
csv = CSV.new(str, headers: true)
|
||||
csv.shift # => #<CSV::Row "Name":"foo" "Count":"0">
|
||||
# With return_headers first row is headers.
|
||||
csv = CSV.new(str, headers: true, return_headers: true)
|
||||
csv.shift # => #<CSV::Row "Name":"Name" "Count":"Count">
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
====== Option +skip_blanks+
|
||||
|
||||
Specifies a boolean that determines whether blank lines in the input will be ignored;
|
||||
a line that contains a column separator is not considered to be blank.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:skip_blanks) # => false
|
||||
|
||||
See also option {skiplines}[#class-CSV-label-Option+skip_lines].
|
||||
|
||||
For examples in this section:
|
||||
str = <<-EOT
|
||||
foo,0
|
||||
|
||||
bar,1
|
||||
baz,2
|
||||
|
||||
,
|
||||
EOT
|
||||
|
||||
Using the default, +false+:
|
||||
ary = CSV.parse(str)
|
||||
ary # => [["foo", "0"], [], ["bar", "1"], ["baz", "2"], [], [nil, nil]]
|
||||
|
||||
Using +true+:
|
||||
ary = CSV.parse(str, skip_blanks: true)
|
||||
ary # => [["foo", "0"], ["bar", "1"], ["baz", "2"], [nil, nil]]
|
||||
|
||||
Using a truthy value:
|
||||
ary = CSV.parse(str, skip_blanks: :foo)
|
||||
ary # => [["foo", "0"], ["bar", "1"], ["baz", "2"], [nil, nil]]
|
||||
@@ -0,0 +1,37 @@
|
||||
====== Option +skip_lines+
|
||||
|
||||
Specifies an object to use in identifying comment lines in the input that are to be ignored:
|
||||
* If a \Regexp, ignores lines that match it.
|
||||
* If a \String, converts it to a \Regexp, ignores lines that match it.
|
||||
* If +nil+, no lines are considered to be comments.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:skip_lines) # => nil
|
||||
|
||||
For examples in this section:
|
||||
str = <<-EOT
|
||||
# Comment
|
||||
foo,0
|
||||
bar,1
|
||||
baz,2
|
||||
# Another comment
|
||||
EOT
|
||||
str # => "# Comment\nfoo,0\nbar,1\nbaz,2\n# Another comment\n"
|
||||
|
||||
Using the default, +nil+:
|
||||
ary = CSV.parse(str)
|
||||
ary # => [["# Comment"], ["foo", "0"], ["bar", "1"], ["baz", "2"], ["# Another comment"]]
|
||||
|
||||
Using a \Regexp:
|
||||
ary = CSV.parse(str, skip_lines: /^#/)
|
||||
ary # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
Using a \String:
|
||||
ary = CSV.parse(str, skip_lines: '#')
|
||||
ary # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
---
|
||||
|
||||
Raises an exception if given an object that is not a \Regexp, a \String, or +nil+:
|
||||
# Raises ArgumentError (:skip_lines has to respond to #match: 0)
|
||||
CSV.parse(str, skip_lines: 0)
|
||||
@@ -0,0 +1,15 @@
|
||||
====== Option +strip+
|
||||
|
||||
Specifies the boolean value that determines whether
|
||||
whitespace is stripped from each input field.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:strip) # => false
|
||||
|
||||
With default value +false+:
|
||||
ary = CSV.parse_line(' a , b ')
|
||||
ary # => [" a ", " b "]
|
||||
|
||||
With value +true+:
|
||||
ary = CSV.parse_line(' a , b ', strip: true)
|
||||
ary # => ["a", "b"]
|
||||
@@ -0,0 +1,27 @@
|
||||
====== Option +unconverted_fields+
|
||||
|
||||
Specifies the boolean that determines whether unconverted field values are to be available.
|
||||
|
||||
Default value:
|
||||
CSV::DEFAULT_OPTIONS.fetch(:unconverted_fields) # => nil
|
||||
|
||||
The unconverted field values are those found in the source data,
|
||||
prior to any conversions performed via option +converters+.
|
||||
|
||||
When option +unconverted_fields+ is +true+,
|
||||
each returned row (\Array or \CSV::Row) has an added method,
|
||||
+unconverted_fields+, that returns the unconverted field values:
|
||||
str = <<-EOT
|
||||
foo,0
|
||||
bar,1
|
||||
baz,2
|
||||
EOT
|
||||
# Without unconverted_fields
|
||||
csv = CSV.parse(str, converters: :integer)
|
||||
csv # => [["foo", 0], ["bar", 1], ["baz", 2]]
|
||||
csv.first.respond_to?(:unconverted_fields) # => false
|
||||
# With unconverted_fields
|
||||
csv = CSV.parse(str, converters: :integer, unconverted_fields: true)
|
||||
csv # => [["foo", 0], ["bar", 1], ["baz", 2]]
|
||||
csv.first.respond_to?(:unconverted_fields) # => true
|
||||
csv.first.unconverted_fields # => ["foo", "0"]
|
||||
@@ -0,0 +1,226 @@
|
||||
== Recipes for Filtering \CSV
|
||||
|
||||
These recipes are specific code examples for specific \CSV filtering tasks.
|
||||
|
||||
For other recipes, see {Recipes for CSV}[./recipes_rdoc.html].
|
||||
|
||||
All code snippets on this page assume that the following has been executed:
|
||||
require 'csv'
|
||||
|
||||
=== Contents
|
||||
|
||||
- {Source and Output Formats}[#label-Source+and+Output+Formats]
|
||||
- {Filtering String to String}[#label-Filtering+String+to+String]
|
||||
- {Recipe: Filter String to String parsing Headers}[#label-Recipe-3A+Filter+String+to+String+parsing+Headers]
|
||||
- {Recipe: Filter String to String parsing and writing Headers}[#label-Recipe-3A+Filter+String+to+String+parsing+and+writing+Headers]
|
||||
- {Recipe: Filter String to String Without Headers}[#label-Recipe-3A+Filter+String+to+String+Without+Headers]
|
||||
- {Filtering String to IO Stream}[#label-Filtering+String+to+IO+Stream]
|
||||
- {Recipe: Filter String to IO Stream parsing Headers}[#label-Recipe-3A+Filter+String+to+IO+Stream+parsing+Headers]
|
||||
- {Recipe: Filter String to IO Stream parsing and writing Headers}[#label-Recipe-3A+Filter+String+to+IO+Stream+parsing+and+writing+Headers]
|
||||
- {Recipe: Filter String to IO Stream Without Headers}[#label-Recipe-3A+Filter+String+to+IO+Stream+Without+Headers]
|
||||
- {Filtering IO Stream to String}[#label-Filtering+IO+Stream+to+String]
|
||||
- {Recipe: Filter IO Stream to String parsing Headers}[#label-Recipe-3A+Filter+IO+Stream+to+String+parsing+Headers]
|
||||
- {Recipe: Filter IO Stream to String parsing and writing Headers}[#label-Recipe-3A+Filter+IO+Stream+to+String+parsing+and+writing+Headers]
|
||||
- {Recipe: Filter IO Stream to String Without Headers}[#label-Recipe-3A+Filter+IO+Stream+to+String+Without+Headers]
|
||||
- {Filtering IO Stream to IO Stream}[#label-Filtering+IO+Stream+to+IO+Stream]
|
||||
- {Recipe: Filter IO Stream to IO Stream parsing Headers}[#label-Recipe-3A+Filter+IO+Stream+to+IO+Stream+parsing+Headers]
|
||||
- {Recipe: Filter IO Stream to IO Stream parsing and writing Headers}[#label-Recipe-3A+Filter+IO+Stream+to+IO+Stream+parsing+and+writing+Headers]
|
||||
- {Recipe: Filter IO Stream to IO Stream Without Headers}[#label-Recipe-3A+Filter+IO+Stream+to+IO+Stream+Without+Headers]
|
||||
|
||||
=== Source and Output Formats
|
||||
|
||||
You can use a Unix-style "filter" for \CSV data.
|
||||
The filter reads source \CSV data and writes output \CSV data as modified by the filter.
|
||||
The input and output \CSV data may be any mixture of \Strings and \IO streams.
|
||||
|
||||
==== Filtering \String to \String
|
||||
|
||||
You can filter one \String to another, with or without headers.
|
||||
|
||||
===== Recipe: Filter \String to \String parsing Headers
|
||||
|
||||
Use class method CSV.filter with option +headers+ to filter a \String to another \String:
|
||||
in_string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
out_string = ''
|
||||
CSV.filter(in_string, out_string, headers: true) do |row|
|
||||
row['Name'] = row['Name'].upcase
|
||||
row['Value'] *= 4
|
||||
end
|
||||
out_string # => "FOO,0000\nBAR,1111\nBAZ,2222\n"
|
||||
|
||||
===== Recipe: Filter \String to \String parsing and writing Headers
|
||||
|
||||
Use class method CSV.filter with option +headers+ and +out_write_headers+ to filter a \String to another \String including header row:
|
||||
in_string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
out_string = ''
|
||||
CSV.filter(in_string, out_string, headers: true, out_write_headers: true) do |row|
|
||||
unless row.is_a?(Array)
|
||||
row['Name'] = row['Name'].upcase
|
||||
row['Value'] *= 4
|
||||
end
|
||||
end
|
||||
out_string # => "Name,Value\nFOO,0000\nBAR,1111\nBAZ,2222\n"
|
||||
|
||||
===== Recipe: Filter \String to \String Without Headers
|
||||
|
||||
Use class method CSV.filter without option +headers+ to filter a \String to another \String:
|
||||
in_string = "foo,0\nbar,1\nbaz,2\n"
|
||||
out_string = ''
|
||||
CSV.filter(in_string, out_string) do |row|
|
||||
row[0] = row[0].upcase
|
||||
row[1] *= 4
|
||||
end
|
||||
out_string # => "FOO,0000\nBAR,1111\nBAZ,2222\n"
|
||||
|
||||
==== Filtering \String to \IO Stream
|
||||
|
||||
You can filter a \String to an \IO stream, with or without headers.
|
||||
|
||||
===== Recipe: Filter \String to \IO Stream parsing Headers
|
||||
|
||||
Use class method CSV.filter with option +headers+ to filter a \String to an \IO stream:
|
||||
in_string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
path = 't.csv'
|
||||
File.open(path, 'w') do |out_io|
|
||||
CSV.filter(in_string, out_io, headers: true) do |row|
|
||||
row['Name'] = row['Name'].upcase
|
||||
row['Value'] *= 4
|
||||
end
|
||||
end
|
||||
p File.read(path) # => "FOO,0000\nBAR,1111\nBAZ,2222\n"
|
||||
|
||||
===== Recipe: Filter \String to \IO Stream parsing and writing Headers
|
||||
|
||||
Use class method CSV.filter with option +headers+ and +out_write_headers+ to filter a \String to an \IO stream including header row:
|
||||
in_string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
path = 't.csv'
|
||||
File.open(path, 'w') do |out_io|
|
||||
CSV.filter(in_string, out_io, headers: true, out_write_headers: true ) do |row|
|
||||
unless row.is_a?(Array)
|
||||
row['Name'] = row['Name'].upcase
|
||||
row['Value'] *= 4
|
||||
end
|
||||
end
|
||||
end
|
||||
p File.read(path) # => "Name,Value\nFOO,0000\nBAR,1111\nBAZ,2222\n"
|
||||
|
||||
===== Recipe: Filter \String to \IO Stream Without Headers
|
||||
|
||||
Use class method CSV.filter without option +headers+ to filter a \String to an \IO stream:
|
||||
in_string = "foo,0\nbar,1\nbaz,2\n"
|
||||
path = 't.csv'
|
||||
File.open(path, 'w') do |out_io|
|
||||
CSV.filter(in_string, out_io) do |row|
|
||||
row[0] = row[0].upcase
|
||||
row[1] *= 4
|
||||
end
|
||||
end
|
||||
p File.read(path) # => "FOO,0000\nBAR,1111\nBAZ,2222\n"
|
||||
|
||||
==== Filtering \IO Stream to \String
|
||||
|
||||
You can filter an \IO stream to a \String, with or without headers.
|
||||
|
||||
===== Recipe: Filter \IO Stream to \String parsing Headers
|
||||
|
||||
Use class method CSV.filter with option +headers+ to filter an \IO stream to a \String:
|
||||
in_string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
path = 't.csv'
|
||||
File.write(path, in_string)
|
||||
out_string = ''
|
||||
File.open(path) do |in_io|
|
||||
CSV.filter(in_io, out_string, headers: true) do |row|
|
||||
row['Name'] = row['Name'].upcase
|
||||
row['Value'] *= 4
|
||||
end
|
||||
end
|
||||
out_string # => "FOO,0000\nBAR,1111\nBAZ,2222\n"
|
||||
|
||||
===== Recipe: Filter \IO Stream to \String parsing and writing Headers
|
||||
|
||||
Use class method CSV.filter with option +headers+ and +out_write_headers+ to filter an \IO stream to a \String including header row:
|
||||
in_string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
path = 't.csv'
|
||||
File.write(path, in_string)
|
||||
out_string = ''
|
||||
File.open(path) do |in_io|
|
||||
CSV.filter(in_io, out_string, headers: true, out_write_headers: true) do |row|
|
||||
unless row.is_a?(Array)
|
||||
row['Name'] = row['Name'].upcase
|
||||
row['Value'] *= 4
|
||||
end
|
||||
end
|
||||
end
|
||||
out_string # => "Name,Value\nFOO,0000\nBAR,1111\nBAZ,2222\n"
|
||||
|
||||
===== Recipe: Filter \IO Stream to \String Without Headers
|
||||
|
||||
Use class method CSV.filter without option +headers+ to filter an \IO stream to a \String:
|
||||
in_string = "foo,0\nbar,1\nbaz,2\n"
|
||||
path = 't.csv'
|
||||
File.write(path, in_string)
|
||||
out_string = ''
|
||||
File.open(path) do |in_io|
|
||||
CSV.filter(in_io, out_string) do |row|
|
||||
row[0] = row[0].upcase
|
||||
row[1] *= 4
|
||||
end
|
||||
end
|
||||
out_string # => "FOO,0000\nBAR,1111\nBAZ,2222\n"
|
||||
|
||||
==== Filtering \IO Stream to \IO Stream
|
||||
|
||||
You can filter an \IO stream to another \IO stream, with or without headers.
|
||||
|
||||
===== Recipe: Filter \IO Stream to \IO Stream parsing Headers
|
||||
|
||||
Use class method CSV.filter with option +headers+ to filter an \IO stream to another \IO stream:
|
||||
in_path = 't.csv'
|
||||
in_string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
File.write(in_path, in_string)
|
||||
out_path = 'u.csv'
|
||||
File.open(in_path) do |in_io|
|
||||
File.open(out_path, 'w') do |out_io|
|
||||
CSV.filter(in_io, out_io, headers: true) do |row|
|
||||
row['Name'] = row['Name'].upcase
|
||||
row['Value'] *= 4
|
||||
end
|
||||
end
|
||||
end
|
||||
p File.read(out_path) # => "FOO,0000\nBAR,1111\nBAZ,2222\n"
|
||||
|
||||
===== Recipe: Filter \IO Stream to \IO Stream parsing and writing Headers
|
||||
|
||||
Use class method CSV.filter with option +headers+ and +out_write_headers+ to filter an \IO stream to another \IO stream including header row:
|
||||
in_path = 't.csv'
|
||||
in_string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
File.write(in_path, in_string)
|
||||
out_path = 'u.csv'
|
||||
File.open(in_path) do |in_io|
|
||||
File.open(out_path, 'w') do |out_io|
|
||||
CSV.filter(in_io, out_io, headers: true, out_write_headers: true) do |row|
|
||||
unless row.is_a?(Array)
|
||||
row['Name'] = row['Name'].upcase
|
||||
row['Value'] *= 4
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
p File.read(out_path) # => "Name,Value\nFOO,0000\nBAR,1111\nBAZ,2222\n"
|
||||
|
||||
===== Recipe: Filter \IO Stream to \IO Stream Without Headers
|
||||
|
||||
Use class method CSV.filter without option +headers+ to filter an \IO stream to another \IO stream:
|
||||
in_path = 't.csv'
|
||||
in_string = "foo,0\nbar,1\nbaz,2\n"
|
||||
File.write(in_path, in_string)
|
||||
out_path = 'u.csv'
|
||||
File.open(in_path) do |in_io|
|
||||
File.open(out_path, 'w') do |out_io|
|
||||
CSV.filter(in_io, out_io) do |row|
|
||||
row[0] = row[0].upcase
|
||||
row[1] *= 4
|
||||
end
|
||||
end
|
||||
end
|
||||
p File.read(out_path) # => "FOO,0000\nBAR,1111\nBAZ,2222\n"
|
||||
@@ -0,0 +1,298 @@
|
||||
== Recipes for Generating \CSV
|
||||
|
||||
These recipes are specific code examples for specific \CSV generating tasks.
|
||||
|
||||
For other recipes, see {Recipes for CSV}[./recipes_rdoc.html].
|
||||
|
||||
All code snippets on this page assume that the following has been executed:
|
||||
require 'csv'
|
||||
|
||||
=== Contents
|
||||
|
||||
- {Output Formats}[#label-Output+Formats]
|
||||
- {Generating to a String}[#label-Generating+to+a+String]
|
||||
- {Recipe: Generate to String with Headers}[#label-Recipe-3A+Generate+to+String+with+Headers]
|
||||
- {Recipe: Generate to String Without Headers}[#label-Recipe-3A+Generate+to+String+Without+Headers]
|
||||
- {Generating to a File}[#label-Generating+to+a+File]
|
||||
- {Recipe: Generate to File with Headers}[#label-Recipe-3A+Generate+to+File+with+Headers]
|
||||
- {Recipe: Generate to File Without Headers}[#label-Recipe-3A+Generate+to+File+Without+Headers]
|
||||
- {Generating to IO an Stream}[#label-Generating+to+an+IO+Stream]
|
||||
- {Recipe: Generate to IO Stream with Headers}[#label-Recipe-3A+Generate+to+IO+Stream+with+Headers]
|
||||
- {Recipe: Generate to IO Stream Without Headers}[#label-Recipe-3A+Generate+to+IO+Stream+Without+Headers]
|
||||
- {Converting Fields}[#label-Converting+Fields]
|
||||
- {Recipe: Filter Generated Field Strings}[#label-Recipe-3A+Filter+Generated+Field+Strings]
|
||||
- {Recipe: Specify Multiple Write Converters}[#label-Recipe-3A+Specify+Multiple+Write+Converters]
|
||||
- {RFC 4180 Compliance}[#label-RFC+4180+Compliance]
|
||||
- {Row Separator}[#label-Row+Separator]
|
||||
- {Recipe: Generate Compliant Row Separator}[#label-Recipe-3A+Generate+Compliant+Row+Separator]
|
||||
- {Recipe: Generate Non-Compliant Row Separator}[#label-Recipe-3A+Generate+Non-Compliant+Row+Separator]
|
||||
- {Column Separator}[#label-Column+Separator]
|
||||
- {Recipe: Generate Compliant Column Separator}[#label-Recipe-3A+Generate+Compliant+Column+Separator]
|
||||
- {Recipe: Generate Non-Compliant Column Separator}[#label-Recipe-3A+Generate+Non-Compliant+Column+Separator]
|
||||
- {Quotes}[#label-Quotes]
|
||||
- {Recipe: Quote All Fields}[#label-Recipe-3A+Quote+All+Fields]
|
||||
- {Recipe: Quote Empty Fields}[#label-Recipe-3A+Quote+Empty+Fields]
|
||||
- {Recipe: Generate Compliant Quote Character}[#label-Recipe-3A+Generate+Compliant+Quote+Character]
|
||||
- {Recipe: Generate Non-Compliant Quote Character}[#label-Recipe-3A+Generate+Non-Compliant+Quote+Character]
|
||||
|
||||
=== Output Formats
|
||||
|
||||
You can generate \CSV output to a \String, to a \File (via its path), or to an \IO stream.
|
||||
|
||||
==== Generating to a \String
|
||||
|
||||
You can generate \CSV output to a \String, with or without headers.
|
||||
|
||||
===== Recipe: Generate to \String with Headers
|
||||
|
||||
Use class method CSV.generate with option +headers+ to generate to a \String.
|
||||
|
||||
This example uses method CSV#<< to append the rows
|
||||
that are to be generated:
|
||||
output_string = CSV.generate('', headers: ['Name', 'Value'], write_headers: true) do |csv|
|
||||
csv << ['Foo', 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
output_string # => "Name,Value\nFoo,0\nBar,1\nBaz,2\n"
|
||||
|
||||
===== Recipe: Generate to \String Without Headers
|
||||
|
||||
Use class method CSV.generate without option +headers+ to generate to a \String.
|
||||
|
||||
This example uses method CSV#<< to append the rows
|
||||
that are to be generated:
|
||||
output_string = CSV.generate do |csv|
|
||||
csv << ['Foo', 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
output_string # => "Foo,0\nBar,1\nBaz,2\n"
|
||||
|
||||
==== Generating to a \File
|
||||
|
||||
You can generate /CSV data to a \File, with or without headers.
|
||||
|
||||
===== Recipe: Generate to \File with Headers
|
||||
|
||||
Use class method CSV.open with option +headers+ generate to a \File.
|
||||
|
||||
This example uses method CSV#<< to append the rows
|
||||
that are to be generated:
|
||||
path = 't.csv'
|
||||
CSV.open(path, 'w', headers: ['Name', 'Value'], write_headers: true) do |csv|
|
||||
csv << ['Foo', 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
p File.read(path) # => "Name,Value\nFoo,0\nBar,1\nBaz,2\n"
|
||||
|
||||
===== Recipe: Generate to \File Without Headers
|
||||
|
||||
Use class method CSV.open without option +headers+ to generate to a \File.
|
||||
|
||||
This example uses method CSV#<< to append the rows
|
||||
that are to be generated:
|
||||
path = 't.csv'
|
||||
CSV.open(path, 'w') do |csv|
|
||||
csv << ['Foo', 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
p File.read(path) # => "Foo,0\nBar,1\nBaz,2\n"
|
||||
|
||||
==== Generating to an \IO Stream
|
||||
|
||||
You can generate \CSV data to an \IO stream, with or without headers.
|
||||
|
||||
==== Recipe: Generate to \IO Stream with Headers
|
||||
|
||||
Use class method CSV.new with option +headers+ to generate \CSV data to an \IO stream:
|
||||
path = 't.csv'
|
||||
File.open(path, 'w') do |file|
|
||||
csv = CSV.new(file, headers: ['Name', 'Value'], write_headers: true)
|
||||
csv << ['Foo', 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
p File.read(path) # => "Name,Value\nFoo,0\nBar,1\nBaz,2\n"
|
||||
|
||||
===== Recipe: Generate to \IO Stream Without Headers
|
||||
|
||||
Use class method CSV.new without option +headers+ to generate \CSV data to an \IO stream:
|
||||
path = 't.csv'
|
||||
File.open(path, 'w') do |file|
|
||||
csv = CSV.new(file)
|
||||
csv << ['Foo', 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
p File.read(path) # => "Foo,0\nBar,1\nBaz,2\n"
|
||||
|
||||
=== Converting Fields
|
||||
|
||||
You can use _write_ _converters_ to convert fields when generating \CSV.
|
||||
|
||||
==== Recipe: Filter Generated Field Strings
|
||||
|
||||
Use option <tt>:write_converters</tt> and a custom converter to convert field values when generating \CSV.
|
||||
|
||||
This example defines and uses a custom write converter to strip whitespace from generated fields:
|
||||
strip_converter = proc {|field| field.respond_to?(:strip) ? field.strip : field }
|
||||
output_string = CSV.generate(write_converters: strip_converter) do |csv|
|
||||
csv << [' foo ', 0]
|
||||
csv << [' bar ', 1]
|
||||
csv << [' baz ', 2]
|
||||
end
|
||||
output_string # => "foo,0\nbar,1\nbaz,2\n"
|
||||
|
||||
==== Recipe: Specify Multiple Write Converters
|
||||
|
||||
Use option <tt>:write_converters</tt> and multiple custom converters
|
||||
to convert field values when generating \CSV.
|
||||
|
||||
This example defines and uses two custom write converters to strip and upcase generated fields:
|
||||
strip_converter = proc {|field| field.respond_to?(:strip) ? field.strip : field }
|
||||
upcase_converter = proc {|field| field.respond_to?(:upcase) ? field.upcase : field }
|
||||
converters = [strip_converter, upcase_converter]
|
||||
output_string = CSV.generate(write_converters: converters) do |csv|
|
||||
csv << [' foo ', 0]
|
||||
csv << [' bar ', 1]
|
||||
csv << [' baz ', 2]
|
||||
end
|
||||
output_string # => "FOO,0\nBAR,1\nBAZ,2\n"
|
||||
|
||||
=== RFC 4180 Compliance
|
||||
|
||||
By default, \CSV generates data that is compliant with
|
||||
{RFC 4180}[https://www.rfc-editor.org/rfc/rfc4180]
|
||||
with respect to:
|
||||
- Column separator.
|
||||
- Quote character.
|
||||
|
||||
==== Row Separator
|
||||
|
||||
RFC 4180 specifies the row separator CRLF (Ruby <tt>"\r\n"</tt>).
|
||||
|
||||
===== Recipe: Generate Compliant Row Separator
|
||||
|
||||
For strict compliance, use option +:row_sep+ to specify row separator <tt>"\r\n"</tt>:
|
||||
output_string = CSV.generate('', row_sep: "\r\n") do |csv|
|
||||
csv << ['Foo', 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
output_string # => "Foo,0\r\nBar,1\r\nBaz,2\r\n"
|
||||
|
||||
===== Recipe: Generate Non-Compliant Row Separator
|
||||
|
||||
For data with non-compliant row separators, use option +:row_sep+ with a different value:
|
||||
This example source uses semicolon (<tt>";'</tt>) as its row separator:
|
||||
output_string = CSV.generate('', row_sep: ";") do |csv|
|
||||
csv << ['Foo', 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
output_string # => "Foo,0;Bar,1;Baz,2;"
|
||||
|
||||
==== Column Separator
|
||||
|
||||
RFC 4180 specifies column separator COMMA (Ruby <tt>","</tt>).
|
||||
|
||||
===== Recipe: Generate Compliant Column Separator
|
||||
|
||||
Because the \CSV default comma separator is <tt>","</tt>,
|
||||
you need not specify option +:col_sep+ for compliant data:
|
||||
output_string = CSV.generate('') do |csv|
|
||||
csv << ['Foo', 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
output_string # => "Foo,0\nBar,1\nBaz,2\n"
|
||||
|
||||
===== Recipe: Generate Non-Compliant Column Separator
|
||||
|
||||
For data with non-compliant column separators, use option +:col_sep+.
|
||||
This example source uses TAB (<tt>"\t"</tt>) as its column separator:
|
||||
output_string = CSV.generate('', col_sep: "\t") do |csv|
|
||||
csv << ['Foo', 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
output_string # => "Foo\t0\nBar\t1\nBaz\t2\n"
|
||||
|
||||
==== Quotes
|
||||
|
||||
IFC 4180 allows most fields to be quoted or not.
|
||||
By default, \CSV does not quote most fields.
|
||||
|
||||
However, a field containing the current row separator, column separator,
|
||||
or quote character is automatically quoted, producing IFC 4180 compliance:
|
||||
# Field contains row separator.
|
||||
output_string = CSV.generate('') do |csv|
|
||||
row_sep = csv.row_sep
|
||||
csv << ["Foo#{row_sep}Foo", 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
output_string # => "\"Foo\nFoo\",0\nBar,1\nBaz,2\n"
|
||||
# Field contains column separator.
|
||||
output_string = CSV.generate('') do |csv|
|
||||
col_sep = csv.col_sep
|
||||
csv << ["Foo#{col_sep}Foo", 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
output_string # => "\"Foo,Foo\",0\nBar,1\nBaz,2\n"
|
||||
# Field contains quote character.
|
||||
output_string = CSV.generate('') do |csv|
|
||||
quote_char = csv.quote_char
|
||||
csv << ["Foo#{quote_char}Foo", 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
output_string # => "\"Foo\"\"Foo\",0\nBar,1\nBaz,2\n"
|
||||
|
||||
===== Recipe: Quote All Fields
|
||||
|
||||
Use option +:force_quotes+ to force quoted fields:
|
||||
output_string = CSV.generate('', force_quotes: true) do |csv|
|
||||
csv << ['Foo', 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
output_string # => "\"Foo\",\"0\"\n\"Bar\",\"1\"\n\"Baz\",\"2\"\n"
|
||||
|
||||
===== Recipe: Quote Empty Fields
|
||||
|
||||
Use option +:quote_empty+ to force quoting for empty fields:
|
||||
output_string = CSV.generate('', quote_empty: true) do |csv|
|
||||
csv << ['Foo', 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['', 2]
|
||||
end
|
||||
output_string # => "Foo,0\nBar,1\n\"\",2\n"
|
||||
|
||||
===== Recipe: Generate Compliant Quote Character
|
||||
|
||||
RFC 4180 specifies quote character DQUOTE (Ruby <tt>"\""</tt>).
|
||||
|
||||
Because the \CSV default quote character is also <tt>"\""</tt>,
|
||||
you need not specify option +:quote_char+ for compliant data:
|
||||
output_string = CSV.generate('', force_quotes: true) do |csv|
|
||||
csv << ['Foo', 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
output_string # => "\"Foo\",\"0\"\n\"Bar\",\"1\"\n\"Baz\",\"2\"\n"
|
||||
|
||||
===== Recipe: Generate Non-Compliant Quote Character
|
||||
|
||||
For data with non-compliant quote characters, use option +:quote_char+.
|
||||
This example source uses SQUOTE (<tt>"'"</tt>) as its quote character:
|
||||
output_string = CSV.generate('', quote_char: "'", force_quotes: true) do |csv|
|
||||
csv << ['Foo', 0]
|
||||
csv << ['Bar', 1]
|
||||
csv << ['Baz', 2]
|
||||
end
|
||||
output_string # => "'Foo','0'\n'Bar','1'\n'Baz','2'\n"
|
||||
@@ -0,0 +1,554 @@
|
||||
== Recipes for Parsing \CSV
|
||||
|
||||
These recipes are specific code examples for specific \CSV parsing tasks.
|
||||
|
||||
For other recipes, see {Recipes for CSV}[./recipes_rdoc.html].
|
||||
|
||||
All code snippets on this page assume that the following has been executed:
|
||||
require 'csv'
|
||||
|
||||
=== Contents
|
||||
|
||||
- {Source Formats}[#label-Source+Formats]
|
||||
- {Parsing from a String}[#label-Parsing+from+a+String]
|
||||
- {Recipe: Parse from String with Headers}[#label-Recipe-3A+Parse+from+String+with+Headers]
|
||||
- {Recipe: Parse from String Without Headers}[#label-Recipe-3A+Parse+from+String+Without+Headers]
|
||||
- {Parsing from a File}[#label-Parsing+from+a+File]
|
||||
- {Recipe: Parse from File with Headers}[#label-Recipe-3A+Parse+from+File+with+Headers]
|
||||
- {Recipe: Parse from File Without Headers}[#label-Recipe-3A+Parse+from+File+Without+Headers]
|
||||
- {Parsing from an IO Stream}[#label-Parsing+from+an+IO+Stream]
|
||||
- {Recipe: Parse from IO Stream with Headers}[#label-Recipe-3A+Parse+from+IO+Stream+with+Headers]
|
||||
- {Recipe: Parse from IO Stream Without Headers}[#label-Recipe-3A+Parse+from+IO+Stream+Without+Headers]
|
||||
- {RFC 4180 Compliance}[#label-RFC+4180+Compliance]
|
||||
- {Row Separator}[#label-Row+Separator]
|
||||
- {Recipe: Handle Compliant Row Separator}[#label-Recipe-3A+Handle+Compliant+Row+Separator]
|
||||
- {Recipe: Handle Non-Compliant Row Separator}[#label-Recipe-3A+Handle+Non-Compliant+Row+Separator]
|
||||
- {Column Separator}[#label-Column+Separator]
|
||||
- {Recipe: Handle Compliant Column Separator}[#label-Recipe-3A+Handle+Compliant+Column+Separator]
|
||||
- {Recipe: Handle Non-Compliant Column Separator}[#label-Recipe-3A+Handle+Non-Compliant+Column+Separator]
|
||||
- {Quote Character}[#label-Quote+Character]
|
||||
- {Recipe: Handle Compliant Quote Character}[#label-Recipe-3A+Handle+Compliant+Quote+Character]
|
||||
- {Recipe: Handle Non-Compliant Quote Character}[#label-Recipe-3A+Handle+Non-Compliant+Quote+Character]
|
||||
- {Recipe: Allow Liberal Parsing}[#label-Recipe-3A+Allow+Liberal+Parsing]
|
||||
- {Special Handling}[#label-Special+Handling]
|
||||
- {Special Line Handling}[#label-Special+Line+Handling]
|
||||
- {Recipe: Ignore Blank Lines}[#label-Recipe-3A+Ignore+Blank+Lines]
|
||||
- {Recipe: Ignore Selected Lines}[#label-Recipe-3A+Ignore+Selected+Lines]
|
||||
- {Special Field Handling}[#label-Special+Field+Handling]
|
||||
- {Recipe: Strip Fields}[#label-Recipe-3A+Strip+Fields]
|
||||
- {Recipe: Handle Null Fields}[#label-Recipe-3A+Handle+Null+Fields]
|
||||
- {Recipe: Handle Empty Fields}[#label-Recipe-3A+Handle+Empty+Fields]
|
||||
- {Converting Fields}[#label-Converting+Fields]
|
||||
- {Converting Fields to Objects}[#label-Converting+Fields+to+Objects]
|
||||
- {Recipe: Convert Fields to Integers}[#label-Recipe-3A+Convert+Fields+to+Integers]
|
||||
- {Recipe: Convert Fields to Floats}[#label-Recipe-3A+Convert+Fields+to+Floats]
|
||||
- {Recipe: Convert Fields to Numerics}[#label-Recipe-3A+Convert+Fields+to+Numerics]
|
||||
- {Recipe: Convert Fields to Dates}[#label-Recipe-3A+Convert+Fields+to+Dates]
|
||||
- {Recipe: Convert Fields to DateTimes}[#label-Recipe-3A+Convert+Fields+to+DateTimes]
|
||||
- {Recipe: Convert Fields to Times}[#label-Recipe-3A+Convert+Fields+to+Times]
|
||||
- {Recipe: Convert Assorted Fields to Objects}[#label-Recipe-3A+Convert+Assorted+Fields+to+Objects]
|
||||
- {Recipe: Convert Fields to Other Objects}[#label-Recipe-3A+Convert+Fields+to+Other+Objects]
|
||||
- {Recipe: Filter Field Strings}[#label-Recipe-3A+Filter+Field+Strings]
|
||||
- {Recipe: Register Field Converters}[#label-Recipe-3A+Register+Field+Converters]
|
||||
- {Using Multiple Field Converters}[#label-Using+Multiple+Field+Converters]
|
||||
- {Recipe: Specify Multiple Field Converters in Option :converters}[#label-Recipe-3A+Specify+Multiple+Field+Converters+in+Option+-3Aconverters]
|
||||
- {Recipe: Specify Multiple Field Converters in a Custom Converter List}[#label-Recipe-3A+Specify+Multiple+Field+Converters+in+a+Custom+Converter+List]
|
||||
- {Converting Headers}[#label-Converting+Headers]
|
||||
- {Recipe: Convert Headers to Lowercase}[#label-Recipe-3A+Convert+Headers+to+Lowercase]
|
||||
- {Recipe: Convert Headers to Symbols}[#label-Recipe-3A+Convert+Headers+to+Symbols]
|
||||
- {Recipe: Filter Header Strings}[#label-Recipe-3A+Filter+Header+Strings]
|
||||
- {Recipe: Register Header Converters}[#label-Recipe-3A+Register+Header+Converters]
|
||||
- {Using Multiple Header Converters}[#label-Using+Multiple+Header+Converters]
|
||||
- {Recipe: Specify Multiple Header Converters in Option :header_converters}[#label-Recipe-3A+Specify+Multiple+Header+Converters+in+Option+-3Aheader_converters]
|
||||
- {Recipe: Specify Multiple Header Converters in a Custom Header Converter List}[#label-Recipe-3A+Specify+Multiple+Header+Converters+in+a+Custom+Header+Converter+List]
|
||||
- {Diagnostics}[#label-Diagnostics]
|
||||
- {Recipe: Capture Unconverted Fields}[#label-Recipe-3A+Capture+Unconverted+Fields]
|
||||
- {Recipe: Capture Field Info}[#label-Recipe-3A+Capture+Field+Info]
|
||||
|
||||
=== Source Formats
|
||||
|
||||
You can parse \CSV data from a \String, from a \File (via its path), or from an \IO stream.
|
||||
|
||||
==== Parsing from a \String
|
||||
|
||||
You can parse \CSV data from a \String, with or without headers.
|
||||
|
||||
===== Recipe: Parse from \String with Headers
|
||||
|
||||
Use class method CSV.parse with option +headers+ to read a source \String all at once
|
||||
(may have memory resource implications):
|
||||
string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
CSV.parse(string, headers: true) # => #<CSV::Table mode:col_or_row row_count:4>
|
||||
|
||||
Use instance method CSV#each with option +headers+ to read a source \String one row at a time:
|
||||
CSV.new(string, headers: true).each do |row|
|
||||
p row
|
||||
end
|
||||
Output:
|
||||
#<CSV::Row "Name":"foo" "Value":"0">
|
||||
#<CSV::Row "Name":"bar" "Value":"1">
|
||||
#<CSV::Row "Name":"baz" "Value":"2">
|
||||
|
||||
===== Recipe: Parse from \String Without Headers
|
||||
|
||||
Use class method CSV.parse without option +headers+ to read a source \String all at once
|
||||
(may have memory resource implications):
|
||||
string = "foo,0\nbar,1\nbaz,2\n"
|
||||
CSV.parse(string) # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
Use instance method CSV#each without option +headers+ to read a source \String one row at a time:
|
||||
CSV.new(string).each do |row|
|
||||
p row
|
||||
end
|
||||
Output:
|
||||
["foo", "0"]
|
||||
["bar", "1"]
|
||||
["baz", "2"]
|
||||
|
||||
==== Parsing from a \File
|
||||
|
||||
You can parse \CSV data from a \File, with or without headers.
|
||||
|
||||
===== Recipe: Parse from \File with Headers
|
||||
|
||||
Use class method CSV.read with option +headers+ to read a file all at once:
|
||||
string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
path = 't.csv'
|
||||
File.write(path, string)
|
||||
CSV.read(path, headers: true) # => #<CSV::Table mode:col_or_row row_count:4>
|
||||
|
||||
Use class method CSV.foreach with option +headers+ to read one row at a time:
|
||||
CSV.foreach(path, headers: true) do |row|
|
||||
p row
|
||||
end
|
||||
Output:
|
||||
#<CSV::Row "Name":"foo" "Value":"0">
|
||||
#<CSV::Row "Name":"bar" "Value":"1">
|
||||
#<CSV::Row "Name":"baz" "Value":"2">
|
||||
|
||||
===== Recipe: Parse from \File Without Headers
|
||||
|
||||
Use class method CSV.read without option +headers+ to read a file all at once:
|
||||
string = "foo,0\nbar,1\nbaz,2\n"
|
||||
path = 't.csv'
|
||||
File.write(path, string)
|
||||
CSV.read(path) # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
Use class method CSV.foreach without option +headers+ to read one row at a time:
|
||||
CSV.foreach(path) do |row|
|
||||
p row
|
||||
end
|
||||
Output:
|
||||
["foo", "0"]
|
||||
["bar", "1"]
|
||||
["baz", "2"]
|
||||
|
||||
==== Parsing from an \IO Stream
|
||||
|
||||
You can parse \CSV data from an \IO stream, with or without headers.
|
||||
|
||||
===== Recipe: Parse from \IO Stream with Headers
|
||||
|
||||
Use class method CSV.parse with option +headers+ to read an \IO stream all at once:
|
||||
string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
path = 't.csv'
|
||||
File.write(path, string)
|
||||
File.open(path) do |file|
|
||||
CSV.parse(file, headers: true)
|
||||
end # => #<CSV::Table mode:col_or_row row_count:4>
|
||||
|
||||
Use class method CSV.foreach with option +headers+ to read one row at a time:
|
||||
File.open(path) do |file|
|
||||
CSV.foreach(file, headers: true) do |row|
|
||||
p row
|
||||
end
|
||||
end
|
||||
Output:
|
||||
#<CSV::Row "Name":"foo" "Value":"0">
|
||||
#<CSV::Row "Name":"bar" "Value":"1">
|
||||
#<CSV::Row "Name":"baz" "Value":"2">
|
||||
|
||||
===== Recipe: Parse from \IO Stream Without Headers
|
||||
|
||||
Use class method CSV.parse without option +headers+ to read an \IO stream all at once:
|
||||
string = "foo,0\nbar,1\nbaz,2\n"
|
||||
path = 't.csv'
|
||||
File.write(path, string)
|
||||
File.open(path) do |file|
|
||||
CSV.parse(file)
|
||||
end # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
Use class method CSV.foreach without option +headers+ to read one row at a time:
|
||||
File.open(path) do |file|
|
||||
CSV.foreach(file) do |row|
|
||||
p row
|
||||
end
|
||||
end
|
||||
Output:
|
||||
["foo", "0"]
|
||||
["bar", "1"]
|
||||
["baz", "2"]
|
||||
|
||||
=== RFC 4180 Compliance
|
||||
|
||||
By default, \CSV parses data that is compliant with
|
||||
{RFC 4180}[https://www.rfc-editor.org/rfc/rfc4180]
|
||||
with respect to:
|
||||
- Row separator.
|
||||
- Column separator.
|
||||
- Quote character.
|
||||
|
||||
==== Row Separator
|
||||
|
||||
RFC 4180 specifies the row separator CRLF (Ruby <tt>"\r\n"</tt>).
|
||||
|
||||
Although the \CSV default row separator is <tt>"\n"</tt>,
|
||||
the parser also by default handles row separator <tt>"\r"</tt> and the RFC-compliant <tt>"\r\n"</tt>.
|
||||
|
||||
===== Recipe: Handle Compliant Row Separator
|
||||
|
||||
For strict compliance, use option +:row_sep+ to specify row separator <tt>"\r\n"</tt>,
|
||||
which allows the compliant row separator:
|
||||
source = "foo,1\r\nbar,1\r\nbaz,2\r\n"
|
||||
CSV.parse(source, row_sep: "\r\n") # => [["foo", "1"], ["bar", "1"], ["baz", "2"]]
|
||||
But rejects other row separators:
|
||||
source = "foo,1\nbar,1\nbaz,2\n"
|
||||
CSV.parse(source, row_sep: "\r\n") # Raised MalformedCSVError
|
||||
source = "foo,1\rbar,1\rbaz,2\r"
|
||||
CSV.parse(source, row_sep: "\r\n") # Raised MalformedCSVError
|
||||
source = "foo,1\n\rbar,1\n\rbaz,2\n\r"
|
||||
CSV.parse(source, row_sep: "\r\n") # Raised MalformedCSVError
|
||||
|
||||
===== Recipe: Handle Non-Compliant Row Separator
|
||||
|
||||
For data with non-compliant row separators, use option +:row_sep+.
|
||||
This example source uses semicolon (<tt>";"</tt>) as its row separator:
|
||||
source = "foo,1;bar,1;baz,2;"
|
||||
CSV.parse(source, row_sep: ';') # => [["foo", "1"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
==== Column Separator
|
||||
|
||||
RFC 4180 specifies column separator COMMA (Ruby <tt>","</tt>).
|
||||
|
||||
===== Recipe: Handle Compliant Column Separator
|
||||
|
||||
Because the \CSV default comma separator is ',',
|
||||
you need not specify option +:col_sep+ for compliant data:
|
||||
source = "foo,1\nbar,1\nbaz,2\n"
|
||||
CSV.parse(source) # => [["foo", "1"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
===== Recipe: Handle Non-Compliant Column Separator
|
||||
|
||||
For data with non-compliant column separators, use option +:col_sep+.
|
||||
This example source uses TAB (<tt>"\t"</tt>) as its column separator:
|
||||
source = "foo,1\tbar,1\tbaz,2"
|
||||
CSV.parse(source, col_sep: "\t") # => [["foo", "1"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
==== Quote Character
|
||||
|
||||
RFC 4180 specifies quote character DQUOTE (Ruby <tt>"\""</tt>).
|
||||
|
||||
===== Recipe: Handle Compliant Quote Character
|
||||
|
||||
Because the \CSV default quote character is <tt>"\""</tt>,
|
||||
you need not specify option +:quote_char+ for compliant data:
|
||||
source = "\"foo\",\"1\"\n\"bar\",\"1\"\n\"baz\",\"2\"\n"
|
||||
CSV.parse(source) # => [["foo", "1"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
===== Recipe: Handle Non-Compliant Quote Character
|
||||
|
||||
For data with non-compliant quote characters, use option +:quote_char+.
|
||||
This example source uses SQUOTE (<tt>"'"</tt>) as its quote character:
|
||||
source = "'foo','1'\n'bar','1'\n'baz','2'\n"
|
||||
CSV.parse(source, quote_char: "'") # => [["foo", "1"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
==== Recipe: Allow Liberal Parsing
|
||||
|
||||
Use option +:liberal_parsing+ to specify that \CSV should
|
||||
attempt to parse input not conformant with RFC 4180, such as double quotes in unquoted fields:
|
||||
source = 'is,this "three, or four",fields'
|
||||
CSV.parse(source) # Raises MalformedCSVError
|
||||
CSV.parse(source, liberal_parsing: true) # => [["is", "this \"three", " or four\"", "fields"]]
|
||||
|
||||
=== Special Handling
|
||||
|
||||
You can use parsing options to specify special handling for certain lines and fields.
|
||||
|
||||
==== Special Line Handling
|
||||
|
||||
Use parsing options to specify special handling for blank lines, or for other selected lines.
|
||||
|
||||
===== Recipe: Ignore Blank Lines
|
||||
|
||||
Use option +:skip_blanks+ to ignore blank lines:
|
||||
source = <<-EOT
|
||||
foo,0
|
||||
|
||||
bar,1
|
||||
baz,2
|
||||
|
||||
,
|
||||
EOT
|
||||
parsed = CSV.parse(source, skip_blanks: true)
|
||||
parsed # => [["foo", "0"], ["bar", "1"], ["baz", "2"], [nil, nil]]
|
||||
|
||||
===== Recipe: Ignore Selected Lines
|
||||
|
||||
Use option +:skip_lines+ to ignore selected lines.
|
||||
source = <<-EOT
|
||||
# Comment
|
||||
foo,0
|
||||
bar,1
|
||||
baz,2
|
||||
# Another comment
|
||||
EOT
|
||||
parsed = CSV.parse(source, skip_lines: /^#/)
|
||||
parsed # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
||||
|
||||
==== Special Field Handling
|
||||
|
||||
Use parsing options to specify special handling for certain field values.
|
||||
|
||||
===== Recipe: Strip Fields
|
||||
|
||||
Use option +:strip+ to strip parsed field values:
|
||||
CSV.parse_line(' a , b ', strip: true) # => ["a", "b"]
|
||||
|
||||
===== Recipe: Handle Null Fields
|
||||
|
||||
Use option +:nil_value+ to specify a value that will replace each field
|
||||
that is null (no text):
|
||||
CSV.parse_line('a,,b,,c', nil_value: 0) # => ["a", 0, "b", 0, "c"]
|
||||
|
||||
===== Recipe: Handle Empty Fields
|
||||
|
||||
Use option +:empty_value+ to specify a value that will replace each field
|
||||
that is empty (\String of length 0);
|
||||
CSV.parse_line('a,"",b,"",c', empty_value: 'x') # => ["a", "x", "b", "x", "c"]
|
||||
|
||||
=== Converting Fields
|
||||
|
||||
You can use field converters to change parsed \String fields into other objects,
|
||||
or to otherwise modify the \String fields.
|
||||
|
||||
==== Converting Fields to Objects
|
||||
|
||||
Use field converters to change parsed \String objects into other, more specific, objects.
|
||||
|
||||
There are built-in field converters for converting to objects of certain classes:
|
||||
- \Float
|
||||
- \Integer
|
||||
- \Date
|
||||
- \DateTime
|
||||
- \Time
|
||||
|
||||
Other built-in field converters include:
|
||||
- +:numeric+: converts to \Integer and \Float.
|
||||
- +:all+: converts to \DateTime, \Integer, \Float.
|
||||
|
||||
You can also define field converters to convert to objects of other classes.
|
||||
|
||||
===== Recipe: Convert Fields to Integers
|
||||
|
||||
Convert fields to \Integer objects using built-in converter +:integer+:
|
||||
source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
parsed = CSV.parse(source, headers: true, converters: :integer)
|
||||
parsed.map {|row| row['Value'].class} # => [Integer, Integer, Integer]
|
||||
|
||||
===== Recipe: Convert Fields to Floats
|
||||
|
||||
Convert fields to \Float objects using built-in converter +:float+:
|
||||
source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
parsed = CSV.parse(source, headers: true, converters: :float)
|
||||
parsed.map {|row| row['Value'].class} # => [Float, Float, Float]
|
||||
|
||||
===== Recipe: Convert Fields to Numerics
|
||||
|
||||
Convert fields to \Integer and \Float objects using built-in converter +:numeric+:
|
||||
source = "Name,Value\nfoo,0\nbar,1.1\nbaz,2.2\n"
|
||||
parsed = CSV.parse(source, headers: true, converters: :numeric)
|
||||
parsed.map {|row| row['Value'].class} # => [Integer, Float, Float]
|
||||
|
||||
===== Recipe: Convert Fields to Dates
|
||||
|
||||
Convert fields to \Date objects using built-in converter +:date+:
|
||||
source = "Name,Date\nfoo,2001-02-03\nbar,2001-02-04\nbaz,2001-02-03\n"
|
||||
parsed = CSV.parse(source, headers: true, converters: :date)
|
||||
parsed.map {|row| row['Date'].class} # => [Date, Date, Date]
|
||||
|
||||
===== Recipe: Convert Fields to DateTimes
|
||||
|
||||
Convert fields to \DateTime objects using built-in converter +:date_time+:
|
||||
source = "Name,DateTime\nfoo,2001-02-03\nbar,2001-02-04\nbaz,2020-05-07T14:59:00-05:00\n"
|
||||
parsed = CSV.parse(source, headers: true, converters: :date_time)
|
||||
parsed.map {|row| row['DateTime'].class} # => [DateTime, DateTime, DateTime]
|
||||
|
||||
===== Recipe: Convert Fields to Times
|
||||
|
||||
Convert fields to \Time objects using built-in converter +:time+:
|
||||
source = "Name,Time\nfoo,2001-02-03\nbar,2001-02-04\nbaz,2020-05-07T14:59:00-05:00\n"
|
||||
parsed = CSV.parse(source, headers: true, converters: :time)
|
||||
parsed.map {|row| row['Time'].class} # => [Time, Time, Time]
|
||||
|
||||
===== Recipe: Convert Assorted Fields to Objects
|
||||
|
||||
Convert assorted fields to objects using built-in converter +:all+:
|
||||
source = "Type,Value\nInteger,0\nFloat,1.0\nDateTime,2001-02-04\n"
|
||||
parsed = CSV.parse(source, headers: true, converters: :all)
|
||||
parsed.map {|row| row['Value'].class} # => [Integer, Float, DateTime]
|
||||
|
||||
===== Recipe: Convert Fields to Other Objects
|
||||
|
||||
Define a custom field converter to convert \String fields into other objects.
|
||||
This example defines and uses a custom field converter
|
||||
that converts each column-1 value to a \Rational object:
|
||||
rational_converter = proc do |field, field_context|
|
||||
field_context.index == 1 ? field.to_r : field
|
||||
end
|
||||
source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
parsed = CSV.parse(source, headers: true, converters: rational_converter)
|
||||
parsed.map {|row| row['Value'].class} # => [Rational, Rational, Rational]
|
||||
|
||||
==== Recipe: Filter Field Strings
|
||||
|
||||
Define a custom field converter to modify \String fields.
|
||||
This example defines and uses a custom field converter
|
||||
that strips whitespace from each field value:
|
||||
strip_converter = proc {|field| field.strip }
|
||||
source = "Name,Value\n foo , 0 \n bar , 1 \n baz , 2 \n"
|
||||
parsed = CSV.parse(source, headers: true, converters: strip_converter)
|
||||
parsed['Name'] # => ["foo", "bar", "baz"]
|
||||
parsed['Value'] # => ["0", "1", "2"]
|
||||
|
||||
==== Recipe: Register Field Converters
|
||||
|
||||
Register a custom field converter, assigning it a name;
|
||||
then refer to the converter by its name:
|
||||
rational_converter = proc do |field, field_context|
|
||||
field_context.index == 1 ? field.to_r : field
|
||||
end
|
||||
CSV::Converters[:rational] = rational_converter
|
||||
source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
parsed = CSV.parse(source, headers: true, converters: :rational)
|
||||
parsed['Value'] # => [(0/1), (1/1), (2/1)]
|
||||
|
||||
==== Using Multiple Field Converters
|
||||
|
||||
You can use multiple field converters in either of these ways:
|
||||
- Specify converters in option +:converters+.
|
||||
- Specify converters in a custom converter list.
|
||||
|
||||
===== Recipe: Specify Multiple Field Converters in Option +:converters+
|
||||
|
||||
Apply multiple field converters by specifying them in option +:converters+:
|
||||
source = "Name,Value\nfoo,0\nbar,1.0\nbaz,2.0\n"
|
||||
parsed = CSV.parse(source, headers: true, converters: [:integer, :float])
|
||||
parsed['Value'] # => [0, 1.0, 2.0]
|
||||
|
||||
===== Recipe: Specify Multiple Field Converters in a Custom Converter List
|
||||
|
||||
Apply multiple field converters by defining and registering a custom converter list:
|
||||
strip_converter = proc {|field| field.strip }
|
||||
CSV::Converters[:strip] = strip_converter
|
||||
CSV::Converters[:my_converters] = [:integer, :float, :strip]
|
||||
source = "Name,Value\n foo , 0 \n bar , 1.0 \n baz , 2.0 \n"
|
||||
parsed = CSV.parse(source, headers: true, converters: :my_converters)
|
||||
parsed['Name'] # => ["foo", "bar", "baz"]
|
||||
parsed['Value'] # => [0, 1.0, 2.0]
|
||||
|
||||
=== Converting Headers
|
||||
|
||||
You can use header converters to modify parsed \String headers.
|
||||
|
||||
Built-in header converters include:
|
||||
- +:symbol+: converts \String header to \Symbol.
|
||||
- +:downcase+: converts \String header to lowercase.
|
||||
|
||||
You can also define header converters to otherwise modify header \Strings.
|
||||
|
||||
==== Recipe: Convert Headers to Lowercase
|
||||
|
||||
Convert headers to lowercase using built-in converter +:downcase+:
|
||||
source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
parsed = CSV.parse(source, headers: true, header_converters: :downcase)
|
||||
parsed.headers # => ["name", "value"]
|
||||
|
||||
==== Recipe: Convert Headers to Symbols
|
||||
|
||||
Convert headers to downcased Symbols using built-in converter +:symbol+:
|
||||
source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
parsed = CSV.parse(source, headers: true, header_converters: :symbol)
|
||||
parsed.headers # => [:name, :value]
|
||||
parsed.headers.map {|header| header.class} # => [Symbol, Symbol]
|
||||
|
||||
==== Recipe: Filter Header Strings
|
||||
|
||||
Define a custom header converter to modify \String fields.
|
||||
This example defines and uses a custom header converter
|
||||
that capitalizes each header \String:
|
||||
capitalize_converter = proc {|header| header.capitalize }
|
||||
source = "NAME,VALUE\nfoo,0\nbar,1\nbaz,2\n"
|
||||
parsed = CSV.parse(source, headers: true, header_converters: capitalize_converter)
|
||||
parsed.headers # => ["Name", "Value"]
|
||||
|
||||
==== Recipe: Register Header Converters
|
||||
|
||||
Register a custom header converter, assigning it a name;
|
||||
then refer to the converter by its name:
|
||||
capitalize_converter = proc {|header| header.capitalize }
|
||||
CSV::HeaderConverters[:capitalize] = capitalize_converter
|
||||
source = "NAME,VALUE\nfoo,0\nbar,1\nbaz,2\n"
|
||||
parsed = CSV.parse(source, headers: true, header_converters: :capitalize)
|
||||
parsed.headers # => ["Name", "Value"]
|
||||
|
||||
==== Using Multiple Header Converters
|
||||
|
||||
You can use multiple header converters in either of these ways:
|
||||
- Specify header converters in option +:header_converters+.
|
||||
- Specify header converters in a custom header converter list.
|
||||
|
||||
===== Recipe: Specify Multiple Header Converters in Option :header_converters
|
||||
|
||||
Apply multiple header converters by specifying them in option +:header_converters+:
|
||||
source = "Name,Value\nfoo,0\nbar,1.0\nbaz,2.0\n"
|
||||
parsed = CSV.parse(source, headers: true, header_converters: [:downcase, :symbol])
|
||||
parsed.headers # => [:name, :value]
|
||||
|
||||
===== Recipe: Specify Multiple Header Converters in a Custom Header Converter List
|
||||
|
||||
Apply multiple header converters by defining and registering a custom header converter list:
|
||||
CSV::HeaderConverters[:my_header_converters] = [:symbol, :downcase]
|
||||
source = "NAME,VALUE\nfoo,0\nbar,1.0\nbaz,2.0\n"
|
||||
parsed = CSV.parse(source, headers: true, header_converters: :my_header_converters)
|
||||
parsed.headers # => [:name, :value]
|
||||
|
||||
=== Diagnostics
|
||||
|
||||
==== Recipe: Capture Unconverted Fields
|
||||
|
||||
To capture unconverted field values, use option +:unconverted_fields+:
|
||||
source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
parsed = CSV.parse(source, converters: :integer, unconverted_fields: true)
|
||||
parsed # => [["Name", "Value"], ["foo", 0], ["bar", 1], ["baz", 2]]
|
||||
parsed.each {|row| p row.unconverted_fields }
|
||||
Output:
|
||||
["Name", "Value"]
|
||||
["foo", "0"]
|
||||
["bar", "1"]
|
||||
["baz", "2"]
|
||||
|
||||
==== Recipe: Capture Field Info
|
||||
|
||||
To capture field info in a custom converter, accept two block arguments.
|
||||
The first is the field value; the second is a +CSV::FieldInfo+ object:
|
||||
strip_converter = proc {|field, field_info| p field_info; field.strip }
|
||||
source = " foo , 0 \n bar , 1 \n baz , 2 \n"
|
||||
parsed = CSV.parse(source, converters: strip_converter)
|
||||
parsed # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
||||
Output:
|
||||
#<struct CSV::FieldInfo index=0, line=1, header=nil>
|
||||
#<struct CSV::FieldInfo index=1, line=1, header=nil>
|
||||
#<struct CSV::FieldInfo index=0, line=2, header=nil>
|
||||
#<struct CSV::FieldInfo index=1, line=2, header=nil>
|
||||
#<struct CSV::FieldInfo index=0, line=3, header=nil>
|
||||
#<struct CSV::FieldInfo index=1, line=3, header=nil>
|
||||
@@ -0,0 +1,6 @@
|
||||
== Recipes for \CSV
|
||||
|
||||
The recipes are specific code examples for specific tasks. See:
|
||||
- {Recipes for Parsing CSV}[./parsing_rdoc.html]
|
||||
- {Recipes for Generating CSV}[./generating_rdoc.html]
|
||||
- {Recipes for Filtering CSV}[./filtering_rdoc.html]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
class Array
|
||||
# Equivalent to CSV::generate_line(self, options)
|
||||
#
|
||||
# ["CSV", "data"].to_csv
|
||||
# #=> "CSV,data\n"
|
||||
def to_csv(**options)
|
||||
CSV.generate_line(self, **options)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class String
|
||||
# Equivalent to CSV::parse_line(self, options)
|
||||
#
|
||||
# "CSV,data".parse_csv
|
||||
# #=> ["CSV", "data"]
|
||||
def parse_csv(**options)
|
||||
CSV.parse_line(self, **options)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,96 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CSV
|
||||
# Note: Don't use this class directly. This is an internal class.
|
||||
class FieldsConverter
|
||||
include Enumerable
|
||||
|
||||
NO_QUOTED_FIELDS = [] # :nodoc:
|
||||
def NO_QUOTED_FIELDS.[](_index)
|
||||
false
|
||||
end
|
||||
NO_QUOTED_FIELDS.freeze
|
||||
|
||||
#
|
||||
# A CSV::FieldsConverter is a data structure for storing the
|
||||
# fields converter properties to be passed as a parameter
|
||||
# when parsing a new file (e.g. CSV::Parser.new(@io, parser_options))
|
||||
#
|
||||
|
||||
def initialize(options={})
|
||||
@converters = []
|
||||
@nil_value = options[:nil_value]
|
||||
@empty_value = options[:empty_value]
|
||||
@empty_value_is_empty_string = (@empty_value == "")
|
||||
@accept_nil = options[:accept_nil]
|
||||
@builtin_converters_name = options[:builtin_converters_name]
|
||||
@need_static_convert = need_static_convert?
|
||||
end
|
||||
|
||||
def add_converter(name=nil, &converter)
|
||||
if name.nil? # custom converter
|
||||
@converters << converter
|
||||
else # named converter
|
||||
combo = builtin_converters[name]
|
||||
case combo
|
||||
when Array # combo converter
|
||||
combo.each do |sub_name|
|
||||
add_converter(sub_name)
|
||||
end
|
||||
else # individual named converter
|
||||
@converters << combo
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
@converters.each(&block)
|
||||
end
|
||||
|
||||
def empty?
|
||||
@converters.empty?
|
||||
end
|
||||
|
||||
def convert(fields, headers, lineno, quoted_fields=NO_QUOTED_FIELDS)
|
||||
return fields unless need_convert?
|
||||
|
||||
fields.collect.with_index do |field, index|
|
||||
if field.nil?
|
||||
field = @nil_value
|
||||
elsif field.is_a?(String) and field.empty?
|
||||
field = @empty_value unless @empty_value_is_empty_string
|
||||
end
|
||||
@converters.each do |converter|
|
||||
break if field.nil? and @accept_nil
|
||||
if converter.arity == 1 # straight field converter
|
||||
field = converter[field]
|
||||
else # FieldInfo converter
|
||||
if headers
|
||||
header = headers[index]
|
||||
else
|
||||
header = nil
|
||||
end
|
||||
quoted = quoted_fields[index]
|
||||
field = converter[field, FieldInfo.new(index, lineno, header, quoted)]
|
||||
end
|
||||
break unless field.is_a?(String) # short-circuit pipeline for speed
|
||||
end
|
||||
field # final state of each field, converted or original
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def need_static_convert?
|
||||
not (@nil_value.nil? and @empty_value_is_empty_string)
|
||||
end
|
||||
|
||||
def need_convert?
|
||||
@need_static_convert or
|
||||
(not @converters.empty?)
|
||||
end
|
||||
|
||||
def builtin_converters
|
||||
@builtin_converters ||= ::CSV.const_get(@builtin_converters_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
require "English"
|
||||
require "stringio"
|
||||
|
||||
class CSV
|
||||
module InputRecordSeparator
|
||||
class << self
|
||||
if RUBY_VERSION >= "3.0.0"
|
||||
def value
|
||||
"\n"
|
||||
end
|
||||
else
|
||||
def value
|
||||
$INPUT_RECORD_SEPARATOR
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,757 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "forwardable"
|
||||
|
||||
class CSV
|
||||
# = \CSV::Row
|
||||
# A \CSV::Row instance represents a \CSV table row.
|
||||
# (see {class CSV}[../CSV.html]).
|
||||
#
|
||||
# The instance may have:
|
||||
# - Fields: each is an object, not necessarily a \String.
|
||||
# - Headers: each serves a key, and also need not be a \String.
|
||||
#
|
||||
# === Instance Methods
|
||||
#
|
||||
# \CSV::Row has three groups of instance methods:
|
||||
# - Its own internally defined instance methods.
|
||||
# - Methods included by module Enumerable.
|
||||
# - Methods delegated to class Array.:
|
||||
# * Array#empty?
|
||||
# * Array#length
|
||||
# * Array#size
|
||||
#
|
||||
# == Creating a \CSV::Row Instance
|
||||
#
|
||||
# Commonly, a new \CSV::Row instance is created by parsing \CSV source
|
||||
# that has headers:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# table.each {|row| p row }
|
||||
# Output:
|
||||
# #<CSV::Row "Name":"foo" "Value":"0">
|
||||
# #<CSV::Row "Name":"bar" "Value":"1">
|
||||
# #<CSV::Row "Name":"baz" "Value":"2">
|
||||
#
|
||||
# You can also create a row directly. See ::new.
|
||||
#
|
||||
# == Headers
|
||||
#
|
||||
# Like a \CSV::Table, a \CSV::Row has headers.
|
||||
#
|
||||
# A \CSV::Row that was created by parsing \CSV source
|
||||
# inherits its headers from the table:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table.first
|
||||
# row.headers # => ["Name", "Value"]
|
||||
#
|
||||
# You can also create a new row with headers;
|
||||
# like the keys in a \Hash, the headers need not be Strings:
|
||||
# row = CSV::Row.new([:name, :value], ['foo', 0])
|
||||
# row.headers # => [:name, :value]
|
||||
#
|
||||
# The new row retains its headers even if added to a table
|
||||
# that has headers:
|
||||
# table << row # => #<CSV::Table mode:col_or_row row_count:5>
|
||||
# row.headers # => [:name, :value]
|
||||
# row[:name] # => "foo"
|
||||
# row['Name'] # => nil
|
||||
#
|
||||
#
|
||||
#
|
||||
# == Accessing Fields
|
||||
#
|
||||
# You may access a field in a \CSV::Row with either its \Integer index
|
||||
# (\Array-style) or its header (\Hash-style).
|
||||
#
|
||||
# Fetch a field using method #[]:
|
||||
# row = CSV::Row.new(['Name', 'Value'], ['foo', 0])
|
||||
# row[1] # => 0
|
||||
# row['Value'] # => 0
|
||||
#
|
||||
# Set a field using method #[]=:
|
||||
# row = CSV::Row.new(['Name', 'Value'], ['foo', 0])
|
||||
# row # => #<CSV::Row "Name":"foo" "Value":0>
|
||||
# row[0] = 'bar'
|
||||
# row['Value'] = 1
|
||||
# row # => #<CSV::Row "Name":"bar" "Value":1>
|
||||
#
|
||||
class Row
|
||||
# :call-seq:
|
||||
# CSV::Row.new(headers, fields, header_row = false) -> csv_row
|
||||
#
|
||||
# Returns the new \CSV::Row instance constructed from
|
||||
# arguments +headers+ and +fields+; both should be Arrays;
|
||||
# note that the fields need not be Strings:
|
||||
# row = CSV::Row.new(['Name', 'Value'], ['foo', 0])
|
||||
# row # => #<CSV::Row "Name":"foo" "Value":0>
|
||||
#
|
||||
# If the \Array lengths are different, the shorter is +nil+-filled:
|
||||
# row = CSV::Row.new(['Name', 'Value', 'Date', 'Size'], ['foo', 0])
|
||||
# row # => #<CSV::Row "Name":"foo" "Value":0 "Date":nil "Size":nil>
|
||||
#
|
||||
# Each \CSV::Row object is either a <i>field row</i> or a <i>header row</i>;
|
||||
# by default, a new row is a field row; for the row created above:
|
||||
# row.field_row? # => true
|
||||
# row.header_row? # => false
|
||||
#
|
||||
# If the optional argument +header_row+ is given as +true+,
|
||||
# the created row is a header row:
|
||||
# row = CSV::Row.new(['Name', 'Value'], ['foo', 0], header_row = true)
|
||||
# row # => #<CSV::Row "Name":"foo" "Value":0>
|
||||
# row.field_row? # => false
|
||||
# row.header_row? # => true
|
||||
def initialize(headers, fields, header_row = false)
|
||||
@header_row = header_row
|
||||
headers.each { |h| h.freeze if h.is_a? String }
|
||||
|
||||
# handle extra headers or fields
|
||||
@row = if headers.size >= fields.size
|
||||
headers.zip(fields)
|
||||
else
|
||||
fields.zip(headers).each(&:reverse!)
|
||||
end
|
||||
end
|
||||
|
||||
# Internal data format used to compare equality.
|
||||
attr_reader :row
|
||||
protected :row
|
||||
|
||||
### Array Delegation ###
|
||||
|
||||
extend Forwardable
|
||||
def_delegators :@row, :empty?, :length, :size
|
||||
|
||||
# :call-seq:
|
||||
# row.initialize_copy(other_row) -> self
|
||||
#
|
||||
# Calls superclass method.
|
||||
def initialize_copy(other)
|
||||
super_return_value = super
|
||||
@row = @row.collect(&:dup)
|
||||
super_return_value
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# row.header_row? -> true or false
|
||||
#
|
||||
# Returns +true+ if this is a header row, +false+ otherwise.
|
||||
def header_row?
|
||||
@header_row
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# row.field_row? -> true or false
|
||||
#
|
||||
# Returns +true+ if this is a field row, +false+ otherwise.
|
||||
def field_row?
|
||||
not header_row?
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# row.headers -> array_of_headers
|
||||
#
|
||||
# Returns the headers for this row:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table.first
|
||||
# row.headers # => ["Name", "Value"]
|
||||
def headers
|
||||
@row.map(&:first)
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# field(index) -> value
|
||||
# field(header) -> value
|
||||
# field(header, offset) -> value
|
||||
#
|
||||
# Returns the field value for the given +index+ or +header+.
|
||||
#
|
||||
# ---
|
||||
#
|
||||
# Fetch field value by \Integer index:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.field(0) # => "foo"
|
||||
# row.field(1) # => "bar"
|
||||
#
|
||||
# Counts backward from the last column if +index+ is negative:
|
||||
# row.field(-1) # => "0"
|
||||
# row.field(-2) # => "foo"
|
||||
#
|
||||
# Returns +nil+ if +index+ is out of range:
|
||||
# row.field(2) # => nil
|
||||
# row.field(-3) # => nil
|
||||
#
|
||||
# ---
|
||||
#
|
||||
# Fetch field value by header (first found):
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.field('Name') # => "Foo"
|
||||
#
|
||||
# Fetch field value by header, ignoring +offset+ leading fields:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.field('Name', 2) # => "Baz"
|
||||
#
|
||||
# Returns +nil+ if the header does not exist.
|
||||
def field(header_or_index, minimum_index = 0)
|
||||
# locate the pair
|
||||
finder = (header_or_index.is_a?(Integer) || header_or_index.is_a?(Range)) ? :[] : :assoc
|
||||
pair = @row[minimum_index..-1].public_send(finder, header_or_index)
|
||||
|
||||
# return the field if we have a pair
|
||||
if pair.nil?
|
||||
nil
|
||||
else
|
||||
header_or_index.is_a?(Range) ? pair.map(&:last) : pair.last
|
||||
end
|
||||
end
|
||||
alias_method :[], :field
|
||||
|
||||
#
|
||||
# :call-seq:
|
||||
# fetch(header) -> value
|
||||
# fetch(header, default) -> value
|
||||
# fetch(header) {|row| ... } -> value
|
||||
#
|
||||
# Returns the field value as specified by +header+.
|
||||
#
|
||||
# ---
|
||||
#
|
||||
# With the single argument +header+, returns the field value
|
||||
# for that header (first found):
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.fetch('Name') # => "Foo"
|
||||
#
|
||||
# Raises exception +KeyError+ if the header does not exist.
|
||||
#
|
||||
# ---
|
||||
#
|
||||
# With arguments +header+ and +default+ given,
|
||||
# returns the field value for the header (first found)
|
||||
# if the header exists, otherwise returns +default+:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.fetch('Name', '') # => "Foo"
|
||||
# row.fetch(:nosuch, '') # => ""
|
||||
#
|
||||
# ---
|
||||
#
|
||||
# With argument +header+ and a block given,
|
||||
# returns the field value for the header (first found)
|
||||
# if the header exists; otherwise calls the block
|
||||
# and returns its return value:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.fetch('Name') {|header| fail 'Cannot happen' } # => "Foo"
|
||||
# row.fetch(:nosuch) {|header| "Header '#{header} not found'" } # => "Header 'nosuch not found'"
|
||||
def fetch(header, *varargs)
|
||||
raise ArgumentError, "Too many arguments" if varargs.length > 1
|
||||
pair = @row.assoc(header)
|
||||
if pair
|
||||
pair.last
|
||||
else
|
||||
if block_given?
|
||||
yield header
|
||||
elsif varargs.empty?
|
||||
raise KeyError, "key not found: #{header}"
|
||||
else
|
||||
varargs.first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# row.has_key?(header) -> true or false
|
||||
#
|
||||
# Returns +true+ if there is a field with the given +header+,
|
||||
# +false+ otherwise.
|
||||
def has_key?(header)
|
||||
!!@row.assoc(header)
|
||||
end
|
||||
alias_method :include?, :has_key?
|
||||
alias_method :key?, :has_key?
|
||||
alias_method :member?, :has_key?
|
||||
alias_method :header?, :has_key?
|
||||
|
||||
#
|
||||
# :call-seq:
|
||||
# row[index] = value -> value
|
||||
# row[header, offset] = value -> value
|
||||
# row[header] = value -> value
|
||||
#
|
||||
# Assigns the field value for the given +index+ or +header+;
|
||||
# returns +value+.
|
||||
#
|
||||
# ---
|
||||
#
|
||||
# Assign field value by \Integer index:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row[0] = 'Bat'
|
||||
# row[1] = 3
|
||||
# row # => #<CSV::Row "Name":"Bat" "Value":3>
|
||||
#
|
||||
# Counts backward from the last column if +index+ is negative:
|
||||
# row[-1] = 4
|
||||
# row[-2] = 'Bam'
|
||||
# row # => #<CSV::Row "Name":"Bam" "Value":4>
|
||||
#
|
||||
# Extends the row with <tt>nil:nil</tt> if positive +index+ is not in the row:
|
||||
# row[4] = 5
|
||||
# row # => #<CSV::Row "Name":"bad" "Value":4 nil:nil nil:nil nil:5>
|
||||
#
|
||||
# Raises IndexError if negative +index+ is too small (too far from zero).
|
||||
#
|
||||
# ---
|
||||
#
|
||||
# Assign field value by header (first found):
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row['Name'] = 'Bat'
|
||||
# row # => #<CSV::Row "Name":"Bat" "Name":"Bar" "Name":"Baz">
|
||||
#
|
||||
# Assign field value by header, ignoring +offset+ leading fields:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row['Name', 2] = 4
|
||||
# row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":4>
|
||||
#
|
||||
# Append new field by (new) header:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row['New'] = 6
|
||||
# row# => #<CSV::Row "Name":"foo" "Value":"0" "New":6>
|
||||
def []=(*args)
|
||||
value = args.pop
|
||||
|
||||
if args.first.is_a? Integer
|
||||
if @row[args.first].nil? # extending past the end with index
|
||||
@row[args.first] = [nil, value]
|
||||
@row.map! { |pair| pair.nil? ? [nil, nil] : pair }
|
||||
else # normal index assignment
|
||||
@row[args.first][1] = value
|
||||
end
|
||||
else
|
||||
index = index(*args)
|
||||
if index.nil? # appending a field
|
||||
self << [args.first, value]
|
||||
else # normal header assignment
|
||||
@row[index][1] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# :call-seq:
|
||||
# row << [header, value] -> self
|
||||
# row << hash -> self
|
||||
# row << value -> self
|
||||
#
|
||||
# Adds a field to +self+; returns +self+:
|
||||
#
|
||||
# If the argument is a 2-element \Array <tt>[header, value]</tt>,
|
||||
# a field is added with the given +header+ and +value+:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row << ['NAME', 'Bat']
|
||||
# row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" "NAME":"Bat">
|
||||
#
|
||||
# If the argument is a \Hash, each <tt>key-value</tt> pair is added
|
||||
# as a field with header +key+ and value +value+.
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row << {NAME: 'Bat', name: 'Bam'}
|
||||
# row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" NAME:"Bat" name:"Bam">
|
||||
#
|
||||
# Otherwise, the given +value+ is added as a field with no header.
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row << 'Bag'
|
||||
# row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" nil:"Bag">
|
||||
def <<(arg)
|
||||
if arg.is_a?(Array) and arg.size == 2 # appending a header and name
|
||||
@row << arg
|
||||
elsif arg.is_a?(Hash) # append header and name pairs
|
||||
arg.each { |pair| @row << pair }
|
||||
else # append field value
|
||||
@row << [nil, arg]
|
||||
end
|
||||
|
||||
self # for chaining
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# row.push(*values) -> self
|
||||
#
|
||||
# Appends each of the given +values+ to +self+ as a field; returns +self+:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.push('Bat', 'Bam')
|
||||
# row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" nil:"Bat" nil:"Bam">
|
||||
def push(*args)
|
||||
args.each { |arg| self << arg }
|
||||
|
||||
self # for chaining
|
||||
end
|
||||
|
||||
#
|
||||
# :call-seq:
|
||||
# delete(index) -> [header, value] or nil
|
||||
# delete(header) -> [header, value] or empty_array
|
||||
# delete(header, offset) -> [header, value] or empty_array
|
||||
#
|
||||
# Removes a specified field from +self+; returns the 2-element \Array
|
||||
# <tt>[header, value]</tt> if the field exists.
|
||||
#
|
||||
# If an \Integer argument +index+ is given,
|
||||
# removes and returns the field at offset +index+,
|
||||
# or returns +nil+ if the field does not exist:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.delete(1) # => ["Name", "Bar"]
|
||||
# row.delete(50) # => nil
|
||||
#
|
||||
# Otherwise, if the single argument +header+ is given,
|
||||
# removes and returns the first-found field with the given header,
|
||||
# of returns a new empty \Array if the field does not exist:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.delete('Name') # => ["Name", "Foo"]
|
||||
# row.delete('NAME') # => []
|
||||
#
|
||||
# If argument +header+ and \Integer argument +offset+ are given,
|
||||
# removes and returns the first-found field with the given header
|
||||
# whose +index+ is at least as large as +offset+:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.delete('Name', 1) # => ["Name", "Bar"]
|
||||
# row.delete('NAME', 1) # => []
|
||||
def delete(header_or_index, minimum_index = 0)
|
||||
if header_or_index.is_a? Integer # by index
|
||||
@row.delete_at(header_or_index)
|
||||
elsif i = index(header_or_index, minimum_index) # by header
|
||||
@row.delete_at(i)
|
||||
else
|
||||
[ ]
|
||||
end
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# row.delete_if {|header, value| ... } -> self
|
||||
#
|
||||
# Removes fields from +self+ as selected by the block; returns +self+.
|
||||
#
|
||||
# Removes each field for which the block returns a truthy value:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.delete_if {|header, value| value.start_with?('B') } # => true
|
||||
# row # => #<CSV::Row "Name":"Foo">
|
||||
# row.delete_if {|header, value| header.start_with?('B') } # => false
|
||||
#
|
||||
# If no block is given, returns a new Enumerator:
|
||||
# row.delete_if # => #<Enumerator: #<CSV::Row "Name":"Foo">:delete_if>
|
||||
def delete_if(&block)
|
||||
return enum_for(__method__) { size } unless block_given?
|
||||
|
||||
@row.delete_if(&block)
|
||||
|
||||
self # for chaining
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# self.fields(*specifiers) -> array_of_fields
|
||||
#
|
||||
# Returns field values per the given +specifiers+, which may be any mixture of:
|
||||
# - \Integer index.
|
||||
# - \Range of \Integer indexes.
|
||||
# - 2-element \Array containing a header and offset.
|
||||
# - Header.
|
||||
# - \Range of headers.
|
||||
#
|
||||
# For +specifier+ in one of the first four cases above,
|
||||
# returns the result of <tt>self.field(specifier)</tt>; see #field.
|
||||
#
|
||||
# Although there may be any number of +specifiers+,
|
||||
# the examples here will illustrate one at a time.
|
||||
#
|
||||
# When the specifier is an \Integer +index+,
|
||||
# returns <tt>self.field(index)</tt>L
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.fields(1) # => ["Bar"]
|
||||
#
|
||||
# When the specifier is a \Range of \Integers +range+,
|
||||
# returns <tt>self.field(range)</tt>:
|
||||
# row.fields(1..2) # => ["Bar", "Baz"]
|
||||
#
|
||||
# When the specifier is a 2-element \Array +array+,
|
||||
# returns <tt>self.field(array)</tt>L
|
||||
# row.fields('Name', 1) # => ["Foo", "Bar"]
|
||||
#
|
||||
# When the specifier is a header +header+,
|
||||
# returns <tt>self.field(header)</tt>L
|
||||
# row.fields('Name') # => ["Foo"]
|
||||
#
|
||||
# When the specifier is a \Range of headers +range+,
|
||||
# forms a new \Range +new_range+ from the indexes of
|
||||
# <tt>range.start</tt> and <tt>range.end</tt>,
|
||||
# and returns <tt>self.field(new_range)</tt>:
|
||||
# source = "Name,NAME,name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.fields('Name'..'NAME') # => ["Foo", "Bar"]
|
||||
#
|
||||
# Returns all fields if no argument given:
|
||||
# row.fields # => ["Foo", "Bar", "Baz"]
|
||||
def fields(*headers_and_or_indices)
|
||||
if headers_and_or_indices.empty? # return all fields--no arguments
|
||||
@row.map(&:last)
|
||||
else # or work like values_at()
|
||||
all = []
|
||||
headers_and_or_indices.each do |h_or_i|
|
||||
if h_or_i.is_a? Range
|
||||
index_begin = h_or_i.begin.is_a?(Integer) ? h_or_i.begin :
|
||||
index(h_or_i.begin)
|
||||
index_end = h_or_i.end.is_a?(Integer) ? h_or_i.end :
|
||||
index(h_or_i.end)
|
||||
new_range = h_or_i.exclude_end? ? (index_begin...index_end) :
|
||||
(index_begin..index_end)
|
||||
all.concat(fields.values_at(new_range))
|
||||
else
|
||||
all << field(*Array(h_or_i))
|
||||
end
|
||||
end
|
||||
return all
|
||||
end
|
||||
end
|
||||
alias_method :values_at, :fields
|
||||
|
||||
# :call-seq:
|
||||
# index(header) -> index
|
||||
# index(header, offset) -> index
|
||||
#
|
||||
# Returns the index for the given header, if it exists;
|
||||
# otherwise returns +nil+.
|
||||
#
|
||||
# With the single argument +header+, returns the index
|
||||
# of the first-found field with the given +header+:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.index('Name') # => 0
|
||||
# row.index('NAME') # => nil
|
||||
#
|
||||
# With arguments +header+ and +offset+,
|
||||
# returns the index of the first-found field with given +header+,
|
||||
# but ignoring the first +offset+ fields:
|
||||
# row.index('Name', 1) # => 1
|
||||
# row.index('Name', 3) # => nil
|
||||
def index(header, minimum_index = 0)
|
||||
# find the pair
|
||||
index = headers[minimum_index..-1].index(header)
|
||||
# return the index at the right offset, if we found one
|
||||
index.nil? ? nil : index + minimum_index
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# row.field?(value) -> true or false
|
||||
#
|
||||
# Returns +true+ if +value+ is a field in this row, +false+ otherwise:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.field?('Bar') # => true
|
||||
# row.field?('BAR') # => false
|
||||
def field?(data)
|
||||
fields.include? data
|
||||
end
|
||||
|
||||
include Enumerable
|
||||
|
||||
# :call-seq:
|
||||
# row.each {|header, value| ... } -> self
|
||||
#
|
||||
# Calls the block with each header-value pair; returns +self+:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.each {|header, value| p [header, value] }
|
||||
# Output:
|
||||
# ["Name", "Foo"]
|
||||
# ["Name", "Bar"]
|
||||
# ["Name", "Baz"]
|
||||
#
|
||||
# If no block is given, returns a new Enumerator:
|
||||
# row.each # => #<Enumerator: #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz">:each>
|
||||
def each(&block)
|
||||
return enum_for(__method__) { size } unless block_given?
|
||||
|
||||
@row.each(&block)
|
||||
|
||||
self # for chaining
|
||||
end
|
||||
|
||||
alias_method :each_pair, :each
|
||||
|
||||
# :call-seq:
|
||||
# row == other -> true or false
|
||||
#
|
||||
# Returns +true+ if +other+ is a /CSV::Row that has the same
|
||||
# fields (headers and values) in the same order as +self+;
|
||||
# otherwise returns +false+:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# other_row = table[0]
|
||||
# row == other_row # => true
|
||||
# other_row = table[1]
|
||||
# row == other_row # => false
|
||||
def ==(other)
|
||||
return @row == other.row if other.is_a? CSV::Row
|
||||
@row == other
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# row.to_h -> hash
|
||||
#
|
||||
# Returns the new \Hash formed by adding each header-value pair in +self+
|
||||
# as a key-value pair in the \Hash.
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.to_h # => {"Name"=>"foo", "Value"=>"0"}
|
||||
#
|
||||
# Header order is preserved, but repeated headers are ignored:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.to_h # => {"Name"=>"Foo"}
|
||||
def to_h
|
||||
hash = {}
|
||||
each do |key, _value|
|
||||
hash[key] = self[key] unless hash.key?(key)
|
||||
end
|
||||
hash
|
||||
end
|
||||
alias_method :to_hash, :to_h
|
||||
|
||||
# :call-seq:
|
||||
# row.deconstruct_keys(keys) -> hash
|
||||
#
|
||||
# Returns the new \Hash suitable for pattern matching containing only the
|
||||
# keys specified as an argument.
|
||||
def deconstruct_keys(keys)
|
||||
if keys.nil?
|
||||
to_h
|
||||
else
|
||||
keys.to_h { |key| [key, self[key]] }
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :to_ary, :to_a
|
||||
|
||||
# :call-seq:
|
||||
# row.deconstruct -> array
|
||||
#
|
||||
# Returns the new \Array suitable for pattern matching containing the values
|
||||
# of the row.
|
||||
def deconstruct
|
||||
fields
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# row.to_csv -> csv_string
|
||||
#
|
||||
# Returns the row as a \CSV String. Headers are not included:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.to_csv # => "foo,0\n"
|
||||
def to_csv(**options)
|
||||
fields.to_csv(**options)
|
||||
end
|
||||
alias_method :to_s, :to_csv
|
||||
|
||||
# :call-seq:
|
||||
# row.dig(index_or_header, *identifiers) -> object
|
||||
#
|
||||
# Finds and returns the object in nested object that is specified
|
||||
# by +index_or_header+ and +specifiers+.
|
||||
#
|
||||
# The nested objects may be instances of various classes.
|
||||
# See {Dig Methods}[rdoc-ref:dig_methods.rdoc].
|
||||
#
|
||||
# Examples:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.dig(1) # => "0"
|
||||
# row.dig('Value') # => "0"
|
||||
# row.dig(5) # => nil
|
||||
def dig(index_or_header, *indexes)
|
||||
value = field(index_or_header)
|
||||
if value.nil?
|
||||
nil
|
||||
elsif indexes.empty?
|
||||
value
|
||||
else
|
||||
unless value.respond_to?(:dig)
|
||||
raise TypeError, "#{value.class} does not have \#dig method"
|
||||
end
|
||||
value.dig(*indexes)
|
||||
end
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# row.inspect -> string
|
||||
#
|
||||
# Returns an ASCII-compatible \String showing:
|
||||
# - Class \CSV::Row.
|
||||
# - Header-value pairs.
|
||||
# Example:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.inspect # => "#<CSV::Row \"Name\":\"foo\" \"Value\":\"0\">"
|
||||
def inspect
|
||||
str = ["#<", self.class.to_s]
|
||||
each do |header, field|
|
||||
str << " " << (header.is_a?(Symbol) ? header.to_s : header.inspect) <<
|
||||
":" << field.inspect
|
||||
end
|
||||
str << ">"
|
||||
begin
|
||||
str.join('')
|
||||
rescue # any encoding error
|
||||
str.map do |s|
|
||||
e = Encoding::Converter.asciicompat_encoding(s.encoding)
|
||||
e ? s.encode(e) : s.force_encoding("ASCII-8BIT")
|
||||
end.join('')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CSV
|
||||
# The version of the installed library.
|
||||
VERSION = "3.3.2"
|
||||
end
|
||||
@@ -0,0 +1,209 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "input_record_separator"
|
||||
require_relative "row"
|
||||
|
||||
class CSV
|
||||
# Note: Don't use this class directly. This is an internal class.
|
||||
class Writer
|
||||
#
|
||||
# A CSV::Writer receives an output, prepares the header, format and output.
|
||||
# It allows us to write new rows in the object and rewind it.
|
||||
#
|
||||
attr_reader :lineno
|
||||
attr_reader :headers
|
||||
|
||||
def initialize(output, options)
|
||||
@output = output
|
||||
@options = options
|
||||
@lineno = 0
|
||||
@fields_converter = nil
|
||||
prepare
|
||||
if @options[:write_headers] and @headers
|
||||
self << @headers
|
||||
end
|
||||
@fields_converter = @options[:fields_converter]
|
||||
end
|
||||
|
||||
#
|
||||
# Adds a new row
|
||||
#
|
||||
def <<(row)
|
||||
case row
|
||||
when Row
|
||||
row = row.fields
|
||||
when Hash
|
||||
row = @headers.collect {|header| row[header]}
|
||||
end
|
||||
|
||||
@headers ||= row if @use_headers
|
||||
@lineno += 1
|
||||
|
||||
if @fields_converter
|
||||
row = @fields_converter.convert(row, nil, lineno)
|
||||
end
|
||||
|
||||
i = -1
|
||||
converted_row = row.collect do |field|
|
||||
i += 1
|
||||
quote(field, i)
|
||||
end
|
||||
line = converted_row.join(@column_separator) + @row_separator
|
||||
if @output_encoding
|
||||
line = line.encode(@output_encoding)
|
||||
end
|
||||
@output << line
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
#
|
||||
# Winds back to the beginning
|
||||
#
|
||||
def rewind
|
||||
@lineno = 0
|
||||
@headers = nil if @options[:headers].nil?
|
||||
end
|
||||
|
||||
private
|
||||
def prepare
|
||||
@encoding = @options[:encoding]
|
||||
|
||||
prepare_header
|
||||
prepare_format
|
||||
prepare_output
|
||||
end
|
||||
|
||||
def prepare_header
|
||||
headers = @options[:headers]
|
||||
case headers
|
||||
when Array
|
||||
@headers = headers
|
||||
@use_headers = true
|
||||
when String
|
||||
@headers = CSV.parse_line(headers,
|
||||
col_sep: @options[:column_separator],
|
||||
row_sep: @options[:row_separator],
|
||||
quote_char: @options[:quote_character])
|
||||
@use_headers = true
|
||||
when true
|
||||
@headers = nil
|
||||
@use_headers = true
|
||||
else
|
||||
@headers = nil
|
||||
@use_headers = false
|
||||
end
|
||||
return unless @headers
|
||||
|
||||
converter = @options[:header_fields_converter]
|
||||
@headers = converter.convert(@headers, nil, 0, [])
|
||||
@headers.each do |header|
|
||||
header.freeze if header.is_a?(String)
|
||||
end
|
||||
end
|
||||
|
||||
def prepare_force_quotes_fields(force_quotes)
|
||||
@force_quotes_fields = {}
|
||||
force_quotes.each do |name_or_index|
|
||||
case name_or_index
|
||||
when Integer
|
||||
index = name_or_index
|
||||
@force_quotes_fields[index] = true
|
||||
when String, Symbol
|
||||
name = name_or_index.to_s
|
||||
if @headers.nil?
|
||||
message = ":headers is required when you use field name " +
|
||||
"in :force_quotes: " +
|
||||
"#{name_or_index.inspect}: #{force_quotes.inspect}"
|
||||
raise ArgumentError, message
|
||||
end
|
||||
index = @headers.index(name)
|
||||
next if index.nil?
|
||||
@force_quotes_fields[index] = true
|
||||
else
|
||||
message = ":force_quotes element must be " +
|
||||
"field index or field name: " +
|
||||
"#{name_or_index.inspect}: #{force_quotes.inspect}"
|
||||
raise ArgumentError, message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def prepare_format
|
||||
@column_separator = @options[:column_separator].to_s.encode(@encoding)
|
||||
row_separator = @options[:row_separator]
|
||||
if row_separator == :auto
|
||||
@row_separator = InputRecordSeparator.value.encode(@encoding)
|
||||
else
|
||||
@row_separator = row_separator.to_s.encode(@encoding)
|
||||
end
|
||||
@quote_character = @options[:quote_character]
|
||||
force_quotes = @options[:force_quotes]
|
||||
if force_quotes.is_a?(Array)
|
||||
prepare_force_quotes_fields(force_quotes)
|
||||
@force_quotes = false
|
||||
elsif force_quotes
|
||||
@force_quotes_fields = nil
|
||||
@force_quotes = true
|
||||
else
|
||||
@force_quotes_fields = nil
|
||||
@force_quotes = false
|
||||
end
|
||||
unless @force_quotes
|
||||
@quotable_pattern =
|
||||
Regexp.new("[\r\n".encode(@encoding) +
|
||||
Regexp.escape(@column_separator) +
|
||||
Regexp.escape(@quote_character.encode(@encoding)) +
|
||||
"]".encode(@encoding))
|
||||
end
|
||||
@quote_empty = @options.fetch(:quote_empty, true)
|
||||
end
|
||||
|
||||
def prepare_output
|
||||
@output_encoding = nil
|
||||
return unless @output.is_a?(StringIO)
|
||||
|
||||
output_encoding = @output.internal_encoding || @output.external_encoding
|
||||
if @encoding != output_encoding
|
||||
if @options[:force_encoding]
|
||||
@output_encoding = output_encoding
|
||||
else
|
||||
compatible_encoding = Encoding.compatible?(@encoding, output_encoding)
|
||||
if compatible_encoding
|
||||
@output.set_encoding(compatible_encoding)
|
||||
@output.seek(0, IO::SEEK_END)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def quote_field(field)
|
||||
field = String(field)
|
||||
encoded_quote_character = @quote_character.encode(field.encoding)
|
||||
encoded_quote_character +
|
||||
field.gsub(encoded_quote_character,
|
||||
encoded_quote_character * 2) +
|
||||
encoded_quote_character
|
||||
end
|
||||
|
||||
def quote(field, i)
|
||||
if @force_quotes
|
||||
quote_field(field)
|
||||
elsif @force_quotes_fields and @force_quotes_fields[i]
|
||||
quote_field(field)
|
||||
else
|
||||
if field.nil? # represent +nil+ fields as empty unquoted fields
|
||||
""
|
||||
else
|
||||
field = String(field) # Stringify fields
|
||||
# represent empty fields as empty quoted fields
|
||||
if (@quote_empty and field.empty?) or (field.valid_encoding? and @quotable_pattern.match?(field))
|
||||
quote_field(field)
|
||||
else
|
||||
field # unquoted field
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user