Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #10907 (new enhancement)

Opened 1 year ago

Last modified 1 year ago

[PATCH] [TEST] Element#match should grok selectors with commas

Reported by: savetheclocktower Assigned to: savetheclocktower
Priority: normal Milestone: 2.x
Component: Prototype Version: edge
Severity: normal Keywords: 1.6.1
Cc: sam

Description

This will require a reorganization of Selector, so it's a goal for the next major version.

Attachments

element_match_with_commas_1.patch (4.6 kB) - added by metavida on 02/04/08 15:07:05.
element_match_benchmarks.html (14.0 kB) - added by metavida on 03/11/08 15:33:52.
Some benchmarks (goes into test/unit)
element_match_with_commas_2.patch (3.8 kB) - added by metavida on 03/12/08 13:12:55.
match implementation by kangax - http://dev.rubyonrails.org/ticket/10907#comment:13

Change History

01/23/08 20:18:33 changed by savetheclocktower

  • summary changed from Element#match, Element#up, etc., should grok selectors with commas to Element#match should grok selectors with commas.

02/04/08 15:07:05 changed by metavida

  • attachment element_match_with_commas_1.patch added.

02/04/08 15:14:43 changed by metavida

  • summary changed from Element#match should grok selectors with commas to [PATCH] [TEST] Element#match should grok selectors with commas.

The patch I submitted supports any combination of simple selectors, selectors with commas, and custom selectors. All related tests are also included in the patch.

I created a slightly less complex version Element#match that only supports only a single selector with commas or custom selector (as suggested by Tobie here) and can upload that patch as well if there is enough interest.

03/11/08 15:14:52 changed by metavida

In #11206, kangax proposes a different solution to this same problem. I'll upload benchmarks soon to compare the two methods.

03/11/08 15:33:52 changed by metavida

  • attachment element_match_benchmarks.html added.

Some benchmarks (goes into test/unit)

03/11/08 15:39:38 changed by metavida

  • cc set to sam.

03/11/08 16:15:59 changed by kangax

Good job on benchmarks, but it's clear that your patch does quite a lot of "heavy" lifting: support for multiple arguments ($A, then shift), #each then #detect (which uses #each once again), then split, concat, etc.

Here's what I get with your benchmarks:

testBenchmarkMatchSingleSelectorOriginal   0.36s
testBenchmarkMatchSingleSelectorMetavida   1.031s
testBenchmarkMatchSingleSelectorKangax	   0.394s

testBenchmarkMatchCustomOriginal	   0.06s
testBenchmarkMatchCustomMetavida	   0.259s
testBenchmarkMatchCustomKangax	           0.06s

testBenchmarkMatchMultipleFirstMetavida	   1.363s
testBenchmarkMatchMultipleFirstKangax	   0.628s

testBenchmarkMatchMultipleSecondMetavida   1.687s
testBenchmarkMatchMultipleSecondKangax	   1.142s

It's good to see the performance results being close to original ones.

03/11/08 17:38:37 changed by metavida

Agreed, my implementation is definitely a bit heavier/slower in the base case (a single, simple selector). However, on my machine the results on more complicated selectors were quite different from yours. I have a Mac, running OS 10.4. With Firebug & Greasemonkey disabled I got the following results

                                          FF2     Safari  FF3b4
testBenchmarkMatchSingleSelectorOriginal  0.59s   0.321s  0.322s
testBenchmarkMatchSingleSelectorMetavida  0.782s  0.348s  0.362s
testBenchmarkMatchSingleSelectorKangax    0.788s  0.368s  0.391s

testBenchmarkMatchCustomOriginal          0.1s    0.046s  0.048s
testBenchmarkMatchCustomMetavida          0.237s  0.058s  0.063s
testBenchmarkMatchCustomKangax            0.1s    0.046s  0.048s

testBenchmarkMatchMultipleFirstMetavida   0.77s   0.384s  0.368s
testBenchmarkMatchMultipleFirstKangax     1.249s  0.63s   0.664s

testBenchmarkMatchMultipleSecondMetavida  1.376s  0.698s  0.673s
testBenchmarkMatchMultipleSecondKangax    1.79s   0.928s  1.009s

Again, the base cases are slower, but as the selector gets more complex my implementation pulled away. The results were similar on my PC in IE6.

Perhaps a mixed approach would be better. If a simple selector is detected (one argument & no commas) the current implementation is used. Otherwise the more complex implementation is triggered.

03/11/08 17:40:40 changed by kangax

I'm sorry, but why multiple arguments?

03/11/08 17:59:14 changed by metavida

I'm trying to emulate the behavior of Element#select and $$ as much as possible. For all selectors the following should return an array of true values:

$$(selector).collect(function(element) {
  return element.match(selector);
});

Multiple arguments are allowed for cases like this:

$$('#p a', 'ul#list li').collect(function(element) {
  return element.match('#p a', 'ul#list li');
});

$$('span', { match: function(element) {return true;}}).collect(function(element) {
  return element.match('span', { match: function(element) { return true }});
});

Granted, these are edge cases, but I think it's important that Element#select & Element#match are as similar as possible. Otherwise the differences in behavior can be quiet confusing.

03/11/08 18:28:32 changed by kangax

Supporting commas and multiple arguments seems a little redundant, don't you think? Both have the same meaning - check if element matches against *ANY* of the expressions. $$ just happens to support both (I believe "commas" were added later on)

I'd rather remove multiple arguments from #match in favor of speed.

// both return same results
$$('div', 'span');
$$('div, span');

element.match('foo, bar', 'baz, qux, quux'); // <= why do this?
element.match('foo, bar, baz, qux, quux'); // <= if we could simply do this

03/11/08 21:03:05 changed by metavida

I agree, multiple arguments does seem a bit redundant, and off the top of my head I'd have a hard time making a really strong case for it. My main concern is that it can be confusing that $$ and Element#match take different arguments. That said, I've got another implementation to propose:

  match: function(element, selector) {
    if (!(element = $(element))) return;
    return Object.isString(selector) 
      ? selector.split(',').detect(function(selector) {
        return (new Selector(selector)).match(element);
      })
      : selector.match(element);
  },

Running it through the benchmarks, it is only slightly slower than the original in testBenchmarkMatchSingleSelectorm runs at the same speed as testBenchmarkMatchCustom, and retains speed with comma-separated selectors tests as well.

The main beef I have with your implementation is that internally Selector.matchElements calls $$. Meanwhile Selector#match only searches the entire DOM tree if it absolutely has to, giving it a potential speed & memory footprint advantage. Additionally, using detect avoids unnecessarily comparing patterns. If the first pattern matches the method returns true without testing further.

03/11/08 21:04:22 changed by metavida

Sorry, one more tweak (replace String#split with Selector.split):

match: function(element, selector) {
    if (!(element = $(element))) return;
    return Object.isString(selector) 
      ? Selector.split(selector).detect(function(selector) {
        return (new Selector(selector)).match(element);
      })
      : selector.match(element);
  },

03/11/08 21:36:14 changed by kangax

Absolutely agree. I thought about $$ but the solution just seemed elegant and small. Well, now we just need to make sure boolean is always returned (using !!)

match: function(element, selector) {
  if (!(element = $(element))) return;
  return Object.isString(selector) 
    ? !!Selector.split(selector).find(function(selector) {
      return new Selector(selector).match(element);
    })
    : selector.match(element);
}

I was also playing with an "optimized" but a little more "cryptic" version:

match: function(element, selector) {
  if (!(element = $(element))) return;
  if (!Object.isString(selector))
    return selector.match(element);
  for (var i=0, arr = Selector.split(selector), l = arr.length; i<l; i++) {
    return new Selector(arr[i]).match(element);
  }
  return false;
}

Seems to be ~3 times faster : )

03/11/08 21:40:02 changed by kangax

Opps, that should have been:

match: function(element, selector) {
  if (!(element = $(element))) return;
  if (!Object.isString(selector))
    return selector.match(element);
  for (var i=0, arr = Selector.split(selector), l = arr.length; i<l; i++) {
    if (new Selector(arr[i]).match(element)) return true;
  }
  return false;
}

03/11/08 21:50:01 changed by metavida

I haven't had a chance to run it through benchmarks yet, but that last, "cryptic" implementation of yours looks the most promising to me. Does the 3x speed benefit come using a for loop instead of detect?

03/11/08 21:56:04 changed by kangax

Of course. Optimized for(;;) is almost always much faster than any enumerable method (take a look at #map/#detect implementation which in turn rely on #each). Some critical parts of prototype use plain loops to squeeze the most out of performance, while enumerables are used where speed is not much of a bottleneck.

03/12/08 13:12:55 changed by metavida

  • attachment element_match_with_commas_2.patch added.

match implementation by kangax - http://dev.rubyonrails.org/ticket/10907#comment:13