Add find filters to optimize where-first chains (#8171)
Merge pull request 8171
This commit is contained in:
parent
aecd937864
commit
0b2c4c9cec
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'benchmark/ips'
|
||||
require_relative '../lib/jekyll'
|
||||
|
||||
puts ''
|
||||
print 'Setting up... '
|
||||
|
||||
SITE = Jekyll::Site.new(
|
||||
Jekyll.configuration({
|
||||
"source" => File.expand_path("../docs", __dir__),
|
||||
"destination" => File.expand_path("../docs/_site", __dir__),
|
||||
"disable_disk_cache" => true,
|
||||
"quiet" => true,
|
||||
})
|
||||
)
|
||||
|
||||
TEMPLATE_1 = Liquid::Template.parse(<<~HTML)
|
||||
{%- assign doc = site.documents | where: 'url', '/docs/assets/' | first -%}
|
||||
{{- doc.title -}}
|
||||
HTML
|
||||
|
||||
TEMPLATE_2 = Liquid::Template.parse(<<~HTML)
|
||||
{%- assign doc = site.documents | find: 'url', '/docs/assets/' -%}
|
||||
{{- doc.title -}}
|
||||
HTML
|
||||
|
||||
[:reset, :read, :generate].each { |phase| SITE.send(phase) }
|
||||
|
||||
puts 'done.'
|
||||
puts 'Testing... '
|
||||
puts " #{'where + first'.cyan} results in #{TEMPLATE_1.render(SITE.site_payload).inspect.green}"
|
||||
puts " #{'find'.cyan} results in #{TEMPLATE_2.render(SITE.site_payload).inspect.green}"
|
||||
|
||||
if TEMPLATE_1.render(SITE.site_payload) == TEMPLATE_2.render(SITE.site_payload)
|
||||
puts 'Success! Procceding to run benchmarks.'.green
|
||||
puts ''
|
||||
else
|
||||
puts 'Something went wrong. Aborting.'.magenta
|
||||
puts ''
|
||||
return
|
||||
end
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.report('where + first') { TEMPLATE_1.render(SITE.site_payload) }
|
||||
x.report('find') { TEMPLATE_2.render(SITE.site_payload) }
|
||||
x.compare!
|
||||
end
|
|
@ -111,6 +111,40 @@
|
|||
|
||||
#
|
||||
|
||||
- name: Find
|
||||
description: >-
|
||||
Return <strong>the first object</strong> in an array for which the queried
|
||||
attribute has the given value or return <code>nil</code> if no item in
|
||||
the array satisfies the given criteria.
|
||||
version_badge: 4.1.0
|
||||
examples:
|
||||
- input: '{{ site.members | find: "graduation_year", "2014" }}'
|
||||
output:
|
||||
|
||||
#
|
||||
|
||||
- name: Find Expression
|
||||
description: >-
|
||||
Return <strong>the first object</strong> in an array for which the given
|
||||
expression evaluates to true or return <code>nil</code> if no item in
|
||||
the array satisfies the evaluated expression.
|
||||
version_badge: 4.1.0
|
||||
examples:
|
||||
- input: |-
|
||||
{{ site.members | find_exp:"item",
|
||||
"item.graduation_year == 2014" }}
|
||||
output:
|
||||
- input: |-
|
||||
{{ site.members | find_exp:"item",
|
||||
"item.graduation_year < 2014" }}
|
||||
output:
|
||||
- input: |-
|
||||
{{ site.members | find_exp:"item",
|
||||
"item.projects contains 'foo'" }}
|
||||
output:
|
||||
|
||||
#
|
||||
|
||||
- name: Group By
|
||||
description: Group an array's items by a given property.
|
||||
examples:
|
||||
|
|
|
@ -210,6 +210,66 @@ module Jekyll
|
|||
end || []
|
||||
end
|
||||
|
||||
# Search an array of objects and returns the first object that has the queried attribute
|
||||
# with the given value or returns nil otherwise.
|
||||
#
|
||||
# input - the object array.
|
||||
# property - the property within each object to search by.
|
||||
# value - the desired value.
|
||||
# Cannot be an instance of Array nor Hash since calling #to_s on them returns
|
||||
# their `#inspect` string object.
|
||||
#
|
||||
# Returns the found object or nil
|
||||
#
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def find(input, property, value)
|
||||
return input if !property || value.is_a?(Array) || value.is_a?(Hash)
|
||||
return input unless input.respond_to?(:find)
|
||||
|
||||
input = input.values if input.is_a?(Hash)
|
||||
input_id = input.hash
|
||||
|
||||
# implement a hash based on method parameters to cache the end-result for given parameters.
|
||||
@find_filter_cache ||= {}
|
||||
@find_filter_cache[input_id] ||= {}
|
||||
@find_filter_cache[input_id][property] ||= {}
|
||||
|
||||
# stash or retrive results to return
|
||||
# Since `enum.find` can return nil or false, we use a placeholder string "<__NO MATCH__>"
|
||||
# to validate caching.
|
||||
result = @find_filter_cache[input_id][property][value] ||= begin
|
||||
input.find do |object|
|
||||
compare_property_vs_target(item_property(object, property), value)
|
||||
end || "<__NO MATCH__>"
|
||||
end
|
||||
return nil if result == "<__NO MATCH__>"
|
||||
|
||||
result
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
|
||||
# Searches an array of objects against an expression and returns the first object for which
|
||||
# the expression evaluates to true, or returns nil otherwise.
|
||||
#
|
||||
# input - the object array
|
||||
# variable - the variable to assign each item to in the expression
|
||||
# expression - a Liquid comparison expression passed in as a string
|
||||
#
|
||||
# Returns the found object or nil
|
||||
def find_exp(input, variable, expression)
|
||||
return input unless input.respond_to?(:find)
|
||||
|
||||
input = input.values if input.is_a?(Hash)
|
||||
|
||||
condition = parse_condition(expression)
|
||||
@context.stack do
|
||||
input.find do |object|
|
||||
@context[variable] = object
|
||||
condition.evaluate(@context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Convert the input into integer
|
||||
#
|
||||
# input - the object string
|
||||
|
|
|
@ -1079,6 +1079,207 @@ class TestFilters < JekyllUnitTest
|
|||
end
|
||||
end
|
||||
|
||||
context "find filter" do
|
||||
should "return any input that is not an array" do
|
||||
assert_equal "some string", @filter.find("some string", "la", "le")
|
||||
end
|
||||
|
||||
should "filter objects in a hash appropriately" do
|
||||
hash = { "a" => { "color" => "red" }, "b" => { "color" => "blue" } }
|
||||
assert_equal({ "color" => "red" }, @filter.find(hash, "color", "red"))
|
||||
end
|
||||
|
||||
should "filter objects appropriately" do
|
||||
assert_equal(
|
||||
{ "color" => "red", "size" => "large" },
|
||||
@filter.find(@array_of_objects, "color", "red")
|
||||
)
|
||||
end
|
||||
|
||||
should "filter objects with null properties appropriately" do
|
||||
array = [{}, { "color" => nil }, { "color" => "" }, { "color" => "text" }]
|
||||
assert_equal({}, @filter.find(array, "color", nil))
|
||||
end
|
||||
|
||||
should "filter objects with numerical properties appropriately" do
|
||||
array = [
|
||||
{ "value" => "555" },
|
||||
{ "value" => 555 },
|
||||
{ "value" => 24.625 },
|
||||
{ "value" => "24.625" },
|
||||
]
|
||||
assert_equal({ "value" => 24.625 }, @filter.find(array, "value", 24.625))
|
||||
assert_equal({ "value" => "555" }, @filter.find(array, "value", 555))
|
||||
end
|
||||
|
||||
should "filter array properties appropriately" do
|
||||
hash = {
|
||||
"a" => { "tags" => %w(x y) },
|
||||
"b" => { "tags" => ["x"] },
|
||||
"c" => { "tags" => %w(y z) },
|
||||
}
|
||||
assert_equal({ "tags" => %w(x y) }, @filter.find(hash, "tags", "x"))
|
||||
end
|
||||
|
||||
should "filter array properties alongside string properties" do
|
||||
hash = {
|
||||
"a" => { "tags" => %w(x y) },
|
||||
"b" => { "tags" => "x" },
|
||||
"c" => { "tags" => %w(y z) },
|
||||
}
|
||||
assert_equal({ "tags" => %w(x y) }, @filter.find(hash, "tags", "x"))
|
||||
end
|
||||
|
||||
should "filter hash properties with null and empty values" do
|
||||
hash = {
|
||||
"a" => { "tags" => {} },
|
||||
"b" => { "tags" => "" },
|
||||
"c" => { "tags" => nil },
|
||||
"d" => { "tags" => ["x", nil] },
|
||||
"e" => { "tags" => [] },
|
||||
"f" => { "tags" => "xtra" },
|
||||
}
|
||||
|
||||
assert_equal({ "tags" => nil }, @filter.find(hash, "tags", nil))
|
||||
assert_equal({ "tags" => "" }, @filter.find(hash, "tags", ""))
|
||||
|
||||
# `{{ hash | find: 'tags', empty }}`
|
||||
assert_equal(
|
||||
{ "tags" => {} },
|
||||
@filter.find(hash, "tags", Liquid::Expression::LITERALS["empty"])
|
||||
)
|
||||
|
||||
# `{{ `hash | find: 'tags', blank }}`
|
||||
assert_equal(
|
||||
{ "tags" => {} },
|
||||
@filter.find(hash, "tags", Liquid::Expression::LITERALS["blank"])
|
||||
)
|
||||
end
|
||||
|
||||
should "not match substrings" do
|
||||
hash = {
|
||||
"a" => { "category" => "bear" },
|
||||
"b" => { "category" => "wolf" },
|
||||
"c" => { "category" => %w(bear lion) },
|
||||
}
|
||||
assert_nil @filter.find(hash, "category", "ear")
|
||||
end
|
||||
|
||||
should "stringify during comparison for compatibility with liquid parsing" do
|
||||
hash = {
|
||||
"The Words" => { "rating" => 1.2, "featured" => false },
|
||||
"Limitless" => { "rating" => 9.2, "featured" => true },
|
||||
"Hustle" => { "rating" => 4.7, "featured" => true },
|
||||
}
|
||||
|
||||
result = @filter.find(hash, "featured", "true")
|
||||
assert_equal 9.2, result["rating"]
|
||||
|
||||
result = @filter.find(hash, "rating", 4.7)
|
||||
assert_equal 4.7, result["rating"]
|
||||
end
|
||||
end
|
||||
|
||||
context "find_exp filter" do
|
||||
should "return any input that is not an array" do
|
||||
assert_equal "some string", @filter.find_exp("some string", "la", "le")
|
||||
end
|
||||
|
||||
should "filter objects in a hash appropriately" do
|
||||
hash = { "a" => { "color"=>"red" }, "b" => { "color"=>"blue" } }
|
||||
assert_equal(
|
||||
{ "color" => "red" },
|
||||
@filter.find_exp(hash, "item", "item.color == 'red'")
|
||||
)
|
||||
end
|
||||
|
||||
should "filter objects appropriately" do
|
||||
assert_equal(
|
||||
{ "color" => "red", "size" => "large" },
|
||||
@filter.find_exp(@array_of_objects, "item", "item.color == 'red'")
|
||||
)
|
||||
end
|
||||
|
||||
should "filter objects appropriately with 'or', 'and' operators" do
|
||||
assert_equal(
|
||||
{ "color" => "teal", "size" => "large" },
|
||||
@filter.find_exp(
|
||||
@array_of_objects, "item", "item.color == 'red' or item.size == 'large'"
|
||||
)
|
||||
)
|
||||
|
||||
assert_equal(
|
||||
{ "color" => "red", "size" => "large" },
|
||||
@filter.find_exp(
|
||||
@array_of_objects, "item", "item.color == 'red' and item.size == 'large'"
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
should "filter objects across multiple conditions" do
|
||||
sample = [
|
||||
{ "color" => "teal", "size" => "large", "type" => "variable" },
|
||||
{ "color" => "red", "size" => "large", "type" => "fixed" },
|
||||
{ "color" => "red", "size" => "medium", "type" => "variable" },
|
||||
{ "color" => "blue", "size" => "medium", "type" => "fixed" },
|
||||
]
|
||||
assert_equal(
|
||||
{ "color" => "red", "size" => "large", "type" => "fixed" },
|
||||
@filter.find_exp(
|
||||
sample, "item", "item.type == 'fixed' and item.color == 'red' or item.color == 'teal'"
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
should "stringify during comparison for compatibility with liquid parsing" do
|
||||
hash = {
|
||||
"The Words" => { "rating" => 1.2, "featured" => false },
|
||||
"Limitless" => { "rating" => 9.2, "featured" => true },
|
||||
"Hustle" => { "rating" => 4.7, "featured" => true },
|
||||
}
|
||||
|
||||
result = @filter.find_exp(hash, "item", "item.featured == true")
|
||||
assert_equal 9.2, result["rating"]
|
||||
|
||||
result = @filter.find_exp(hash, "item", "item.rating == 4.7")
|
||||
assert_equal 4.7, result["rating"]
|
||||
end
|
||||
|
||||
should "filter with other operators" do
|
||||
assert_equal 3, @filter.find_exp([1, 2, 3, 4, 5], "n", "n >= 3")
|
||||
end
|
||||
|
||||
objects = [
|
||||
{ "id" => "a", "groups" => [1, 2] },
|
||||
{ "id" => "b", "groups" => [2, 3] },
|
||||
{ "id" => "c" },
|
||||
{ "id" => "d", "groups" => [1, 3] },
|
||||
]
|
||||
should "filter with the contains operator over arrays" do
|
||||
result = @filter.find_exp(objects, "obj", "obj.groups contains 1")
|
||||
assert_equal "a", result["id"]
|
||||
end
|
||||
|
||||
should "filter with the contains operator over hash keys" do
|
||||
result = @filter.find_exp(objects, "obj", "obj contains 'groups'")
|
||||
assert_equal "a", result["id"]
|
||||
end
|
||||
|
||||
should "filter posts" do
|
||||
site = fixture_site.tap(&:read)
|
||||
posts = site.site_payload["site"]["posts"]
|
||||
result = @filter.find_exp(posts, "obj", "obj.title == 'Foo Bar'")
|
||||
assert_equal(result, site.posts.find { |p| p.title == "Foo Bar" })
|
||||
end
|
||||
|
||||
should "filter by variable values" do
|
||||
@filter.site.tap(&:read)
|
||||
posts = @filter.site.site_payload["site"]["posts"]
|
||||
result = @filter.find_exp(posts, "post", "post.date > site.dont_show_posts_before")
|
||||
assert result.date > @sample_time
|
||||
end
|
||||
end
|
||||
|
||||
context "group_by_exp filter" do
|
||||
should "successfully group array of Jekyll::Page's" do
|
||||
@filter.site.process
|
||||
|
|
Loading…
Reference in New Issue