This is my response to The Weekly Challenge #179.
$n
.
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
@n
.
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.