読者です 読者をやめる 読者になる 読者になる

プログラミング 美徳の不幸

Ruby, Rails, JavaScriptなどのプログラミングまとめ、解説、備忘録。

Arrayの前後を取得したいとき

f:id:tkot:20150825215416p:plain

こういうのあるじゃないですか。jQueryの話じゃないですよ。

食べログの写真一覧みたいなのをイメージしてください。画像ごとにURLがあって、真ん中が今の画像、一個右の画像を押すと画像スライダーみたいな挙動なんだけどページが変わる。(理想的には画像のあたりだけが)

これどう実装しますか?


少し勘がいい人ならKaminariを使えば実装できんじゃない?って思うかもしれませんが、たぶんできない気がしますね。っていうのは、これは3枚目の画像がimages?page=3&per=1とかのURLじゃ困るわけで。一時的な挙動としてはいいかもしれませんが、画像へのリンクとかつくろうと思うとめんどくさそうだしページの内容的にURLが頻繁に変わるのはおかしいし。

で、いろいろ考えた上、効率的にベストじゃないと思うんですがいったんソートされたidの配列を受け取って、そこから前後のidを見つければいいかなと思ったんですね。ベストじゃないと思うのは、なんかうまくやればidの配列を作らなくてもいい気がするんですがそれは思いつかなかった。誰か教えてください。

要件をまとめると

array = [1,2,3,4,5,6,7,8,9,10]

# 取得する件数が5件、中央が5なら
[3,4,5,6,7]

# 取得する件数が5件、中央が9なら
[6,7,8,9,10]


# 取得する件数が4件、中央が3なら
[1,2,3,4]


これができてれば次のようにできますね。

ids = @restaurant.photos.published.order(:published_at).pluck(:id)
@photo = @restaurant.photos.published.find(params[:photo_id])
@list_photos = Photo.where_with_order(id: find_prev_and_next_ids(ids, @photo.id))

ちなみにwhere_with_orderみたいなメソッドRailsにはないですが現実問題けっこう使うんで自分で用意してください。

問題は、Arrayの中心部と幅を指定すれば要素が返ってくるのって、メソッドありそうでないんですよね。
しょうがないので作りました。

class ArrayPrevNext
  attr_accessor :center

  def initialize(array, center)
    @array = array
    @center = center
  end

  def prev_and_next(n)
    return [@center] if n == 1

    if n.odd?
      # n = 2a + 1 として表現
      a = (n - 1) / 2

      if center_index < a
        x = a*2
        @array[0..x]
      elsif last_index - a < center_index
        @array[(last_index - 2*a)..last_index]
      else
        @array[(center_index - a)..(center_index + a)]
      end
    else
      # n = 2a
      a = n / 2
      if center_index <= a
        @array[0..(n - 1)]
      elsif last_index - a < center_index
        @array[(last_index - (2*a - 1))..last_index]
      else
        @array[(center_index - a)..(center_index + a - 1)]
      end
    end
  end

  def prev(n)
    first = [center_index - n, 0].max
    last = center_index - 1
    return nil unless first < last
    @array[first..last]
  end

  def next(n)
    first = center_index + 1
    last = center_index + n
    @array[first..last]
  end

  def center_index
    @array.index(@center)
  end

  def last_index
    @array.length - 1
  end
end

使い方は

@photo = @restaurant.photos.published.find(params[:photo_id])
ids = @restaurant.photos.published.order(:published_at).pluck(:id)
pn = ArrayPrevNext.new(ids, @photo.id)
@list = Photo.where_with_order(ids: obj.prev_and_next(9))

これで前後4件と自身を含んだ9件が返ってくる。


これは常に中心から前後が等しくなるように、偶数の場合は右が1個小さくなるように取ってくるけどその辺が設定で変えられるぐらいがいいかも。