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

Hi monks. My math is somewhat rusty, so I need your help converting some data:

I'm reading a flat file database into arrays; One condition I need to convert (after successfully reading it) is if one of the elements is "weekly". If so, $days_mask[$i] is the sum of 2**x (that's 2 to the power of x) + 2**x (could repeat up to 7 times, one each day, or it could be just one day), where x is the value of the day - Sunday = 0, Monday = 1, etc.

Some sample data: $days_mask[$i] = 10

This corresponds to an event repeating on Monday and Wednesday (2**1 + 2**3).

What would the algorithm look like in perl to extract the individual days from the above information?? Day number would suffice (eg, 1 and 3 for Monday and Wednesday respectively).

Thanks.

Replies are listed 'Best First'.
Re: help with algorithm
by Zaxo (Archbishop) on Nov 17, 2002 at 00:37 UTC

    Think of this in terms of bits, 2 ** $x == 1 << $x. That allows you to decode with bitwise operations. Here's a simple method that uses a hash to translate between names and numbers.

    our %daycode = ( Sunday => 1, Monday => 2, Tuesday => 4, Wednesday => 8, Thursday => 16, Friday => 32, Saturday => 64, ); our %codeday; @codeday{ values(%daycode)} = keys %daycode; sub days { my $mask = shift || 127; sort { $daycode($a} <=> $daycode{$b} } grep { $mask & $daycode{$_} } keys %daycode; }
    by default, days() returns all the days of the week if no argument is given.

    After Compline,
    Zaxo

      OK, this seems to almost work; I want to make sure I'm using this correctly; I'm trying:
      #!/usr/bin/perl $test = 12; print "Days mask is $test.\n"; our %daycode = ( Sunday => 1, Monday => 2, Tuesday => 4, Wednesday => 8, Thursday => 16, Friday => 32, Saturday => 64, ); our %codeday; @codeday{ values(%daycode)} = keys %daycode; sub days { my $mask = shift || 127; sort { $daycode{$a} <=> $daycode{$b} } grep { $mask & $daycode{$_} } keys %daycode; } @newtest = days($test); foreach $num (@newtest) { print "$newtest[$num]\n"; }
      If I try 1 value for $test, like 4, it accurately returns Tuesday. However, if I use a complex value, like say 12, it returns Tuesday, Tuesday (where it should be Tuesday, Wednesday).

      Am I doing something wrong in interpreting this?

        That code is working fine. You just make an error in the way you check the returned values.

        @newtest = days($test); foreach $num (@newtest) { print "$newtest[$num]\n"; }

        The array, @newtest, is assigned ('Tuesday', 'Wednesday') as expected but you iterate over it and use each element as an index back into @newtest. You are printing $newtest['Tuesday'] and $newtest['Wednesday'] but both 'Tuesday' and 'Wednesday' evaluate to 0 in numerical context. So, You print $newtest[0] twice and $newtest[0] holds 'Tuesday'. That's why you print 'Tuesday' twice.

        Try writing that loop as:

        foreach $day (@newtest) { print $day,"\n"; }
        -sauoq
        "My two cents aren't worth a dime.";
        

        Sub days() returns a list of names of days, just try: print "@newtest",$/; or

        for my $day (@newtest) { print $day, $/; }
        You are indexing with strings which numify to zero, so you get $newtest["Wednesday"] as $newtest[0].

        After Compline,
        Zaxo

Re: help with algorithm
by BrowserUk (Patriarch) on Nov 17, 2002 at 01:37 UTC

    Yet another way. (Corrected ordering of days)

    #! perl -slw use strict; sub getDays{ qw( Sunday Monday Teusday Wednesday Thurday Friday Saturday ) [ grep{ $_[0] & (1 << $_) } 0..6 ] } print "@{[getDays $_]}" for 1 .. 127; __END__ C:\test>213473 Monday Teusday Monday Teusday Wednesday Monday Wednesday Teusday Wednesday Monday Teusday Wednesday ...118 omitted... Teusday Wednesday Thurday Friday Saturday Sunday Monday Teusday Wednesday Thurday Friday Saturday Sunday C:\test>
Re: help with algorithm
by larsen (Parson) on Nov 17, 2002 at 00:23 UTC
    First you convert $days_mask[$i] from decimal to binary: look here for an example (and it could be a good opportunity to dig into the mysteries of pack). Then you use the string of binary digits you obtained to inspest the "weeklyness" of an event.
    In your example, 10 is 1010 in binary: the 1 in the second position (position number 1, since you count from 0) tells that the event repeats every Monday. The same "trick" for the 1 in the third position.
      Ok, maybe I'm a little dense: if I understand you correctly, I should be able to determine "monday" from the 1 in the first position of the binary number "1010". However, how does this work for other values, like say, Tuesday, which would show up as 4, which is 100 in binary??

        Maybe this will make things clearer...

        my $Sun = 2**0; # That's 1 or 00000001 in binary. my $Mon = 2**1; # That's 2 or 00000010 in binary. my $Tue = 2**2; # That's 4 or 00000100 in binary. my $Wed = 2**3; # That's 8 or 00001000 in binary. my $Thu = 2**4; # That's 16 or 00010000 in binary. my $Fri = 2**5; # That's 32 or 00100000 in binary. my $Sat = 2**6; # That's 64 or 01000000 in binary. my $mask = 19; # For example. 00010011 in binary. print "Sunday\n" if ($mask & $Sun); # True print "Monday\n" if ($mask & $Mon); # True print "Tuesday\n" if ($mask & $Tue); print "Wednesay\n" if ($mask & $Wed); print "Thursday\n" if ($mask & $Thu); # True print "Friday\n" if ($mask & $Fri); print "Saturday\n" if ($mask & $Sat);
        -sauoq
        "My two cents aren't worth a dime.";
        
        The first example is 1010, that is (I'm considering the binary digits from right to left):

        0 * 20 + 1 * 21 + 0 * 22 + 1 * 23

        Your second example is 100. As usual, from right to left...

        0 * 20 + 0 * 21 + 1 * 22

        Got the pattern? Now I'm going to check what powers of 2 are on (i.e. multiplied by 1). In the first example they are 21 (Monday) and 23 (Wednesday). In the second example only 22, Tuesday.

        I hope this will clarify my node.
Re: help with algorithm
by sauoq (Abbot) on Nov 17, 2002 at 01:14 UTC

    Your variable name, $days_mask, gives a clue. You are working with a bitmask. As Zaxo pointed out, you need to use a bitwise-and to extract the value of individual bits. If you just want the day numbers you might do it like this:

    my @day_numbers = grep $days_mask[$i] & 2**$_, 0..6;
    -sauoq
    "My two cents aren't worth a dime.";
    
Re: help with algorithm
by pg (Canon) on Nov 17, 2002 at 01:20 UTC
    try this, it tested, it works:
    for (1 .. 7) {push @days, $_ if ((2 ** $_) & $day_mask)}
      for (1 .. 7) {push @days, $_ if ((2 ** $_) & $day_mask)}

      You didn't test it very well. You need 0..6 not 1..7.

      -sauoq
      "My two cents aren't worth a dime.";
      
        Oops, I check back, he said Sunday = 0, you are right.