The Miserable Metal Ox: Perl Weekly Challenge #013
TASK #1 › Chinese Zodiac
Submitted by: Mohammad S Anwar
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.
At this point, I would like to quote Dave Rolsky:
Do Not Write Your Own Date and Time Manipulation Code!
Do Not Write Your Own Date and Time Manipulation Code!
Do Not Write Your Own Date and Time Manipulation Code!
Do Not Write Your Own Date and Time Manipulation Code!
I bring this up because while I could probably write code that works through the cycle from some “Year 0” and count forward and such, there is bound to be confusion based on when this group or that group changed what time means, so it’s safer to go this way.
Also, Chinese New Year was February 12, which means that for almost a month an a half, a naïve implementation will tell you it’s Metal Ox when it’s really still Metal Rat.
So I used DateTime::Calendar::Chinese.
Which requires DateTime::Astro.
Which requires Multiple-Precision Floating-point computations, and thus MFPR.
Which means it wasn’t the easiest install, and I may never need it again, but it worked here. So, yes, I went for the Mercenary solution, but sometimes, if you want the job done right, you call the experts.
Show Me The Code
#!/usr/bin/env perl
use strict;
use warnings;
use feature qw{ postderef say signatures state };
no warnings qw{ experimental };
# Do Not Write Your Own Date and Time Manipulation Code!
# Do Not Write Your Own Date and Time Manipulation Code!
# Do Not Write Your Own Date and Time Manipulation Code!
# Do Not Write Your Own Date and Time Manipulation Code!
# Do Not Write Your Own Date and Time Manipulation Code!
# Do Not Write Your Own Date and Time Manipulation Code!
# -- Dave Rolsky
# Time is broken.
use Carp;
use DateTime;
use DateTime::Calendar::Chinese
; # which requires DateTime::Astro, which requires MPFR or won't build
# we can specify a specific year, or we can get a whole cycle
my ($year) = @ARGV;
if ( defined $year ) {
croak 'Not A Year' unless $year !~ /\D/;
my ( $year, $animal, $branch, $stem, $name, $element ) =
chinese_zodiac($year);
say join " ", "\t", $year, "\t", $element, $animal;
}
else {
# Show The Current Cycle
for my $y ( 1984 .. 2043 ) {
my ( $year, $animal, $branch, $stem, $name, $element ) =
chinese_zodiac($y);
say join " ", "\t", $year, "\t", $element, $animal;
}
}
exit;
sub chinese_zodiac ( $greg_year ) {
# there is a problem I'm ignoring here.
# chinese new year is not bound specifically to a point in the
# planet's orbit around the local star. it floats, and occurs
# somewhere between mid-January and mid-February by the Gregorian
# calendar.
# this means that, if run on Jan 2 2021, we MIGHT expect to see
# that it's Metal Ox, but it was still Metal Rat (which is the
# name of an effects pedal from ProCo, IIRC, and implies a series
# of SF novels from Harry Harrison).
# this code *should* respect the truth and not the failed expectations
# of the user, but any code that goes from first principles would
# likely blow it for about the first month of the year
my $dt = DateTime->now();
$dt->set_year($greg_year);
my $chdt = DateTime::Calendar::Chinese->from_object( object => $dt );
$chdt->set_time_zone("Asia/Hong_Kong");
my $animal = ucfirst $chdt->zodiac_animal;
my $branch = ucfirst $chdt->terrestrial_branch_py;
my $stem = ucfirst $chdt->celestial_stem_py;
my $name = ucfirst $chdt->year_name_py;
my $element = translate_element($stem);
return $greg_year, $animal, $branch, $stem, $name, $element;
}
sub translate_element ( $stem ) {
# There's also Yin and Yang, which we were not
# told to identify, but explains the two results
# in the following hash
# Wood, Fire, Earth, Metal, Water
my %elements = (
Jia3 => 'Wood',
Yi3 => 'Wood',
Bing3 => 'Fire',
Ding1 => 'Fire',
Ji3 => 'Earth',
Wu4 => 'Earth',
Geng1 => 'Metal',
Xin1 => 'Metal',
Gui3 => 'Water',
Ren2 => 'Water',
);
return $elements{$stem};
}
1984 Wood Rat
1985 Wood Ox
1986 Fire Tiger
1987 Fire Hare
1988 Earth Dragon
1989 Earth Snake
1990 Metal Horse
1991 Metal Sheep
1992 Water Monkey
1993 Water Fowl
1994 Wood Dog
1995 Wood Pig
1996 Fire Rat
1997 Fire Ox
1998 Earth Tiger
1999 Earth Hare
2000 Metal Dragon
2001 Metal Snake
2002 Water Horse
2003 Water Sheep
2004 Wood Monkey
2005 Wood Fowl
2006 Fire Dog
2007 Fire Pig
2008 Earth Rat
2009 Earth Ox
2010 Metal Tiger
2011 Metal Hare
2012 Water Dragon
2013 Water Snake
2014 Wood Horse
2015 Wood Sheep
2016 Fire Monkey
2017 Fire Fowl
2018 Earth Dog
2019 Earth Pig
2020 Metal Rat
2021 Metal Ox
2022 Water Tiger
2023 Water Hare
2024 Wood Dragon
2025 Wood Snake
2026 Fire Horse
2027 Fire Sheep
2028 Earth Monkey
2029 Earth Fowl
2030 Metal Dog
2031 Metal Pig
2032 Water Rat
2033 Water Ox
2034 Wood Tiger
2035 Wood Hare
2036 Fire Dragon
2037 Fire Snake
2038 Earth Horse
2039 Earth Sheep
2040 Metal Monkey
2041 Metal Fowl
2042 Water Dog
2043 Water Pig
TASK #2 › What’s playing?
Submitted by: Albert Croft
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 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.
This is of some interest to me, as one of my sons idolizes Orson Welles, founder of the Mercury Theater, and another is obsessed with Les Miserables, but honestly, once I started diving into this, those weren’t the things my mind went to.
I could play with CSV-parsing, but really, with Text::CSV(https://metacpan.org/pod/Text::CSV) around, why would you?
From there … I’ll just pull from my comments.
4:50pm - Restate my assumptions
- Mathematics is the language of nature.
- Everything around us can be represented and understood through numbers.
- The streamer adds no time between recordings, nor crossfades between trackss, so there is no time unaccounted for between tracks
- The streamer will get to the end of the list and start all over again
- Pi is a movie that every developer of a certain age will recognize immediately
- Current time will be higher than start time, meaning we’re not querying the past
- The episodes in the CSV are roughly a half-hour each*
Hypothesis: We can subtract the start time from the time in question to find the total number of seconds that the streamer has played, then go through the whole playlist over and over until the current total is less than the run time, but the current total plus the next media file would be greater than the run time. This would indicate that this is the currently streaming file.
Show Me The Code
#!/usr/bin/env perl
use strict;
use warnings;
use feature qw{ postderef say signatures state };
no warnings qw{ experimental };
use Carp;
use Text::CSV qw( csv );
use Getopt::Long;
my $starttime = 1606134123; # 2020-11-23 12:22:03
my $currtime = 1614591276; # 2021-03-01 09:34:36
my $file = 'input.csv';
my $np = now_playing( $starttime, $currtime, $file );
say join "\n\t", $np->@*;
sub now_playing ( $starttime, $currtime, $file ) {
croak 'Bad Times' unless $currtime > $starttime;
croak 'No Input File' unless -f $file;
# $starttime is when the streaming started
# $currtime is the time during the stream we're asking about
# $endtime is the total time in seconds that the streamer
# has been running
my $endtime = $currtime - $starttime;
my @data = get_data($file);
# 4:50pm - Restate my assumptions
# One: Mathematics is the language of nature.
# Two: Everything around us can be represented
# and understood through numbers.
# Three: The streamer adds no time between recordings,
# nor crossfades between trackss, so there is no
# time unaccounted for between tracks
# Four: The streamer will get to the end of the list and
# start all over again
# Five: Pi is a movie that every developer of a certain age
# will recognize immediately
# Six: Current time will be higher than start time, meaning
# we're not querying the past
# Seven: The episodes in the CSV are roughly a half-hour each
# Hypothesis:
# We can subtract the start time from the time in
# question to find the total number of seconds that
# the streamer has played, then go through the whole
# playlist over and over until the current total is
# less than the run time, but the current total plus
# the next media file would be greater than the run
# time. This would indicate that this is the currently
# streaming file
my $ctime = 0;
while (1) {
for my $c ( 0 .. -1 + scalar @data ) {
my $row = $data[$c];
my ( $mill, $name ) = $row->@*;
my $length = $mill / 1000;
my $c2 = $ctime;
$ctime += $length;
if ( $ctime >= $endtime && $c2 <= $endtime ) {
my $time = int $endtime - $c2;
my $formatted = join ':',
( int $time / 60 ),
( sprintf '%02d', $time % 60 );
my @output = ( $row->[1], $formatted );
return \@output;
}
}
}
croak 'This should never happen';
}
# getting to the CSV is a separate function for ease
sub get_data ($file=undef) {
$file //= 'input.csv';
if ( -f $file ) {
my $data = csv( in => $file );
return $data->@*;
}
else {
croak 'no data';
}
}
Les Miserables Episode 1: The Bishop (broadcast date: 1937-07-23)
10:24
Because none of the files were much longer than a half-hour, I went with minute:second
formatting rather than adding hours, but the more I think about it, I think I should’ve specified hour, minute and second. I mean, as soon as you discard scientific rigor, you’re no longer a mathematician, you’re a numerologist.