55 votes

Indicateur magique de premier et de dernier dans une boucle en Ruby/Rails ?

Ruby/Rails fait beaucoup de choses cool quand il s'agit de sucre pour les choses de base, et je pense qu'il y a un scénario très commun pour lequel je me demandais si quelqu'un avait fait un helper 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 permet d'atteindre ce que l'on pourrait vouloir... existe-t-il un moyen en ruby de faire quelque chose au début et à la fin d'une boucle ?

[EDIT] Je pense que l'idéal serait une extension de Array avec des paramètres (instance de array, fonction all elements, fonction first elements, fonction last elements)... mais je suis ouvert à d'autres idées.

31voto

Matchu Points 37755

Vous pourriez prendre le premier et le dernier élément 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

14voto

sepp2k Points 157757

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

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 un certain code en commun, votre façon de faire est bonne.

9voto

Wayne Conrad Points 31052

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 ça ?

%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

Dans MRI >= 1.8.7, il suffit de ce monkey-patch :

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 parce qu'il doit regarder en avant une itération.

L'astuce est que each, each_with_index, &c. renvoient un Enumerator s'ils ne reçoivent pas de bloc. Les Enumerators font tout ce que fait un Enumerable et un peu plus. Mais pour nous, l'important est que nous pouvons modifier l'Enumerator pour ajouter un moyen supplémentaire d'itérer, en "enveloppant" l'itération existante, quelle qu'elle soit.

7voto

Wayne Conrad Points 31052

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 fonctionner :

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 on pouvait passer plusieurs blocs à une fonction Ruby, alors on pourrait avoir une solution simple et astucieuse à cette question." Puis j'ai réalisé que les DSL jouent des petits tours qui sont presque comme passer plusieurs blocs.

5voto

clord Points 117

Si vous êtes prêt à ajouter un peu de texte passe-partout, 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 le résultat suivant :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X