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#tapnil확인하는 데 필요한 임시 변수를 제거하는 데 사용합니다 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.