web-dev-qa-db-fra.com

Les premier et dernier indicateurs magiques d'une boucle Ruby/Rails?

Ruby/Rails fait beaucoup de choses intéressantes quand il s'agit de sucre, et je pense qu'il est très courant que je me demande si quelqu'un a déjà fait une aide ou quelque chose de similaire.

   a = Array.new(5, 1)

   a.each_with_index do |x, i|
     if i == 0
       print x+1
     elsif i == (a.length - 1)
       print x*10
     else
        print x
     end
   end

Pardonnez la laideur, mais cela donne ce que l’on voudrait peut-être… y at-il un moyen Ruby de faire quelque chose au début et à la fin d’une boucle?

[EDIT] Je pense que ce serait idéalement une extension sur Array avec des paramètres (instance de tableau, fonction de tous les éléments, fonction de premier élément, fonction de dernier élément) ... mais je suis ouvert à d'autres pensées.

52
aronchick

Vous pouvez saisir les premier et dernier éléments et les traiter différemment, si vous le souhaitez.

first = array.shift
last = array.pop
process_first_one
array.each { |x| process_middle_bits }
process_last_one
31
Matchu

Si le code de la première et de la dernière itération n'a rien de commun avec le code des autres itérations, vous pouvez également procéder comme suit:

do_something( a.first )
a[1..-2].each do |x|
  do_something_else( x )
end
do_something_else_else( a.last )

Si les différents cas ont du code en commun, tout va bien.

14
sepp2k

Et si vous pouviez faire ça?

%w(a b c d).each.with_position do |e, position|
  p [e, position]    # => ["a", :first]
                     # => ["b", :middle]
                     # => ["c", :middle]
                     # => ["d", :last]
end

Ou ca?

%w(a, b, c, d).each_with_index.with_position do |(e, index), position|
  p [e, index, position]    # => ["a,", 0, :first]
                            # => ["b,", 1, :middle]
                            # => ["c,", 2, :middle]
                            # => ["d", 3, :last]
end

En IRM> = 1.8.7, il suffit de ce patch-singe:

class Enumerable::Enumerator

  def with_position(&block)
    state = :init
    e = nil
    begin
      e_last = e
      e = self.next
      case state
      when :init
        state = :first
      when :first
        block.call(e_last, :first)
        state = :middle
      when :middle
        block.call(e_last, :middle)
      end
    rescue StopIteration
      case state
      when :first
        block.call(e_last, :first)
      when :middle
        block.call(e_last, :last)
      end
      return
    end while true
  end

end

Il a un petit moteur d'état car il doit anticiper une itération.

Le truc, c’est que chacun, chacun_avec_index, etc. retourne un énumérateur s'il n'y a pas de bloc. Les énumérateurs font tout ce qu'un Enumerable fait et un peu plus. Mais pour nous, l’important est que nous puissions monkey-patch Enumerator pour ajouter un moyen supplémentaire d’itérer, en "encapsulant" l’itération existante, quelle qu’elle soit.

9
Wayne Conrad

Ou un tout petit langage spécifique à un domaine:

a = [1, 2, 3, 4]

FirstMiddleLast.iterate(a) do
  first do |e|
    p [e, 'first']
  end
  middle do |e|
    p [e, 'middle']
  end
  last do |e|
    p [e, 'last']
  end
end

# => [1, "first"]
# => [2, "middle"]
# => [3, "middle"]
# => [4, "last"]

et le code qui le fait aller:

class FirstMiddleLast

  def self.iterate(array, &block)
    fml = FirstMiddleLast.new(array)
    fml.instance_eval(&block)
    fml.iterate
  end

  attr_reader :first, :middle, :last

  def initialize(array)
    @array = array
  end

  def first(&block)
    @first = block
  end

  def middle(&block)
    @middle = block
  end

  def last(&block)
    @last = block
  end

  def iterate
    @first.call(@array.first) unless @array.empty?
    if @array.size > 1
      @array[1..-2].each do |e|
        @middle.call(e)
      end
      @last.call(@array.last)
    end
  end

end

J'ai commencé à penser, "si seulement vous pouviez passer plusieurs blocs à une fonction Ruby, alors vous pourriez avoir une solution simple et astucieuse à cette question." Ensuite, j'ai réalisé que les DSL jouent de petits trucs qui ressemblent presque à passer plusieurs blocs.

7
Wayne Conrad

Comme beaucoup l'ont fait remarquer, each_with_index semble être la clé du problème. J'ai ce bloc de code que j'ai aimé.

array.each_with_index do |item,index|
  if index == 0
    # first item
  elsif index == array.length-1
    # last item
  else
    # middle items
  end
  # all items
end

Ou

array.each_with_index do |item,index|
  if index == 0
    # first item
  end
  # all items
  if index == array.length-1
    # last item
  end
end

Ou par extensions de tableau

class Array

  def each_with_position
    array.each_with_index do |item,index|
      if index == 0
        yield item, :first
      elsif index == array.length-1
        yield item, :last
      else
        yield item, :middle
      end
    end
  end

  def each_with_index_and_position
    array.each_with_index do |item,index|
      if index == 0
        yield item, index, :first
      elsif index == array.length-1
        yield item, index, :last
      else
        yield item, index, :middle
      end
    end
  end

  def each_with_position_and_index
    array.each_with_index do |item,index|
      if index == 0
        yield item, :first, index
      elsif index == array.length-1
        yield item, :last, index
      else
        yield item, :middle, index
      end
    end
  end

end
6
Nate

Si vous souhaitez ajouter du standard, vous pouvez ajouter quelque chose comme ceci à la classe array:

class Array
  def each_fl
    each_with_index do |x,i|
      yield [i==0 ? :first : (i==length-1 ? :last : :inner), x]
    end
  end
end

et ensuite, partout où vous en avez besoin, vous obtenez la syntaxe suivante:

[1,2,3,4].each_fl do |t,x|
  case t
    when :first
      puts "first: #{x}"
    when :last
      puts "last: #{x}"
    else
      puts "otherwise: #{x}"
  end
end

pour la sortie suivante:

first: 1
otherwise: 2
otherwise: 3
last: 4
5
clord

Il n'y a pas de syntaxe "faire ceci la (première | dernière) fois" en Ruby. Mais si vous cherchez de la concision, vous pouvez le faire:

a.each_with_index do |x, i|
  print (i > 0 ? (i == a.length - 1 ? x*10 : x) : x+1)
end

Le résultat est ce que vous attendez:

irb(main):001:0> a = Array.new(5,1)
=> [1, 1, 1, 1, 1]
irb(main):002:0> a.each_with_index do |x,i|
irb(main):003:1*   puts (i > 0 ? (i == a.length - 1 ? x*10 : x) : x+1)
irb(main):004:1> end
2
1
1
1
10
3
John Feminella

Question intéressante, et à laquelle j'ai aussi réfléchi un peu.

Je pense que vous devez créer trois blocs/procs/quel que soit leur nom, puis créer une méthode qui appelle le bon bloc/proc/quoique ce soit. (Désolé pour le vague - je ne suis pas encore un métaprogrammeur de ceinture noire) [Edit: cependant, j'ai copié de quelqu'un qui est en bas)

class FancyArray
  def initialize(array)
    @boring_array = array
    @first_code = nil
    @main_code = nil
    @last_code = nil
  end

  def set_first_code(&code)
    @first_code = code
  end

  def set_main_code(&code)
    @main_code = code
  end

  def set_last_code(&code)
    @last_code = code
  end

  def run_fancy_loop
    @boring_array.each_with_index do |item, i|
      case i
      when 0 then @first_code.call(item)
      when @boring_array.size - 1 then @last_code.call(item)
      else @main_code.call(item)
      end
    end
  end
end

fancy_array = FancyArray.new(["Matti Nykanen", "Erik Johnsen", "Michael Edwards"])
fancy_array.set_first_code {|item| puts "#{item} came first in ski jumping at the 1988 Winter Olympics"}
fancy_array.set_main_code {|item| puts "#{item} did not come first or last in ski jumping at the 1988 Winter Olympics"}
fancy_array.set_last_code {|item| puts "#{item} came last in ski jumping at the 1988 Winter Olympics"}
fancy_array.run_fancy_loop

produit

Matti Nykanen came first in ski jumping at the 1988 Winter Olympics
Erik Johnsen did not come first or last in ski jumping at the 1988 Winter Olympics
Michael Edwards came last in ski jumping at the 1988 Winter Olympics

Edit: La réponse de Svante (avec la suggestion de molf) à une question connexe montre comment passer de multiples blocs de code à une seule méthode:

class FancierArray < Array
  def each_with_first_last(first_code, main_code, last_code)
    each_with_index do |item, i|
      case i
        when 0 then first_code.call(item)
        when size - 1 then last_code.call(item)
        else main_code.call(item)
      end
    end
  end
end

fancier_array = FancierArray.new(["Matti Nykanen", "Erik Johnsen", "Michael Edwards"])
fancier_array.each_with_first_last(
  lambda {|person| puts "#{person} came first in ski jumping at the 1988 Winter Olympics"},
  lambda {|person| puts "#{person} did not come first or last in ski jumping at the 1988 Winter Olympics"},
  lambda {|person| puts "#{person} came last in ski jumping at the 1988 Winter Olympics"})
3
Andrew Grimm

KISS

arr.each.with_index do |obj, index|
  p 'first' if index == 0        
  p 'last' if index == arr.count-1                  
end
2
Sagiv Ofek

J'avais besoin de cette fonctionnalité de temps en temps, alors j'ai créé une petite classe pour cela.

La dernière version est à: https://Gist.github.com/3823837

Échantillon:

("a".."m").to_a.each_pos do |e|
  puts "Char\tfirst?\tlast?\tprev\tnext\twrapped?\tindex\tposition" if e.first?
  print "#{e.item}\t"
  print "#{e.first?}\t"
  print "#{e.last?}\t"
  print "#{e.prev}\t"
  print "#{e.next}\t"
  print "#{e.wrapped?}\t\t"
  print "#{e.index}\t"
  puts  "#{e.position}\t"
end

# Char  first?  last?  prev  next  wrapped?  index  position
# a     true    false        b     false     0      1
# b     false   false  a     c     true      1      2
# c     false   false  b     d     true      2      3
# d     false   false  c     e     true      3      4
# e     false   false  d     f     true      4      5
# f     false   false  e     g     true      5      6
# g     false   false  f     h     true      6      7
# h     false   false  g     i     true      7      8
# i     false   false  h     j     true      8      9
# j     false   false  i     k     true      9      10
# k     false   false  j     l     true      10     11
# l     false   false  k     m     true      11     12
# m     false   true   l           false     12     13



{
  a: "0",
  b: "1",
  c: "2",
  d: "3",
  e: "4",
  f: "5",
  g: "6",
  h: "7",
  i: "8",
  j: "9",
  k: "10",
  l: "11",
  m: "12",
}.each_pos do |(k, v), e|
  puts "KV\tChar\t\tfirst?\tlast?\tprev\t\tnext\t\twrapped?\tindex\tposition" if e.first?
  print "#{k} => #{v}\t"
  print "#{e.item}\t"
  print "#{e.first?}\t"
  print "#{e.last?}\t"
  print "#{e.prev || "\t"}\t"
  print "#{e.next || "\t"}\t"
  print "#{e.wrapped?}\t\t"
  print "#{e.index}\t"
  puts  "#{e.position}\t"
end

# KV      Char        first?  last?   prev        next        wrapped?  index position
# a => 0  [:a, "0"]   true    false               [:b, "1"]   false     0     1
# b => 1  [:b, "1"]   false   false   [:a, "0"]   [:c, "2"]   true      1     2
# c => 2  [:c, "2"]   false   false   [:b, "1"]   [:d, "3"]   true      2     3
# d => 3  [:d, "3"]   false   false   [:c, "2"]   [:e, "4"]   true      3     4
# e => 4  [:e, "4"]   false   false   [:d, "3"]   [:f, "5"]   true      4     5
# f => 5  [:f, "5"]   false   false   [:e, "4"]   [:g, "6"]   true      5     6
# g => 6  [:g, "6"]   false   false   [:f, "5"]   [:h, "7"]   true      6     7
# h => 7  [:h, "7"]   false   false   [:g, "6"]   [:i, "8"]   true      7     8
# i => 8  [:i, "8"]   false   false   [:h, "7"]   [:j, "9"]   true      8     9
# j => 9  [:j, "9"]   false   false   [:i, "8"]   [:k, "10"]  true      9     10
# k => 10 [:k, "10"]  false   false   [:j, "9"]   [:l, "11"]  true      10    11
# l => 11 [:l, "11"]  false   false   [:k, "10"]  [:m, "12"]  true      11    12
# m => 12 [:m, "12"]  false   true    [:l, "11"]              false     12    13

Classe actuelle:

module Enumerable
  # your each_with_position method
  def each_pos &block
    EachWithPosition.each(self, &block)
  end
end

class EachWithPosition
  attr_reader :index

  class << self
    def each *a, &b
      handler = self.new(*a, :each, &b)
    end
  end

  def initialize collection, method, &block
    @index = 0
    @item, @prev, @next = nil
    @collection = collection
    @callback = block
    self.send(method)
  end

  def count
    @collection.count
  end
  alias_method :length, :count
  alias_method :size, :count

  def rest
    count - position
  end

  def first?
    @index == 0
  end

  def last?
    @index == (count - 1)
  end

  def wrapped?
    !first? && !last?
  end
  alias_method :inner?, :wrapped?

  def position
    @index + 1
  end

  def prev
    @prev
  end

  def next
    @next
  end

  def current
    @item
  end
  alias_method :item, :current
  alias_method :value, :current

  def call
    if @callback.arity == 1
      @callback.call(self)
    else
      @callback.call(@item, self)
    end
  end

  def each
    @collection.each_cons(2) do |e, n|
      @prev = @item
      @item = e
      @next = n

      self.call
      @index += 1

      # fix cons slice behaviour
      if last?
        @prev, @item, @next = @item, @next, nil
        self.call
        @index += 1
      end
    end
  end
end
2
2called-chaos

Si ça ne vous dérange pas que la "dernière" action se passe avant le contenu du milieu, alors ce patch singe

class Array

  def for_first
    return self if empty?
    yield(first)
    self[1..-1]
  end

  def for_last
    return self if empty?
    yield(last)
    self[0...-1]
  end

end

Permet ceci:

%w(a b c d).for_first do |e|
  p ['first', e]
end.for_last do |e|
  p ['last', e]
end.each do |e|
  p ['middle', e]
end

# => ["first", "a"]
# => ["last", "d"]
# => ["middle", "b"]
# => ["middle", "c"]
1
Wayne Conrad

Je n'ai pas pu résister :) Ce n'est pas réglé pour la performance, bien que je suppose qu'il ne devrait pas être beaucoup plus lent que la plupart des autres réponses ici Tout tourne autour du sucre!

class Array
  class EachDSL
    attr_accessor :idx, :max

    def initialize arr
      self.max = arr.size
    end

    def pos
      idx + 1
    end

    def inside? range
      range.include? pos
    end

    def nth? i
      pos == i
    end

    def first?
      nth? 1
    end

    def middle?
      not first? and not last?
    end

    def last?
      nth? max
    end

    def inside range
      yield if inside? range
    end

    def nth i
      yield if nth? i
    end

    def first
      yield if first?
    end

    def middle
      yield if middle?
    end

    def last
      yield if last?
    end
  end

  def each2 &block
    dsl = EachDSL.new self
    each_with_index do |x,i|
      dsl.idx = i
      dsl.instance_exec x, &block
    end
  end
end

Exemple 1:

[1,2,3,4,5].each2 do |x|
  puts "#{x} is first"  if first?
  puts "#{x} is third"  if nth? 3
  puts "#{x} is middle" if middle?
  puts "#{x} is last"   if last?
  puts
end

# 1 is first
# 
# 2 is middle
# 
# 3 is third
# 3 is middle
# 
# 4 is middle
# 
# 5 is last

Exemple 2:

%w{some short simple words}.each2 do |x|
  first do
    puts "#{x} is first"
  end

  inside 2..3 do
    puts "#{x} is second or third"
  end

  middle do
    puts "#{x} is middle"
  end

  last do
    puts "#{x} is last"
  end
end

# some is first
# short is second or third
# short is middle
# simple is second or third
# simple is middle
# words is last
1
Patrick Oscity

Je vois ici beaucoup de hacks assez proches, mais qui dépendent tous fortement du fait que l’itérateur donné a une taille fixe et ne soit PAS un itérateur. Je voudrais également proposer de sauvegarder l'élément précédent lorsque vous parcourez le système pour connaître le premier/dernier élément parcouru. 

previous = {}
elements.each do |element|
  unless previous.has_key?(:element)
    # will only execute the first time
  end

  # normal each block here

  previous[:element] = element
end

# the last element will be stored in previous[:element] 
0
ted

Partitionnez le tableau en plages où les éléments de chaque plage sont supposés se comporter différemment. Mappez chaque plage ainsi créée à un bloc.

class PartitionEnumerator
    include RangeMaker

    def initialize(array)
        @array = array
        @handlers = {}
    end

    def add(range, handler)
        @handlers[range] = handler
    end

    def iterate
        @handlers.each_pair do |range, handler|
          @array[range].each { |value| puts handler.call(value) }
        end
    end
end

Pourrait créer des plages à la main, mais ces aides ci-dessous facilitent les choses:

module RangeMaker
  def create_range(s)
    last_index = @array.size - 1
    indexes = (0..last_index)
    return (indexes.first..indexes.first) if s == :first
    return (indexes.second..indexes.second_last) if s == :middle
    return (indexes.last..indexes.last) if s == :last
  end  
end

class Range
  def second
    self.first + 1
  end

  def second_last
    self.last - 1
  end
end

Usage:

a = [1, 2, 3, 4, 5, 6]

e = PartitionEnumerator.new(a)
e.add(e.create_range(:first), Proc.new { |x| x + 1 } )
e.add(e.create_range(:middle), Proc.new { |x| x * 10 } )
e.add(e.create_range(:last), Proc.new { |x| x } )

e.iterate
0
Anurag