Skip to content

Commit

Permalink
Merge pull request duckduckgo#2060 from GuiltyDolphin/datemath-improv…
Browse files Browse the repository at this point in the history
…ements

DateMath: Improvements
  • Loading branch information
mintsoft committed Feb 21, 2016
2 parents 94eb077 + ed990cc commit 503dc9d
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 196 deletions.
176 changes: 120 additions & 56 deletions lib/DDG/Goodie/DateMath.pm
Original file line number Diff line number Diff line change
Expand Up @@ -4,77 +4,141 @@ package DDG::Goodie::DateMath;
use strict;
use DDG::Goodie;
with 'DDG::GoodieRole::Dates';
with 'DDG::GoodieRole::NumberStyler';
use DateTime::Duration;
use Lingua::EN::Numericalize;

triggers any => qw( plus minus + - date day week month year days weeks months years);
triggers any => qw(second minute hour day week month year);
triggers any => qw(seconds minutes hours days weeks months years);
triggers any => qw(plus minus + -);
triggers any => qw(date time);

zci is_cached => 1;
zci is_cached => 0;
zci answer_type => 'date_math';

my $datestring_regex = datestring_regex();

handle query_lc => sub {
my $query = $_;

my $relative_regex = qr!(?<number>\d+|[a-z\s-]+)\s+(?<unit>(?:day|week|month|year)s?)!;

return unless $query =~ qr!^(?:date\s+)?(
(?<date>$datestring_regex)(?:\s+(?<action>plus|\+|\-|minus)\s+$relative_regex)?|
$relative_regex\s+(?<action>from)\s+(?<date>$datestring_regex)?
)$!x;

if (!exists $+{'number'}) {
my $out_date = date_output_string(parse_datestring_to_date($+{'date'}));
return $out_date,
structured_answer => {
input => [$+{'date'}],
operation => 'Date math',
result => $out_date
};
}

my $input_date = parse_datestring_to_date($+{date});
my $input_number = str2nbr($+{number});
my $unit = $+{unit};

# check/tweak other (non-date) input
my %action_map = (
plus => '+',
'+' => '+',
minus => '-',
'-' => '-',
from => '+'
sub get_duration {
my ($number, $unit) = @_;
$unit = lc $unit . 's';
my $dur = DateTime::Duration->new(
$unit => $number,
);
my $action = $action_map{$+{action}} || return;
}

my $number = $action eq '-' ? 0 - $input_number : $input_number;
sub get_action_for {
my $action = shift;
return '+' if $action =~ /\+|plus|from/i;
return '-' if $action =~ /\-|minus|ago/i;
}

$unit =~ s/s$//g;
sub is_clock_unit {
my $unit = shift;
return $unit =~ /hour|minute|second/i if defined $unit;
return 0;
}

my ($years, $months, $days, $weeks) = (0, 0, 0, 0);
$years = $number if $unit eq "year";
$months = $number if $unit eq "month";
$days = $number if $unit eq "day";
$days = 7 * $number if $unit eq "week";
sub should_use_clock {
my ($unit, $form) = @_;
return 1 if is_clock_unit($unit);
return $form =~ /time/i if defined $form;
return 0;
}

my $dur = DateTime::Duration->new(
years => $years,
months => $months,
days => $days
);
sub format_result {
my ($out_date, $use_clock) = @_;
my $output_date = date_output_string($out_date, $use_clock);
return $output_date;
}

$unit .= 's' if $input_number > 1; # plural?
my $out_date = date_output_string($input_date->clone->add_duration($dur));
my $in_date = date_output_string($input_date);
sub format_input {
my ($input_date, $action, $unit, $input_number, $use_clock) = @_;
my $in_date = date_output_string($input_date, $use_clock);
my $out_action = "$action $input_number $unit";
return "$in_date $out_action";
}

my $number_re = number_style_regex();
my $datestring_regex = datestring_regex();

my $units = qr/(?<unit>second|minute|hour|day|week|month|year)s?/i;

my $relative_regex = qr/(?<number>$number_re|[a-z\s-]+)\s+$units/i;

my $action_re = qr/(?<action>plus|\+|\-|minus)/i;
my $date_re = qr/(?<date>$datestring_regex)/i;

my $operation_re = qr/$date_re(?:\s+$action_re\s+$relative_regex)?/i;
my $from_re = qr/$relative_regex\s+(?<action>from)\s+$date_re?/i;
my $ago_re = qr/$relative_regex\s+(?<action>ago)/i;
my $time_24h = time_24h_regex();
my $time_12h = time_12h_regex();
my $relative_dates = relative_dates_regex();

sub build_result {
my ($result, $formatted) = @_;
return $result, structured_answer => {
id => 'date_math',
name => 'Answer',
data => {
title => "$result",
subtitle => "$formatted",
},
templates => {
group => 'text',
},
};

}

sub get_result_relative {
my ($date, $use_clock) = @_;
return unless $date =~ $relative_dates;
my $parsed_date = parse_datestring_to_date($date);
my $result = format_result $parsed_date, $use_clock or return;
return build_result($result, ucfirst $date);
}

sub calculate_new_date {
my ($compute_number, $unit, $input_date) = @_;
my $dur = get_duration $compute_number, $unit;
return $input_date->clone->add_duration($dur);
}

sub get_result_action {
my ($action, $date, $number, $unit, $use_clock) = @_;
$action = get_action_for $action or return;
my $input_number = str2nbr($number);
my $style = number_style_for($input_number) or return;
my $compute_num = $style->for_computation($input_number);
my $out_num = $style->for_display($input_number);

my $input_date = parse_datestring_to_date(
defined($date) ? $date : "today") or return;

my $compute_number = $action eq '-' ? 0 - $compute_num : $compute_num;
my $out_date = calculate_new_date $compute_number, $unit, $input_date;
$unit .= 's' if abs($compute_number) != 1;
my $result = format_result($out_date, $use_clock);
my $formatted_input = format_input($input_date, $action, $unit, $out_num, $use_clock);
return build_result($result, $formatted_input);
}

handle query_lc => sub {
my $query = $_;

return unless $query =~ /^(?:(?<dort>date|time)\s+)?($operation_re|$from_re|$ago_re)$/i;

my $action = $+{action};
my $date = $+{date};
my $number = $+{number};
my $unit = $+{unit};
my $dort = $+{dort};

my $specified_time = $query =~ /$time_24h|$time_12h/;
my $use_clock = $specified_time || should_use_clock $unit, $dort;

return "$in_date $out_action is $out_date",
structured_answer => {
input => [$in_date . ' ' . $out_action],
operation => 'Date math',
result => $out_date
};
return get_result_relative($date, $use_clock) unless defined $number;
return get_result_action $action, $date, $number, $unit, $use_clock;
};

1;
31 changes: 25 additions & 6 deletions lib/DDG/GoodieRole/Dates.pm
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ my $relative_dates = qr#
now | today | tomorrow | yesterday |
(?:(?:current|previous|next)\sday) |
(?:next|last|this)\s(?:week|month|year) |
(?:in\s(?:a|[0-9]+)\s(?:day|week|month|year)[s]?)(?:\stime)? |
(?:in\s(?:a|[0-9]+)\s(?:day|week|month|year)[s]?)(?:\stime)? |
(?:(?:a|[0-9]+)\s(?:day|week|month|year)[s]?\sago)
#ix;

Expand Down Expand Up @@ -292,6 +292,12 @@ sub short_day_of_week_regex {
sub relative_dates_regex {
return $relative_dates;
}
sub time_24h_regex {
return $time_24h;
}
sub time_12h_regex {
return $time_12h;
}

# Accessors for matching regexes
# These matches are for "in the right format"/"looks about right"
Expand Down Expand Up @@ -326,6 +332,9 @@ sub build_datestring_regex {
# HTTP: Sat, 09 Aug 2014 18:20:00
push @regexes, qr#$short_day_of_week, [0-9]{2} $short_month $full_year $time_24h?#i;

# HTTP (without day) any TZ: 09 Aug 2014 18:20:00 UTC
push @regexes, qr#[0-9]{2} $short_month $full_year $time_24h(?: ?$tz_suffixes)?#i;

# RFC850 08-Feb-94 14:15:29 GMT
push @regexes, qr#[0-9]{2}-$short_month-(?:[0-9]{2}|$full_year) $time_24h?(?: ?$tz_suffixes)#i;

Expand Down Expand Up @@ -385,8 +394,17 @@ sub parse_formatted_datestring_to_date {
$d =~ s/,//i; # Strip any random commas.
$d =~ s/($full_month)/$full_month_to_short{lc $1}/i; # Parser deals better with the shorter month names.
$d =~ s/^($short_month)$date_delim(\d{1,2})/$2-$short_month_fix{lc $1}/i; # Switching Jun-01-2012 to 01 Jun 2012
$d =~ s/(?<tz>$tz_strings)$/$tz_offsets{uc $1}/i; # Convert trailing timezones to actual offsets.

my $maybe_date_object = try { DateTime::Format::HTTP->parse_datetime($d) }; # Don't die no matter how bad we did with checking our string.
if (ref $maybe_date_object eq 'DateTime') {
if (exists $+{tz}) {
try { $maybe_date_object->set_time_zone(uc $+{tz}) };
}
if ($maybe_date_object->strftime('%Z') eq 'floating') {
$maybe_date_object->set_time_zone(_get_timezone());
};
};

return $maybe_date_object;
}
Expand All @@ -407,12 +425,12 @@ sub parse_all_datestrings_to_date {
return if $month > 12; #there's a mish-mash of formats; give up
$date = "$year-$month-$day";
}

my $date_object = ($dates_to_return[0]
? parse_datestring_to_date($date, $dates_to_return[0])
: parse_datestring_to_date($date)
);

return unless $date_object;
push @dates_to_return, $date_object;
}
Expand Down Expand Up @@ -518,14 +536,15 @@ sub _util_add_unit {
# Takes a DateTime object (or a string which can be parsed into one)
# and returns a standard formatted output string or an empty string if it cannot be parsed.
sub date_output_string {
my $dt = shift;
my ($dt, $use_clock) = @_;

my $ddg_format = "%d %b %Y"; # Just here to make it easy to see.
my $ddg_clock_format = "%d %b %Y %T %Z"; # 01 Jan 2012 00:00:00 UTC (HTTP without day)
my $date_format = $use_clock ? $ddg_clock_format : $ddg_format;
my $string = ''; # By default we've got nothing.
# They didn't give us a DateTime object, let's try to make one from whatever we got.
$dt = parse_datestring_to_date($dt) if (ref($dt) !~ /DateTime/);

$string = $dt->strftime($ddg_format) if ($dt);
$string = $dt->strftime($date_format) if ($dt);

return $string;
}
Expand Down
35 changes: 29 additions & 6 deletions t/00-roles.t
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,20 @@ subtest 'Dates' => sub {
# RFC850
'08-Feb-94 14:15:29 GMT' => 760716929,
# date(1) default
'Sun Sep 7 15:57:56 EST 2014' => 1410123476,
'Sun Sep 7 15:57:56 EDT 2014' => 1410119876,
'Sun Sep 14 15:57:56 UTC 2014' => 1410710276,
'Sun Sep 7 20:11:44 CET 2014' => 1410117104,
'Sun Sep 7 20:11:44 BST 2014' => 1410117104,
# RFC 2822
'Sat, 13 Mar 2010 11:29:05 -0800' => 1268508545,
# HTTP (without day) - any TZ
# %d %b %Y %H:%M:%S %Z
'01 Jan 2012 00:01:20 UTC' => 1325376080,
'22 Jun 1998 00:00:02 GMT' => 898473602,
'07 Sep 2014 20:11:44 CET' => 1410117104,
'07 Sep 2014 20:11:44 cet' => 1410117104,
'09 Aug 2014 18:20:00' => 1407608400,
#Undefined/Natural formats:
'13/12/2011' => 1323734400, #DMY
'01/01/2001' => 978307200, #Ambiguous, but valid
Expand Down Expand Up @@ -234,9 +243,9 @@ subtest 'Dates' => sub {

restore_time();
};

subtest 'Relative naked months' => sub {

my %time_strings = (
"2015-01-13T00:00:00Z" => {
src => ['january', 'february'],
Expand All @@ -254,16 +263,16 @@ subtest 'Dates' => sub {
src => ['january', 'february'],
output => ['2015-01-01T00:00:00', '2015-02-01T00:00:00'],
},

);

foreach my $query_time (sort keys %time_strings) {
set_fixed_time($query_time);

my @source = @{$time_strings{$query_time}{src}};
my @expectation = @{$time_strings{$query_time}{output}};
my @result = DatesRoleTester::parse_all_datestrings_to_date(@source);

is_deeply(\@result, \@expectation);
}
};
Expand Down Expand Up @@ -322,6 +331,20 @@ subtest 'Dates' => sub {
}
}
};
subtest 'Valid clock string format' => sub {
my %date_strings = (
'01 Jan 2012 00:01:20 UTC' => ['01 Jan 2012 00:01:20 UTC', '01 Jan 2012 00:01:20 utc'],
'22 Jun 1998 00:00:02 UTC' => ['22 Jun 1998 00:00:02 GMT'],
'07 Sep 2014 20:11:44 EST' => ['07 Sep 2014 20:11:44 EST'],
'07 Sep 2014 20:11:44 -0400' => ['07 Sep 2014 20:11:44 EDT'],
'09 Aug 2014 18:20:00 UTC' => ['09 Aug 2014 18:20:00'],
);
foreach my $result (sort keys %date_strings) {
foreach my $test_string (@{$date_strings{$result}}) {
is(DatesRoleTester::date_output_string($test_string, 1), $result, $test_string . ' normalizes for output as ' . $result);
}
}
};
subtest 'Invalid standard string format' => sub {
my %bad_stuff = (
'Empty string' => '',
Expand Down
Loading

0 comments on commit 503dc9d

Please sign in to comment.