Techioz Blog

すべての兄弟キーを使用して Ruby でハッシュを埋めるか完成させる

概要

Rails API を使用して車両をクエリし、さまざまな属性ごとに数を数えてグループ化することができます。 group by を使用する場合、応答にゼロ値を入力したいと考えています。

簡単な例を次に示します。

data = {
  AUDI: {
    Petrol: 379,
    Diesel: 326,
    Electric: 447
  },
  TESLA: {
    Electric: 779
  }
}

テスラにはガソリン車やディーゼル車がないため、そのキーは応答に含まれていません。これは、結果にゼロカウントが含まれない postgres の group_by の結果だと思います。

これらの欠落したゼロ値でハッシュを「埋める」ことができる関数を作成しようとしています。

fill_hash(data)

返すべきです

data = {
  AUDI: {
    Petrol: 379,
    Diesel: 326,
    Electric: 447
  },
  TESLA: {
    Petrol: 0,
    Diesel: 0,
    Electric: 779
  }
}

この単純なケースでは、次の 3 つの方法を使用しました。

def collect_keys(hash, keys = [])
  hash.each do |_, value|
    if value.is_a?(Hash)
      keys.concat(value.keys)
      collect_keys(value, keys)
    end
  end
  keys.uniq
end

def fill_missing_keys(hash, all_keys, default_value = 0)
  hash.each do |_, value|
    if value.is_a?(Hash)
      all_keys.each do |k|
        value[k] = default_value unless value.key?(k)
      end
      fill_missing_keys(value, all_keys, default_value)
    end
  end
  hash
end

def fill_hash(hash, default_value = 0)
  all_keys = collect_keys(hash)
  fill_missing_keys(hash, all_keys, default_value)
end

# Example usage:
hash = { a: { x: 1 }, b: { y: 1 } }
filled_hash = complete_hash(hash)
puts filled_hash
# Output should be: {:a=>{:x=>1, :y=>0}, :b=>{:y=>1, :x=>0}}

私の問題は、ハッシュがより複雑になる可能性があることです。単純なケースでは 2 つの属性でグループ化するだけでしたが、ここでは 3 つの属性でグループ化する例を示します。

{
  AUDI: {
    Deregistered: {
      Diesel: 56
    },
    Registered: {
      Petrol: 379,
      Diesel: 270,
      Electric: 447
    }
  },
  TESLA: {
    Registered: {
      Electric: 779
    }
  }
}

私の希望する出力は次のとおりです。

{
  AUDI: {
    Deregistered: {
      Petrol: 0,
      Diesel: 56,
      Electric: 0
    },
    Registered: {
      Petrol: 379,
      Diesel: 270,
      Electric: 447
    }
  },
  TESLA: {
    Deregistered: {
      Petrol: 0,
      Diesel: 0,
      Electric: 0
    },
    Registered: {
      Petrol: 0,
      Diesel: 0,
      Electric: 779
    }
  }
}

解決策

# This will build a "Default" Hash
# for Example: 
# {:AUDI=>{:Petrol=>0, :Diesel=>0, :Electric=>0},
#  :TESLA=>{:Petrol=>0, :Diesel=>0, :Electric=>0}}
def build_default_hash(obj, default_value: 0)
  return obj.transform_values {default_value} unless obj.values.first.is_a?(Hash)
  m = obj.values.reduce(&:deep_merge)
  obj.keys.product([build_default_hash(m, default_value:)]).to_h
end 

# deep_merge the default hash with the existing Hash
def deep_normalize_hash(h, default_value:0) 
  build_default_hash(h, default_value:).deep_merge(h)
end
h = {
  AUDI: {
    Deregistered: {
      Diesel: 56
    },
    Registered: {
      Petrol: 379,
      Diesel: 270,
      Electric: 447
    }
  },
  TESLA: {
    Registered: {
      Electric: 779
    }
  }
}
deep_normalize_hash(h)
#=> {:AUDI=>
#     {:Deregistered=>{:Diesel=>56, :Petrol=>0, :Electric=>0},
#      :Registered=>{:Diesel=>270, :Petrol=>379, :Electric=>447}},
#    :TESLA=>
#     {:Deregistered=>{:Diesel=>0, :Petrol=>0, :Electric=>0},
#      :Registered=>{:Diesel=>0, :Petrol=>0, :Electric=>779}}}

これを Hash クラスに簡単にブレンドして、使用方法を h.deep_normalize にすることもできます。一般的にはそうすることはお勧めしませんが、これが Rails コンテキスト内にあることを考えると、コア クラスの操作がもう少し多くても誰も気付かないでしょう。