Волшебный первый и последний индикатор в петле в Ruby/Rails?

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 с параметрами (массив, функция всех элементов, функция первых элементов, функция последних элементов)... но я открыт для другие мысли.

Ответ 1

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

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

Ответ 2

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

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

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

Ответ 3

Что делать, если вы можете это сделать?

%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

В MRI >= 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

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

Фокус в том, что каждый, каждый_with_index, и c. return Enumerator, если не задан блок. Перечислители делают все, что делает Enumerable, и немного больше. Но для нас важно то, что мы можем Enumerator Monkey-patch добавить еще один способ итерации, "обертывания" существующей итерации, что бы это ни было.

Ответ 4

Или небольшой маленький доменный язык:

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 играет небольшие трюки, почти похожие на несколько блоков.

Ответ 5

Как указывали многие, 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

Или с помощью расширений массива

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

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

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

Ответ 7

Там нет "сделать это синтаксис (первый | последний)" в 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

Ответ 8

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

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

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

Изменить: Svante answer (с предложением molf) к связанному вопросу показывает, как передать несколько блоков кода одному методу:

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"})

Ответ 9

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

Последняя версия: 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

Ответ 10

ПОЦЕЛУЙ

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

Ответ 11

Если вы не возражаете, что "последнее" действие происходит раньше, чем материал в середине, то этот патч обезьяны:

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

Позволяет:

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

Ответ 12

Я не мог удержаться:) Это не настроено на производительность, хотя я думаю, что это не должно быть намного медленнее, чем большинство других ответов здесь. Все о сахаре!

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

Пример 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

Пример 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

Ответ 13

Разделите массив на диапазоны, где элементы внутри каждого диапазона должны вести себя по-разному. Сопоставьте каждый диапазон, созданный таким образом для блока.

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

Может создавать диапазоны вручную, но эти помощники ниже упрощают:

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

Использование:

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

Ответ 14

Я вижу здесь много хакеров, которые довольно близки, но все они сильно зависят от заданного итератора с фиксированным размером и НЕ являются итератором. Я также хотел бы предложить сохранить предыдущий элемент при повторном запуске, чтобы узнать первый/последний элемент, который был повторен.

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] 

Ответ 15

Если вы знаете, что элементы в массиве уникальны (в отличие от этого случая), вы можете сделать это:

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

a.each_with_index do |x, i|
  if x == a.first
    print x+1
  elsif x == a.last
    print x*10
  else
    print x
  end
end

Ответ 16

Иногда цикл for - это всего лишь ваш лучший вариант

if(array.count > 0)
   first= array[0]
   #... do something with the first

   cx = array.count -2 #so we skip the last record on a 0 based array
   for x in 1..cx
     middle = array[x]
     #... do something to the middle
   end

   last = array[array.count-1]
   #... do something with the last item. 
end

Я знаю, что на этот вопрос был дан ответ, но этот метод не имеет побочных эффектов и не проверяет, является ли запись 13-го, 14-го, 15-го.. 10thousandth, 10,001th... первой записью или последней.

Предыдущие ответы не удалили назначение в любом классе структур данных.