PWC123 - Ugly Numbers

TL;DR

Here we are with TASK #1 from [The Weekly Challenge][]. Enjoy!

The challenge

You are given an integer $n >= 1.

Write a script to find the $nth element of Ugly Numbers.

Ugly numbers are those number whose prime factors are 2, 3 or 5. For example, the first 10 Ugly Numbers are 1, 2, 3, 4, 5, 6, 8, 9, 10, 12.

Example

Input: $n = 7
Output: 8

Input: $n = 10
Output: 12

The questions

Well, I have a few!

First of all, I totally love low numbers from 1 to 6… I saw them growing one by one and they always seemed beautiful to me! How dare you call them ugly?!?

Then, on a more serious note, the way the puzzle is phrased is debatable. It’s surely very subtle to make reference to the prime factors of course, but that “are” in my opinion implies that at least one of them should be present. I’ve seen less obscure definitions around, although I’d probably say that:

Ugly numbers are of the form $2^x 3^y 5^z$, with $x$, $y$, and $z$ being non-negative integers.

This easily accounts for that pesky 1 at the beginning, being it the only one of the lot that is not divisible by any of 2, 3, or 5.

And, of course, this also stresses something that is indeed also in the original definition: whatever is divisible also by any other prime number is not ugly. So, for example, 14 is not ugly because it’s divisible by prime factor 7.

The solution

We’ll go Raku first. In pure bottom-up spirit let’s first take a look at a test for checking whether a number is ugly or not:

# check that $x is of the form 2^x * 3^y * 5^z
sub is-ugly (Int() $k is copy) {
   # remove all 2, 3, and 5 factors in $k
   for 2, 3, 5 -> $d {
      $k /= $d while $k %% $d;
   }

   # if we're left with anything that's not 1, the number is *not* ugly
   return $k == 1;
}

The idea is to divide our input value $k by the highest possible powers of 2, 3, and 5 and see what’s left. If the number is ugly, i.e. of the form $2^x 3^y 5^z$, we will be left with nothing else than 1, otherwise we will have some other factor (we don’t care which).

The attentive reader surely noted that the signature of is-ugly is super-interesting:

sub is-ugly (Int() $k is copy) { ...

What’s with this Int() with the parentheses? By all means this is taking advantage of the teachings of gfldex, which you can read about in previous post Raku community answered to shift || 5.

The gist of it is that doing $k /= $d tries to fit a Rat into $k. If we just declare Int $k this yields an error; with the parentheses, though, Raku is told to coerce the result of the division into an Int.

OK, enough showing off of my latest trembling skills!

Now we just have to use this test in another function that takes care to calculate the n-th of these alleged ugly numbers. To do this, we keep a cache of numbers, so that we don’t have to re-calculate the first items over and over; we also warm-up the cache with the initial few elements, i.e. all integers from 1 to 6 included:

sub ugly-number-at-position (Int:D $n where * > 0) {
   # keep a cache of values for fun and profit
   state @cache = 1 .. 6;

   # We add elements to the cache as we need them, otherwise leveraging
   # previous calculations
   while $n > @cache.elems {
      # we start testing immediately after the latest element we put
      my $c = 1 + @cache[*-1];

      # anything that yields a rest when divided by 2 and by 3 and by 5
      # is not ugly and gets us to look for the next candidate
      $c++ until is-ugly($c);

      # our candidate $c is divisible by one of 2, 3, or 5, so it's
      # "ugly" and we add it to the lot, in order
      @cache.push: $c;
   }

   # our input $n has an off-by-one difference from how we index arrays
   return @cache[$n - 1];
}

Here’s an interesting twist to one of my house rules, i.e. I practically never use until. Well… until now, where I think that it fits perfectly 🙄

So much for Raku, let’s move on to Perl. The test first:

sub is_ugly ($k) {
   for my $d (2, 3, 5) {
      $k /= $d until $k % $d;
   }
   return $k == 1;
}

It’s extremely hard to fail using something that is not present, and in this case the lack of Raku’s sophisticated, incremental type system is an advantage because we don’t have to fight against type mismatches between integers and rationals, nor to call coercions to our help.

Perl sports a superior whipuptitude in this case, not because we cannot do the same in Raku (of course incremental means that we can just stop at no type system at all), but because it does not lure us to use it.

I know, it might exist only in my head… but it does not make it any less real, right?!?

One fun thing is that I’m using until again here. Perl does not have an is divisble by operator %% like Raku, so we can resort to the negation of has a rest in the division by, i.e. our old friend % operator. The negation being expressed by until.

The other function to calculate the n-th position is pretty much a translation of Raku’s corresponding code:

sub ugly_number_at_position ($n) {
   die "invalid input '$n'\n" if $n !~ m{\A [1-9]\d* \z}mxs;
   state $cache = [1..6];
   while ($n > $cache->@*) {
      my $c = 1 + $cache->[-1];
      $c++ until is_ugly($c);
      push $cache->@*, $c;
   }
   return $cache->[$n - 1];
}

The test on the input takes one line only, so I decided to put it 😅. Raku is superior here, because we can put the constraint directly in the signature, which I find useful in way more than 80% of the cases.

We have to take into account that state variables needed to be scalars until 5.28, and I’m still using 5.24. For this reason, I’m using an array reference $cache instead of a full-fledged array here, which sacrifices something on the altar of readability. It’s not Perl’s fault though, only my laziness in upgrading it.

Well… I guess it’s everything for this post!


Comments? Octodon, , GitHub, Reddit, or drop me a line!