saintmike has asked for the wisdom of the Perl Monks concerning the following question:
Hey fellow monks,
here's an easy problem, but I'm looking for an elegant solution, high in readability, low in use of additional modules.
How to insert a new element right after a given element in an array?
my @arr = qw(a b c d c e f);
insert_after_first(\@arr, "c", "x");
should result in @arr containing
a b c x d c e f
Note that it shouldn't be touching duplicate entries (like the second "c"). An obvious solution would be
sub insert_after_first {
my($arr, $element, $insert) = @_;
for(my $idx=0; $idx < @$arr; $idx++) {
if($arr->[$idx] eq $element) {
splice @$arr, $idx+1, 0, $insert;
return @$arr;
}
}
}
but that's nowhere as perlish or short as I'd like it to be.
Who's up for the challenge?
Re: Inserting an element into an array after a certain element
by ambs (Pilgrim) on Mar 31, 2005 at 18:40 UTC
|
Say, you have "a,b,c,d,e" and want a "f" after "c":
@array = map { $_ eq "c" ? ("c","f") : $_ } @array
but this would insert "f" for each "c" you have.
| [reply] [d/l] |
|
You could easily fix that by adding a counter:
@array = map { $_ eq "c" && !$found++ ? ("c","f") : $_ } @array
(as a sub)
sub insert_after_first {
my $arr_ref = shift;
my $to_insert = shift;
my $insert_after_me = shift;
my $found = 0;
map { $_ eq $insert_after_me && !$found++ ? ( $insert_after_me, $to
+_insert ) : $_ } @$arr_ref;
}
--------------
"But what of all those sweet words you spoke in private?"
"Oh that's just what we call pillow talk, baby, that's all."
| [reply] [d/l] [select] |
|
my $first = 1;
@array = map {
if ($first && $_ eq "c") {
$first = 0;
($_, "x")
} else {
$_
}
} @array;
hum, ugly. How about:
my $first = 1;
@array = map {
my @a = $_;
push(@a, "x") if ($first && $_ eq "c");
$first ||= @a-1;
@a
} @array
Nope, still ugly. Well, you could always do:
foreach (0..$#array) {
if ($array[$_] eq "c") {
splice(@array, $_+1, 0, "x");
last;
}
}
| [reply] [d/l] [select] |
|
Actually, I think this is along the lines of what I'm looking to do with something I'm working on.
| [reply] |
Re: Inserting an element into an array after a certain element
by BrowserUk (Patriarch) on Mar 31, 2005 at 18:56 UTC
|
sub insertAfter{
use List::Util qw[ first ];
my( $aref, $before, $insert ) = @_;
my $first = first{ $aref->[ $_ ] eq $before } 0 .. $#$aref ;
return unless defined $first;
splice @{ $aref }, ++$first, 0, $insert;
}
Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
Lingua non convalesco, consenesco et abolesco.
Rule 1 has a caveat! -- Who broke the cabal?
| [reply] [d/l] |
|
| [reply] [d/l] [select] |
Re: Inserting an element into an array after a certain element
by Roy Johnson (Monsignor) on Mar 31, 2005 at 19:05 UTC
|
This might be a modest improvement:
use List::Util 'first';
sub insert_after_first {
my ($arr, $element, $insert) = @_;
if (my $first = first {$arr->[$_] eq $element} 0..$#$arr) {
splice @$arr, $first+1, 0, $insert;
return 1;
}
}
There's no point in your returning the array; you're modifying it in-place. You just want to indicate success or failure.
Caution: Contents may have been coded under pressure.
| [reply] [d/l] |
|
| [reply] [d/l] [select] |
Re: Inserting an element into an array after a certain element
by kelan (Deacon) on Mar 31, 2005 at 20:15 UTC
|
If elegance is more important than effeciency, try this recursive version:
sub insert_after_first {
return if @_ < 3;
my ( $elem, $new, $front ) = splice @_, 0, 3;
return ( $front, $new, @_ ) if $front eq $elem;
return ( $front, insert_after_first( $elem, $new, @_ ) );
}
Otherwise, I'd say just keep the version that you've got, although I'd change the loop initialization to for (0 .. $#arr), like ikegami shows above.
| [reply] [d/l] |
|
Maybe factor out the common $front in each return statement to make one return statement:
sub insert_after_first {
return if @_ < 3;
my ( $elem, $new, $front ) = splice @_, 0, 3;
return ( $front,
$front eq $elem
? ($new, @_ )
: insert_after_first( $elem, $new, @_ )
);
}
Update: Oo! In fact, you don't do anything with $new, except stick it back on the front of @_, so don't take it off:
sub insert_after_first {
return if @_ < 3;
my ( $elem, $front ) = (splice(@_, 0, 1), splice(@_, 1, 1));
return ( $front,
$front eq $elem
? @_
: insert_after_first( $elem, @_ )
);
}
Then, to change Lispish elegance to Perlish elegance (or madness), make your argument list the way you want it for the recursion, and use & without parentheses to recurse:
sub insert_after_first {
return if @_ < 3;
my $front = splice(@_, 2, 1);
return ( $front,
$front eq $_[0]
? @_[1..$#_]
: &insert_after_first
);
}
Caution: Contents may have been coded under pressure.
| [reply] [d/l] [select] |
|
| [reply] |
|
sub insert_after_first( $elem, $new, *@data ) {
return if @data.elems < 1;
my $front = @data.shift;
return ( $front, $new, *@data ) if $front ~~ $new;
return ( $front, insert_after_first( $elem, $new, *@data ) );
}
I thought the parameter naming being specified in the declaration would help, but you still neet to shift the front element off of the data list for comparison or it gets real ugly.
| [reply] [d/l] |
Re: Inserting an element into an array after a certain element
by Tanktalus (Canon) on Mar 31, 2005 at 19:02 UTC
|
use strict;
my @arr = qw(a b c d c e f);
insert_after_first(\@arr, "c", "x");
sub insert_after_first
{
my $arr = shift;
my $before = shift;
my @new = @_;
@$arr = map {
defined $before && $_ eq $before ?
do { $before = undef; @new } :
$_
} @$arr
}
use Data::Dumper;
print Dumper(\@arr);
| [reply] [d/l] |
Re: Inserting an element into an array after a certain element
by gam3 (Curate) on Mar 31, 2005 at 19:44 UTC
|
sub insert_after_first {
my ($element, $insert, @array) = @_;
my @ret = ();
while (push(@ret, shift @array) && $ret[-1] ne $element && @array)
{};
# (@ret, $insert, @array);
(@ret, scalar @array ? $insert : (), @array);
}
print join(' ', insert_after_first('c', 'x',
qw ( a b c d c e f ))), "\n";
print join(' ', insert_after_first('q', 'x',
qw ( a b c d c e f ))), "\n";
Update: fixed per Roy Johnson
-- gam3
A picture is worth a thousand words, but takes 200K.
| [reply] [d/l] |
|
I still don't think you should insert if the elment you specify isn't there.
sub insert_after_first {
my ($element, $insert, @array) = @_;
my $hit;
map {($_, ($_ eq $element and !$hit++) ? $insert : ())} @arr;
}
Caution: Contents may have been coded under pressure.
| [reply] [d/l] |
Re: Inserting an element into an array after a certain element
by tlm (Prior) on Mar 31, 2005 at 21:23 UTC
|
% perl -le 'print"@{[split//,join q(x),split/(?<=c)/,join(q(),qw(a b c
+ d c e f)),2]}"'
a b c x d c e f
Pretty perverse (perl-verse?).
| [reply] [d/l] |
|
This works for the example given, but will break if one of the elements contain white-space.
CountZero "If you have four groups working on a compiler, you'll get a 4-pass compiler." - Conway's Law
| [reply] |
|
% perl -le 'print"@{[split//,join q(x),split/(?<=c)/,join(q(),qw(a b c
+ d c e foo f)),2]}"'
a b c x d c e f o o f
but single spaces are fine:
% perl -le 'print"@{[split//,join q(x),split/(?<=c)/,join(q(),qw(a b c
+ d c e),q( ),q(f)),2]}"'
a b c x d c e f
To fix this, I guess one could do something like
perl -le 'print"@{[split/\0/,join qq(\0x),split/(?<=c)/,join(qq(0),qw(
+a b c d c e foo f)),2]}"'
a b c x d c e foo f
| [reply] [d/l] [select] |
Re: Inserting an element into an array after a certain element
by tall_man (Parson) on Apr 01, 2005 at 00:52 UTC
|
I haven't seen a solution with "grep" yet, which feels natural to me for this problem:
sub insert_after_first {
my ($arr, $find, $insert) = @_;
my $found = 0;
my ($pos) = grep { $arr->[$_] eq $find and !$found++ } 0..$#$arr;
splice @$arr, $pos+1, 0, $insert if $found;
}
Update: A shorter, slightly neater variant:
sub insert_after_first {
my ($arr, $find, $insert) = @_;
my ($pos) = grep { $arr->[$_] eq $find } 0..$#$arr;
splice @$arr, $pos+1, 0, $insert if defined($pos);
}
| [reply] [d/l] [select] |
Re: Inserting an element into an array after a certain element
by bobf (Monsignor) on Apr 01, 2005 at 04:17 UTC
|
sub insert_after_first
{
my ( $a_ref, $afterthis, $insertme ) = @_;
my $pos = index( join( '', @$a_ref ), $afterthis );
splice( @$a_ref, $pos+1, 0, $insertme ) if $pos >= 0;
}
Update: As tall_man mentioned, this only works when the elements of the array are single characters, because the offset for splice represents the number of chars from the beginning of the string/array (rather than the number of individual array elements). Thanks for pointing it out. (I still thought it was a neat idea for the sample data, though!)
| [reply] [d/l] |
|
This doesn't work if you try it on a non-toy example where the array contains more than single-letter keys.
| [reply] |
Re: Inserting an element into an array after a certain element
by Anonymous Monk on Apr 01, 2005 at 02:17 UTC
|
An obvious solution would be
snip
but that's nowhere as perlish or short as I'd like it to be.
Boring and obviously correct beats clever and potentially wrong, at least in my books. There's nothing like complexity to introduce bugs into software...
| [reply] |
Re: Inserting an element into an array after a certain element
by .ib (Initiate) on Apr 01, 2005 at 16:06 UTC
|
Combining map and ?: you can write this sub in one command:
sub insert_after_first {
my ($count, $arr, $elt, $ins) = (0, @_);
return map {($_ eq $elt and $count++ == 0) ? ($_,$ins) : $_} @$arr;
}
Simply, yeah? And perlish enough, I hope. | [reply] [d/l] |
Re: Inserting an element into an array after a certain element
by DrWhy (Chaplain) on Apr 01, 2005 at 15:16 UTC
|
Okay, I gotta throw my hat in the ring here...
sub insert_after_first {
my ($ndx, $arr, $elt, $ins) = (0, @_);
$ndx > @$arr and return while $arr->[$ndx++] ne $elt;
splice @$arr, $ndx, 0, $ins;
}
Update: Here's a version that is a little more
efficient, removing one boolean comparison from the loop.
sub insert_after_first {
my ($ndx, $arr, $elt, $ins) = (0, @_);
$arr->[$ndx++] eq $elt and last for 0 .. @$arr;
splice @$arr, $ndx, 0, $ins if $ndx <= @$arr;
}
--DrWhy
"If God had meant for us to think for ourselves he would have given us brains. Oh, wait..."
| [reply] [d/l] [select] |
Re: Inserting an element into an array after a certain element
by Anonymous Monk on Apr 01, 2005 at 17:34 UTC
|
splice() is fun. (Pls. to pardon ugly.)
#!/usr/bin/perl
@a=("a","b","c","b","a");
$targ="b";
$ins="d";
THINGY: foreach(@a) {
$_ =~ $targ && do {
splice(@a,$i,1,($_, $ins));
last THINGY
};
$i++;
};
print join ", ", @a;
print "\n"
Output:
a, b, d, c, b, a
It's not entirely cool; it does take an iterator, which I accept is fairly cheesy, but it does the job in a single foreach() that probably won't even make it to the end of the array. | [reply] [d/l] [select] |
|
|