Last active
July 8, 2022 01:07
-
-
Save JohnMertz/11158df0f59a71a556f6097dc1164e28 to your computer and use it in GitHub Desktop.
Convert `remind -p` output to JSON
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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