Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions lib/liquid/standardfilters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ module StandardFilters
I64_RANGE = MIN_I64..MAX_I64
private_constant :MIN_I64, :MAX_I64, :I64_RANGE

INFINITY_ARRAY = [Float::INFINITY].freeze
private_constant :INFINITY_ARRAY

HTML_ESCAPE = {
'&' => '&',
'>' => '>',
Expand Down Expand Up @@ -422,6 +425,31 @@ def sort_natural(input, property = nil)
end
end

# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Sorts an array by numerical values extracted from runs of digits within each item.
# @liquid_syntax array | sort_numeric
# @liquid_return [array[untyped]]
def sort_numeric(input, property = nil)
ary = InputIterator.new(input, context)

return [] if ary.empty?

if property.nil?
ary.sort do |a, b|
numeric_compare(a, b)
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| numeric_compare(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
end
end
end

# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
Expand Down Expand Up @@ -1029,6 +1057,21 @@ def nil_safe_casecmp(a, b)
end
end

def numeric_compare(a, b)
numbers(a) <=> numbers(b)
end

def numbers(obj)
if obj.is_a?(Numeric)
[obj]
else
numeric = obj.to_s.scan(/(?<=\.)0+|-?\d+/)
return INFINITY_ARRAY if numeric.empty?

numeric.map(&:to_i)
end
end

class InputIterator
include Enumerable

Expand Down
26 changes: 26 additions & 0 deletions test/integration/filter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,32 @@ def test_sort_natural
assert_equal('A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context))
end

def test_sort_numeric
assert_template_result(
"1 2 3 10",
"{{ array | sort_numeric | join }}",
{ "array" => ["10", "3", "1", "2"] },
)

assert_template_result(
"0001 02 17 042 107",
"{{ array | sort_numeric | join }}",
{ "array" => ["107", "042", "0001", "02", "17"] },
)

assert_template_result(
"1 2 10",
"{{ hash | sort_numeric: 'a' | map: 'a' | join }}",
{ "hash" => [{ "a" => "10" }, { "a" => 1 }, { "a" => "2" }] },
)

assert_template_result(
"v0.1 v1.1.0 v1.10",
"{{ objects | sort_numeric: 'a' | map: 'a' | join }}",
{ "objects" => [TestObject.new("v0.1"), TestObject.new("v1.10"), TestObject.new("v1.1.0")] },
)
end

def test_compact
# Test strings
assert_template_result(
Expand Down
44 changes: 44 additions & 0 deletions test/integration/standard_filter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,50 @@ def test_numerical_vs_lexicographical_sort
assert_equal([{ "a" => "10" }, { "a" => "2" }], @filters.sort([{ "a" => "10" }, { "a" => "2" }], "a"))
end

def test_sort_numeric
assert_equal([], @filters.sort_numeric([]))
assert_equal([1, 2, 3, 10], @filters.sort_numeric([10, 3, 1, 2]))
assert_equal(["1", "2", "3", "10"], @filters.sort_numeric(["10", "3", "1", "2"]))
assert_equal([1.01, 1.1, 2.3, 3.5, 10.1], @filters.sort_numeric([10.1, 3.5, 2.3, 1.1, 1.01]))
assert_equal(["1.01", "1.1", "2.3", "3.5", "10.1"], @filters.sort_numeric(["10.1", "3.5", "2.3", "1.1", "1.01"]))
assert_equal(["-1", "1"], @filters.sort_numeric(["1", "-1"]))
assert_equal(["1", "2", "3", "10", nil], @filters.sort_numeric(["10", "3", nil, "1", "2"]))
assert_equal(["v0.1", "v1.1.0", "v1.2", "v1.9", "v1.10", "v10.0"], @filters.sort_numeric(["v1.2", "v1.9", "v10.0", "v0.1", "v1.10", "v1.1.0"]))
assert_equal(["0001", "02", "17", "042", "107"], @filters.sort_numeric(["107", "042", "0001", "02", "17"]))
assert_equal(["7 The Street", "42 The Street", "101 The Street"], @filters.sort_numeric(["42 The Street", "7 The Street", "101 The Street"]))
assert_equal(["1", "2", "no numbers"], @filters.sort_numeric(["no numbers", "1", "2"]))
assert_equal(["1", 2, 3, "10"], @filters.sort_numeric(["10", 3, "1", 2]))
assert_equal([1, 2, 3], @filters.sort_numeric([[1], [3], [2]])) # nested arrays get flattened
assert_equal([1.1, 2, BigDecimal(3), 10], @filters.sort_numeric([10, BigDecimal(3), 1.1, 2]))
end

def test_sort_numeric_with_property
assert_equal([], @filters.sort_numeric([], "a"))
assert_equal([{}], @filters.sort_numeric([{}], "a"))
assert_equal([{ "a" => 2 }, { "a" => 10 }], @filters.sort_numeric([{ "a" => 10 }, { "a" => 2 }], "a"))
assert_equal([{ "a" => "2" }, { "a" => "10" }], @filters.sort_numeric([{ "a" => "10" }, { "a" => "2" }], "a"))
assert_equal([{ "a" => "2" }, { "a" => "10" }, { "b" => "1" }], @filters.sort_numeric([{ "a" => "10" }, { "b" => "1" }, { "a" => "2" }], "a"))
assert_equal([{ "a" => 1 }, { "a" => "2" }, { "a" => "10" }], @filters.sort_numeric([{ "a" => "10" }, { "a" => 1 }, { "a" => "2" }], "a"))
assert_equal([1, 2, 3], @filters.sort_numeric([[1], [3], [2]]), "a")
end

def test_sort_numeric_input_is_not_an_array
assert_equal([], @filters.sort_numeric(nil))
assert_equal([true], @filters.sort_numeric(true))
assert_equal([false], @filters.sort_numeric(false))
assert_equal([42], @filters.sort_numeric(42))
assert_equal([42.5], @filters.sort_numeric(42.5))
assert_equal(["42.5"], @filters.sort_numeric("42.5"))
assert_equal([{ "a" => "42.5" }], @filters.sort_numeric({ "a" => "42.5" }))
assert_equal([], @filters.sort_numeric(nil, "a"))
assert_nil(@filters.sort_numeric(true, "a"))
assert_nil(@filters.sort_numeric(false, "a"))
assert_equal([42], @filters.sort_numeric(42, "a"))
assert_nil(@filters.sort_numeric(42.5, "a"))
assert_equal(["42.5"], @filters.sort_numeric("42.5", "a"))
assert_equal([{ "a" => "42.5" }], @filters.sort_numeric({ "a" => "42.5" }, "a"))
end

def test_uniq
assert_equal(["foo"], @filters.uniq("foo"))
assert_equal([1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1]))
Expand Down