二次元の表から構造化データに変換する

f:id:sonota88:20200524133650p:plain

上の図のようにスプレッドシートで入力して、それを読んで 次のようにテキストファイルに出力したい。 *1

{"a":111,"b":{"d":121,"e":122},"c":{"f":{"h":131,"i":132},"g":141}}
{"a":211,"b":{"d":221,"e":222},"c":{"f":{"h":231,"i":232},"g":241}}

スプレッドシートから読んで前処理した時点で次のようになっているとして、 これをもとに組み立てる処理を書く 。 *2

# 項目のキーの情報
[
  [ "a",          ],
  [ "b", "d"      ],
  [ "b", "e"      ],
  [ "c", "f", "h" ],
  [ "c", "f", "i" ],
  [ "c", "g"      ]
]

# 各レコード
[
  [111, 121, 122, 131, 132, 141],
  [211, 221, 222, 231, 232, 241]
]

適当に書いたもの。

# convert.rb

def set_value(obj, item_name, value)
  current = obj
  keys = item_name.dup

  while keys.size >= 2 do
    key = keys.shift()
    current[key] ||= {}
    current = current[key]
  end

  key = keys.shift()
  current[key] = value

  obj
end

def convert(item_names, values)
  obj = {}

  item_names.zip(values)
    .each do |item_name, value|
      obj = set_value(obj, item_name, value)
    end

  obj
end

# --------------------------------

require "yaml"

item_names = [
  [ "a",          ],
  [ "b", "d"      ],
  [ "b", "e"      ],
  [ "c", "f", "h" ],
  [ "c", "f", "i" ],
  [ "c", "g"      ]
]

rows = [
  [111, 121, 122, 131, 132, 141],
  [211, 221, 222, 231, 232, 241]
]

rows.each do |values|
  obj = convert(item_names, values)
  puts YAML.dump(obj)
end

結果:

$ ruby convert.rb
---
a: 111
b:
  d: 121
  e: 122
c:
  f:
    h: 131
    i: 132
  g: 141
---
a: 211
b:
  d: 221
  e: 222
c:
  f:
    h: 231
    i: 232
  g: 241

項目ごとの処理をくりかえす部分は Enumerable#inject を使って書き直せる。

def convert_v2(item_names, values)
  item_names.zip(values)
    .inject({}) do |obj, item_name_value|
      set_value(obj, *item_name_value)
    end
end

扱っている対象が木なので、再帰で書くと良さそう?

def set_value_v2(parent, item_name, value)
  parent ||= {}
  key, *rest_keys = item_name

  parent[key] =
    if rest_keys.empty?
      value
    else
      set_value_v2(parent[key], rest_keys, value)
    end

  parent
end

*1:さらに言うと Hive の JsonSerDe なテーブルに投入したい。

*2:スプレッドシートからの読み込み部分と最後に JSON に変換する部分は本題ではないので省略。