code

루비 스타일 : 중첩 된 해시 요소가 있는지 확인하는 방법

codestyles 2020. 12. 29. 07:07
반응형

루비 스타일 : 중첩 된 해시 요소가 있는지 확인하는 방법


해시에 저장된 "사람"을 고려하십시오. 두 가지 예는 다음과 같습니다.

fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}} 

"person"에 자식이 없으면 "children"요소가 없습니다. 따라서 Mr. Slate의 경우 부모가 있는지 확인할 수 있습니다.

slate_has_children = !slate[:person][:children].nil?

그렇다면 "슬레이트"가 "사람"해시라는 사실을 모른다면 어떨까요? 중히 여기다:

dino = {:pet => {:name => "Dino"}}

더 이상 어린이를 쉽게 확인할 수 없습니다.

dino_has_children = !dino[:person][:children].nil?
NoMethodError: undefined method `[]' for nil:NilClass

그렇다면 특히 해시가 깊이 중첩 된 경우 (여기에 제공된 예제보다 더 깊이) 해시의 구조를 어떻게 확인할 수 있습니까? 더 나은 질문은 다음과 같습니다.이 작업을 수행하는 "루비 방식"은 무엇입니까?


이를 수행하는 가장 확실한 방법은 단순히 각 단계를 확인하는 것입니다.

has_children = slate[:person] && slate[:person][:children]

.nil 사용? 자리 표시 자 값으로 false를 사용할 때만 실제로 필요하며 실제로는 드뭅니다. 일반적으로 단순히 존재하는지 테스트 할 수 있습니다.

업데이트 : Ruby 2.3 이상을 사용하는 dig경우이 답변에 설명 된 작업을 수행 하는 기본 제공 메서드가 있습니다.

그렇지 않은 경우이를 상당히 단순화 할 수있는 자체 Hash "dig"메서드를 정의 할 수도 있습니다.

class Hash
  def dig(*path)
    path.inject(self) do |location, key|
      location.respond_to?(:keys) ? location[key] : nil
    end
  end
end

이 메서드는 각 단계를 확인하고 nil 호출에 대한 트립을 방지합니다. 얕은 구조의 경우 유틸리티는 다소 제한적이지만 깊이 중첩 된 구조의 경우 매우 유용합니다.

has_children = slate.dig(:person, :children)

예를 들어 : children 항목이 실제로 채워 졌는지 테스트하는 등 더 강력하게 만들 수도 있습니다.

children = slate.dig(:person, :children)
has_children = children && !children.empty?

Ruby 2.3에서는 안전한 탐색 연산자를 지원합니다 : https://www.ruby-lang.org/en/news/2015/11/11/ruby-2-3-0-preview1-released/

has_children 이제 다음과 같이 작성할 수 있습니다.

has_children = slate[:person]&.[](:children)

dig 다음과 같이 추가됩니다.

has_children = slate.dig(:person, :children)

또 다른 대안 :

dino.fetch(:person, {})[:children]

andandgem을 사용할 수 있습니다 .

require 'andand'

fred[:person].andand[:children].nil? #=> false
dino[:person].andand[:children].nil? #=> true

http://andand.rubyforge.org/ 에서 자세한 설명을 찾을 수 있습니다 .


기본값 {}-빈 해시로 해시를 사용할 수 있습니다. 예를 들면

dino = Hash.new({})
dino[:pet] = {:name => "Dino"}
dino_has_children = !dino[:person][:children].nil? #=> false

이미 생성 된 Hash에서도 작동합니다.

dino = {:pet=>{:name=>"Dino"}}
dino.default = {}
dino_has_children = !dino[:person][:children].nil? #=> false

또는 nil 클래스에 대해 [] 메소드를 정의 할 수 있습니다.

class NilClass
  def [](* args)
     nil
   end
end

nil[:a] #=> nil

전통적으로 다음과 같은 작업을 수행해야했습니다.

structure[:a] && structure[:a][:b]

그러나 Ruby 2.3에는 이러한 방식을보다 우아하게 만드는 기능이 추가되었습니다.

structure.dig :a, :b # nil if it misses anywhere along the way

ruby_dig당신을 위해 이것을 백 패치 할 보석 이 있습니다.


def flatten_hash(hash)
  hash.each_with_object({}) do |(k, v), h|
    if v.is_a? Hash
      flatten_hash(v).map do |h_k, h_v|
        h["#{k}_#{h_k}"] = h_v
      end
    else
      h[k] = v
    end
  end
end

irb(main):012:0> fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
=> {:person=>{:name=>"Fred", :spouse=>"Wilma", :children=>{:child=>{:name=>"Pebbles"}}}}

irb(main):013:0> slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}}
=> {:person=>{:name=>"Mr. Slate", :spouse=>"Mrs. Slate"}}

irb(main):014:0> flatten_hash(fred).keys.any? { |k| k.include?("children") }
=> true

irb(main):015:0> flatten_hash(slate).keys.any? { |k| k.include?("children") }
=> false

이것은 모든 해시를 하나로 합친 다음 어떤 것입니까? 하위 문자열 "children"과 일치하는 키가 있으면 true를 반환합니다. 이것은 또한 도움이 될 수 있습니다.


dino_has_children = !dino.fetch(person, {})[:children].nil?

레일에서 다음을 수행 할 수도 있습니다.

dino_has_children = !dino[person].try(:[], :children).nil?   # 

다음은 Ruby Hash 클래스를 몽키 패치하지 않고 해시 및 중첩 된 해시의 잘못된 값을 심층 검사 할 수있는 방법입니다 (Ruby 클래스에 몽키 패치를 적용하지 마십시오. .

(Rails를 가정하면 Rails 외부에서 작동하도록 쉽게 수정할 수 있습니다.)

def deep_all_present?(hash)
  fail ArgumentError, 'deep_all_present? only accepts Hashes' unless hash.is_a? Hash

  hash.each do |key, value|
    return false if key.blank? || value.blank?
    return deep_all_present?(value) if value.is_a? Hash
  end

  true
end

여기에서 위의 답변을 단순화하십시오.

다음과 같이 값이 nil 일 수없는 Recursive Hash 메서드를 만듭니다.

def recursive_hash
  Hash.new {|key, value| key[value] = recursive_hash}
end

> slate = recursive_hash 
> slate[:person][:name] = "Mr. Slate"
> slate[:person][:spouse] = "Mrs. Slate"

> slate
=> {:person=>{:name=>"Mr. Slate", :spouse=>"Mrs. Slate"}}
slate[:person][:state][:city]
=> {}

키에 대한 값이 존재하지 않으면 빈 해시를 만들어도 괜찮다면 :)


당신은 함께 놀 수 있습니다

dino.default = {}

또는 예 :

empty_hash = {}
empty_hash.default = empty_hash

dino.default = empty_hash

그렇게하면

empty_hash[:a][:b][:c][:d][:e] # and so on...
dino[:person][:children] # at worst it returns {}

주어진

x = {:a => {:b => 'c'}}
y = {}

다음 같이 xy를 확인할 수 있습니다 .

(x[:a] || {})[:b] # 'c'
(y[:a] || {})[:b] # nil

대답을 위해 @tadman을 Thks.

perfs를 원하고 (그리고 ruby ​​<2.3으로 고착 된)이 방법은 2.5 배 더 빠릅니다 :

unless Hash.method_defined? :dig
  class Hash
    def dig(*path)
      val, index, len = self, 0, path.length
      index += 1 while(index < len && val = val[path[index]])
      val
    end
  end
end

and if you use RubyInline, this method is 16x faster:

unless Hash.method_defined? :dig
  require 'inline'

  class Hash
    inline do |builder|
      builder.c_raw '
      VALUE dig(int argc, VALUE *argv, VALUE self) {
        rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
        self = rb_hash_aref(self, *argv);
        if (NIL_P(self) || !--argc) return self;
        ++argv;
        return dig(argc, argv, self);
      }'
    end
  end
end

You can also define a module to alias the brackets methods and use the Ruby syntax to read/write nested elements.

UPDATE: Instead of overriding the bracket accessors, request Hash instance to extend the module.

module Nesty
  def []=(*keys,value)
    key = keys.pop
    if keys.empty? 
      super(key, value) 
    else
      if self[*keys].is_a? Hash
        self[*keys][key] = value
      else
        self[*keys] = { key => value}
      end
    end
  end

  def [](*keys)
    self.dig(*keys)
  end
end

class Hash
  def nesty
    self.extend Nesty
    self
  end
end

Then you can do:

irb> a = {}.nesty
=> {}
irb> a[:a, :b, :c] = "value"
=> "value"
irb> a
=> {:a=>{:b=>{:c=>"value"}}}
irb> a[:a,:b,:c]
=> "value"
irb> a[:a,:b]
=> {:c=>"value"}
irb> a[:a,:d] = "another value"
=> "another value"
irb> a
=> {:a=>{:b=>{:c=>"value"}, :d=>"another value"}}

I don't know how "Ruby" it is(!), but the KeyDial gem which I wrote lets you do this basically without changing your original syntax:

has_kids = !dino[:person][:children].nil?

becomes:

has_kids = !dino.dial[:person][:children].call.nil?

This uses some trickery to intermediate the key access calls. At call, it will try to dig the previous keys on dino, and if it hits an error (as it will), returns nil. nil? then of course returns true.


You can use a combination of & and key? it is O(1) compared to dig which is O(n) and this will make sure person is accessed without NoMethodError: undefined method `[]' for nil:NilClass

fred[:person]&.key?(:children) //=>true
slate[:person]&.key?(:children)

ReferenceURL : https://stackoverflow.com/questions/1820451/ruby-style-how-to-check-whether-a-nested-hash-element-exists

반응형