Skip to content

Instantly share code, notes, and snippets.

@JohnMertz
Last active July 8, 2022 01:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JohnMertz/11158df0f59a71a556f6097dc1164e28 to your computer and use it in GitHub Desktop.
Save JohnMertz/11158df0f59a71a556f6097dc1164e28 to your computer and use it in GitHub Desktop.
Convert `remind -p` output to JSON
#!/usr/bin/perl
use strict;
use warnings;
use constant MONTH => sub{{
'January' => 1,
'February' => 2,
'March' => 3,
'April' => 4,
'May' => 5,
'June' => 6,
'July' => 7,
'August' => 8,
'September' => 9,
'October' => 10,
'November' => 11,
'December' => 12
}->{ +shift }
};
use constant DAYS => sub{{
'sunday' => 0,
'monday' => 1,
'tuesday' => 2,
'wednesday' => 3,
'thursday' => 4,
'friday' => 5,
'saturday' => 6
}->{ +shift }
};
sub leading_zero {
my $in = shift;
if (length($in) == 1) {
$in = "0" . $in;
}
return $in;
}
sub help {
my $extra = shift;
print <<EOF;
usage: remind -pM /path/to/sources | $0 [W] [-bp] [--<weekday>]
Explanation
This script is designed to take input directly from remind with the -p flag.
M in the above is the number of months output from remind, formatted for rem2ps.
If M is not provided, remind will output just this month.
W in the above is the number of weeks, starting from this week, to ouput.
If W is not provided, all remaining weeks with data will be printed.
Additional Options
-b --blank Include blank days. By default, a day with no events is not listed.
With this option, these days are included as a blank hash: {1:{}}
-h --help This output, plus extra details.
-p --pretty Output pretty JSON. By default an unspaced string is provided.
--<weekday> Day to be treated as the first day of the week. (Default: --sunday)
-0 -1 ... --\$(date +%A) can be useful here to always start from today.
Caution
The script will only go far enough into a month with no data to complete the
current week. If you request more weeks that the script has data for, it will
throw an error.
EOF
if ($extra) {
print <<EOF;
Remind and rem2ps are produced by Dianne Skoll.
This script depends on JSON::XS
Copyright
2019 - John Mertz - <git at john.me.tz> https://john.me.tz
License
GPLv2
EOF
}
exit 1;
}
my ($weeks,$blank,$pretty,$wstart);
foreach (@ARGV) {
if ($_ =~ m/--(sunday|monday|tuesday|wednesday|thursday|friday|saturday)/i ) {
if (defined $wstart) {
print "Second conflicting arguments found: $_\n\n";
help(0);
} else {
$wstart = lc($_);
$wstart =~ s/--(.*)/$1/;
$wstart = DAYS->($wstart);
}
} elsif ($_ =~ m/^-\d$/ ) {
if (defined $wstart) {
print "Second conflicting arguments found: $_\n\n";
help(0);
} else {
$wstart = $_;
$wstart =~ s/-(\d)/$1/;
}
} elsif ($_ eq '-b' || $_ eq '--blank') {
if (defined $blank) {
print "Redundant 'blank' argument found: $_\n\n";
help(0);
} else {
$blank = 1;
}
} elsif ($_ eq '-p' || $_ eq '--pretty') {
if (defined $pretty) {
print "Redundant 'pretty' argument found: $_\n\n";
help(0);
} else {
$pretty = 1;
}
} elsif ($_ =~ m/^\d+$/) {
if (defined $weeks) {
print "Second conflicting 'weeks' arguments found: $_\n\n";
help(0);
} else {
$weeks = $_;
}
} else {
my $extra = 1;
unless ($_ eq '-h' || $_ eq '--help' || $_ eq '?') {
$extra = 0;
print "Unrecognized argument: $_\n\n";
}
help($extra);
}
}
$pretty = 0 unless (defined $pretty);
$blank = 0 unless (defined $blank);
$wstart = 0 unless (defined $wstart);
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Collect the input from remind
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
my @input;
if (<STDIN>) {
@input = <STDIN>;
} else {
print "No input received\n";
help(0);
}
# Try to get list of months and events from input
my (@months, @events, $previous_month, $following_month, $following_year);
foreach (@input) {
chomp $_;
if ($_ =~ m/^\w+ \d{4} \d\d \d \d$/) {
push @months, $_;
if (!defined $following_year) {
$following_year = $_;
$following_year =~ s/^\w+ (\d{4}) \d\d \d \d$/$1/;
} elsif ($_ =~ m/^January \d{4} \d\d \d \d$/) {
$following_year++;
}
} elsif ($_ =~ m|^\d{4}/\d\d/\d\d |) {
push @events, $_;
} elsif ($_ =~ m|^\w+ \d\d$|) {
if (!defined $previous_month) {
$previous_month = $_;
} else {
$following_month = $_;
}
}
}
unless (scalar @months) {
print "The input provided does not appear to be from: remind -p\n";
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Determine starting date
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Today
my ($sec,$min,$hour,$day,$mon,$year,$wday,$yday,$isdst) = localtime(time);
$year += 1900;
$mon += 1;
my $today = "$year/" . leading_zero($mon) . "/" . leading_zero($day);
# Current Month
my ($mname, $myear, $mdays, $mstart, $flag) = split ' ', $months[0];
# Calendar begins on the $wstart day on or prior to today.
# Check to see if this day falls in the previous month.
if ($day-$wday+$wstart < 1) {
my ($previous_month, $previous_days) = split ' ', $previous_month;
my $previous_start = ($previous_days-$mstart)%7;
if ($previous_month eq 'December') {
$year -= 1;
$mon = 12;
} else {
$mon -= 1;
}
$day = $previous_days-$mstart+$wstart+1;
# Prepend previous month
@months = ( "$previous_month $year $previous_days $previous_start no_data", @months );
} else {
$day = $day-$wday+$wstart;
}
my $start_day = "$year/" . leading_zero($mon) . "/" . leading_zero($day);
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Determine number of printable weeks
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Start counter with total number of days from last month used to fill out the week.
my $days_to_go = $mstart - $wstart;
# Add day count for all remaining months
foreach (@months) {
$days_to_go += (split ' ', $_)[2];
}
# Unless the last day actually closes out the week, round up and add days from next month
if ($days_to_go%7) {
my ($following_month, $following_days) = split ' ', $following_month;
push @months, "$following_month " . $following_year . " $following_days " . $days_to_go%7 . " no_data";
$days_to_go = (sprintf("%d",$days_to_go/7))*7+7;
}
# Reduce by the number of weeks already completed
$days_to_go -= (sprintf("%d",($day+$mstart-$wstart)/7)*7);
# If a specific number of weeks were requested, ensure we can fulfill the request
if (defined $weeks) {
if ($weeks > $days_to_go/7) {
print "!! ERROR !!\n";
print "You've requested more weeks of output than I have data to satisfy.\n";
print "The maximum I can print with the data provided is " . sprintf("%d",($days_to_go/7)) . " weeks.\n";
print "You must either reduce the requested weeks or provide enough months of input to\n";
print "satisfy this requirement. The month of output from remind are defined with M here:\n";
print " remind -pM /path/to/sources | $0";
print " " . $_ foreach (@ARGV);
print "\n";
exit 1;
} else {
$days_to_go = $weeks * 7;
}
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Setup main Hash
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
my %output = (
"meta" => {
"current_dow" => $wday,
"start_year" => $year,
"start_month" => $mon,
"start_day" => $day,
"start_dow" => $wstart,
"total_days" => $days_to_go
},
"data" => {}
);
($output{meta}{current_year},$output{meta}{current_month},$output{meta}{current_day}) = split '/', $today;
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Discard events prior to start date
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
my ($event_date, $special, $tag, $dur, $start, @body) = split ' ', shift @events;
my ($time,$body);
while ($event_date lt $year . "/" . leading_zero($mon) . "/" . leading_zero($day)) {
($event_date, $special, $tag, $dur, $start, @body) = split ' ', shift @events;
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Build Hash data for events
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Loop through months in order
for (my $i = 0; $i < scalar @months; $i++) {
# Collect month's meta data and add to hash
($mon, $year, $mdays, $mstart, $flag) = split ' ', $months[$i];
my @flags;
$mon = MONTH->($mon);
if ($i == 0) {
push @flags, "first";
}
if ($i == ((scalar @months) - 1)) {
push @flags, "last";
}
if ($flag) {
push @flags, $flag;
}
$output{data}{$year}{$mon}{meta}{start_day} = $day;
$output{data}{$year}{$mon}{meta}{first_dow} = $mstart;
$output{data}{$year}{$mon}{meta}{days} = $mdays;
if (scalar @flags) {
@{$output{data}{$year}{$mon}{meta}{flags}} = @flags;
}
# Before beginning the loop through the month's days, make sure that
# we don't need to stop part way through the month.
if ($mdays-$day > $days_to_go) {
$mdays = $days_to_go;
}
# Loop through the days of the month
for $day ($day .. $mdays) {
# Keep track of the days left so that we can finish appropriately
$days_to_go--;
# Each day can have multiple events, reset counter
my $event = 1;
# If no further events are found, this will be undef. Just finish
# with the blank dates, if necessary.
if (!defined $event_date) {
if ($blank) {
%{$output{data}{$year}{$mon}{$day}} = ();
next();
} else {
last();
}
# Otherwise, there is still an event in the queue looking for the
# correct day.
} else {
# Keep track of whether there was an event today, in case of -b
my $hit = 0;
# If the next event is after today, this look never executes.
# Otherwise all events get added to the hash until the day changes
while (defined $event_date && "$year/" . leading_zero($mon) . "/" . leading_zero($day) eq $event_date) {
$hit = 1;
$time = '*';
if ($body[0] =~ m/^\d{1,2}\:\d\d(am)?\-\d{1,2}\:\d\d[ap]m(\+\d)?$/) {
$time = shift @body;
}
$body = join ' ', @body;
$output{data}{$year}{$mon}{$day}{$event}{special} = $special;
$output{data}{$year}{$mon}{$day}{$event}{tag} = $tag;
$output{data}{$year}{$mon}{$day}{$event}{dur} = $dur;
$output{data}{$year}{$mon}{$day}{$event}{start} = $start;
$output{data}{$year}{$mon}{$day}{$event}{time} = $time;
$output{data}{$year}{$mon}{$day}{$event}{body} = $body;
$event++;
# If there are still events left, add the next to the queue
if (scalar @events) {
($event_date, $special, $tag, $dur, $time, @body) = split ' ', shift @events;
# Otherwise, undef signals the loop to stop
} else {
$event_date = undef;
}
}
# If -b provided and there were no events, add a blank hash
if ($blank && !$hit) {
$output{data}{$year}{$mon}{$day} = {};
}
}
}
# Reset day counter for next month
$day = 1;
}
# Setup a JSON object. -p will make output pretty
use JSON::XS;
my $json = JSON::XS->new->pretty($pretty);
# JSON::XS takes a reference; print to STDOUT
my $outref = \%output;
print $json->encode($outref);
exit 0;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment