mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-18 10:20:43 -07:00
Optimize range iteration
For loops support a range syntax for iterating a fixed number of times,
which looks like this.
```liquid
{% for i in range (1..100) %}
...
{% endfor %}
```
Previously, we converted these ranges to arrays using `to_a`, which
initialized an array containing each number in the range. Since all we
use these ranges for is iteration, this is far less efficient than
using a range iterator.
Doing this means that iterating over ranges now takes O(1) rather than O(n)
memory. See the PR for more benchmarks.
* Remove to_a cast on ranges
* Add ReversableRange iterator
* Add custom range-specific slicing logic
This commit is contained in:
@@ -74,6 +74,7 @@ require 'liquid/condition'
|
||||
require 'liquid/utils'
|
||||
require 'liquid/tokenizer'
|
||||
require 'liquid/parse_context'
|
||||
require 'liquid/reversable_range'
|
||||
|
||||
# Load all the tags of the standard library
|
||||
#
|
||||
|
||||
@@ -6,7 +6,7 @@ module Liquid
|
||||
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
|
||||
new(start_obj, end_obj)
|
||||
else
|
||||
start_obj.to_i..end_obj.to_i
|
||||
ReversableRange.new(start_obj.to_i, end_obj.to_i)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@ module Liquid
|
||||
def evaluate(context)
|
||||
start_int = to_integer(context.evaluate(@start_obj))
|
||||
end_int = to_integer(context.evaluate(@end_obj))
|
||||
start_int..end_int
|
||||
ReversableRange.new(start_int, end_int)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
module Liquid
|
||||
class ReversableRange
|
||||
include Enumerable
|
||||
|
||||
def initialize(min, max)
|
||||
@min = min
|
||||
@max = max
|
||||
@reversed = false
|
||||
end
|
||||
|
||||
def each
|
||||
if reversed
|
||||
index = max
|
||||
while index >= min
|
||||
yield index
|
||||
index -= 1
|
||||
end
|
||||
else
|
||||
index = min
|
||||
while index <= max
|
||||
yield index
|
||||
index += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def reverse!
|
||||
@reversed = !reversed
|
||||
self
|
||||
end
|
||||
|
||||
def empty?
|
||||
max < min
|
||||
end
|
||||
|
||||
def size
|
||||
difference = max - min
|
||||
if difference > 0
|
||||
difference + 1
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
def load_slice(from, to = nil)
|
||||
to ||= max
|
||||
slice_max = [max, to].min
|
||||
slice_min = [min, from].max
|
||||
range = ReversableRange.new(slice_min, slice_max)
|
||||
range.reverse! if reversed
|
||||
range
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
other.is_a?(self.class) &&
|
||||
other.min == min &&
|
||||
other.max == max &&
|
||||
other.reversed == reversed
|
||||
end
|
||||
|
||||
def to_s
|
||||
if reversed
|
||||
"#{max}..#{min}"
|
||||
else
|
||||
"#{min}..#{max}"
|
||||
end
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :min, :max, :reversed
|
||||
end
|
||||
end
|
||||
@@ -133,7 +133,6 @@ module Liquid
|
||||
end
|
||||
|
||||
collection = context.evaluate(@collection_name)
|
||||
collection = collection.to_a if collection.is_a?(Range)
|
||||
|
||||
limit_value = context.evaluate(@limit)
|
||||
to = if limit_value.nil?
|
||||
@@ -145,14 +144,14 @@ module Liquid
|
||||
segment = Utils.slice_collection(collection, from, to)
|
||||
segment.reverse! if @reversed
|
||||
|
||||
offsets[@name] = from + segment.length
|
||||
offsets[@name] = from + segment.size
|
||||
|
||||
segment
|
||||
end
|
||||
|
||||
def render_segment(context, segment)
|
||||
for_stack = context.registers[:for_stack] ||= []
|
||||
length = segment.length
|
||||
length = segment.size
|
||||
|
||||
result = ''
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
module Liquid
|
||||
module Utils
|
||||
def self.slice_collection(collection, from, to)
|
||||
return collection.load_slice(from, to) if collection.is_a?(ReversableRange)
|
||||
|
||||
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
|
||||
collection.load_slice(from, to)
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user