From b1729a13e2297e6d41eac294d0495b82a4a84620 Mon Sep 17 00:00:00 2001 From: Pragmatic Software Date: Thu, 6 Jun 2019 21:46:00 -0700 Subject: [PATCH] ParseDate: complete rewrite, replacing Time::ParseDate with DateTime::Format::Flexible Now supports more types of input, including those containing timezones! Converted PBot::Utils::ParseDate to an internal PBot module. --- PBot/ChanOpCommands.pm | 6 +-- PBot/IgnoreListCommands.pm | 4 +- PBot/PBot.pm | 2 + PBot/Plugins/ParseDate.pm | 15 +----- PBot/Plugins/RemindMe.pm | 3 +- PBot/Utils/ParseDate.pm | 108 +++++++++++++++++++++++++++++++------ 6 files changed, 100 insertions(+), 38 deletions(-) diff --git a/PBot/ChanOpCommands.pm b/PBot/ChanOpCommands.pm index 13820289..a266b6ec 100644 --- a/PBot/ChanOpCommands.pm +++ b/PBot/ChanOpCommands.pm @@ -15,8 +15,6 @@ use strict; use Carp (); use Time::Duration; -use PBot::Utils::ParseDate; - sub new { if (ref($_[1]) eq 'HASH') { Carp::croak("Options to ChanOpCommands should be key/value pairs, not hash reference"); @@ -75,7 +73,7 @@ sub ban_user { $length = 60 * 60 * 24; # 24 hours } else { my $error; - ($length, $error) = parsedate($length); + ($length, $error) = $self->{pbot}->{parsedate}->parsedate($length); return $error if defined $error; } @@ -183,7 +181,7 @@ sub mute_user { $length = 60 * 60 * 24; # 24 hours } else { my $error; - ($length, $error) = parsedate($length); + ($length, $error) = $self->{pbot}->{parsedate}->parsedate($length); return $error if defined $error; } diff --git a/PBot/IgnoreListCommands.pm b/PBot/IgnoreListCommands.pm index 8c2e5295..2dd3bfd7 100644 --- a/PBot/IgnoreListCommands.pm +++ b/PBot/IgnoreListCommands.pm @@ -16,8 +16,6 @@ use Time::HiRes qw(gettimeofday); use Time::Duration; use Carp (); -use PBot::Utils::ParseDate; - sub new { if (ref($_[1]) eq 'HASH') { Carp::croak("Options to IgnoreListCommands should be key/value pairs, not hash reference"); @@ -77,7 +75,7 @@ sub ignore_user { $length = -1; # permanently } else { my $error; - ($length, $error) = parsedate($length); + ($length, $error) = $self->{pbot}->{parsedate}->parsedate($length); return $error if defined $error; } diff --git a/PBot/PBot.pm b/PBot/PBot.pm index 3ec108af..3c0259df 100644 --- a/PBot/PBot.pm +++ b/PBot/PBot.pm @@ -44,6 +44,7 @@ use PBot::Timer; use PBot::Refresher; use PBot::Plugins; use PBot::WebPaste; +use PBot::Utils::ParseDate; sub new { if (ref($_[1]) eq 'HASH') { @@ -120,6 +121,7 @@ sub initialize { $self->{chanops} = PBot::ChanOps->new(pbot => $self, %conf); $self->{nicklist} = PBot::NickList->new(pbot => $self, %conf); $self->{webpaste} = PBot::WebPaste->new(pbot => $self, %conf); + $self->{parsedate} = PBot::Utils::ParseDate->new(pbot => $self, %conf); $self->{interpreter} = PBot::Interpreter->new(pbot => $self, %conf); $self->{interpreter}->register(sub { return $self->{commands}->interpreter(@_); }); diff --git a/PBot/Plugins/ParseDate.pm b/PBot/Plugins/ParseDate.pm index 9fdf010f..a9e5f1bc 100644 --- a/PBot/Plugins/ParseDate.pm +++ b/PBot/Plugins/ParseDate.pm @@ -2,11 +2,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -# This module is intended to provide a "magic" command that allows -# the bot owner to trigger special arbitrary code (by editing this -# module and refreshing loaded modules before running the magical -# command). - # Just a quick interface to test/play with PBot::Utils::ParseDate package PBot::Plugins::ParseDate; @@ -16,7 +11,6 @@ use strict; use Carp (); -use PBot::Utils::ParseDate; use Time::Duration qw/duration/; sub new { @@ -29,7 +23,6 @@ sub new { sub initialize { my ($self, %conf) = @_; - $self->{pbot} = delete $conf{pbot} // Carp::croak("Missing pbot reference to " . __FILE__); $self->{pbot}->{commands}->register(sub { return $self->pd(@_)}, "pd", 0); } @@ -42,13 +35,9 @@ sub unload { sub pd { my $self = shift; my ($from, $nick, $user, $host, $arguments) = @_; - - my ($length, $error) = parsedate($arguments); + my ($seconds, $error) = $self->{pbot}->{parsedate}->parsedate($arguments); return $error if defined $error; - - $length = duration $length; - return $length; + return duration $seconds; } - 1; diff --git a/PBot/Plugins/RemindMe.pm b/PBot/Plugins/RemindMe.pm index 40e8956a..04eef08a 100644 --- a/PBot/Plugins/RemindMe.pm +++ b/PBot/Plugins/RemindMe.pm @@ -15,7 +15,6 @@ use DBI; use Time::Duration qw/concise duration/; use Time::HiRes qw/gettimeofday/; use Getopt::Long qw(GetOptionsFromString); -use PBot::Utils::ParseDate; Getopt::Long::Configure ("bundling"); @@ -316,7 +315,7 @@ sub remindme { print "alarm: $alarm\n"; - my ($length, $error) = parsedate($alarm); + my ($length, $error) = $self->{pbot}->{parsedate}->parsedate($alarm); print "length: $length, error: $error!\n"; return $error if $error; diff --git a/PBot/Utils/ParseDate.pm b/PBot/Utils/ParseDate.pm index 430a72d5..51e00d18 100644 --- a/PBot/Utils/ParseDate.pm +++ b/PBot/Utils/ParseDate.pm @@ -9,34 +9,110 @@ use strict; package PBot::Utils::ParseDate; -require Exporter; -our @ISA = qw/Exporter/; -our @EXPORT = qw/parsedate/; +use DateTime; +use DateTime::Format::Flexible; +use DateTime::Format::Duration; -use Time::HiRes qw/gettimeofday/; +sub new { + Carp::croak("Options to " . __FILE__ . " should be key/value pairs, not hash reference") if ref $_[1] eq 'HASH'; + my ($class, %conf) = @_; + my $self = bless {}, $class; + $self->initialize(%conf); + return $self; +} -require Time::ParseDate; +sub initialize { + my ($self, %conf) = @_; + $self->{pbot} = delete $conf{pbot} // Carp::croak("Missing pbot reference to " . __FILE__); +} +# parses English natural language date strings into seconds +# does not accept times or dates in the past sub parsedate { - my $input = shift @_; - my $now = gettimeofday; + my ($self, $input) = @_; + + # make some aliases + $input =~ s/\bsecs?\b/seconds/g; + $input =~ s/\bmins?\b/minutes/g; + $input =~ s/\bhrs?\b/hours/g; + $input =~ s/\bwks?\b/weeks/g; + $input =~ s/\byrs?\b/years/g; + + # sanitizers + $input =~ s/\s+(am?|pm?)/$1/; # remove leading spaces from am/pm + + # split input on "and" or comma, then we'll add up the seconds my @inputs = split /(?:,?\s+and\s+|\s*,\s*)/, $input; + # adjust timezone to user-override if user provides a timezone + # we don't know if a timezone was provided until it is parsed + my $timezone; + my $tz_override = 'UTC'; + + ADJUST_TIMEZONE: + $timezone = $tz_override; + my $now = DateTime->now(time_zone => $timezone); + my $seconds = 0; + my $from_now_added = 0; + foreach my $input (@inputs) { return -1 if $input =~ m/forever/i; $input .= ' seconds' if $input =~ m/^\s*\d+\s*$/; - my $parse = Time::ParseDate::parsedate($input, NOW => $now); - - print "parsedate: now => $now, input => $input, parse => $parse\n"; - - if (not defined $parse) { - $input =~ s/\s+$//; - return (0, "I don't know what '$input' means. I expected a time duration like '5 minutes' or '24 hours' or 'next tuesday'.\n"); - } else { - $seconds += $parse - $now; + # DateTime::Format::Flexible doesn't support seconds, but that's okay; + # we can take care of that easily here! + if ($input =~ m/^\s*(\d+)\s+seconds$/) { + $seconds += $1; + next; } + + # First, attempt to parse as-is... + my $to = eval { return DateTime::Format::Flexible->parse_datetime($input, lang => ['en'], base => $now); }; + + # If there was an error, then append "from now" and attempt to parse as a relative time... + if ($@) { + $from_now_added = 1; + $input .= ' from now'; + $to = eval { return DateTime::Format::Flexible->parse_datetime($input, lang => ['en'], base => $now); }; + + # If there's still an error, it's bad input + if ($@) { + $@ =~ s/ from now at PBot.*$//; + return (0, $@); + } + } + + # there was a timezone parsed, set the override and try again + if ($to->time_zone_short_name ne 'floating' and $to->time_zone_short_name ne 'UTC' and $tz_override eq 'UTC') { + $tz_override = $to->time_zone_long_name; + goto ADJUST_TIMEZONE; + } + + $to->set_time_zone('UTC'); + my $duration = $to->subtract_datetime_absolute($now); + + # If the time is in the past, prepend "tomorrow" and reparse + if ($duration->is_negative) { + $input = "tomorrow $input"; + $to = eval { return DateTime::Format::Flexible->parse_datetime($input, lang => ['en'], base => $now); }; + + if ($@) { + $@ =~ s/format: tomorrow /format: /; + if ($from_now_added) { + $@ =~ s/ from now at PBot.*//; + } else { + $@ =~ s/ at PBot.*//; + } + return (0, $@); + } + + $to->set_time_zone('UTC'); + $duration = $to->subtract_datetime_absolute($now); + } + + # add the seconds from this input chunk + $seconds += $duration->seconds; } return $seconds;