diff options
author | Burdette Lamar <BurdetteLamar@Yahoo.com> | 2020-07-15 15:37:17 -0500 |
---|---|---|
committer | Nobuyoshi Nakada <nobu@ruby-lang.org> | 2020-07-20 02:32:54 +0900 |
commit | d9749b4715168ccce020dd43b4815e365881f73e (patch) | |
tree | 45b6a98de91e0f33faff08262e82d241ec3ed055 | |
parent | d7c42df0b103fb24d39d5f52b792f21afa71daa7 (diff) | |
download | ruby-d9749b4715168ccce020dd43b4815e365881f73e.tar.gz |
[ruby/csv] RDoc for converters (#157)
* More on RDoc for converters
* More on RDoc for converters
* Fix indent
Co-authored-by: Sutou Kouhei <kou@cozmixng.org>
https://github.com/ruby/csv/commit/6044976160
-rw-r--r-- | doc/csv/options/generating/write_converters.rdoc | 14 | ||||
-rw-r--r-- | doc/csv/options/parsing/converters.rdoc | 37 | ||||
-rw-r--r-- | doc/csv/options/parsing/header_converters.rdoc | 40 | ||||
-rw-r--r-- | lib/csv.rb | 617 |
4 files changed, 484 insertions, 224 deletions
diff --git a/doc/csv/options/generating/write_converters.rdoc b/doc/csv/options/generating/write_converters.rdoc index c7367b96fd..6e5fae5fda 100644 --- a/doc/csv/options/generating/write_converters.rdoc +++ b/doc/csv/options/generating/write_converters.rdoc @@ -1,7 +1,7 @@ ====== Option +write_converters+ -Specifies the \Proc or \Array of Procs that are to be called -for converting each output field. +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 @@ -11,21 +11,23 @@ With no write converter: str # => "\"\na\n\",\tb\t, c \n" With a write converter: - strip_converter = lambda {|field| field.strip } + 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 = lambda {|field| field.upcase } - downcase_converter = lambda {|field| field.downcase } + 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] + --- Raises an exception if the converter returns a value that is neither +nil+ nor \String-convertible: - bad_converter = lambda {|field| BasicObject.new } + bad_converter = proc {|field| BasicObject.new } # Raises NoMethodError (undefined method `is_a?' for #<BasicObject:>) CSV.generate_line(['a', 'b', 'c'], write_converters: bad_converter)
\ No newline at end of file diff --git a/doc/csv/options/parsing/converters.rdoc b/doc/csv/options/parsing/converters.rdoc index 993803c5d0..211fa48de6 100644 --- a/doc/csv/options/parsing/converters.rdoc +++ b/doc/csv/options/parsing/converters.rdoc @@ -1,41 +1,42 @@ ====== Option +converters+ -Specifies a single field converter name or \Proc, -or an \Array of field converter names and Procs. - +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 single field converter name: +The value may be a field converter name +(see {Stored Converters}[#class-CSV-label-Stored+Converters]): str = '1,2,3' # Without a converter - ary = CSV.parse_line(str) - ary # => ["1", "2", "3"] + array = CSV.parse_line(str) + array # => ["1", "2", "3"] # With built-in converter :integer - ary = CSV.parse_line(str, converters: :integer) - ary # => [1, 2, 3] + array = CSV.parse_line(str, converters: :integer) + array # => [1, 2, 3] -The value may be an \Array of field converter names: +The value may be a converter list +(see {Converter Lists}[#class-CSV-label-Converter+Lists]): str = '1,3.14159' # Without converters - ary = CSV.parse_line(str) - ary # => ["1", "3.14159"] + array = CSV.parse_line(str) + array # => ["1", "3.14159"] # With built-in converters - ary = CSV.parse_line(str, converters: [:integer, :float]) - ary # => [1, 3.14159] + 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 - ary = CSV.parse_line(str) - ary # => [" foo ", " bar ", " baz "] + array = CSV.parse_line(str) + array # => [" foo ", " bar ", " baz "] # With a custom converter - ary = CSV.parse_line(str, converters: proc {|field| field.strip }) - ary # => ["foo", "bar", "baz"] + array = CSV.parse_line(str, converters: proc {|field| field.strip }) + array # => ["foo", "bar", "baz"] -See also {Custom Converters}[#class-CSV-label-Custom+Converters] +See also {Custom Field Converters}[#class-CSV-label-Custom+Field+Converters] --- diff --git a/doc/csv/options/parsing/header_converters.rdoc b/doc/csv/options/parsing/header_converters.rdoc index 329d96a897..309180805f 100644 --- a/doc/csv/options/parsing/header_converters.rdoc +++ b/doc/csv/options/parsing/header_converters.rdoc @@ -1,6 +1,7 @@ ====== Option +header_converters+ -Specifies a \String converter name or an \Array of converter names. +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 @@ -10,22 +11,33 @@ except that: - The converters apply only to the header row. - The built-in header converters are +:downcase+ and +:symbol+. -Examples: +This section assumes prior execution of: str = <<-EOT + Name,Value foo,0 bar,1 baz,2 EOT - headers = ['Name', 'Value'] # With no header converter - csv = CSV.parse(str, headers: headers) - csv.headers # => ["Name", "Value"] - # With header converter :downcase - csv = CSV.parse(str, headers: headers, header_converters: :downcase) - csv.headers # => ["name", "value"] - # With header converter :symbol - csv = CSV.parse(str, headers: headers, header_converters: :symbol) - csv.headers # => [:name, :value] - # With both - csv = CSV.parse(str, headers: headers, header_converters: [:downcase, :symbol]) - csv.headers # => [:name, :value] + 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] + diff --git a/lib/csv.rb b/lib/csv.rb index 6efb3ab1cb..83898890c7 100644 --- a/lib/csv.rb +++ b/lib/csv.rb @@ -34,7 +34,7 @@ # I'm sure I'll miss something, but I'll try to mention most of the major # differences I am aware of, to help others quickly get up to speed: # -# === CSV Parsing +# === \CSV Parsing # # * This parser is m17n aware. See CSV for full details. # * This library has a stricter parser and will throw MalformedCSVErrors on @@ -440,54 +440,188 @@ using CSV::MatchP if CSV.const_defined?(:MatchP) # data = CSV.parse('Bob,Engineering,1000', headers: %i[name department salary]) # data.first #=> #<CSV::Row name:"Bob" department:"Engineering" salary:"1000"> # -# === \CSV \Converters +# === \Converters +# +# By default, each value (field or header) parsed by \CSV is formed into a \String. +# You can use a _field_ _converter_ or _header_ _converter_ +# to intercept and modify the parsed values: +# - See {Field Converters}[#class-CSV-label-Field+Converters]. +# - See {Header Converters}[#class-CSV-label-Header+Converters]. +# +# Also by default, each value to be written during generation is written 'as-is'. +# You can use a _write_ _converter_ to modify values before writing. +# - See {Write Converters}[#class-CSV-label-Write+Converters]. +# +# ==== Specifying \Converters +# +# You can specify converters for parsing or generating in the +options+ +# argument to various \CSV methods: +# - Option +converters+ for converting parsed field values. +# - Option +header_converters+ for converting parsed header values. +# - Option +write_converters+ for converting values to be written (generated). +# +# There are three forms for specifying converters: +# - A converter proc: executable code to be used for conversion. +# - A converter name: the name of a stored converter. +# - A converter list: an array of converter procs, converter names, and converter lists. +# +# ===== Converter Procs +# +# This converter proc, +strip_converter+, accepts a value +field+ +# and returns <tt>field.strip</tt>: +# strip_converter = proc {|field| field.strip } +# In this call to <tt>CSV.parse</tt>, +# the keyword argument <tt>converters: string_converter</tt> +# specifies that: +# - \Proc +string_converter+ is to be called for each parsed field. +# - The converter's return value is to replace the +field+ value. +# Example: +# string = " foo , 0 \n bar , 1 \n baz , 2 \n" +# array = CSV.parse(string, converters: strip_converter) +# array # => [["foo", "0"], ["bar", "1"], ["baz", "2"]] +# +# A converter proc can receive a second argument, +field_info+, +# that contains details about the field. +# This modified +strip_converter+ displays its arguments: +# strip_converter = proc do |field, field_info| +# p [field, field_info] +# field.strip +# end +# string = " foo , 0 \n bar , 1 \n baz , 2 \n" +# array = CSV.parse(string, converters: strip_converter) +# array # => [["foo", "0"], ["bar", "1"], ["baz", "2"]] +# Output: +# [" foo ", #<struct CSV::FieldInfo index=0, line=1, header=nil>] +# [" 0 ", #<struct CSV::FieldInfo index=1, line=1, header=nil>] +# [" bar ", #<struct CSV::FieldInfo index=0, line=2, header=nil>] +# [" 1 ", #<struct CSV::FieldInfo index=1, line=2, header=nil>] +# [" baz ", #<struct CSV::FieldInfo index=0, line=3, header=nil>] +# [" 2 ", #<struct CSV::FieldInfo index=1, line=3, header=nil>] +# Each CSV::Info object shows: +# - The 0-based field index. +# - The 1-based line index. +# - The field header, if any. +# +# ===== Stored \Converters +# +# A converter may be given a name and stored in a structure where +# the parsing methods can find it by name. +# +# The storage structure for field converters is the \Hash CSV::Converters. +# It has several built-in converter procs: +# - <tt>:integer</tt>: converts each \String-embedded integer into a true \Integer. +# - <tt>:float</tt>: converts each \String-embedded float into a true \Float. +# - <tt>:date</tt>: converts each \String-embedded date into a true \Date. +# - <tt>:date_time</tt>: converts each \String-embedded date-time into a true \DateTime +# . +# This example creates a converter proc, then stores it: +# strip_converter = proc {|field| field.strip } +# CSV::Converters[:strip] = strip_converter +# Then the parsing method call can refer to the converter +# by its name, <tt>:strip</tt>: +# string = " foo , 0 \n bar , 1 \n baz , 2 \n" +# array = CSV.parse(string, converters: :strip) +# array # => [["foo", "0"], ["bar", "1"], ["baz", "2"]] +# +# The storage structure for header converters is the \Hash CSV::HeaderConverters, +# which works in the same way. +# It also has built-in converter procs: +# - <tt>:downcase</tt>: Downcases each header. +# - <tt>:symbol</tt>: Converts each header to a \Symbol. +# +# There is no such storage structure for write headers. +# +# ===== Converter Lists +# +# A _converter_ _list_ is an \Array that may include any assortment of: +# - Converter procs. +# - Names of stored converters. +# - Nested converter lists. +# +# Examples: +# numeric_converters = [:integer, :float] +# date_converters = [:date, :date_time] +# [numeric_converters, strip_converter] +# [strip_converter, date_converters, :float] +# +# Like a converter proc, a converter list may be named and stored in either +# \CSV::Converters or CSV::HeaderConverters: +# CSV::Converters[:custom] = [strip_converter, date_converters, :float] +# CSV::HeaderConverters[:custom] = [:downcase, :symbol] +# +# There are two built-in converter lists: +# CSV::Converters[:numeric] # => [:integer, :float] +# CSV::Converters[:all] # => [:date_time, :numeric] # -# By default, each field parsed by \CSV is formed into a \String. -# You can use a _converter_ to convert certain fields into other Ruby objects. +# ==== Field \Converters # -# When you specify a converter for parsing, -# each parsed field is passed to the converter; -# its return value becomes the new value for the field. +# With no conversion, all parsed fields in all rows become Strings: +# string = "foo,0\nbar,1\nbaz,2\n" +# ary = CSV.parse(string) +# ary # => # => [["foo", "0"], ["bar", "1"], ["baz", "2"]] +# +# When you specify a field converter, each parsed field is passed to the converter; +# its return value becomes the stored value for the field. # A converter might, for example, convert an integer embedded in a \String # into a true \Integer. # (In fact, that's what built-in field converter +:integer+ does.) # -# There are additional built-in \converters, and custom \converters are also supported. -# -# All \converters try to transcode fields to UTF-8 before converting. -# The conversion will fail if the data cannot be transcoded, leaving the field unchanged. -# -# ==== Field \Converters -# -# There are three ways to use field \converters; -# these examples use built-in field converter +:integer+, -# which converts each parsed integer string to a true \Integer. -# -# Option +converters+ with a singleton parsing method: -# ary = CSV.parse_line('0,1,2', converters: :integer) -# ary # => [0, 1, 2] -# -# Option +converters+ with a new \CSV instance: -# csv = CSV.new('0,1,2', converters: :integer) -# # Field converters in effect: -# csv.converters # => [:integer] -# csv.shift # => [0, 1, 2] -# -# Method #convert adds a field converter to a \CSV instance: -# csv = CSV.new('0,1,2') +# There are three ways to use field \converters. +# +# - Using option {converters}[#class-CSV-label-Option+converters] with a parsing method: +# ary = CSV.parse(string, converters: :integer) +# ary # => [0, 1, 2] # => [["foo", 0], ["bar", 1], ["baz", 2]] +# - Using option {converters}[#class-CSV-label-Option+converters] with a new \CSV instance: +# csv = CSV.new(string, converters: :integer) +# # Field converters in effect: +# csv.converters # => [:integer] +# csv.read # => [["foo", 0], ["bar", 1], ["baz", 2]] +# - Using method #convert to add a field converter to a \CSV instance: +# csv = CSV.new(string) +# # Add a converter. +# csv.convert(:integer) +# csv.converters # => [:integer] +# csv.read # => [["foo", 0], ["bar", 1], ["baz", 2]] +# +# Installing a field converter does not affect already-read rows: +# csv = CSV.new(string) +# csv.shift # => ["foo", "0"] # # Add a converter. # csv.convert(:integer) # csv.converters # => [:integer] -# csv.shift # => [0, 1, 2] +# csv.read # => [["bar", 1], ["baz", 2]] # -# --- +# There are additional built-in \converters, and custom \converters are also supported. # -# The built-in field \converters are in \Hash CSV::Converters. -# The \Symbol keys there are the names of the \converters: +# ===== Built-In Field \Converters # -# CSV::Converters.keys # => [:integer, :float, :numeric, :date, :date_time, :all] +# The built-in field converters are in \Hash CSV::Converters: +# - Each key is a field converter name. +# - Each value is one of: +# - A \Proc field converter. +# - An \Array of field converter names. # -# Converter +:integer+ converts each field that +Integer()+ accepts: +# Display: +# CSV::Converters.each_pair do |name, value| +# if value.kind_of?(Proc) +# p [name, value.class] +# else +# p [name, value] +# end +# end +# Output: +# [:integer, Proc] +# [:float, Proc] +# [:numeric, [:integer, :float]] +# [:date, Proc] +# [:date_time, Proc] +# [:all, [:date_time, :numeric]] +# +# Each of these converters transcodes values to UTF-8 before attempting conversion. +# If a value cannot be transcoded to UTF-8 the conversion will +# fail and the value will remain unconverted. +# +# Converter +:integer+ converts each field that Integer() accepts: # data = '0,1,2,x' # # Without the converter # csv = CSV.parse_line(data) @@ -496,7 +630,7 @@ using CSV::MatchP if CSV.const_defined?(:MatchP) # csv = CSV.parse_line(data, converters: :integer) # csv # => [0, 1, 2, "x"] # -# Converter +:float+ converts each field that +Float()+ accepts: +# Converter +:float+ converts each field that Float() accepts: # data = '1.0,3.14159,x' # # Without the converter # csv = CSV.parse_line(data) @@ -507,7 +641,7 @@ using CSV::MatchP if CSV.const_defined?(:MatchP) # # Converter +:numeric+ converts with both +:integer+ and +:float+.. # -# Converter +:date+ converts each field that +Date::parse()+ accepts: +# Converter +:date+ converts each field that Date::parse accepts: # data = '2001-02-03,x' # # Without the converter # csv = CSV.parse_line(data) @@ -516,7 +650,7 @@ using CSV::MatchP if CSV.const_defined?(:MatchP) # csv = CSV.parse_line(data, converters: :date) # csv # => [#<Date: 2001-02-03 ((2451944j,0s,0n),+0s,2299161j)>, "x"] # -# Converter +:date_time+ converts each field that +DateTime::parse() accepts: +# Converter +:date_time+ converts each field that DateTime::parse accepts: # data = '2020-05-07T14:59:00-05:00,x' # # Without the converter # csv = CSV.parse_line(data) @@ -536,17 +670,16 @@ using CSV::MatchP if CSV.const_defined?(:MatchP) # csv.convert(:date) # csv.converters # => [:integer, :date] # -# You can add a custom field converter to \Hash CSV::Converters: -# strip_converter = proc {|field| field.strip} -# CSV::Converters[:strip] = strip_converter -# CSV::Converters.keys # => [:integer, :float, :numeric, :date, :date_time, :all, :strip] -# -# Then use it to convert fields: -# str = ' foo , 0 ' -# ary = CSV.parse_line(str, converters: :strip) -# ary # => ["foo", "0"] +# ===== Custom Field \Converters # -# See {Custom Converters}[#class-CSV-label-Custom+Converters]. +# You can define a custom field converter: +# strip_converter = proc {|field| field.strip } +# Add it to the \Converters \Hash: +# CSV::Converters[:strip] = strip_converter +# Use it by name: +# string = " foo , 0 \n bar , 1 \n baz , 2 \n" +# array = CSV.parse(string, converters: strip_converter) +# array # => [["foo", "0"], ["bar", "1"], ["baz", "2"]] # # ==== Header \Converters # @@ -556,43 +689,42 @@ using CSV::MatchP if CSV.const_defined?(:MatchP) # these examples use built-in header converter +:dowhcase+, # which downcases each parsed header. # -# Option +header_converters+ with a singleton parsing method: -# str = "Name,Count\nFoo,0\n,Bar,1\nBaz,2" -# tbl = CSV.parse(str, headers: true, header_converters: :downcase) -# tbl.class # => CSV::Table -# tbl.headers # => ["name", "count"] -# -# Option +header_converters+ with a new \CSV instance: -# csv = CSV.new(str, header_converters: :downcase) -# # Header converters in effect: -# csv.header_converters # => [:downcase] -# tbl = CSV.parse(str, headers: true) -# tbl.headers # => ["Name", "Count"] -# -# Method #header_convert adds a header converter to a \CSV instance: -# csv = CSV.new(str) -# # Add a header converter. -# csv.header_convert(:downcase) -# csv.header_converters # => [:downcase] -# tbl = CSV.parse(str, headers: true) -# tbl.headers # => ["Name", "Count"] -# -# --- -# -# The built-in header \converters are in \Hash CSV::Converters. -# The \Symbol keys there are the names of the \converters: -# +# - Option +header_converters+ with a singleton parsing method: +# string = "Name,Count\nFoo,0\n,Bar,1\nBaz,2" +# tbl = CSV.parse(string, headers: true, header_converters: :downcase) +# tbl.class # => CSV::Table +# tbl.headers # => ["name", "count"] +# +# - Option +header_converters+ with a new \CSV instance: +# csv = CSV.new(string, header_converters: :downcase) +# # Header converters in effect: +# csv.header_converters # => [:downcase] +# tbl = CSV.parse(string, headers: true) +# tbl.headers # => ["Name", "Count"] +# +# - Method #header_convert adds a header converter to a \CSV instance: +# csv = CSV.new(string) +# # Add a header converter. +# csv.header_convert(:downcase) +# csv.header_converters # => [:downcase] +# tbl = CSV.parse(string, headers: true) +# tbl.headers # => ["Name", "Count"] +# +# ===== Built-In Header \Converters +# +# The built-in header \converters are in \Hash CSV::HeaderConverters. +# The keys there are the names of the \converters: # CSV::HeaderConverters.keys # => [:downcase, :symbol] # # Converter +:downcase+ converts each header by downcasing it: -# str = "Name,Count\nFoo,0\n,Bar,1\nBaz,2" -# tbl = CSV.parse(str, headers: true, header_converters: :downcase) +# string = "Name,Count\nFoo,0\n,Bar,1\nBaz,2" +# tbl = CSV.parse(string, headers: true, header_converters: :downcase) # tbl.class # => CSV::Table # tbl.headers # => ["name", "count"] # -# Converter +:symbol+ by making it into a \Symbol: -# str = "Name,Count\nFoo,0\n,Bar,1\nBaz,2" -# tbl = CSV.parse(str, headers: true, header_converters: :symbol) +# Converter +:symbol+ converts each header by making it into a \Symbol: +# string = "Name,Count\nFoo,0\n,Bar,1\nBaz,2" +# tbl = CSV.parse(string, headers: true, header_converters: :symbol) # tbl.headers # => [:name, :count] # Details: # - Strips leading and trailing whitespace. @@ -601,46 +733,44 @@ using CSV::MatchP if CSV.const_defined?(:MatchP) # - Removes non-word characters. # - Makes the string into a \Symbol. # -# You can add a custom header converter to \Hash CSV::HeaderConverters: -# strip_converter = proc {|field| field.strip} -# CSV::HeaderConverters[:strip] = strip_converter -# CSV::HeaderConverters.keys # => [:downcase, :symbol, :strip] -# -# Then use it to convert headers: -# str = " Name , Value \nfoo,0\nbar,1\nbaz,2" -# tbl = CSV.parse(str, headers: true, header_converters: :strip) -# tbl.headers # => ["Name", "Value"] -# -# See {Custom Converters}[#class-CSV-label-Custom+Converters]. -# -# ==== Custom \Converters +# ===== Custom Header \Converters # -# You can define custom \converters. +# You can define a custom header converter: +# upcase_converter = proc {|header| header.upcase } +# Add it to the \HeaderConverters \Hash: +# CSV::HeaderConverters[:upcase] = upcase_converter +# Use it by name: +# string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n" +# table = CSV.parse(string, headers: true, converters: upcase_converter) +# table # => #<CSV::Table mode:col_or_row row_count:4> +# table.headers # => ["Name", "Value"] # -# The \converter is a \Proc that is called with two arguments, -# \String +field+ and CSV::FieldInfo +field_info+; -# it returns a \String that will become the field value: -# converter = proc {|field, field_info| <some_string> } +# ===== Write \Converters # -# To illustrate: -# converter = proc {|field, field_info| p [field, field_info]; field} -# ary = CSV.parse_line('foo,0', converters: converter) -# -# Produces: -# ["foo", #<struct CSV::FieldInfo index=0, line=1, header=nil>] -# ["0", #<struct CSV::FieldInfo index=1, line=1, header=nil>] -# -# In each of the output lines: -# - The first \Array element is the passed \String field. -# - The second is a \FieldInfo structure containing information about the field: -# - The 0-based column index. -# - The 1-based line number. -# - The header for the column, if available. -# -# If the \converter does not need +field_info+, it can be omitted: -# converter = proc {|field| ... } +# When you specify a write converter for generating \CSV, +# each field to be written is passed to the converter; +# its return value becomes the new value for the field. +# A converter might, for example, strip whitespace from a field. # -# === CSV and Character Encodings (M17n or Multilingualization) +# - Using no write converter (all fields unmodified): +# output_string = CSV.generate do |csv| +# csv << [' foo ', 0] +# csv << [' bar ', 1] +# csv << [' baz ', 2] +# end +# output_string # => " foo ,0\n bar ,1\n baz ,2\n" +# - Using option +write_converters+: +# 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" +# +# === Character Encodings (M17n or Multilingualization) # # This new CSV parser is m17n savvy. The parser works in the Encoding of the IO # or String object being read from or written to. Your data is never transcoded @@ -721,30 +851,12 @@ class CSV # The encoding used by all converters. ConverterEncoding = Encoding.find("UTF-8") + # A \Hash containing the names and \Procs for the built-in field converters. + # See {Built-In Field Converters}[#class-CSV-label-Built-In+Field+Converters]. # - # This Hash holds the built-in converters of CSV that can be accessed by name. - # You can select Converters with CSV.convert() or through the +options+ Hash - # passed to CSV::new(). - # - # <b><tt>:integer</tt></b>:: Converts any field Integer() accepts. - # <b><tt>:float</tt></b>:: Converts any field Float() accepts. - # <b><tt>:numeric</tt></b>:: A combination of <tt>:integer</tt> - # and <tt>:float</tt>. - # <b><tt>:date</tt></b>:: Converts any field Date::parse() accepts. - # <b><tt>:date_time</tt></b>:: Converts any field DateTime::parse() accepts. - # <b><tt>:all</tt></b>:: All built-in converters. A combination of - # <tt>:date_time</tt> and <tt>:numeric</tt>. - # - # All built-in converters transcode field data to UTF-8 before attempting a - # conversion. If your data cannot be transcoded to UTF-8 the conversion will - # fail and the field will remain unchanged. - # - # This Hash is intentionally left unfrozen and users should feel free to add - # values to it that can be accessed by all CSV objects. - # - # To add a combo field, the value should be an Array of names. Combo fields - # can be nested with other combo fields. - # + # This \Hash is intentionally left unfrozen, and may be extended with + # custom field converters. + # See {Custom Field Converters}[#class-CSV-label-Custom+Field+Converters]. Converters = { integer: lambda { |f| Integer(f.encode(ConverterEncoding)) rescue f @@ -772,27 +884,12 @@ class CSV all: [:date_time, :numeric], } + # A \Hash containing the names and \Procs for the built-in header converters. + # See {Built-In Header Converters}[#class-CSV-label-Built-In+Header+Converters]. # - # This Hash holds the built-in header converters of CSV that can be accessed - # by name. You can select HeaderConverters with CSV.header_convert() or - # through the +options+ Hash passed to CSV::new(). - # - # <b><tt>:downcase</tt></b>:: Calls downcase() on the header String. - # <b><tt>:symbol</tt></b>:: Leading/trailing spaces are dropped, string is - # downcased, remaining spaces are replaced with - # underscores, non-word characters are dropped, - # and finally to_sym() is called. - # - # All built-in header converters transcode header data to UTF-8 before - # attempting a conversion. If your data cannot be transcoded to UTF-8 the - # conversion will fail and the header will remain unchanged. - # - # This Hash is intentionally left unfrozen and users should feel free to add - # values to it that can be accessed by all CSV objects. - # - # To add a combo field, the value should be an Array of names. Combo fields - # can be nested with other combo fields. - # + # This \Hash is intentionally left unfrozen, and may be extended with + # custom field converters. + # See {Custom Header Converters}[#class-CSV-label-Custom+Header+Converters]. HeaderConverters = { downcase: lambda { |h| h.encode(ConverterEncoding).downcase }, symbol: lambda { |h| @@ -1726,9 +1823,14 @@ class CSV # :call-seq: # csv.converters -> array # - # Returns an \Array containing field converters; used for parsing; - # see {Option +converters+}[#class-CSV-label-Option+converters]: - # CSV.new('').converters # => [] + # Returns an \Array containing field converters; + # see {Field Converters}[#class-CSV-label-Field+Converters]: + # csv = CSV.new('') + # csv.converters # => [] + # csv.convert(:integer) + # csv.converters # => [:integer] + # csv.convert(proc {|x| x.to_s }) + # csv.converters def converters parser_fields_converter.map do |converter| name = Converters.rassoc(converter) @@ -1789,7 +1891,7 @@ class CSV # csv.header_converters -> array # # Returns an \Array containing header converters; used for parsing; - # see {Option +header_converters+}[#class-CSV-label-Option+header_converters]: + # see {Header Converters}[#class-CSV-label-Header+Converters]: # CSV.new('').header_converters # => [] def header_converters header_fields_converter.map do |converter| @@ -1833,7 +1935,7 @@ class CSV # csv.encoding -> endcoding # # Returns the encoding used for parsing and generating; - # see {CSV and Character Encodings (M17n or Multilingualization)}[#class-CSV-label-CSV+and+Character+Encodings+-28M17n+or+Multilingualization-29]: + # see {Character Encodings (M17n or Multilingualization)}[#class-CSV-label-Character+Encodings+-28M17n+or+Multilingualization-29]: # CSV.new('').encoding # => #<Encoding:UTF-8> attr_reader :encoding @@ -1965,13 +2067,56 @@ class CSV ### End Delegation ### + # :call-seq: + # csv.<< row + # + # Appends a row to +self+. + # + # - Argument +row+ must be an \Array object or a CSV::Row object. + # - The output stream must be open for writing. + # + # --- # - # The primary write method for wrapped Strings and IOs, +row+ (an Array or - # CSV::Row) is converted to CSV and appended to the data source. When a - # CSV::Row is passed, only the row's fields() are appended to the output. + # Append Arrays: + # CSV.generate do |csv| + # csv << ['foo', 0] + # csv << ['bar', 1] + # csv << ['baz', 2] + # end # => "foo,0\nbar,1\nbaz,2\n" + # + # Append CSV::Rows: + # headers = [] + # CSV.generate do |csv| + # csv << CSV::Row.new(headers, ['foo', 0]) + # csv << CSV::Row.new(headers, ['bar', 1]) + # csv << CSV::Row.new(headers, ['baz', 2]) + # end # => "foo,0\nbar,1\nbaz,2\n" + # + # Headers in CSV::Row objects are not appended: + # headers = ['Name', 'Count'] + # CSV.generate do |csv| + # csv << CSV::Row.new(headers, ['foo', 0]) + # csv << CSV::Row.new(headers, ['bar', 1]) + # csv << CSV::Row.new(headers, ['baz', 2]) + # end # => "foo,0\nbar,1\nbaz,2\n" + # + # --- # - # The data source must be open for writing. + # Raises an exception if +row+ is not an \Array or \CSV::Row: + # CSV.generate do |csv| + # # Raises NoMethodError (undefined method `collect' for :foo:Symbol) + # csv << :foo + # end # + # Raises an exception if the output stream is not open for writing: + # path = 't.csv' + # File.write(path, '') + # File.open(path) do |file| + # CSV.open(file) do |csv| + # # Raises IOError (not opened for writing) + # csv << ['foo', 0] + # end + # end def <<(row) writer << row self @@ -1979,36 +2124,136 @@ class CSV alias_method :add_row, :<< alias_method :puts, :<< - # # :call-seq: - # convert( name ) - # convert { |field| ... } - # convert { |field, field_info| ... } + # convert(converter_name) -> array_of_procs + # convert {|field, field_info| ... } -> array_of_procs + # + # - With no block, installs a field converter (a \Proc). + # - With a block, defines and installs a custom field converter. + # - Returns the \Array of installed field converters. + # + # - Argument +converter_name+, if given, should be the name + # of an existing field converter. + # + # See {Field Converters}[#class-CSV-label-Field+Converters]. + # --- + # + # With no block, installs a field converter: + # csv = CSV.new('') + # csv.convert(:integer) + # csv.convert(:float) + # csv.convert(:date) + # csv.converters # => [:integer, :float, :date] + # + # --- + # + # The block, if given, is called for each field: + # - Argument +field+ is the field value. + # - Argument +field_info+ is a CSV::FieldInfo object + # containing details about the field. + # + # The examples here assume the prior execution of: + # string = "foo,0\nbar,1\nbaz,2\n" + # path = 't.csv' + # File.write(path, string) + # + # Example giving a block: + # csv = CSV.open(path) + # csv.convert {|field, field_info| p [field, field_info]; field.upcase } + # csv.read # => [["FOO", "0"], ["BAR", "1"], ["BAZ", "2"]] # - # You can use this method to install a CSV::Converters built-in, or provide a - # block that handles a custom conversion. + # Output: + # ["foo", #<struct CSV::FieldInfo index=0, line=1, header=nil>] + # ["0", #<struct CSV::FieldInfo index=1, line=1, header=nil>] + # ["bar", #<struct CSV::FieldInfo index=0, line=2, header=nil>] + # ["1", #<struct CSV::FieldInfo index=1, line=2, header=nil>] + # ["baz", #<struct CSV::FieldInfo index=0, line=3, header=nil>] + # ["2", #<struct CSV::FieldInfo index=1, line=3, header=nil>] + # + # The block need not return a \String object: + # csv = CSV.open(path) + # csv.convert {|field, field_info| field.to_sym } + # csv.read # => [[:foo, :"0"], [:bar, :"1"], [:baz, :"2"]] # - # If you provide a block that takes one argument, it will be passed the field - # and is expected to return the converted value or the field itself. If your - # block takes two arguments, it will also be passed a CSV::FieldInfo Struct, - # containing details about the field. Again, the block should return a - # converted field or the field itself. + # If +converter_name+ is given, the block is not called: + # csv = CSV.open(path) + # csv.convert(:integer) {|field, field_info| fail 'Cannot happen' } + # csv.read # => [["foo", 0], ["bar", 1], ["baz", 2]] # + # --- + # + # Raises a parse-time exception if +converter_name+ is not the name of a built-in + # field converter: + # csv = CSV.open(path) + # csv.convert(:nosuch) => [nil] + # # Raises NoMethodError (undefined method `arity' for nil:NilClass) + # csv.read def convert(name = nil, &converter) parser_fields_converter.add_converter(name, &converter) end - # # :call-seq: - # header_convert( name ) - # header_convert { |field| ... } - # header_convert { |field, field_info| ... } + # header_convert(converter_name) -> array_of_procs + # header_convert {|header, field_info| ... } -> array_of_procs # - # Identical to CSV#convert(), but for header rows. + # - With no block, installs a header converter (a \Proc). + # - With a block, defines and installs a custom header converter. + # - Returns the \Array of installed header converters. # - # Note that this method must be called before header rows are read to have any - # effect. + # - Argument +converter_name+, if given, should be the name + # of an existing header converter. # + # See {Header Converters}[#class-CSV-label-Header+Converters]. + # --- + # + # With no block, installs a header converter: + # csv = CSV.new('') + # csv.header_convert(:symbol) + # csv.header_convert(:downcase) + # csv.header_converters # => [:symbol, :downcase] + # + # --- + # + # The block, if given, is called for each header: + # - Argument +header+ is the header value. + # - Argument +field_info+ is a CSV::FieldInfo object + # containing details about the header. + # + # The examples here assume the prior execution of: + # string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n" + # path = 't.csv' + # File.write(path, string) + # + # Example giving a block: + # csv = CSV.open(path, headers: true) + # csv.header_convert {|header, field_info| p [header, field_info]; header.upcase } + # table = csv.read + # table # => #<CSV::Table mode:col_or_row row_count:4> + # table.headers # => ["NAME", "VALUE"] + # + # Output: + # ["Name", #<struct CSV::FieldInfo index=0, line=1, header=nil>] + # ["Value", #<struct CSV::FieldInfo index=1, line=1, header=nil>] + + # The block need not return a \String object: + # csv = CSV.open(path, headers: true) + # csv.header_convert {|header, field_info| header.to_sym } + # table = csv.read + # table.headers # => [:Name, :Value] + # + # If +converter_name+ is given, the block is not called: + # csv = CSV.open(path, headers: true) + # csv.header_convert(:downcase) {|header, field_info| fail 'Cannot happen' } + # table = csv.read + # table.headers # => ["name", "value"] + # --- + # + # Raises a parse-time exception if +converter_name+ is not the name of a built-in + # field converter: + # csv = CSV.open(path, headers: true) + # csv.header_convert(:nosuch) + # # Raises NoMethodError (undefined method `arity' for nil:NilClass) + # csv.read def header_convert(name = nil, &converter) header_fields_converter.add_converter(name, &converter) end |