Волшебный первый и последний индикатор в цикле в Ruby / Rails? - PullRequest
55 голосов
/ 11 февраля 2010

Ruby / Rails делает много крутых вещей, когда дело доходит до сахара для базовых вещей, и я думаю, что есть очень распространенный сценарий, который меня интересует, если кто-то сделал помощника или что-то подобное для.

   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

Прошу прощения за уродство, но это дает то, что вы могли бы хотеть ... есть ли рубиновый способ сделать что-то с первым и последним из цикла?

[EDIT] Я думаю, в идеале это было бы расширение для Array с параметрами (экземпляр массива, функция всех элементов, функция первых элементов, функция последних элементов) ... но я открыт для других мысли.

Ответы [ 16 ]

31 голосов
/ 11 февраля 2010

Вы можете захватить первый и последний элементы и обрабатывать их по-разному, если хотите.

first = array.shift
last = array.pop
process_first_one
array.each { |x| process_middle_bits }
process_last_one
14 голосов
/ 11 февраля 2010

Если код для первой и последней итерации не имеет ничего общего с кодом для других итераций, вы также можете сделать:

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

Если в разных случаях есть какой-то общий код, ваш путь в порядке.

9 голосов
/ 11 февраля 2010

Что если бы ты мог сделать это?

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

Или это?

%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

В МРТ> = 1.8.7, все, что нужно, это патч обезьяны:

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

У него небольшой движок состояний, потому что он должен смотреть вперед на одну итерацию.

Хитрость заключается в том, что каждый, каждый с индексом и т. Д. вернуть перечислитель, если не дано никакого блока. Перечислители делают все, что делает Enumerable, и немного больше. Но для нас важно то, что мы можем использовать Enumerator-патч для добавления еще одного способа итерации, «оборачивая» существующую итерацию, какой бы она ни была.

7 голосов
/ 11 февраля 2010

Или крошечный маленький домен-специфический язык:

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"]

и код, который заставляет его идти:

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

Я начал думать: «Если бы вы только могли передать несколько блоков в функцию Ruby, у вас было бы простое и простое решение этого вопроса» Затем я понял, что DSL разыгрывают маленькие трюки, которые почти как прохождение нескольких блоков.

6 голосов
/ 11 февраля 2014

Как отмечали многие, ключ к этому, похоже, each_with_index. У меня есть этот блок кода, который мне понравился.

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

Или

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

Или по расширениям Array

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
5 голосов
/ 11 февраля 2010

Если вы хотите добавить шаблон, вы можете добавить что-то вроде этого в класс массива:

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

и тогда, где вам нужно, вы получите следующий синтаксис:

[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

для следующего вывода:

first: 1
otherwise: 2
otherwise: 3
last: 4
3 голосов
/ 11 февраля 2010

Интересный вопрос, о котором я тоже немного подумал.

Я думаю, вам нужно создать три разных блока / procs / как бы они ни назывались, а затем создать метод, который вызывает правильный блок / proc / что угодно. (Извините за неопределенность - я еще не метапрограммист черного пояса) [ Редактировать : однако я скопировал с того, кто внизу)

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

производит

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

Редактировать : * Сванте ответ (с предложением Мольфа) на связанный вопрос показывает, как передать несколько блоков кода в один метод:

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 голосов
/ 11 февраля 2010

В Ruby нет синтаксиса "сделать это (первый | последний) раз". Но если вы ищете краткость, вы можете сделать это:

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

Результат, который вы ожидаете:

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
2 голосов
/ 18 декабря 2012

KISS

arr.each.with_index do |obj, index|
  p 'first' if index == 0        
  p 'last' if index == arr.count-1                  
end
2 голосов
/ 03 октября 2012

Время от времени мне нужны были эти функции, поэтому я создал небольшой класс для этой цели.

Последняя версия: https://gist.github.com/3823837

Пример:

("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

Фактический класс:

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
...