From d079ccabdb6933a61c5d34de87451566c5b247da Mon Sep 17 00:00:00 2001 From: James Prior Date: Thu, 8 May 2025 08:20:43 +0100 Subject: [PATCH 1/4] Add `sort_numeric` filter --- lib/liquid/standardfilters.rb | 43 ++++++++++++++++++++++++ test/integration/filter_test.rb | 26 ++++++++++++++ test/integration/standard_filter_test.rb | 43 ++++++++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index 6e072fcf5..bbeab1324 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -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 = { '&' => '&', '>' => '>', @@ -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 the string representation of 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 @@ -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?(Integer) || obj.is_a?(Float) || obj.is_a?(BigDecimal) + [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 diff --git a/test/integration/filter_test.rb b/test/integration/filter_test.rb index 133b57b3b..a974cb4d8 100644 --- a/test/integration/filter_test.rb +++ b/test/integration/filter_test.rb @@ -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( diff --git a/test/integration/standard_filter_test.rb b/test/integration/standard_filter_test.rb index e1370fae1..ae13f5532 100644 --- a/test/integration/standard_filter_test.rb +++ b/test/integration/standard_filter_test.rb @@ -425,6 +425,49 @@ 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 + 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])) From 92c0411c89dc81828a15ab8452a65687bac15675 Mon Sep 17 00:00:00 2001 From: James Prior Date: Fri, 9 May 2025 07:30:21 +0100 Subject: [PATCH 2/4] Use `is_a?(Numeric)` --- lib/liquid/standardfilters.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index bbeab1324..430c1e346 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -1062,7 +1062,7 @@ def numeric_compare(a, b) end def numbers(obj) - if obj.is_a?(Integer) || obj.is_a?(Float) || obj.is_a?(BigDecimal) + if obj.is_a?(Numeric) [obj] else numeric = obj.to_s.scan(/(?<=\.)0+|-?\d+/) From 7764fcc038b7432c17f6784abf0f85946eb6c463 Mon Sep 17 00:00:00 2001 From: James Prior Date: Fri, 9 May 2025 07:34:18 +0100 Subject: [PATCH 3/4] Simplify sort_numeric summary doc --- lib/liquid/standardfilters.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index 430c1e346..3a030e7be 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -429,7 +429,7 @@ def sort_natural(input, property = nil) # @liquid_type filter # @liquid_category array # @liquid_summary - # Sorts an array by numerical values extracted from runs of digits within the string representation of each item. + # 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) From db54c4bd5416aa7ed433d85379b1e015f111c249 Mon Sep 17 00:00:00 2001 From: James Prior Date: Fri, 9 May 2025 07:39:04 +0100 Subject: [PATCH 4/4] Test compare int, float and decimal --- test/integration/standard_filter_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/integration/standard_filter_test.rb b/test/integration/standard_filter_test.rb index ae13f5532..d9ad90f64 100644 --- a/test/integration/standard_filter_test.rb +++ b/test/integration/standard_filter_test.rb @@ -439,6 +439,7 @@ def test_sort_numeric 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