Playing the Zodiac with Raku

by Arne Sommer

Playing the Zodiac with Raku

[119] Published 14. March 2021.

This is my response to the Perl Weekly Challenge #103.

Challenge #103.1: Chinese Zodiac

You are given a year $year.

Write a script to determine the Chinese Zodiac for the given year $year. Please check out wikipage for more information about it.

The animal cycle
Rat, Ox, Tiger, Rabbit, Dragon, Snake, Horse, Goat, Monkey, Rooster, Dog, Pig.
The element cycle
Wood, Fire, Earth, Metal, Water
Example 1:
    Input: 2017
    Output: Fire Rooster
Example 2:
    Input: 1938
    Output: Earth Tiger

Note that the element cycle has 10 values, and not 5 as shown in the challenge, as each value is duplicated. (First as a «Yang» version, followed by a «Yin» version.) I'll ignore the «Yin and Yang», so that we get exactly the same output as requested in the challenge.

The base year (as defined in the wikipedia article) is 1924 (or 1984). All we have to do, is computing the offset (difference) from the year we are given, and count our way. Modulo using the number of elements in the cycles does just that.

File: chinese-zodiac
#! /usr/bin/env raku

unit sub MAIN (Int $year);                      # [1]

my @animals   = <Rat Ox Tiger Rabbit Dragon Snake Horse Goat \    # [2]
                 Monkey Rooster Dog Pig>;
my @elements  = <Wood Wood Fire Fire Earth Earth Metal Metal \    # [3]
                 Water Water>;

my $animals   = @animals.elems;                 # [4]
my $elements  = @elements.elems;                # [5]

my $base-year = 1924;                           # [6]
my $year-diff = $year - $base-year;             # [7]

say "{ @elements[ $year-diff % $elements] } \   # [8]
     { @animals[ $year-diff % $animals] }";     # [9]

[1] The year (as an integer). The wikipedia page shows the years 1924 to 2043, but we can extrapolate the sequence in both directions.

[2] The sequence of animals.

[3] The sequence of elements. They come as pairs, according to the wikipedia article, so the challenge text is misleading here.

[4] The number of animals (i.e. the number of values before we start from the beginning again).

[5] Ditto for the elements.

[6] The base year (1924), which have the first element in each list. I could just as well have chosen 1984.

[7] The current year is so many years off from the base year.

[8] The modulo operator % gives us the index to the elements array.

[9] As above, for the animals.

See docs.raku.org/routine/% for more information about the modulo operator %.

Running it:

$ ./chinese-zodiac 2017
Fire Rooster

$ ./chinese-zodiac 1938
Earth Tiger

$ ./chinese-zodiac 1939
Earth Rabbit

$ ./chinese-zodiac 1940
Metal Dragon

Looking good...

Except that we should perhaps show the «Yin» and «Yang» prefixes for the elements, as given by the wikipedia article.

And show off our Unicode knowledge by supporting the Chinese characters as well.

File: chinese-zodiac-c
#! /usr/bin/env raku

unit sub MAIN (Int $year, :c(:$chinese));                         # [1]

my @animals   = <Rat Ox Tiger Rabbit Dragon Snake Horse Goat Monkey Rooster Dog Pig>;
my @elements  = <Yang/Wood Yin/Wood Yang/Fire Yin/Fire Yang/Earth Yin/Earth \
                 Yang/Metal Yin/Metal Yang/Water Yin/Water>;   # [2]

my %chinese   = ( Rat          => '子',   Ox          => '丑',   Tiger       => '寅',
                  Rabbit       => '卯',   Dragon      => '辰',   Snake       => '巳',
                  Horse        => '午',   Goat        => '未',   Monkey      => '申',
                  Rooster      => '酉',   Dog         => '戌',   Pig         => '亥',
                  'Yang/Wood'  => '甲',  'Yin/Wood'   => '乙',  'Yang/Fire'  => '丙',
                  'Yin/Fire'   => '丁',  'Yang/Earth' => '戊',  'Yin/Earth'  => '己',
                  'Yang/Metal' => '庚',  'Yin/Metal'  => '辛',  'Yang/Water' => '壬',
                  'Yin/Water' => '癸');                           # [3]

my $animals   = @animals.elems;
my $elements  = @elements.elems;

my $base-year = 1924;
my $year-diff = $year - $base-year;

say $chinese                                                     # [4]
?? "{ %chinese{ @elements[ $year-diff % $elements] } } \
    { %chinese{ @animals[ $year-diff % $animals] } }" 
!! "{ @elements[ $year-diff % $elements] } { @animals[ $year-diff % $animals] }";

[1] Use the «-c» or «--chinese» command line option if you want the program to print chinese characters.

[2] This time, prefixed with «Yin» and «Yang».

[3] The mapping between the names and the chinese characters.

[4] Print the chinese characters (the ?? line) if requested, and the names if not (the !! line).

Running it:

$ ./chinese-zodiac-c 2017
Yin/Fire Rooster

$ ./chinese-zodiac-c 1938
Yang/Earth Tiger

$ ./chinese-zodiac-c 1939
Yin/Earth Rabbit

$ ./chinese-zodiac-c 1940
Yang/Metal Dragon

$ ./chinese-zodiac-c -c 2017
丁 酉

$ ./chinese-zodiac-c -c 1938
戊 寅

$ ./chinese-zodiac-c -c 1939
己 卯

$ ./chinese-zodiac-c -c 1940
庚 辰

Note that we did not have to quote the keys (in [3]) for the animals, as they use letters only. Digits would also work out here, but the first character has to be a letter. (And by letter, Raku allows anything that is a Unicode letter.)

Challenge #103.2: What’s playing?

Working from home, you decided that on occasion you wanted some background noise while working. You threw together a network streamer to continuously loop through the files and launched it in a tmux (or screen) session, giving it a directory tree of files to play. During the day, you connected an audio player to the stream, listening through the workday, closing it when done.

For weeks you connect to the stream daily, slowly noticing a gradual drift of the media. After several weeks, you take vacation. When you return, you are pleasantly surprised to find the streamer still running. Before connecting, however, if you consider the puzzle of determining which track is playing.

After looking at a few modules to read info regarding the media, a quick bit of coding gave you a file list. The file list is in a simple CSV format, each line containing two fields: the first the number of milliseconds in length, the latter the media’s title (this example is of several episodes available from the MercuryTheatre.info):

 1709363,"Les Miserables Episode 1: The Bishop (broadcast date: 1937-07-23)"
 1723781,"Les Miserables Episode 2: Javert (broadcast date: 1937-07-30)"
 1723781,"Les Miserables Episode 3: The Trial (broadcast date: 1937-08-06)"
 1678356,"Les Miserables Episode 4: Cosette (broadcast date: 1937-08-13)"
 1646043,"Les Miserables Episode 5: The Grave (broadcast date: 1937-08-20)"
 1714640,"Les Miserables Episode 6: The Barricade (broadcast date: 1937-08-27)"
 1714640,"Les Miserables Episode 7: Conclusion (broadcast date: 1937-09-03)"
For this script, you can assume to be provided the following information:
  • the value of $^T ($BASETIME) of the streamer script,
  • the value of time(), and
  • a CSV file containing the media to play consisting of the length in milliseconds and an identifier for the media (title, filename, or other).
Write a program to output which file is currently playing. For purposes of this script, you may assume gapless playback, and format the output as you see fit.

Optional: Also display the current position in the media as a time-like value.

Example:
UPDATED: Input parameters as reported by many members [2021-03-08 16:20 UK TIME].
Input: 3 command line parameters: start time, current time, file name

    # starttime
    1606134123

    # currenttime
    1614591276

    # filelist.csv

Output:

    "Les Miserables Episode 1: The Bishop (broadcast date: 1937-07-23)"
    00:10:24

File: whats-playing
#! /usr/bin/env raku

unit sub MAIN (Int $start,                            # [1]
               Int $current where $current > $start,  # [2]
               $file where $file.IO.f && $file.IO.r,  # [3]
               :v(:$verbose));

my @duration;                                         # [4]
my @title;                                            # [5]

my $start-ms = $start * 1000;                         # [6]
my $current-ms   = $current   * 1000;                 # [7]

for $file.IO.lines -> $line                           # [8]
{
  my ($time, $title) = $line.split(",", 2);           # [9]

  @duration.push: $time;                              # [10]
  @title.push: $title;                                # [10a]
}

my $time-ms = $start-ms;                              # [11]
my $title   = 0;                                      # [11a]

LOOP: loop                                            # [12]
{
  for ^@duration.elems -> $index                      # [13]
  {
    $title = $index;                                          # [14]

    last LOOP if $time-ms + @duration[$index] > $current-ms;  # [15]

    $time-ms += @duration[$index];                            # [16]

    say ": $time-ms starting @title[$index]" if $verbose;
  }
}

say @title[$title];                                           # [17]

my $d = ($current-ms - $time-ms) / 1000;                      # [18]

my ($sec, $min, $hour) = $d.polymod(60,60);                   # [19]

say "{ $hour.fmt('%02d') }:{ $min.fmt('%02d') }:{ $sec.fmt('%02d') }";  # [20]

[1] The start time (in seconds since the epoch, 1. january 1970).

[2] The current time (also in seconds sine the epoch).

[3] The CSV file. Ensure that it is a file (IO.f) and that the program can read it (IO.r).

[4] The duration fields from the CSV file goes here.

[5] Ditto for the text (title). The index is the key for both arrays.

[6] The start time in milliseconds, as the CSV file uses milliseconds.

[7] Ditto for the current time.

[8] For each line in the CSV file:

[9] Split the line on the first comma, in two parts only (the second argument to split).

[10] Add the duration part to the duartion array, and the title (description) to the title array [10a].

[11] Where we are in time as we play the movies. (The current movie is in [11a].)

[12] An eternal loop. Note the label, so that we can terminate the outer loop later on.

[13] Iterate over the movies, by their index.

[14] We are watching the current movie.

[15] Terminate the outer loop (the label «LOOP») if the end (timewise) of playing the current movie lies in the future.

[16] The last (in [15]) did not kick in, so we can safely add the current movie to the time, before the next iteration has a go at yet another movie.

[17] We are done (have reached the current time). Print the current movie.

[18] The number of seconds into the current movie, converted from milliseconds.

[19] Convert the number of seconds to hours, minutes and seconds with polymod.

[20] Print the hours, minutes and seconds, with two digits each (zero padded).

See docs.raku.org/routine/split for more information about split.

See docs.raku.org/routine/polymod for more information about polymod.

Running it:

$ ./whats-playing 1606134123 1614591276 filelist.csv 
"Les Miserables Episode 1: The Bishop (broadcast date: 1937-07-23)"
00:10:24

Spot on.

Verbose mode is really verbose here, with 4972 lines of output:

$ ./whats-playing -v 1606134123 1614591276 filelist.csv | wc
   4972   57520  458027

And that's it.