Skip to content
This repository has been archived by the owner on Oct 15, 2022. It is now read-only.

Commit

Permalink
Merge pull request #3409 from duckduckgo/gd/random-date-past-future
Browse files Browse the repository at this point in the history
RandomDate: Add support for ranges
  • Loading branch information
tagawa authored Jul 28, 2016
2 parents 0656319 + 8347689 commit 51b64c2
Show file tree
Hide file tree
Showing 2 changed files with 227 additions and 40 deletions.
101 changes: 91 additions & 10 deletions lib/DDG/Goodie/RandomDate.pm
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use DateTime;
use DateTime::Locale; # Need it here to force Travis to build
# the dependency.
use List::Util qw(first);
with 'DDG::GoodieRole::Dates';

zci answer_type => 'random_date';

Expand All @@ -16,6 +17,9 @@ zci is_cached => 0;
triggers start => 'random';
triggers any => 'date';

# TODO: Replace these with $RE{...} when #2810 is merged.
my $date_re = datestring_regex();

my %standard_queries = (
'(week ?)?day' => ['%A', 'Weekday'],
'month( of the year)?' => ['%B', 'Month'],
Expand All @@ -36,6 +40,13 @@ my %standard_queries = (
'year' => ['%Y', 'Year'],
);

# TODO: Add support for other types after #2810 is merged.
my %supports_range = map { $_ => $date_re } (
'Date', 'Date and Time',
);

my @standard_query_forms = keys %standard_queries;

my @blacklist = (
'\\%{[^}]*}', # Access to any DateTime method.
);
Expand All @@ -44,35 +55,57 @@ my $blacklist_re = join '|', map { "($_)" } @blacklist;

my $standard_re = join '|', map { "($_)" } (keys %standard_queries);

my $range_form = qr/ ?((in the )?(past|future)|between.+)/;

handle query => sub {
my $query = shift;
$query =~ s/\s*past(.+)/$1 past/i;
$query =~ s/\s*future(.+)/$1 future/i;
my $format;
my $range_type = 'none';
my $type = 'format';
my $force_cldr = 0;
my $range_text = '';
# TODO: Allow blacklisted elements, but escape them for formatting.
return if $query =~ /$blacklist_re/;
if ($query =~ /^random ($standard_re)$/i) {
if ($query =~ /^random ($standard_re)(?<rt>$range_form)?$/i) {
$range_text = $+{rt} // '';
my $standard_query = $1;
my $k = first { $standard_query =~ qr/^$_$/i } (keys %standard_queries);
my $k = first { $standard_query =~ qr/^$_$/i } @standard_query_forms;
($format, $type) = @{$standard_queries{$k}};
$range_type = $supports_range{$type} // 'none';
} else {
return unless $query =~ /^((random|example) )?date for (?<format>.+?)(?<cldr> \(cldr\))?$/i;
$format = $+{'format'};
$force_cldr = defined $+{cldr};
}
srand();
my $random_date = get_random_date($lang->locale);
my $formatted = format_date($format, $random_date, $force_cldr) or return;
return if $range_text && $range_type eq 'none';
my ($min_date, $max_date) = parse_range($range_type, $lang->locale, $range_text);
return if $range_text && !(defined $min_date && defined $max_date);
my $random_date = get_random_date(
$lang->locale, $min_date, $max_date
) or return;
my ($formatted, $min_date_formatted, $max_date_formatted) = map {
format_date($format, $_, $force_cldr);
} ($random_date, $min_date, $max_date);
return unless $formatted;

return if $formatted eq $format;

my $subtitle = build_subtitle(
type => $type,
format => $format,
min => $min_date_formatted,
max => $max_date_formatted,
);

return html_enc("$formatted"),
structured_answer => {

data => {
title => html_enc("$formatted"),
subtitle => $type eq 'format'
? "Random date for: " . html_enc($format) : "Random $type",
title => html_enc("$formatted"),
subtitle => $subtitle,
},

templates => {
Expand All @@ -81,6 +114,19 @@ handle query => sub {
};
};

sub build_subtitle {
my %options = @_;
my $type = $options{type};
my $format = $options{format};
my ($min, $max) = @options{qw(min max)};
my $range_text = $supports_range{$type}
? " between $min and $max"
: '';
$type eq 'format'
? "Random date for: " . html_enc($format)
: "Random $type$range_text";
}

sub format_date {
my ($format, $date, $force_cldr) = @_;
my $formatted;
Expand All @@ -97,13 +143,48 @@ sub format_date {
my $MAX_DATE = 253_402_300_799;
# 0000-01-01T00:00:00Z
my $MIN_DATE = -62_167_219_200;
my $MAX_RAND = $MAX_DATE - $MIN_DATE;
sub get_random_date {
my ($locale, $min_date, $max_date) = @_;
my $range = abs($max_date->epoch - $min_date->epoch);
return if $range == 0;
my $rand_num = int(rand($range));
my $rand_epoch = $min_date->epoch + $rand_num;
return DateTime->from_epoch(
epoch => $rand_epoch, locale => $locale
);
}

sub now {
my $locale = shift;
my $rand_num = int(rand($MAX_RAND));
DateTime->now(locale => $locale);
}

sub from_epoch {
my ($epoch, $locale) = @_;
return DateTime->from_epoch(
epoch => ($rand_num + $MIN_DATE), locale => $locale
epoch => $epoch,
locale => $locale,
);
}

sub parse_range {
my ($range_type, $locale, $range_text) = @_;
my $start = from_epoch($MIN_DATE, $locale);
my $end = from_epoch($MAX_DATE, $locale);
return ($start, $end) if $range_type eq 'none' || $range_text eq '';
if ($range_text =~ s/(in the )?(?<t>past|future)//i) {
lc $+{t} eq 'past' and $end = now($locale);
lc $+{t} eq 'future' and $start = now($locale);
} elsif (my ($start_text, $end_text) = $range_text
=~ /between (.+) and (.+)/) {
$start = parse_datestring_to_date($start_text) or return;
$end = parse_datestring_to_date($end_text) or return;
$start->set_locale($locale);
$end->set_locale($locale);
} else {
return;
}
return ($start, $end);
}

1;
166 changes: 136 additions & 30 deletions t/RandomDate.t
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,43 @@ use Test::Deep;
use Test::More;
use DDG::Test::Goodie;
use DDG::Test::Language;
use Test::MockTime qw(set_fixed_time);

zci answer_type => "random_date";
zci is_cached => 0;

my %MAX = (
Date => 'Dec 31, 9999',
'Date and Time' => 'Dec 31, 9999, 11:59:59 PM',
);

my %MIN = (
Date => 'Jan 1, 0',
'Date and Time' => 'Jan 1, 0, 12:00:00 AM',
);

my %NOW = (
Date => 'Jan 1, 2000',
);

sub build_subtitle {
my %options = @_;
my $type = $options{type};
my $range_text = $MAX{$type} ?
" between $options{min} and $options{max}" : '';
$options{is_standard}
? "Random $options{type}$range_text"
: "Random date for: $options{format}";
}

sub build_structured_answer {
my ($format, $result, $is_standard) = @_;
return $result,
my %options = @_;
return $options{match},
structured_answer => {

data => {
title => $result,
subtitle => $is_standard
? "Random " . $format : "Random date for: $format",
title => $options{match},
subtitle => $options{subtitle},
},

templates => {
Expand All @@ -27,15 +51,23 @@ sub build_structured_answer {
};
}

sub build_test { test_zci(build_structured_answer(@_)) }
sub build_test {
my %options = @_;
$options{is_standard} //= 1;
$options{min} //= $MIN{$options{type}} // '';
$options{max} //= $MAX{$options{type}} // '';
$options{match} = re(qr/^$options{match}$/);
$options{subtitle} = build_subtitle(%options);
test_zci(build_structured_answer(%options))
}

sub language_test {
my ($code, $query, @test_params) = @_;
my ($code, $query, %test_params) = @_;
my $lang = test_language($code);
DDG::Request->new(
language => $lang,
query_raw => $query
) => build_test(@test_params);
) => build_test(%test_params);
}

my $time_24 = qr/\d{2}:\d{2}:\d{2}/;
Expand All @@ -51,35 +83,109 @@ my $day_of_month = qr/\d{1,2}/;
my $month_of_year = qr/\d{2}/;
my $month_letter = qr/[JFMASOND]/;

set_fixed_time('2000-01-01T00:00:00');

my %type_matches = (
'12-hour Time' => $time_12,
'24-hour Time' => $time_24,
'Date' => "$short_name $day_of_month, $year",
'Date and Time' => "$short_name $day_of_month, $year, $time_12",
'Day of the Week' => qr/\d/,
'Day of the Year' => qr/\d{3}/,
'ISO-8601 Date' => "$year-$month_of_year-$day_of_month",
'Month' => $long_name,
'Time' => $time_12,
'Week' => $week,
'Weekday' => $day_en,
);

sub build_format_test {
my ($format, $re) = @_;
build_test(
type => 'format',
format => $format,
match => $re,
is_standard => 0,
);
}

sub build_standard_test {
my %options = @_ == 1 ? (type => $_[0]) : @_;
build_test(
%options,
match => $type_matches{$options{type}},
);
}

sub build_range_test {
my ($type, $min, $max) = @_;
$max = $MAX{$type} if $max eq 'max';
$min = $MIN{$type} if $min eq 'min';
$max = $NOW{$type} if $max eq 'now';
$min = $NOW{$type} if $min eq 'now';
build_standard_test(
type => $type,
min => $min,
max => $max,
);
}

ddg_goodie_test(
[qw( DDG::Goodie::RandomDate )],
# strftime Formats
'random date for %Y' => build_test('%Y', re(qr/$year/)),
'date for %a, %b %T' => build_test('%a, %b %T', re(qr/$short_name, $short_name $time_24/)),
'example date for %a' => build_test('%a', re(qr/$short_name/)),
'random date for %Y' => build_format_test('%Y', qr/$year/),
'date for %a, %b %T' => build_format_test('%a, %b %T', qr/$short_name, $short_name $time_24/),
'example date for %a' => build_format_test('%a', qr/$short_name/),
# CLDR Formats
'date for MMMM' => build_test('MMMM', re(qr/$long_name/)),
'date for MMMd' => build_test('MMMd', re(qr/$short_name$day_of_month/)),
'date for EEEE, MMMMM' => build_test('EEEE, MMMMM', re(qr/$day_en, $month_letter/)),
'date for %K (cldr)' => build_test('%K', re(qr/%\d{1,2}/)),
'date for %m (cldr)' => build_test('%m', re(qr/%\d{1,2}/)),
'date for MMMM' => build_format_test('MMMM', $long_name),
'date for MMMd' => build_format_test('MMMd', "$short_name$day_of_month"),
'date for EEEE, MMMMM' => build_format_test('EEEE, MMMMM', "$day_en, $month_letter"),
'date for %K (cldr)' => build_format_test('%K', qr/%\d{1,2}/),
'date for %m (cldr)' => build_format_test('%m', qr/%\d{1,2}/),
# 'Standard' Queries
'random weekday' => build_test('Weekday', re($day_en), 1),
'random month' => build_test('Month', re($long_name), 1),
'random time' => build_test('Time', re($time_12), 1),
'random 12-hour time' => build_test('12-hour Time', re($time_12), 1),
'random 24-hour time' => build_test('24-hour Time', re($time_24), 1),
'random week' => build_test('Week', re($week), 1),
'random datetime' => build_test('Date and Time', re(qr/$short_name $day_of_month, $year, $time_12/), 1),
'random day of the week' => build_test('Day of the Week', re(qr/\d/), 1),
'random day of the year' => build_test('Day of the Year', re(qr/\d{3}/), 1),
'random iso-8601 date' => build_test('ISO-8601 Date', re(qr/$year-$month_of_year-$day_of_month/), 1),
'random weekday' => build_standard_test('Weekday'),
'random month' => build_standard_test('Month'),
'random time' => build_standard_test('Time'),
'random 12-hour time' => build_standard_test('12-hour Time'),
'random 24-hour time' => build_standard_test('24-hour Time'),
'random week' => build_standard_test('Week'),
'random datetime' => build_standard_test('Date and Time'),
'random day of the week' => build_standard_test('Day of the Week'),
'random day of the year' => build_standard_test('Day of the Year'),
'random iso-8601 date' => build_standard_test('ISO-8601 Date'),
# Other locales
language_test('my', 'random time', 'Time', re($time_12_my), 1),
language_test('my', 'random day', 'Weekday', re($day_my), 1),
language_test('my', 'random date for EEEE', 'EEEE', re($day_my)),
language_test('my', 'random time',
type => 'Time',
match => $time_12_my,
),
language_test('my', 'random day',
type => 'Weekday',
match => $day_my,
),
language_test('my', 'random date for EEEE',
type => 'format',
format => 'EEEE',
match => $day_my,
is_standard => 0,
),
# With HTML
'random date for <p>%a</p>' => build_test('&lt;p&gt;%a&lt;/p&gt;', re(qr/&lt;p&gt;$short_name&lt;\/p&gt;/)),
'random date for <p>%a</p>' => build_format_test('&lt;p&gt;%a&lt;/p&gt;', qr/&lt;p&gt;$short_name&lt;\/p&gt;/),
# With ranges
'random date in the past' => build_range_test('Date', 'min', 'now'),
'random past date' => build_range_test('Date', 'min', 'now'),
'random date past' => build_range_test('Date', 'min', 'now'),
'random date in the future' => build_range_test('Date', 'now', 'max'),
'random future date' => build_range_test('Date', 'now', 'max'),
'random date future' => build_range_test('Date', 'now', 'max'),
'random date between 2005-06-10 and 2006-06-11' => build_range_test(
'Date', 'Jun 10, 2005', 'Jun 11, 2006',
),
'random date between 2005-06-10 and 2005-06-10' => undef,
'random date between' => undef,
'random date between now and bar' => undef,
'random date between now' => undef,
# Not supported
'random century in the past' => undef,
# Invalid Queries
'date for %K' => undef,
'date for %{year}' => undef,
Expand Down

0 comments on commit 51b64c2

Please sign in to comment.