Perl 6 .polymod: Break Up a Number Into Denominations

2016-05-18 | 1380 words | Everything about .polymod method.

Back in the day, I wrote Perl 5 module Number::Denominal that breaks up a number into "units," say, 3661 becomes '1 hour, 1 minute, and 1 second'. I felt it was the pinnacle of achievement and awesome to boot. Later, I ported that module to Perl 6, and recently I found out that Perl 6 actually has .polymod method built in, which makes half of my cool module entirely useless.

Today, we'll examine what .polymod does and how to use it. And then I'll talk a bit about my reinvented wheel as well.

Denominated

The .polymod method takes a number of divisors and breaks up its invocant into pieces:

my $seconds = 1 * 60*60*24 # days
            + 3 * 60*60    # hours
            + 4 * 60       # minutes
            + 5;           # seconds

say $seconds.polymod: 60, 60;
say $seconds.polymod: 60, 60, 24;

# OUTPUT:
# (5 4 27)
# (5 4 3 1)

The divisors we pass as arguments in this case are time related: 60 (seconds per minute), 60 (minutes per hour), and 24 (hours in a day). From the smallest unit, we're progressing to the largest one, with the numbers being how many of the unit in question fit into the next larger unit.

Matching up the output to the expression we assigned to $seconds we can see that output also progresses—same as input divisors—from smallest unit to largest: 5 seconds, 4 minutes, 3 hours, and 1 day.

Notice how in the first call, we did not specify a divisor for hours-in-a-day, and so we got our days expressed as hours (24 hours for one day, plus the 3 hours we had originally). So this form of .polymod simply uses up all the divisors and the number of returned items is one more than the number of given divisors.

Handmade

Another code example useful to understanding of .polymod is one showing the previous calculation done with a loop instead, without involving .polymod:

my $seconds = 2 * 60*60*24 # days
            + 3 * 60*60    # hours
            + 4 * 60       # minutes
            + 5;           # seconds

my @pieces;
for 60, 60, 24 -> $divisor {
    @pieces.push: $seconds mod $divisor;
    $seconds div= $divisor
}
@pieces.push: $seconds;

say @pieces;

# OUTPUT:
# [5 4 3 2]

For each of the divisors, we take the remainder of integer division of $seconds and the divisor being processed and then change the $seconds to the integer divison between $seconds and that divisor.

To Infinity and Beyond!

Perl 6 is advanced enough to have infinite things in it without blowing up and that's accomplished with lazy lists. When the divisors given to .polymod method are in a lazy list, it'll run until the remainder is zero and not through the whole list:

say 120.polymod:      10¹, 10², 10³, 10⁴, 10⁵;
say 120.polymod: lazy 10¹, 10², 10³, 10⁴, 10⁵;
say 120.polymod:      10¹, 10², 10³ … ∞;

# OUTPUT:
# (0 12 0 0 0 0)
# (0 12)
# (0 12)

In the first call, we have a series of numbers increasing by a power of 10. The output of that call includes 4 trailing zeros, because .polymod evaluated each divisor. In the second call, we explicitly create a lazy list using lazy keyword and now we have just two items in the returned list.

The first divisor (10) results in zero remainder, which is our first item in the returned list, and integer division changes our 120 to just 12 for the next divisor. The remainder of division of 12 by 100 is 12, which is our second item in the returned list. Now, integer division of 12 by 100 is zero, which stops the execution of .polymod and gives us our two-item result.

In the last call, we use an ellipsis, which is the sequence operator, to create the same series of numbers increasing by a power of 10, except this time that series is infinite. Since it's lazy, the result is, once again, just two elements.

Zip It, Lock It, Put It In The Pocket

Numbers alone are great and all, but aren't too descriptive about the units they represent. Let's use a Zip meta operator, to fix that issue:

my @units  = <ng μg mg g kg>;
my @pieces = 42_666_555_444_333.polymod: 10³ xx ∞;

say @pieces Z~ @units;
# OUTPUT:
# (333ng 444μg 555mg 666g 42kg)

For the purposes of our calculation, I'll be breaking up forty two trillion, six hundred sixty six billion, five hundred fifty five million, four hundred forty four thousand, three hundred and thirty three (😸) nanograms into several larger units.

We store unit names in the array @units. Then, we call .polymod on our huge number and give it an infinite list with number 1000 for each divisor and store what it gives us in @pieces.

The Zip meta operator one-by-one takes elements from lists on the left and right hand sides and applies to them the operator given to it. In this case, we're using the string concatenation operator (~), and thus our final result is a list of strings with numbers and units.

That Denominated Quickly

You're not limited to just Ints for both the invocant and the divisors, but can use others too. In this mode, regular division and not integer one will be used with the divisors and the remainder of the division will be simply subtracted. Note that this mode is triggered by the invocant not being an Int, so if it is, simply coerce it into a Rat, a Num, or anything else that does the Real role:

say ⅔.polymod: ⅓;

say 5.Rat.polymod: .3, .2;
say 3.Rat.polymod: ⅔, ⅓;

# OUTPUT:
# (0 2)
# (0.2 0 80)
# (0.333333 0 12)

In the first call, our invocant is already a Rat, so we can just call .polymod and be done with it. In the second and third, we start off with Ints, so we coerce them into Rats. The reason I didn't use a Num here is because it adds floating point math noise into the results, which Rats can often avoid:

say 5.Num.polymod: .3, .2;
say 3.Num.polymod: ⅔, ⅓;

# OUTPUT:
# (0.2 0.199999999999999 79)
# (0.333333333333333 2.22044604925031e-16 12)

This imprecision of floating point math is also something to be very careful about when using lazy list mode of .polymod, since it may never reach exact zero (at least at the time of this writing). For example, on my machine this is a nearly infinite loop as the numbers fluctuate wildly. Change put to say to print the first hundred numbers:

put 4343434343.Num.polymod: ⅓ xx ∞

Making it Human

All we've seen so far is nice and useful, but Less Than Awesome when we want to present the results to squishy humans. Even if we use the Zip meta operator to add the units, we're still not handling the differences between singular and plural names for units, for example. Luckily, some crazy guy wrote a module to help us: Number::Denominate.

use Number::Denominate;

my $seconds = 1 * 60*60*24 # days
            + 3 * 60*60    # hours
            + 4 * 60       # minutes
            + 5;           # seconds

say denominate $seconds;
say denominate $seconds, :set<weight>;

# OUTPUT:
# 1 day, 3 hours, 4 minutes, and 5 seconds
# 97 kilograms and 445 grams

By default, the module uses time units and the first call to denominate gives us a nice, pretty string. Several sets of units are pre-defined and in the second call we use the weight unit set.

You can even define your own units:

say denominate 449, :units( foo => 3, <bar boors> => 32, 'ber' );

# OUTPUT:
# 4 foos, 2 boors, and 1 ber

The module offers precision control and a couple of other options, and I encourage you to check out the docs if denominating things is what you commonly do.

Conclusion

Perl 6's built-in .polymod method is a powerful tool for breaking up numbers into denominations. You can use it on Ints or other types of numbers, with latter allowing for use of non-integer divisors. You can alter the mode of its operation by providing the divisors as an infinite list. Lastly, module Number::Denominate can assist with presenting your denominated number in a human-friendly fashion.

Enjoy!