Ruby에서 nil 값을 매핑하고 제거하는 방법


361

나는이 map중 하나 값 또는 전무로 설정을 변경한다. 그런 다음 목록에서 nil 항목을 제거하고 싶습니다. 목록을 유지할 필요는 없습니다.

이것이 내가 현재 가지고있는 것입니다 :

# A simple example function, which returns a value or nil
def transform(n)
  rand > 0.5 ? n * 10 : nil }
end

items.map! { |x| transform(x) } # [1, 2, 3, 4, 5] => [10, nil, 30, 40, nil]
items.reject! { |x| x.nil? } # [10, nil, 30, 40, nil] => [10, 30, 40]

루프를 수행하고 조건부로 다음과 같은 다른 배열로 수집 할 수 있다는 것을 알고 있습니다.

new_items = []
items.each do |x|
    x = transform(x)
    new_items.append(x) unless x.nil?
end
items = new_items

그러나 그것은 관용적 인 것처럼 보이지 않습니다. 목록에 함수를 매핑하고 nil을 제거 / 제외하는 좋은 방법이 있습니까?


3
Ruby 2.7에서는 filter_map이를 위해 완벽 해 보입니다. 처음부터 원하는대로 배열을 다시 처리 할 필요가 없습니다. 자세한 정보는 여기에 있습니다.
— SRack

답변:


21

루비 2.7+

지금 있습니다!

Ruby 2.7 filter_map은이 정확한 목적을 위해 소개 하고 있습니다. 관용적이고 성능이 뛰어나며 곧 표준이 될 것으로 기대합니다.

예를 들면 다음과 같습니다.

numbers = [1, 2, 5, 8, 10, 13]
enum.filter_map { |i| i * 2 if i.even? }
# => [4, 16, 20]

귀하의 경우 블록이 거짓으로 평가되면 간단히 다음과 같이하십시오.

items.filter_map { |x| process_x url }

" Ruby 2.7은 Enumerable # filter_map을 추가합니다 "는이 문제에 대한 초기 접근 방식 중 일부에 대한 일부 성능 벤치 마크와 함께 주제에 대해 잘 읽었습니다.

N = 1_00_000
enum = 1.upto(1_000)
Benchmark.bmbm do |x|
  x.report("select + map")  { N.times { enum.select { |i| i.even? }.map{|i| i + 1} } }
  x.report("map + compact") { N.times { enum.map { |i| i + 1 if i.even? }.compact } }
  x.report("filter_map")    { N.times { enum.filter_map { |i| i + 1 if i.even? } } }
end

# Rehearsal -------------------------------------------------
# select + map    8.569651   0.051319   8.620970 (  8.632449)
# map + compact   7.392666   0.133964   7.526630 (  7.538013)
# filter_map      6.923772   0.022314   6.946086 (  6.956135)
# --------------------------------------- total: 23.093686sec
# 
#                     user     system      total        real
# select + map    8.550637   0.033190   8.583827 (  8.597627)
# map + compact   7.263667   0.131180   7.394847 (  7.405570)
# filter_map      6.761388   0.018223   6.779611 (  6.790559)

1
좋은! 업데이트 주셔서 감사합니다 :) 일단 루비 2.7.0이 출시되면, 나는 받아 들여진 대답을 이것으로 바꾸는 것이 합리적이라고 생각합니다. 그래도 에티켓이 무엇인지 잘 모르겠습니다. 일반적으로 기존의 승인 된 응답에 업데이트 기회를 제공하는지 여부는 무엇입니까? 나는 이것이 2.7의 새로운 접근법을 참조하는 첫 번째 답변이라고 주장하고 있으므로 받아 들여 져야합니다. @ the-tin-man이 테이크에 동의하십니까?
— 피트 해밀턴

@PeterHamilton에게 감사드립니다. 의견을 보내 주셔서 감사합니다. 많은 사람들에게 유용한 정보가 되길 바랍니다. 나는 분명히 당신이 한 주장을 좋아하지만, 당신의 결정에 기뻐합니다 :)
— SRack

그렇습니다. 핵심 팀이있는 언어에 대해서는 좋은 점입니다.
— Tin Man

선택한 답변을 변경하는 것이 좋습니다. 그러나 거의 발생하지 않습니다. SO는 활동이 있었다고 말하지 않는 한 사람들과 사람들이 일반적으로 물어 본 오래된 질문을 다시 방문하지 않도록 상기시켜주는 티커를 제공하지 않습니다. 사이드 바로 서, Fruity 에서 벤치 마크를 찾아 보는 것이 좋습니다 . 훨씬 덜 까다 롭고 현명한 테스트를하기가 더 쉽기 때문입니다.
— Tin Man

930

당신은 사용할 수 있습니다 compact:

[1, nil, 3, nil, nil].compact
=> [1, 3] 

사람들에게 map블록 의 출력으로 nil을 포함하는 배열을 얻고 그 블록이 조건부로 값을 반환하려고하면 코드 냄새가 나고 논리를 다시 생각해야한다고 사람들에게 상기시키고 싶습니다 .

예를 들어, 이렇게하는 일을하는 경우 :

[1,2,3].map{ |i|
  if i % 2 == 0
    i
  end
}
# => [nil, 2, nil]

그럼 하지마 대신, 이전에 map, reject당신이 원하는하지 않거나 물건 select당신은 무엇을 하시겠습니까? :

[1,2,3].select{ |i| i % 2 == 0 }.map{ |i|
  i
}
# => [2]

나는 compact우리가 올바르게 다룰 수없는 것들을 제거하기위한 마지막 도랑 노력으로 혼란을 정리하는 것을 고려한다 . 보통 우리는 무엇이 우리에게 올 것인지 알지 못했기 때문이다. 우리는 항상 프로그램에서 어떤 종류의 데이터가 던져 지는지 알아야합니다. 예기치 않은 / 알 수없는 데이터가 잘못되었습니다. 내가 일하고있는 배열에서 nil을 볼 때마다 왜 존재하는지 파헤쳐 서 루비가 nil을 생성하는 시간을 낭비하고 배열을 통해 제거하기 위해 시간을 낭비하지 않고 배열을 생성하는 코드를 개선 할 수 있는지 확인합니다. 나중에.

'Just my $%0.2f.' % [2.to_f/100]

29
이제는 루비입니다!
— Christophe Marois

4
왜 그래야합니까? OP는 nil빈 문자열이 아닌 항목 을 제거해야합니다 . BTW nil는 빈 문자열과 동일하지 않습니다.
— Tin Man

9
두 솔루션은 컬렉션을 통해 두 번 반복 ... 왜 사용 reduce또는 inject?
— Ziggy

4
OP 질문이나 답변을 읽는 것처럼 들리지 않습니다. 문제는 배열에서 nil을 제거하는 방법입니다. compact가장 빠르지 만 실제로 처음에 코드를 올바르게 작성하면 nil을 완전히 처리 할 필요가 없습니다.
— Tin Man

3
동의하지 않습니다! 문제는 "없음 값 매핑 및 제거"입니다. 음, nil 값을 매핑하고 제거하는 것은 줄이는 것입니다. 그들의 예에서, OP는 매핑되고 nil을 선택합니다. 전화를 걸고 압축하거나 선택하고 매핑하면 같은 실수를 저지 릅니다. 답변에서 지적한 것처럼 코드 냄새입니다.
— Ziggy

96

사용해보십시오 reduce또는 inject.

[1, 2, 3].reduce([]) { |memo, i|
  if i % 2 == 0
    memo << i
  end

  memo
}

내가 허용 대답에 동의 우리가 안된다고 map하고 compact있지만 같은 이유.

나는 그 깊은 느끼는 map다음 compact에 해당 select다음을 map. 고려하십시오 : map일대일 기능입니다. 당신을 일부 값 세트에서 매핑하고있는 경우 map에, 당신은 원하는 입력 세트의 각 값에 대한 출력 설정 한 값입니다. 사전에해야한다면 select아마 map세트를 원하지 않을 것 입니다. select나중에 (또는 compact) 해야하는 경우 map세트에서 원하지 않을 것 입니다. 두 경우 모두 reduce한 번만 이동하면 전체 세트에서 두 번 반복됩니다 .

또한 영어에서는 "정수 세트를 짝수 정수 세트로 줄이기"를 시도하고 있습니다.


4
불쌍한 Ziggy, 당신의 제안에 대한 사랑이 없습니다. lol. 플러스 하나, 다른 사람은 수백 개의 공짜를 가지고 있습니다!
— DDDD

2
나는 언젠가 당신의 도움 으로이 대답이 받아 들여질 것임을 믿습니다. ^ o ^ //
— Ziggy

2
+1 현재 승인 된 답변으로는 선택 단계에서 수행 한 작업의 결과를 사용할 수 없습니다
— chees

1
수락 된 답변과 같이 통과 만 필요한 경우 열거 가능한 데이터 구조를 두 번 반복하면 낭비되는 것처럼 보입니다. 따라서 reduce를 사용하여 패스 수를 줄이십시오! 감사합니다 @Ziggy
— sebisnow

사실이야! 그러나 n 개의 요소 모음을 두 번 통과하는 것은 여전히 ​​O (n)입니다. 컬렉션이 너무 커서 캐시에 맞지 않는 한 두 번의 패스를 수행하는 것이 좋습니다 (저는 이것이 더 우아하고 표현력이 있으며 루프가 떨어지는 미래에 버그로 이어질 가능성이 낮다고 생각합니다) 동기화되지 않음). 한 번의 패스로도 일을 좋아한다면 트랜스 듀서에 대해 배우고 싶을 것입니다! github.com/cognitect-labs/transducers-ruby
— Ziggy

33

귀하의 예에서 :

items.map! { |x| process_x url } # [1, 2, 3, 4, 5] => [1, nil, 3, nil, nil]

로 바뀌지 않고 값이 변경된 것처럼 보이지 않습니다 nil. 이 경우 다음과 같습니다.

items.select{|x| process_x url}

충분할 것입니다.


27

빈 문자열과 nil을 거부하는 것과 같이 거부에 대한 느슨한 기준을 원한다면 다음을 사용할 수 있습니다.

[1, nil, 3, 0, ''].reject(&:blank?)
 => [1, 3, 0] 

더 나아가 0 값을 거부하거나 프로세스에 더 복잡한 논리를 적용하려면 블록을 전달하여 거부하십시오.

[1, nil, 3, 0, ''].reject do |value| value.blank? || value==0 end
 => [1, 3]

[1, nil, 3, 0, '', 1000].reject do |value| value.blank? || value==0 || value>10 end
 => [1, 3]

5
.공백? 레일에서만 사용할 수 있습니다.
— ewalk

나중에 참조 blank?할 수 있도록 레일에서만 사용할 수 있으므로 items.reject!(&:nil?) # [1, nil, 3, nil, nil] => [1, 3]레일에 연결되지 않은 것을 사용할 수 있습니다 . (빈 문자열이나 0은 제외하지 않음)
— Fotis

27

compact이 작업을 해결하는 가장 좋은 방법은 확실 합니다. 그러나 간단한 빼기만으로 동일한 결과를 얻을 수 있습니다.

[1, nil, 3, nil, nil] - [nil]
 => [1, 3]

4
그렇습니다. 뺄셈은 효과가 있지만 오버 헤드로 인해 절반 정도 빠릅니다.
— Tin Man

4

each_with_object 아마도 가장 깨끗한 방법입니다.

new_items = items.each_with_object([]) do |x, memo|
    ret = process_x(x)
    memo << ret unless ret.nil?
end

제 생각에는, each_with_object더 나은보다 inject/ reduce당신이 블록의 반환 값에 대해 걱정할 필요가 없기 때문에 조건의 경우입니다.


0

그것을 달성하는 또 다른 방법은 다음과 같습니다. 여기서는 Enumerable#each_with_object값을 수집하고 메소드 결과 Object#tap를 nil확인하는 데 필요한 임시 변수를 제거하는 데 사용합니다 process_x.

items.each_with_object([]) {|x, obj| (process x).tap {|r| obj << r unless r.nil?}}

예시를위한 완벽한 예 :

items = [1,2,3,4,5]
def process x
    rand(10) > 5 ? nil : x
end

items.each_with_object([]) {|x, obj| (process x).tap {|r| obj << r unless r.nil?}}

다른 접근법 :

호출하는 메소드를 보면 해당 메소드 process_x url의 입력 목적이 무엇인지 명확하지 않습니다 x. 값을 x전달 하여 값을 처리한다고 가정 하고 어떤 것이 유효하지 않은 비 결과로 처리 url되는지 결정하는 경우보다 낫습니다 .xEnumerabble.group_byEnumerable#map

h = items.group_by {|x| (process x).nil? ? "Bad" : "Good"}
#=> {"Bad"=>[1, 2], "Good"=>[3, 4, 5]}

h["Good"]
#=> [3,4,5]
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.