gsr has asked for the wisdom of the Perl Monks concerning the following question:

I am trying to create a perl script, that reads a list of names from a text file, and randomly couples them. So, it must be random, and after a name is used once, it cannot be used again. Here is the code I have written: File: random-couples.pl
#!/usr/bin/perl srand(); # initiate random generator for rand() command $totalline = 0; # var for hold total number of lines (names); start +s it at 0 if (!$ARGV[0] || !$ARGV[1]) { &usage; } else { &openread; &compile_list; &makegroups; } # Usage Subroutine. Displayed if program not used properly sub usage { print "Random Couple Generator\n"; print "\n Usage \n"; print "./random-couples.pl name-file target-file\n"; print "Example: ./random-couples friends couples\n"; print "\t Where friends is your list of friends, and couples is the new list of couples\n\n"; } # opens specifed read file, and processes it to be parsed sub openread { open (READ, "< $ARGV[0]") or die "Cant open $ARGV[0] : $!"; while(<READ>) { s/#.*//; # ignore comments (remove them) next if /^(\s)*$/; # skip blank lines s/\d//g; # remove any numbers in the text file s/\.//g; # remove periods chomp; # remove newline (\n) push @file_contents, $_;# push line into array } } # calculates how many lines are in the file sub randomize { while (@file_contents) { $totalline++; # find total number of names } } # create array with random names sub compile_list { foreach $names (@file_contents) { &randomize; # find total number of names $ng = int(rand($totalline)); # random number from 0 + to $totalline, made into an integer for array $finalizedlist[$tmp] = $file_contents[$ng]; # set finalized +list element $tmp to random number splice(@file_contents, $ng, $ng); # take person out $tmp++ # goto next value of finalizedli +st array } } # open write file, write two random people, close write file sub makegroups { $fark = 0; $fark2 = $fark + 1; while ($fark2 != $totalline) { open (WRITE, ">> $ARGV[1]") or die "Can\'t open $ARGV[1] : $!" +; print WRITE "Group $fark: ".$finalizedlist[$fark]." and ".$fin +alizedlist[$fark2]." are partners\n"; close WRITE; $fark++; $fark2 = $fark + 1; } }
When I execute the code on a text file containing: File: names
1.bobby 2.jane 3.charleen 4.markus 5.gabriel 6.Alex
with the command perl random-couples.pl names results then the results file is filled with crap: File: Results
Group 0: Alex and are partners Group 1: and are partners Group 2: and are partners Group 3: and bobby are partners Group 4: bobby and are partners Group 5: and are partners and then it goes like Group 5, until it reaches 24.
any help or suggestions are much appreciated

Replies are listed 'Best First'.
Re: Random Couple Script
by Zaxo (Archbishop) on Nov 09, 2004 at 03:38 UTC

    You can do that by shuffling an array of names and coupling neighbors in the result. I've omitted the file I/O parts for clarity.

    This imports an excellent shuffle() function, use List::Util 'shuffle'; This reads an array full of the names with numbers and dot stripped, and then strips newlines, too,

    my @friends = map { (split /\./)[1] } <DATA>; chomp @friends;
    Shuffle them and stuff them into a hash, which naturally pairs them,
    @friends = shuffle @friends; my %couple = (@friends);
    Finally, print the pairs,
    for (keys %couple) { print "$_ and $couple{$_} are partners\n"; }
    Here's the "file" that code reads. To read from a named file, instead, just do as you did above.
    __DATA__ 1.bobby 2.jane 3.charleen 4.markus 5.gabriel 6.Alex

    Perl data structures and the shuffle function make this a lot easier.

    After Compline,
    Zaxo

Re: Random Couple Script
by tachyon (Chancellor) on Nov 09, 2004 at 03:48 UTC

    The typical approach would be to read the data into an array, shuffle it, then just pull off the elements (in this case pairs)

    # simulate chomp( my @people = <FILE> ); my @people = qw( bobby jane charlen markus gabriel alex ); fisher_yates_shuffle(\@people); printf "Pair %s %s\n", shift(@people),shift(@people) while @people; sub fisher_yates_shuffle { my $array = shift; for (my $i = @$array; --$i; ) { my $j = int rand ($i+1); next if $i == $j; @$array[$i,$j] = @$array[$j,$i]; } }

    cheers

    tachyon

Re: Random Couple Script
by Roy Johnson (Monsignor) on Nov 09, 2004 at 03:43 UTC
    Here's how I'd write it:
    # Read names, extracting what's after the dot and before the end of li +ne my @names = map /\.(.*)/, <DATA>; # Shuffle the list use List::Util 'shuffle'; @names = shuffle(@names); # Print them out two by two for (1..@names/2) { print "Group $_: $names[$_*2-2] and $names[$_*2-1] are partners\n"; } __DATA__ 1.bobby 2.jane 3.charleen 4.markus 5.gabriel 6.Alex

    Caution: Contents may have been coded under pressure.
Re: Random Couple Script
by BrowserUk (Patriarch) on Nov 09, 2004 at 04:30 UTC

    He he! But is it fair?

    #! perl -slw use strict; local $, = ' '; my @array; splice @array, rand( @array ), 0, $_ for map{ /^..(.*$)/ } <DATA>; my %partners = @array; print %partners; __DATA__ 1.bobby 2.jane 3.charleen 4.markus 5.gabriel 6.Alex

    Examine what is said, not who speaks.
    "Efficiency is intelligent laziness." -David Dunham
    "Think for yourself!" - Abigail
    "Memory, processor, disk in that order on the hardware side. Algorithm, algorithm, algorithm on the code side." - tachyon
      Heh, this code is the most interesting of the lot since it *seems* wrong.

      If the splice line had been

      splice @array, rand(1+@array ), 0, $_ for map{ /^..(.*$)/ } <DATA>;
      it would obviously have been fair. Now it seems to have a slight preference for left, so that makes you worry that e.g. maybe the last two elements don't get paired often enough. But that impression is wrong. The bias only causes the order inside one pair to be lopsided. The element inserted first becomes the last element in @array, and will remain last during the loop. The other n-1 elements will get a fair shuffle. So after pairing the result is fair, but the first element inserted will always be last in its pair.
      Using splice to deconstruct the list, instead of to construct it:
      # Read names, chop off numbers my @names = map /\.(.*)/, <DATA>; # Pair them up randomly for (1..@names/2) { printf "Group $_: %s and %s are partners\n", splice(@names, rand(@na +mes), 1) , splice(@names, rand(@names), 1); }
      Update: using Thospel's idea, one of the splices could be replaced with a simple shift:
      printf "Group $_: %s and %s are partners\n", shift(@names), splice(@ +names, rand(@names), 1);

      Caution: Contents may have been coded under pressure.
Re: Random Couple Script
by jethro (Monsignor) on Nov 09, 2004 at 14:36 UTC
    If all you wanted was working code, the other replies should work nicely, but if you are interested to know what's wrong with your script, read on:

    1) When I execute the code, I don't get any output. That's because your function randomize has an endless loop.

    while (@file_contents) { $totalline++; }
    This code increments $totalline until @file_contents is empty. Since there is no code that empties the array, this will never happen.

    Probably you wanted to say 'foreach', so that the function tells you the size of @file_contents. But then the sub shouldn't be called 'randomize'. As a side note, you get the size of the array simply with 'scalar(@file_contents)'.

    2) The splice in sub compile_list takes not only person $ng out, but $ng persons begining at position $ng. Use 'splice(@file_contents,$ng,1);'

    By the way, in this sub a 'while (@file_contents)' as main loop would have been better, since you are finished when @file_contents is empty.

    3) in sub makegroups you select two adjacent names out of the list, but you increment $fark only by 1. Try it out, you will see that every name except the first will be printed twice.

    Ok, that's what I found out. If there is still something wrong with the script, put in 'print' statements to show you what is going on. The first thing I did with your script was to insert code like

    print "Position 1: @file_contents\n";
    between "interesting" lines in the sub compile_list to find out what it is doing. The endless loop was apparent immediately because the second print statement was never reached

Re: Random Couple Script
by thospel (Hermit) on Nov 09, 2004 at 11:39 UTC
    A direct way without explicit shuffle (only half the rand calls):
    #!/usr/bin/perl -w use strict; use warnings; my @people = map /\.(.*)/, <DATA>; my $group = 1; while (@people) { my $person1 = shift @people; my $pos = rand @people; my $person2 = $people[$pos]; $people[$pos] = $people[-1]; pop @people; printf "Group %d: %s and %s\n", $group++, $person1, $person2; } __DATA__ 1.bobby 2.jane 3.charleen 4.markus 5.gabriel 6.Alex
Re: Random Couple Script
by TedPride (Priest) on Nov 09, 2004 at 08:50 UTC
    Also using Fisher-Yates:
    use strict; use warnings; my ($x, $t); my @n = map (/\.(.*)/, <DATA>); for (0..($#n - 1)) { $x = int(rand() * ($#n - $_ + 1)) + $_; $t = $n[$x]; $n[$x] = $n[$_]; $n[$_] = $t; } for ($_ = 0; $_ < $#n; $_ += 2) { print "Group ".($_ / 2).": ".$n[$_]." and ".$n[$_+1]." are partner +s\n"; } __DATA__ 1.bobby 2.jane 3.charleen 4.markus 5.gabriel 6.Alex