Perl Weekly Challenge 103
Challenge two this week was… interesting, to say the least. But, we are through it, and it’s the weekend! 🍻
Task 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.
Solution
See below for explanation and any implementation-specific comments.
sub challenge(Int $year) returns Str {
constant $root-year = 1924; # From Wikipedia
my $difference = $year - $root-year;
my $element-difference = $difference < 0 ?? $difference + 10 !! $difference; # [1]
my $animal-difference = $difference < 0 ?? $difference + 12 !! $difference;
my $element = do given $element-difference % 10 { # [2][3]
when 0|1 { 'Wood' }
when 2|3 { 'Fire' }
when 4|5 { 'Earth' }
when 6|7 { 'Metal' }
when 8|9 { 'Water' }
}
my $animal = do given $animal-difference % 12 {
when 0 { 'Rat' }
when 1 { 'Ox' }
when 2 { 'Tiger' }
when 3 { 'Rabbit' }
when 4 { 'Dragon' }
when 5 { 'Snake' }
when 6 { 'Horse' }
when 7 { 'Goat' }
when 8 { 'Monkey' }
when 9 { 'Rooster' }
when 10 { 'Dog' }
when 11 { 'Pig' }
}
"$element $animal"; # [4]
}
sub MAIN(Int $year) {
say challenge($year);
}
This program runs as such:
$ raku ch-1.raku 2017
Fire Rooster
Explanation
I’m sure there is a better root than 1924, but that is the first year in the table on the Wikipedia page. Reading through the Wikipedia page, we can see two things:
- The element cycle has 5 rotations (Wood, Fire, Earth, Metal, and Water), and each one lasts for 2 years (a Yin year and a Yang year [which we don’t care about]).
- The animal cycle has 12 rotations (Rat, Ox, Tiger, Rabbit, Dragon, Snake, Horse, Goat, Monkey, Rooster, Dog, Pig), and each one lasts for 1 year.
This makes our logic pretty simple – we just need to find our distance from the known year of 1924 and find which cycle that year falls in. For element, we find the difference and then find the remainder after dividing by 10 (5 cycles x 2 years each). If it is 0 or 1, it is Wood, if it is 2 or 3 it is Fire, etc. Similarly, we find the difference and find the remainder after dividing by 12 for the animals. If it is 0 it is Rat, if it is 1 it is Ox, etc. That’s it!
Specific comments
- We can’t just do
abs($year - $root-year)
because for years before$root-year
, it would yield the wrong cycle (for example, 1923 would have a remainder of 1 for$element
instead of9
). Because of this, if the difference is negative, we need to add the size of the cycle (10 and 12, respectively) to put it in its correct place. given
is a flow control keyword and doesn’t actually return anything. To make thegiven
block return its value, we need to add thedo
keyword before it.- You’ll notice there is no
default
block for thesegiven
s. That is intentional, since we know all the possible outcomes. Best practice would probably be something likedefault { die "Unexpected input: $_" }
. - Double quotes tell the Raku compiler to interpolate this string, so we get
"Fire Rooster"
instead of the literal'$element $animal'
.
Task 2: What’s playing?
Note: This was copied directly from perlweeklychallenge.org
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
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
Solution
See below for explanation and any implementation-specific comments.
use Text::CSV; # imports `csv` function
sub challenge(Int $start-time, Int $current-time, Str $file-name) returns Str {
my @playlist = csv(in => $file-name);
my $playlist-length = @playlist[*;0].sum; # [1]
my $playlist-position = ($current-time - $start-time) * 1000 % $playlist-length;
my ($track, $timestamp);
for @playlist -> ($track-length, $track-name) {
# If we are <= the playlist position, skip to the next track
if $track-length <= $playlist-position {
$playlist-position -= $track-length;
next; # [2]
}
# We know we are in the right track now, so find how far in we are
$track = $track-name;
my $total-seconds = ($playlist-position / 1000).Int;
my $hour = ($total-seconds / 3600).Int;
my $minutes = ($total-seconds % 3600 / 60).Int;
my $seconds = $total-seconds % 60;
$timestamp = sprintf('%02d:%02d:%02d', $hour, $minutes, $seconds);
last; # [3]
}
"$track\n$timestamp";
}
sub MAIN(Int $start-time, Int $current-time, Str $file-name) {
say challenge($start-time, $current-time, $file-name);
}
This program runs as such:
# Assumes `filelist.csv` contains the above input
$ raku ch-2.raku 1606134123 1614591276 filelist.csv
Les Miserables Episode 1: The Bishop (broadcast date: 1937-07-23)
00:10:24
Explanation
I found this question to be incredibly confusing. Maybe it is the wording, or the fact that it was written by a guest author, but it didn’t make sense to me at first. Additionally, we are explicitly told that one set of numbers (the ones in the CSV) are in milliseconds, and the others are not specified; I started programming this assuming they are also in milliseconds only to have to figure out later on they are in seconds. With that out of the way, let’s look at the actual logic:
- Read the CSV into a list of lists.
- Find the total length of the playlist.
- Find the position that we are in the playlist (in milliseconds).
- Iterate through each track in the playlist:
- If the length of the track is less than the position in the playlist, decrement the position in the playlist by the length of the track and continue
- Otherwise, we are in the track we need to be in, and we need to find the position in the track. We do this by calculating the total seconds we are into the track, then formatting that using some simple division. It’s also important we remember to break after we have found what track we are in.
Specific Comments
- This is an interesting way to find the sum of a particular index in the list. This says “give me position 0 for the whole list, then sum it.”
next
is the equivalent of something likecontinue
in other languages.last
is the equivalent of something likebreak
in other languages.
Final Thoughts
Overall, not my favorite set of questions this week, but what can you do! Hope y’all enjoy the rest of the Metal Ox
year!