9556


国、州/県、都市を定義した、こんなCSVテキストがあったとします。

[code]

    日本,東京,新宿
    ,,渋谷
    ,兵庫,神戸
    アメリカ,カリフォルニア,サンフランシスコ
    ,,ロサンゼルス
    
[/code]

データは予めソートされていて、国や州が前のデータと同じであれば空欄になっています。

このテキストを変換して、セルを全部埋めるプログラムを作りたいと思っています。  
すなわち、期待するのは以下のようなアウトプットです。

[code]

    日本,東京,新宿
    日本,東京,渋谷
    日本,兵庫,神戸
    アメリカ,カリフォルニア,サンフランシスコ
    アメリカ,カリフォルニア,ロサンゼルス
    
[/code]

とりあえず僕の方で愚直にこんなロジックを考えたんですが、ブロックの外で定義する`current_country`、`current_state`や、`if
country.size > 0 ... end` がDRYになってない感じが自分でもちょっとしっくりきません。

[code]

      def fill_blank(csv_text)
        current_country, current_state = nil, nil
        csv_text.lines.map { |line|
          country, state, city = line.chomp.split(',')
          if country.size > 0
            current_country = country
          end
          if state.size > 0
            current_state = state
          end
          [current_country, current_state, city].join(',')
        }.join("\n")
      end
    
[/code]

もうちょっと短くてわかりやすい、スマートなロジックは作れないでしょうか？  
Railsで動かすのでActiveSupportのメソッドを使うのもOKです。  
参考までにテストコード付きのサンプルコードを載せておきます。

[code]

    require 'test/unit'
    
    class FillBlankTest < Test::Unit::TestCase
      def fill_blank(csv_text)
        current_country, current_state = nil, nil
        csv_text.lines.map { |line|
          country, state, city = line.chomp.split(',')
          if country.size > 0
            current_country = country
          end
          if state.size > 0
            current_state = state
          end
          [current_country, current_state, city].join(',')
        }.join("\n")
      end
    
      def test_fill_blank
        input = <<-TEXT
    日本,東京,新宿
    ,,渋谷
    ,兵庫,神戸
    アメリカ,カリフォルニア,サンフランシスコ
    ,,ロサンゼルス
        TEXT
    
        output = <<-TEXT.chomp
    日本,東京,新宿
    日本,東京,渋谷
    日本,兵庫,神戸
    アメリカ,カリフォルニア,サンフランシスコ
    アメリカ,カリフォルニア,ロサンゼルス
        TEXT
    
        assert_equal output, fill_blank(input)
      end
    end
    
[/code]

みなさんのアイデアをお待ちしています。


CC BY-SA 3.0
2015-04-25T12:43:09.563
9546
2015-05-07T14:30:38.990
85
question
ruby
前の行と同じであれば空欄になっているCSVテキストのセルを全部埋めたい


Ruby暦1週間ほどなため  
文法におかしな点があるかもしれませんが  
ご容赦ください

行列の入れ替え(transpose)を使えば  
スマートになるかと思いチャレンジしてみました

。。。結果可読性が落ちただけな気が  
アイディアの足しになれば幸いです

[code]

    def fill_blank(csv_text)
        shwo = ""
        csv_text.lines.map{|line| line.chomp.split(',')}
                    .transpose.map{|row| row.map{|i|
                        shwo = i.empty? ? shwo : i
                        shwo
                    }}.transpose.map{|line|line.join(',')}.join("\n")
    end
    
[/code]


CC BY-SA 3.0
2015-04-25T14:51:26.623
9550
2015-04-25T14:51:26.623
7793
9546
answer


前の行をまるごと保存して、文字列が空なら前の行の項目を使う方法を考えてみました。

[code]

    def fill_blank(csv_text)
      prev = []
    
      csv_text.each_line.map do |line|
        row = line.chomp.split ","
        prev = row.zip(prev).map{|a, b| a.empty? ? b : a }
        prev * ","
      end * "\n"
    end
    
[/code]

CSVライブラリを使えばもう少し短くなります。(空文字列はnilになります)

[code]

    require 'csv'
    
    def fill_blank(csv_text)
      prev = []
    
      CSV.parse(csv_text).map do |row|
        prev = row.zip(prev).map{|a, b| a || b }
        prev * ","
      end * "\n"
    end
    
[/code]


CC BY-SA 3.0
2015-04-25T18:47:14.313
9556
2015-04-25T19:02:45.693
2015-04-25T19:02:45.693
8484
8484
9546
answer


CSVの`converters`に値が`nil`なら前の行の値を引き継ぐようなカスタムコンバータを指定するとよいのではないでしょうか。

[code]

    require 'csv'
    
    def fill_blank(csv_text)
      prev_vals = []
      preview_row = proc do |field, field_info|
        prev_vals[field_info.index] = field.nil? ? prev_vals[field_info.index] : field
      end
    
      CSV.generate do |csv|
        CSV.parse(csv_text, converters: [preview_row]) do |row|
          csv << row
        end
      end
    end
    
[/code]

Ruby 2.1.x以前であればうまく実行出来ると思います。

※Ruby 2.2.xではCSVライブラリの挙動が変わっているようなのでバグ報告しました

[Bug #11126: CSV field converters doesn't attempt to convert nil value. - Ruby
trunk - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/11126)


CC BY-SA 3.0
2015-05-07T14:30:38.990
9909
2015-05-07T14:30:38.990
2599
9546
answer