首先,让我们创建一个包含该信息的文件。
str =<<~_
Name , Sales
Randy, 200
Robin, 502
Randy, 200
Raj, 502
Randy, 500
Robin, 102
Mano, 220
Raj, 502
Randy, 285
Robin, 385
Randy, 295
Raj, 596
_
FNAME = 'f.csv'
File.write(FNAME, str)
#=> 157
CSV 文件有一个固定的列分隔符(字符串)。按照惯例,分隔符是逗号,在这种情况下,该文件的内容将如下所示:
Name,Sales
Randy,200
Robin,502
Randy,200
Raj,502
Randy,500
Robin,102
Mano,220
Raj,502
Randy,285
Robin,385
Randy,295
Raj,596
或者,分隔符可以是", ",但这不是所有行的分隔符。第一行在逗号之前包含一个空格,而其他一些行在逗号之后包含两个或多个空格。因此,您应该使用逗号(默认)作为列分隔符,然后在需要时去掉剩余内容的开头和结尾空格。
我们可以先打开文件创建一个CSV实例。
require 'csv'
csv = CSV.open(FNAME, headers: true)
#=> #<CSV io_type:File io_path:"f.csv" encoding:UTF-8 lineno:0 col_sep:",
" row_sep:"\n" quote_char:"\"" headers:true>
我们会发现csv.class #=> CSV 和csv.headers #=> true。后者只是确认我们已经规定文件有标题。由于尚未读取文件中的任何内容,因此不会返回标头本身。一旦读取了标题后的第一行csv.headers,将返回标题数组。您不需要col_sep: ",",因为默认的列分隔符是逗号。
我们现在读取文件并计算感兴趣的哈希值。
h = csv.each_with_object(Hash.new(0)) do |csv_row, h|
name, sales = csv.headers
h[csv_row[name].strip] += csv_row[sales].to_i
end
#=> {"Randy"=>1480, "Robin"=>989, "Raj"=>1600, "Mano"=>220}
作为CSV.included_modules.include?(Enumerable) #=> true,Enumerable#each_with_object 枚举csv 的元素,它们是CSV_Row 的实例(块变量csv_row 的值)并创建一个计数哈希,其形式为方法Hash::new 接受一个参数(默认值)并且没有块。请注意,在计算name #=> "Name " 和sales #=> " Sales"。
我们现在可以检索标题:
name, sales = csv.headers.map(&:strip)
#=> ["Name", "Sales"]
关闭文件是个好主意:
csv.close
虽然方法 CSV#close 没有记录。
我们现在可以操纵h 和headers 以满足要求。例如,我们可以计算以下内容:
names, all_sales = h.sort_by { |name, sales| -sales }
.map { |name,sales| [name, sales.to_s] }
.transpose
#=> [["Raj", "Randy", "Robin", "Mano"], ["1600", "1480", "989", "220"]]
max_name_len = [name.size, names.max_by(&:size).size].max
#=> 5
max_sales_len = [sales.size, all_sales.max_by(&:size).size].max
#=> 4
RANK_NAME = "Rank"
max_rank_len = [RANK_NAME.size, names.size].max
#=> 4
然后以格式良好的方式呈现结果。
puts name.ljust(max_name_len) + ' ' + sales.rjust(max_sales_len) +
' ' + RANK_NAME.rjust(max_sales_len)
(0..names.size-1).each { |i| puts names[i].ljust(max_name_len) + ' ' +
all_sales[i].rjust(max_sales_len) + ' ' + (i+1).to_s.rjust(max_rank_len) }
这将显示以下内容。
Name Sales Rank
Raj 1600 1
Randy 1480 2
Robin 989 3
Mano 220 4
见String#ljust 和String#rjust。也可以使用String#% 或Kernel#sprintf。
CSV 具有内置的转换器(和标头转换器)。如果有人写:
csv = CSV.open(FNAME, headers: true, converters: :integer)
文件正文中的所有值都将转换为整数。不幸的是,这会将name 字段的值以及sales 字段的值转换为整数,这不是我们想要的。但是,我们可以创建一个自定义转换器,仅将 sales 值转换为整数。这是按如下方式完成的。
proc = ->(s) { s.match(/ *\d+ */) ? s.to_i : s }
csv = CSV.open(FNAME, headers: true, converters: proc)
然后我们可能会将csv_row[sales].to_i 更改为csv_row[sales]:
h = csv.each_with_object(Hash.new(0)) do |csv_row, h|
name, sales = csv.headers
h[csv_row[name].strip] += csv_row[sales]
end
#=> {"Randy"=>1480, "Robin"=>989, "Raj"=>1600, "Mano"=>220}
如果有多个自定义转换器,我们将编写:
CSV.open(FNAME, headers: true, converters: [proc1, proc2,...])
proc1, proc2,... 是实现自定义转换器的过程。如果只有一个自定义转换器,如这里,我们可以写成converters: proc 或converters: [proc]。
最后,如果您对 CSV 文件有些生疏,您可以简单地将文件视为普通文件:
headers, *body = File.readlines(FNAME)
.map { |s| s.strip.split(/ *, */) }
#=> [["Name", "Sales"],
# ["Randy", "200"], ["Robin", "502"], ["Randy", "200"],
# ["Raj", "502"], ["Randy", "500"], ["Robin", "102"],
# ["Mano", "220"], ["Raj", "502"], ["Randy", "285"],
# ["Robin", "385"], ["Randy", "295"], ["Raj", "596"]]
因此:
headers
#=> ["Name", "Sales"]
body
#=> [["Randy", "200"], ["Robin", "502"], ["Randy", "200"],
# ["Raj", "502"], ["Randy", "500"], ["Robin", "102"],
# ["Mano", "220"], ["Raj", "502"], ["Randy", "285"],
# ["Robin", "385"], ["Randy", "295"], ["Raj", "596"]]
h = body.each_with_object(Hash.new(0)) do |(name, sales),h|
h[name] += sales.to_i
end
#=> {"Randy"=>1480, "Robin"=>989, "Raj"=>1600, "Mano"=>220}
然后像以前一样继续。