Beefy Boxes and Bandwidth Generously Provided by pair Networks
Think about Loose Coupling
 
PerlMonks  

Inserting an element into an array after a certain element

by saintmike (Vicar)
on Mar 31, 2005 at 18:32 UTC ( [id://443923]=perlquestion: print w/replies, xml ) Need Help??

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?

Replies are listed 'Best First'.
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.

    Alberto Simões

      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."

      Ajusted to only do it once:

      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; } }
      Actually, I think this is along the lines of what I'm looking to do with something I'm working on.
Re: Inserting an element into an array after a certain element
by BrowserUk (Patriarch) on Mar 31, 2005 at 18:56 UTC

    Updated to address Roy Johnson's concerns below

    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?
      If the $before isn't found, though, it inserts $insert after the first element.

      Caution: Contents may have been coded under pressure.
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.
      There's no point in your returning the array; you're modifying it in-place. You just want to indicate success or failure.

      Agreed, but return the value of $first+1 to indicate that it was successful, and it may be of use to some caller (undef otherwise).

      --
      [ e d @ h a l l e y . c c ]

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.

      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.

        Getting a bit unreadable, though, I'd say. ;)

      The Perl6 version is about the same:

      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.

Re: Inserting an element into an array after a certain element
by Tanktalus (Canon) on Mar 31, 2005 at 19:02 UTC

    My offering (similar to above):

    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);

Re: Inserting an element into an array after a certain element
by gam3 (Curate) on Mar 31, 2005 at 19:44 UTC
    Doing it without splice.
    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.
      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.
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?).

    the lowliest monk

      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

        Oh, it breaks if any of the elements consists of more than one character:

        % 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

        the lowliest monk

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); }
Re: Inserting an element into an array after a certain element
by bobf (Monsignor) on Apr 01, 2005 at 04:17 UTC

    A variation using index (you didn't say it had to be efficient!)

    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!)

      This doesn't work if you try it on a non-toy example where the array contains more than single-letter keys.
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...
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.
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..."

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.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://443923]
Approved by perlfan
Front-paged by perlfan
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others learning in the Monastery: (4)
As of 2024-04-26 01:13 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found