Ruby equivalent for perl multi-index sort?


(Rick Bradley) #1

Coming from a perl background I have a lot of programs which do
operations on arrays, including sorts. For example today I came across
one of mine where I wrote:

@filelist =
map { $->[0] . ($->[1] ? ‘.’.$_->[1] : ‘’) }
sort {$a->[0] <=> $b->[0] || $a->[1] <=> $b->[1] }
map { /(\d+)(?:.(\d+))?/; [ $1, $2 || 0 ] }
grep {/^\d+(?:.\d+)?$/}
readdir(INDIR);

It takes a list of files, extracts the numeric ones and orders them
numerically. If they have fractional parts it numbers them by treating
the fractional parts as integers (don’t ask why ;-).

The bulk of that is easy to rewrite the Ruby way, except for
the multi-index sort:

sort { $a->[0] <=> $b->[0]
>>
$a->[1] <=> $b->[1] }

The spaceship returns 0 when the first pair of elements are 0 and this
perl idiom takes advantage of the fact that 0 is false and therefore
’||’ will evaluate its second operand. It’s akin to saying “if the
first elements are the same then order on the second elements”.

In Ruby 0 isn’t false so the comparable Ruby expression won’t trigger
the comparison on later indices. I’ve been wondering if there’s a way
to efficiently implement this idiom in Ruby.

Ideas?

Rick

···


http://www.rickbradley.com MUPRN: 957 (82F/90F)
> like being trapped in
random email haiku | a video game. i woke
> up thrashing around.


(Pit) #2

(problem of chaining comparisons)

Try

Numeric#nonzero?

It’s designed for chaining comparisons. It returns nil for a zero argument and the
non-zero value otherwise.

Regards,
Pit


(Matthew Diephouse) #3

Rick Bradley wrote:

Coming from a perl background I have a lot of programs which do
operations on arrays, including sorts. For example today I came across
one of mine where I wrote:

@filelist =
map { $->[0] . ($->[1] ? ‘.’.$_->[1] : ‘’) }
sort {$a->[0] <=> $b->[0] || $a->[1] <=> $b->[1] }
map { /(\d+)(?:.(\d+))?/; [ $1, $2 || 0 ] }
grep {/^\d+(?:.\d+)?$/}
readdir(INDIR);

It takes a list of files, extracts the numeric ones and orders them
numerically. If they have fractional parts it numbers them by treating
the fractional parts as integers (don’t ask why ;-).

That code seems redundant, because you’re doing a seperate sort on the
fractions when you could instead use just one sort. Consider the
following functionally equivalent (and a lot more efficient) perl:

@filelist =
sort { $a <=> $b }
grep {/^\d+(?:.\d+)?$/}
readdir(INDIR);

That would also making translating it to ruby a lot easier.

md |- m:att d:iephouse

(WATANABE Hirofumi) #4

Hi,

Rick Bradley rick@rickbradley.com writes:

:@filelist =
: map { $->[0] . ($->[1] ? ‘.’.$_->[1] : ‘’) }
: sort {$a->[0] <=> $b->[0] || $a->[1] <=> $b->[1] }
: map { /(\d+)(?:.(\d+))?/; [ $1, $2 || 0 ] }
: grep {/^\d+(?:.\d+)?$/}
: readdir(INDIR);

filelist = Dir.open(".").
grep(/^\d+(?:.\d+)?$/).
map {|x| x =~ /^(\d+)(?:.(\d+))?/; [ Integer($1), Integer($2) ]}.
sort {|a, b| (a[0] <=> b[0]).nonzero? || a[1] <=> b[1]}.
map {|x| x[0].to_s + (x[1] != 0 ? ‘.’ + x[1].to_s : ‘’)}

:In Ruby 0 isn’t false so the comparable Ruby expression won’t trigger
:the comparison on later indices. I’ve been wondering if there’s a way
:to efficiently implement this idiom in Ruby.
:
:Ideas?

The Schwartzian Transform:

filelist = Dir.open(".").
grep(/^\d+(?:.\d+)?$/).
map {|x| x =~ /^(\d+)(?:.(\d+))?/; [ Integer($1), Integer($2), x ]}.
sort.
map {|x| x.last}

You can use Enumerable#sort_by in Ruby 1.7.

filelist = Dir.open(".").
grep(/^\d+(?:.\d+)?$/).
sort_by {|x| x =~ /^(\d+)(?:.(\d+))?/; [ Integer($1), Integer($2) ]}

···


eban
filelist = Dir.open(".").grep(/^\d+(?:.\d+)?$/).sort_by {|x| x.to_f}


(Rick Bradley) #5

That code seems redundant, because you’re doing a seperate sort on the
fractions when you could instead use just one sort. Consider the
following functionally equivalent (and a lot more efficient) perl:

@filelist =
sort { $a <=> $b }
grep {/^\d+(?:.\d+)?$/}
readdir(INDIR);

Except for two things:

(1) the results are different:

$ perl -e '@a=(1,2,“2.1”,“2.10”, “2.2”, 3); @a = sort { $a <=> $b } @a; print join(", “, @a).”\n";'
1, 2, 2.1, 2.10, 2.2, 3

whereas the other sort will produce:

1, 2, 2.1, 2.2, 2.10, 3

on the same input. It’s not a numeric sort even though the strings
look like fractions (think of them as document section numbers, which
is quite similar to what they represented in the problem domain).

(2) I asked the question primarily to find out how to convert

  sort { foo($a) <=> foo($b) || bar($a) <=> bar($b) || ... }

into a Ruby idiom.  Were that example reducible to a simple sort
there would still be plenty of similar cases which won't be.

Thanks to Pit’s suggestion I converted the original perl expression to
the Ruby equivalent:

indir.entries.grep(/^\d+(?:.\d+)?$/).collect{ |x|
x += ‘.0’ unless x =~ /./
x
}.sort{ |a,b|
(a.to_i <=> b.to_i).nonzero? ||
(a.sub(/^../, ‘’).to_i <=> b.sub(/^../,’’).to_i)
}.collect{|x|
x.sub(/.0$/, ‘’)
x
}

Rick

···


http://www.rickbradley.com MUPRN: 10 (90F/97F)
> compare the data
random email haiku | from the real-time output and
> data from these files.


(Rick Bradley) #6

filelist = Dir.open(".").
grep(/^\d+(?:.\d+)?$/).
map {|x| x =~ /^(\d+)(?:.(\d+))?/; [ Integer($1), Integer($2) ]}.
sort {|a, b| (a[0] <=> b[0]).nonzero? || a[1] <=> b[1]}.
map {|x| x[0].to_s + (x[1] != 0 ? ‘.’ + x[1].to_s : ‘’)}
[…]
filelist = Dir.open(".").
grep(/^\d+(?:.\d+)?$/).
map {|x| x =~ /^(\d+)(?:.(\d+))?/; [ Integer($1), Integer($2), x ]}.
sort.
map {|x| x.last}
[…]
filelist = Dir.open(".").
grep(/^\d+(?:.\d+)?$/).
sort_by {|x| x =~ /^(\d+)(?:.(\d+))?/; [ Integer($1), Integer($2) ]}
[…]
filelist = Dir.open(".").grep(/^\d+(?:.\d+)?$/).sort_by {|x| x.to_f}

Thanks!

It amazes me how similar these can be written to perl’s constructs in a
language which is so object-oriented.

Rick

···


http://www.rickbradley.com MUPRN: 404 (92F/98F)
> lunches. please pay soon
random email haiku | or else you may starve. haha
> TGIT see ya later.