Skip to content

Conversation

@pushrax
Copy link
Contributor

@pushrax pushrax commented Sep 13, 2018

Closes #980

@pushrax pushrax requested a review from fw42 September 13, 2018 21:17
ary.sort do |a, b|
Utils.to_number(a) <=> Utils.to_number(b)
end
elsif ary.empty? # The next two cases assume a non-empty array.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this unnecessary (because ary.first.respond_to?(:[]) will be false if ary is empty

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but wouldn't the else branch return nil instead of [] as this branch does?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep I'm an idiot

Copy link
Contributor

@fw42 fw42 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 maybe add a test for the case where the property doesn't exist?

ary.sort do |a, b|
Utils.to_number(a[property]) <=> Utils.to_number(b[property])
end
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If none of these if/elsif branches are taken, then it seems like we should treat this as an error. Are you just returning nil in that case to keep consistency with the sort filter? Is consistency with the sort filter more important that providing a clearer error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we change sort as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could result in broken liquid being more obviously broken (i.e. showing Liquid Error: ...). Perhaps we should add logging for in the sort filter so that we will know whether it would break things, then we could turn it into a liquid error if it won't.

@ChrisBr
Copy link

ChrisBr commented Sep 21, 2018

Sorry for just popping in but why not introducing a more generic sort_by which accepts a block instead of another very specify sort method? Similar to what is in Enumerable? 🤔

@pushrax
Copy link
Contributor Author

pushrax commented Sep 24, 2018

Accepting a block is not yet a concept in Liquid. I'm not going to merge this yet though, because it's possible that could change.

@dylanahsmith
Copy link
Contributor

What happened to Travis CI? It doesn't seem to be running anymore

@pushrax
Copy link
Contributor Author

pushrax commented Sep 25, 2018

For some reason Travis CI was disabled for this repo. I enabled it again, new commits should be built.

@ashmaroli
Copy link
Contributor

ashmaroli commented Feb 23, 2019

@pushrax I've a couple of questions regarding the last branch of the conditional block in the current state:

elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
  ary.sort do |a, b|
    Utils.to_number(a[property]) <=> Utils.to_number(b[property])
  end
end
  • Why check only if ary.first.respond_to?(:[])?
    What if b[property] in the sort block fails with a NoMethodError?
  • Why check for !ary.first[property].nil? when Utils.to_number(nil) returns 0 anyways..

Refactoring to..

def sort_numeric(input, property = nil)
  ary = InputIterator.new(input)
  if property.nil?
    ary.sort { |a, b| Utils.to_number(a) <=> Utils.to_number(b) }
  elsif ary.empty?
    []
  else
    ary.sort do |a, b|
      next unless a.respond_to?(:[]) && b.respond_to?(:[])
      Utils.to_number(a[property]) <=> Utils.to_number(b[property])
    end
  end
end

@pushrax
Copy link
Contributor Author

pushrax commented Feb 23, 2019

Checking just the first element for [] is a heuristic used by other filters already, and in Shopify seems to work fine while being much faster than checking in the loop. You make a totally fair point, perhaps we should reconsider.

The nil check is also reasonably questionable, also copied from sort. I think the idea is to have a sort of respond_to?(property) check, but it could also just happen to be nil. I wouldn’t be surprised if this check is causing a rare bug in a Liquid template.

@ashmaroli
Copy link
Contributor

seems to work fine while being much faster than checking in the loop.

@pushrax I wrote a benchmark script to test this..:

# frozen_string_literal: true

require 'benchmark/ips'
require 'liquid'

def sort_numeric_original(input, property = nil)
  ary = Liquid::StandardFilters::InputIterator.new(input)
  if property.nil?
    ary.sort do |a, b|
      Liquid::Utils.to_number(a) <=> Liquid::Utils.to_number(b)
    end
  elsif ary.empty?
    []
  elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
    ary.sort do |a, b|
      Liquid::Utils.to_number(a[property]) <=> Liquid::Utils.to_number(b[property])
    end
  end
end

def sort_numeric_proposed(input, property = nil)
  ary = Liquid::StandardFilters::InputIterator.new(input)
  if property.nil?
    ary.sort { |a, b| Liquid::Utils.to_number(a) <=> Liquid::Utils.to_number(b) }
  elsif ary.empty?
    []
  else
    ary.sort do |a, b|
      next unless a.respond_to?(:[]) && b.respond_to?(:[])
      Liquid::Utils.to_number(a[property]) <=> Liquid::Utils.to_number(b[property])
    end
  end
end

INPUT = [{ "a" => '10' }, { "a" => '3' }, { "a" => '1' }, { "a" => '2' }]
return unless sort_numeric_original(INPUT) == sort_numeric_proposed(INPUT)
return unless sort_numeric_original(INPUT, "a") == sort_numeric_proposed(INPUT, "a")

Benchmark.ips do |x|
  x.report('original sort_numeric no-property') { sort_numeric_original(INPUT) }
  x.report('proposed sort_numeric no-property') { sort_numeric_proposed(INPUT) }
  x.compare!
end

Benchmark.ips do |x|
  x.report('original sort_numeric') { sort_numeric_original(INPUT, "a") }
  x.report('proposed sort_numeric') { sort_numeric_proposed(INPUT, "a") }
  x.compare!
end

On my system, the numbers are:

Warming up --------------------------------------
original sort_numeric no-property
                         6.001k i/100ms
proposed sort_numeric no-property
                         6.048k i/100ms
Calculating -------------------------------------
original sort_numeric no-property
                         67.734k (A± 1.2%) i/s -    342.057k in   5.050704s
proposed sort_numeric no-property
                         67.734k (A± 0.4%) i/s -    338.688k in   5.000315s

Comparison:
proposed sort_numeric no-property:    67734.4 i/s
original sort_numeric no-property:    67734.2 i/s - same-ish: difference falls within error

Warming up --------------------------------------
original sort_numeric
                         2.093k i/100ms
proposed sort_numeric
                         2.925k i/100ms
Calculating -------------------------------------
original sort_numeric
                         21.918k (A± 4.1%) i/s -    110.929k in   5.071298s
proposed sort_numeric
                         31.508k (A± 0.5%) i/s -    157.950k in   5.013203s

Comparison:
proposed sort_numeric:    31507.5 i/s
original sort_numeric:    21917.8 i/s - 1.44x  slower

@pushrax
Copy link
Contributor Author

pushrax commented Feb 23, 2019

The constant overhead is dominating in that benchmark with just 4 elements. With an order of magnitude or two more elements the result should be different. Removing !ary.first[property].nil? is the big change to constant overhead. to_number is likely to be the big factor in the block anyway (looks like it could use optimization, the allocations from obj.strip =~ /\A-?\d+\.\d+\z/ are unnecessary).

In any case, as mentioned your points were fair and the perf loss of checking all the elements for [] is probably worth it. However, the implicit contract of sort filters so far is to return nil when sorting by a property and the input elements don't respond to [], so I guess we need something more like ary.all? { |x| x.respond_to?(:[]) } instead of ary.first.respond_to?(:[]) or change the contract.

I'm was going to abandon this PR since I've been thinking about a more general change to let Liquid developers pass something like a block to filters, but that idea isn't fleshed out enough to happen soon and this filter is indeed useful and generic enough to be included. I'll probably circle back to it this week some time, thanks for the ping.

@mvxt
Copy link

mvxt commented Jun 5, 2019

Hey @pushrax, was browsing around for resources when I found the GH Issue and then this PR. Totally understand if you're busy and aren't planning on workin on this PR, but do you know of any workarounds or alternative implementations?

Use case is that we're sorting a bunch of Docker image tags using Jekyll + Liquid, and we're pulling those values from a JSON file. So essentially we're sorting through a list of values that looks like this:
["10.12.0", "somewords", "hahaha", "3.5.21"]. We're running into the lexicographical issue where all of the 9.x.x and 8.x.x are showing up as "later" than 12.x.x and 11.x.x versions in the list when sorted. Pointers would be appreciated if/when you have a second!

@lubeskih
Copy link

lubeskih commented Aug 28, 2019

Hi @pushrax!

Are we going to see this in Liquid soon? It will be almost a year since this PR was opened, and people truly need this feature.

Thank you, and all the best!

@shopmike shopmike added the Filter Request Issues and PRs specific to Liquid Filters label Sep 19, 2019
@shopmike shopmike self-requested a review October 4, 2019 05:28
@shopmike
Copy link
Contributor

shopmike commented Oct 4, 2019

This could likely be optimized after the fact, but for now I think this should just make it through, it seems to be some valid use cases held off by this.

@plewandowski
Copy link

What about this PR?

@pudiva
Copy link

pudiva commented Jan 4, 2023

bump? :3

@spencer-j spencer-j mentioned this pull request Feb 2, 2023
@jg-rp jg-rp mentioned this pull request May 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Filter Request Issues and PRs specific to Liquid Filters

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Numeric sort