-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Optimize range iteration #1109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize range iteration #1109
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| module Liquid | ||
| class ReversableRange | ||
| include Enumerable | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We've been moving away from using Ruby builtin modules in Liquid-accessible types, as their public interface can change between Ruby versions. However, it looks okay to use Enumerable here as this is not a drop and doesn't expose its methods to Liquid. |
||
|
|
||
| def initialize(min, max) | ||
| @min = min | ||
| @max = max | ||
| @reversed = false | ||
| end | ||
|
|
||
| def each | ||
| if reversed | ||
| index = max | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's likely a premature optimization, but using ivars directly is a fair bit faster in general. with 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
endusing |
||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we care about making ranges with switched min/max and opposite reverse being equal? |
||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -133,7 +133,6 @@ def collection_segment(context) | |
| 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 @@ def collection_segment(context) | |
| segment = Utils.slice_collection(collection, from, to) | ||
| segment.reverse! if @reversed | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, actually that would make |
||
|
|
||
| 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 = '' | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| require 'test_helper' | ||
|
|
||
| class ReversableRangeTest < Minitest::Test | ||
| include Liquid | ||
|
|
||
| def test_each_iterates_through_items_in_the_range | ||
| actual_items = [] | ||
| ReversableRange.new(1, 10).each { |item| actual_items << item } | ||
|
|
||
| expected_items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] | ||
| assert_equal expected_items, actual_items | ||
| end | ||
|
|
||
| def test_implements_enumerable | ||
| actual_items = ReversableRange.new(1, 10).select(&:even?) | ||
|
|
||
| expected_items = [2, 4, 6, 8, 10] | ||
| assert_equal expected_items, actual_items | ||
| end | ||
|
|
||
| def test_is_not_empty_max_greater_than_min | ||
| range = ReversableRange.new(9, 10) | ||
|
|
||
| refute_predicate range, :empty? | ||
| end | ||
|
|
||
| def test_is_not_empty_max_equal_to_min | ||
| range = ReversableRange.new(10, 10) | ||
|
|
||
| refute_predicate range, :empty? | ||
| end | ||
|
|
||
| def test_is_empty_if_not_reversed_and_max_less_than_min | ||
| range = ReversableRange.new(10, 9) | ||
|
|
||
| assert_predicate range, :empty? | ||
| end | ||
|
|
||
| def test_ranges_with_the_same_max_and_min_have_one_element | ||
| actual_items = ReversableRange.new(1337, 1337).to_a | ||
| expected_items = [1337] | ||
| assert_equal expected_items, actual_items | ||
| end | ||
|
|
||
| def test_load_slice_returns_a_sub_range | ||
| actual_items = ReversableRange.new(1, 10).load_slice(5, 8).to_a | ||
|
|
||
| expected_items = [5, 6, 7, 8] | ||
| assert_equal expected_items, actual_items | ||
| end | ||
|
|
||
| def test_load_slice_returns_a_reversed_sub_range_if_reversed | ||
| range = ReversableRange.new(1, 10) | ||
| range.reverse! | ||
| actual_items = range.load_slice(5, 8).to_a | ||
|
|
||
| expected_items = [8, 7, 6, 5] | ||
| assert_equal expected_items, actual_items | ||
| end | ||
|
|
||
| def test_is_equal_to_other_if_also_a_reversable_range_and_has_same_properties | ||
| one = ReversableRange.new(1, 10) | ||
| one.reverse! | ||
|
|
||
| two = ReversableRange.new(1, 10) | ||
| two.reverse! | ||
|
|
||
| assert_equal one, two | ||
| end | ||
|
|
||
| def test_is_not_equal_to_a_non_reversable_range | ||
| range = ReversableRange.new(1, 10) | ||
| range.reverse! | ||
|
|
||
| refute_equal range, :something_else | ||
| end | ||
|
|
||
| def test_is_not_equal_if_ranges_have_different_mins | ||
| one = ReversableRange.new(1, 10) | ||
| two = ReversableRange.new(2, 10) | ||
|
|
||
| refute_equal one, two | ||
| end | ||
|
|
||
| def test_is_not_equal_if_ranges_have_different_maxes | ||
| one = ReversableRange.new(1, 10) | ||
| two = ReversableRange.new(1, 11) | ||
|
|
||
| refute_equal one, two | ||
| end | ||
|
|
||
| def test_is_not_equal_if_only_one_is_reversed | ||
| one = ReversableRange.new(1, 10) | ||
|
|
||
| two = ReversableRange.new(1, 10) | ||
| two.reverse! | ||
|
|
||
| refute_equal one, two | ||
| end | ||
|
|
||
| def test_to_s_mirrors_rubys_range_syntax | ||
| range = ReversableRange.new(1, 10) | ||
| assert_equal '1..10', range.to_s | ||
| end | ||
|
|
||
| def test_to_s_reverses_when_reversed | ||
| range = ReversableRange.new(1, 10) | ||
| range.reverse! | ||
| assert_equal '10..1', range.to_s | ||
| end | ||
|
|
||
| def test_size | ||
| range = ReversableRange.new(1, 10) | ||
| assert_equal 10, range.size | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reversible is a more correct spelling I think.