Ordinal Spark
with Raku

by Arne Sommer

Ordinal Spark with Raku

[198] Published 28. August 2022.

This is my response to The Weekly Challenge #179.

Challenge #179.1: Ordinal Number Spelling

You are given a positive number, $n.

Write a script to spell the ordinal number.

For Example:
11 => eleventh
62 => sixty-second
99 => ninety-ninth

Ok. Starting at 1 is the obvious choice, but where do we stop?

Wikipedia to the rescue: en.wikipedia.org/wiki/English_numerals#Ordinal_numbers-

Ordinal Numbers (or integers as we really should call them in a programming setting) are given from 0 to 99, and then for 100, 1,000, 10,000 and so on. Let us start with the one and two digit numbers (i.e. integers).

File: ons
#! /usr/bin/env raku

unit sub MAIN (UInt $integer where $integer <= 99);  # [1]

my %mapping =                   # [2]
(
    0  => "zeroth",             # [3]
    1  => "first",
    2  => "second",
    3  => "third",
    4  => "fourth",
    5  => "fifth",
    6  => "sixth",
    7  => "seventh",
    8  => "eighth",
    9  => "ninth",
   10  => "tenth",
   11  => "eleventh",
   12  => "twelfth",
   13  => "thirteenth",
   14  => "fourteenth",
   15  => "fifteenth",
   16  => "sixteenth",
   17  => "seventeenth",
   18  => "eighteenth",
   19  => "nineteenth",
   20  => "twenty",             # [3]
  '2x' => "twentieth",          # [4]
   30  => "thirty",
  '3x' => "thirtieth",
   40  => "forty",
  '4x' => "fortieth",
   50  => "fifty",
  '5x' => "fiftieth",
   60  => "sixty",
  '6x' => "sixtieth",
   70  => "seventieth",
  '7x' => "seventy",
   80  => "eightieth",
  '8x' => "eighty",
   90  => "ninetieth",
  '9x' => "ninety",
);

if %mapping{$integer}                                     # [5]
{
  say %mapping{$integer};                                 # [5a]
}
else                                                      # [6]
{
  my ($first, $second) = $integer.comb;                   # [6a]

  say "{ %mapping{$first ~ "x"} }-{ %mapping{$second} }"; # [6b]
}

[1] Using the Uint (Unsigned Int) type takes care of negative integers, and the where clause sets the upper limit of acceptable values at 99.

See docs.raku.org/type/UInt for more information about the Uint type.

See docs.raku.org/type/Signature#index-entry-where_clause for more information about where.

[2] Using a hash for the mappings seemed like a good idea.

[3] The existence of this one was a surprise, but Wikipedia has it. All the hard coded values have their own entry in the hash, e.g. «0», «19», 20» and «90».

[4] The missing values follows the rule of combining the textual form of the first digit with a hyphen and the textual form of the last digit. We get the first digit by looking it up prefixed with an «x», i.e. the number «29» gives «2x» and «9». (Note that I had to quote these keys, as Raku does not like unquoted strings. Unquoted numbers are fine.)

[5] Do we have a hard coded translation for the value? I so, use it [5a].

[6] If not, split the two digits in two [5a], do the lookups described in [4], and print the result [6b].

Running it:

$ ./ons 11
eleventh

$ ./ons 62
sixtieth-second

$ ./ons 99
ninety-ninth

Looking good.

Ok. Let us add 100 and 1000 as well. Higher values does not make sense in my view.

File: ons100
#! /usr/bin/env raku

unit sub MAIN (UInt $integer);  # [1]

my %mapping =
(
    0  => "zeroth",
    1  => "first",
    2  => "second",
    3  => "third",
    4  => "fourth",
    5  => "fifth",
    6  => "sixth",
    7  => "seventh",
    8  => "eighth",
    9  => "ninth",
   10  => "tenth",
   11  => "eleventh",
   12  => "twelfth",
   13  => "thirteenth",
   14  => "fourteenth",
   15  => "fifteenth",
   16  => "sixteenth",
   17  => "seventeenth",
   18  => "eighteenth",
   19  => "nineteenth",
   20  => "twenty",
  '2x' => "twentieth",
   30  => "thirty",
  '3x' => "thirtieth",
   40  => "forty",
  '4x' => "fortieth",
   50  => "fifty",
  '5x' => "fiftieth",
   60  => "sixty",
  '6x' => "sixtieth",
   70  => "seventieth",
  '7x' => "seventy",
   80  => "eightieth",
  '8x' => "eighty",
   90  => "ninetieth",
  '9x' => "ninety",
  100  => "hundreth",       # [2]
  1000 => "thousandth",     # [2]
);

if %mapping{$integer}       # [2a]
{
  say %mapping{$integer};
}
else
{
  die "Unsupported value. Use 0-100,1000 only" if $integer.chars > 2;  # [3]
  my ($first, $second) = $integer.comb;

  say "{ %mapping{$first ~ "x"} }-{ %mapping{$second} }";
}

[1] The where clause has gone, as we support non-disjunct values. Unsupported values are now handled by the code (in [3]).

[2] The new values 100 and 1000 have been added, and the hash lookup (in [2a] takes care of them). Adding e.g. 10,000 is easy, if you want to.

[3] Abort if we have a non-supported integer value.

Running it:

$ ./ons100 1
first

$ ./ons100 100
hundreth

$ ./ons100 101
Unsupported value. Use 0-100,1000 only

$ ./ons100 1000
thousandth

Challenge #179.2: Unicode Sparkline

You are given a list of positive numbers, @n.

Write a script to print sparkline in Unicode for the given list of numbers.

I confess. I had to look it up... (I do know what a Sparkline is, and I do know what Unicode is, but the combination...)

I found the (or rather, an) explanation at www.rosettacode.org/wiki/Sparkline_in_unicode. I have only read the Task and Notes sections, not the code sections.

The article makes a point of not allowing spaces in the sparkline, so even a zero will lead to a bar (the Unicode character, not a place to consume alcohol). Trial and error led me to cuncur, but I have decided to support spaces as well - as it is quite easy to do so. It was a bad idea, as I'll show later on.

File: unicode-sparkline-space
#! /usr/bin/env raku

subset PositiveInt of Int where * > 0;                        # [2a]

unit sub MAIN (*@numbers where @numbers.elems > 0             # [1]
                            && all(@numbers) ~~ PositiveInt,  # [2]
               :s(:$use-space));                              # [3]

my $max = max(@numbers);                                      # [4]

my @chars = $use-space                                        # [5]
  ?? " ▁▂▃▄▅▆▇█".comb                                        # [5a]
  !! "▁▂▃▄▅▆▇█".comb;                                        # [5b]

for @numbers -> $c                                            # [6]
{
  print @chars[$c / $max * (@chars.elems - 1)];               # [6a]
}

say "";                                                       # [7]

[1] A slurpy array, with a where clause to ensure that we get at least one element.

[2] Ensure that the values are all positive integers, with an all junction and a custom type (set up with subset in [2a]).

[3] Use this flag if you want to use spaces in the sparkline.

[4] We have to map the values in the input to the sparkline symbols. The first step is to get the range; we can assume that 0 (zero) is the lowest value, and we use map to get the highest.

[5] Why use Unicode codepoints (as e.g. U+2588), when we can use the characters themselves? (See e.g. en.wikipedia.org/wiki/Block_Elements for a summary of these characters.) This gives us an array, with [5a] or without a leading space [5b] depending in the flag from [3].

[6] Then we simply iterate over the values, and print the value from the unicode array with the index we get when we translate the original range to the index range of the array. Raku copes with non-integer indices, so we do not have to coerce the index value to an integer.

[7] Add the ending newline.

Running it with the first example from the Rosette Code article:

$ ./unicode-sparkline-space 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1
▁▂▃▄▅▆▇█▇▆▅▄▃▂▁

The second example uses floating point numbers, which I have chosen not to support. (As I made the same assumption as in the first part of the challenge, that «number» should be understood as «integer».) It is easy to allow them in the input, but we get a problem as we values are stripped of the non-integer part when used as indices. The values «2, 2.9, 3» will result in «2, 2, 3» which clearly is wrong. Rounding up would solve this, but I'll refrain from doing so. Integers are fine.

Let us have a look at why spaces are problematic:

$ ./unicode-sparkline-space 8 7 6 5 4 3 2 1 2 3 4 5 6 7 8
█▇▆▅▄▃▂▁▂▃▄▅▆▇█

Looking good, as I have used the exact same amount of unique values that we have symbols for. Adding a ninth shows the rounding problem for values that are close to each other:

$ ./unicode-sparkline-space 9 8 7 6 5 4 3 2 1 2 3 4 5 6 7 8 9
█▇▆▅▄▄▃▂▁▂▃▄▄▅▆▇█

Adding a space gives us 9 values:

$ ./unicode-sparkline-space -s 9 8 7 6 5 4 3 2 1 2 3 4 5 6 7 8 9
█▇▆▅▄▃▂▁ ▁▂▃▄▅▆▇█

But showing «1» as a space is clearly wrong. We should only show a space for values that actually are zero. The challenge asked for «positive numbers», and that excludes zero. So we can safely remove the support for spaces:

File: unicode-sparkline
#! /usr/bin/env raku

subset PositiveInt of Int where * > 0;

unit sub MAIN (*@numbers where @numbers.elems > 0
                  && all(@numbers) ~~ PositiveInt);

say @numbers.map({ "▁▂▃▄▅▆▇█".comb.[$_ / max(@numbers) * 7] }).join;

[1] I have replaced the explicit loop with map, and inlined the characters and sizes. This version is shorter, but harder to read. Perhaps.

Let us have a look at the Rosetta Code article suggestion that values should be grouped, so that the following example (where I have used «1» instead of the unsupported «0») should give a three character sparkline (i.e. ▁▁▅▅██) instead of the actual six character result:

$ ./unicode-sparkline 1 999 4000 4999 7000 7999
▁▁▄▅▇█

But that is hard...

So I'll leave it at that.

But, here is a version with verbose mode:

File: unicode-sparkline-verbose
#! /usr/bin/env raku

subset PositiveInt of Int where * > 0;

unit sub MAIN (*@numbers where @numbers.elems > 0
                   && all(@numbers) ~~ PositiveInt, :v(:$verbose));

my $max = max(@numbers);

my @chars = "▁▂▃▄▅▆▇█".comb;

my @output;

for @numbers -> $c
{
  my $index = $c / $max * (@chars.elems - 1);

  say ": $c -> $index" if $verbose;

  @output.push: @chars[$index];
}

say @output.join;

Running this one shows why the sparkline values are what they are:

$ ./unicode-sparkline-verbose -v 1 999 4000 4999 7000 7999
: 1 -> 0.000875
: 999 -> 0.874234
: 4000 -> 3.500438
: 4999 -> 4.374672
: 7000 -> 6.125766
: 7999 -> 7
▁▁▄▅▇█

And that's it.