Is er in Ruby een Array-methode die ‘select’ en ‘map’ combineert?

Ik heb een Ruby-array met enkele tekenreekswaarden. Ik moet:

  1. Zoek alle elementen die overeenkomen met een predikaat
  2. Laat de overeenkomende elementen door een transformatie lopen
  3. Retourneer de resultaten als een array

Op dit moment ziet mijn oplossing er als volgt uit:

def example
  matchingLines = @lines.select{ |line| ... }
  results = matchingLines.map{ |line| ... }
  return results.uniq.sort
end

Is er een Array- of Enumerable-methode die select en map combineert in een enkele logische instructie?


Antwoord 1, autoriteit 100%

Ik gebruik meestal mapen compactsamen met mijn selectiecriteria als een postfix if. compacthaalt de nulpunten weg.

jruby-1.5.0 > [1,1,1,2,3,4].map{|n| n*3 if n==1}    
 => [3, 3, 3, nil, nil, nil] 
jruby-1.5.0 > [1,1,1,2,3,4].map{|n| n*3 if n==1}.compact
 => [3, 3, 3] 

Antwoord 2, autoriteit 55%

Ruby 2.7+

Er is nu!

Ruby 2.7 introduceert filter_mapvoor precies dit doel. Het is idiomatisch en performant, en ik verwacht dat het heel snel de norm zal worden.

Bijvoorbeeld:

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

Hier is een goed gelezen over de onderwerp.

Ik hoop dat iemand er iets aan heeft!


Antwoord 3, autoriteit 46%

U kunt hiervoor reducegebruiken, waarvoor slechts één pas nodig is:

[1,1,1,2,3,4].reduce([]) { |a, n| a.push(n*3) if n==1; a }
=> [3, 3, 3] 

Met andere woorden, initialiseer de status zodat deze is wat u wilt (in ons geval een lege lijst om in te vullen: []), en zorg er dan altijd voor dat u deze waarde retourneert met wijzigingen voor elk element in de originele lijst (in ons geval het gewijzigde element naar de lijst gepusht).

Dit is het meest efficiënt omdat het de lijst slechts met één doorgang doorloopt (map+ selectof compactvereist twee doorgangen).

In jouw geval:

def example
  results = @lines.reduce([]) do |lines, line|
    lines.push( ...(line) ) if ...
    lines
  end
  return results.uniq.sort
end

Antwoord 4, autoriteit 18%

Een andere manier om dit te benaderen is het gebruik van de nieuwe (ten opzichte van deze vraag) Enumerator::Lazy:

def example
  @lines.lazy
        .select { |line| line.property == requirement }
        .map    { |line| transforming_method(line) }
        .uniq
        .sort
end

De .lazymethode retourneert een luie enumerator. Het aanroepen van .selectof .mapop een luie enumerator levert een andere luie enumerator op. Pas als je .uniqaanroept, wordt de enumerator daadwerkelijk geforceerd en wordt een array geretourneerd. Dus wat er effectief gebeurt, is dat uw .selecten .map-aanroepen worden gecombineerd tot één – u herhaalt slechts eenmaal @linesom beide .selecten .map.

Mijn instinct is dat Adams reduce-methode iets sneller zal zijn, maar ik denk dat dit veel leesbaarder is.


Het primaire gevolg hiervan is dat er geen intermediaire array-objecten worden gemaakt voor elke volgende methode-oproep. In een normale @lines.select.mapsituatie, selectretourneert een array die vervolgens wordt gewijzigd door map, opnieuw een array retourneren. Ter vergelijking, de luie evaluatie creëert slechts eenmaal een array. Dit is handig wanneer uw eerste verzamelobject groot is. Het machtigt je ook om met oneindige makelaars te werken – b.v. random_number_generator.lazy.select(&:odd?).take(10).


Antwoord 5

Probeer mijn bibliotheek Rearmed Rubyte gebruiken waarin ik de methode Enumerable#select_map. Hier is een voorbeeld:

items = [{version: "1.1"}, {version: nil}, {version: false}]
items.select_map{|x| x[:version]} #=> [{version: "1.1"}]
# or without enumerable monkey patch
Rearmed.select_map(items){|x| x[:version]}

Antwoord 6

Als je geen twee verschillende arrays wilt maken, kun je compact!gebruiken, maar wees er voorzichtig mee.

array = [1,1,1,2,3,4]
new_array = map{|n| n*3 if n==1}
new_array.compact!

Interessant is dat compact!een in-place verwijdering van nul doet. De geretourneerde waarde van compact!is dezelfde array als er wijzigingen waren, maar nul als er geen nulwaarden waren.

array = [1,1,1,2,3,4]
new_array = map{|n| n*3 if n==1}.tap { |array| array.compact! }

Zou een oneliner zijn.


Antwoord 7

Jouw versie:

def example
  matchingLines = @lines.select{ |line| ... }
  results = matchingLines.map{ |line| ... }
  return results.uniq.sort
end

Mijn versie:

def example
  results = {}
  @lines.each{ |line| results[line] = true if ... }
  return results.keys.sort
end

Dit zal 1 iteratie doen (behalve de sortering), en heeft de toegevoegde bonus dat het uniek blijft (als u niet om uniq geeft, maak dan de resultaten gewoon een array en results.push(line) if ...


Antwoord 8

Hier is een voorbeeld. Het is niet hetzelfde als uw probleem, maar het kan zijn wat u wilt, of het kan een aanwijzing zijn voor uw oplossing:

def example
  lines.each do |x|
    new_value = do_transform(x)
    if new_value == some_thing
      return new_value    # here jump out example method directly.
    else
      next                # continue next iterate.
    end
  end
end

Other episodes