2012-09-27 23:24:30 -07:00
|
|
|
package DDG::Goodie::TimezoneConverter;
|
2014-08-20 11:45:33 -07:00
|
|
|
# ABSTRACT: Convert times between timezones.
|
2012-09-27 23:24:30 -07:00
|
|
|
|
|
|
|
use 5.010;
|
|
|
|
use strict;
|
|
|
|
use warnings;
|
|
|
|
use DDG::Goodie;
|
2015-07-18 21:43:36 -07:00
|
|
|
use DDG::GoodieRole::Dates;
|
2014-10-28 05:57:28 -07:00
|
|
|
|
|
|
|
use DateTime;
|
2012-09-27 23:24:30 -07:00
|
|
|
use POSIX qw(fmod);
|
|
|
|
|
2015-02-06 12:42:14 -08:00
|
|
|
attribution github => ['GlitchMr', 'GlitchMr'],
|
|
|
|
github => ['https://github.com/samph', 'samph'];
|
|
|
|
|
2012-11-06 13:56:31 -08:00
|
|
|
|
|
|
|
primary_example_queries '10:00AM MST to PST';
|
|
|
|
secondary_example_queries '19:00 UTC to EST', '1am UTC to PST';
|
|
|
|
description 'convert times between timezones';
|
|
|
|
name 'Timezone Converter';
|
|
|
|
code_url 'https://github.com/duckduckgo/zeroclickinfo-goodies/blob/master/lib/DDG/Goodie/TimezoneConverter.pm';
|
|
|
|
category 'calculations';
|
|
|
|
topics 'travel';
|
|
|
|
|
2012-09-28 01:17:20 -07:00
|
|
|
triggers any => qw(in into to);
|
2012-09-27 23:24:30 -07:00
|
|
|
|
|
|
|
zci is_cached => 1;
|
|
|
|
zci answer_type => 'timezone_converter';
|
|
|
|
|
2015-07-18 21:43:36 -07:00
|
|
|
my %timezones = %DDG::GoodieRole::Dates::tz_offsets;
|
2012-09-27 23:24:30 -07:00
|
|
|
|
2014-10-28 07:04:01 -07:00
|
|
|
my $default_tz = 'UTC';
|
|
|
|
my $localtime_re = qr/(?:(?:my|local|my local)\s*time(?:zone)?)/i;
|
|
|
|
my $timezone_re = qr/(?:\w+(?:\s*[+-]0*[0-9]{1,5}(?::[0-5][0-9])?)?|$localtime_re)?/;
|
2014-10-28 05:57:28 -07:00
|
|
|
|
|
|
|
sub parse_timezone {
|
2012-09-27 23:24:30 -07:00
|
|
|
my $timezone = shift;
|
2014-10-28 05:57:28 -07:00
|
|
|
|
|
|
|
# They said "my timezone" or similar.
|
2014-10-28 07:04:01 -07:00
|
|
|
if ($timezone =~ /$localtime_re/i) {
|
2014-10-28 05:57:28 -07:00
|
|
|
my $dt = DateTime->now(time_zone => $loc->time_zone || $default_tz );
|
|
|
|
return ($dt->time_zone_short_name, $dt->offset / 3600);
|
|
|
|
}
|
|
|
|
|
|
|
|
# Normalize
|
|
|
|
$timezone ||= $default_tz;
|
|
|
|
$timezone = uc $timezone;
|
|
|
|
$timezone =~ s/\s+//g;
|
|
|
|
|
|
|
|
my ($name, $modifier, $minutes) = $timezone =~ /\A(\w+)(?:([+-]\d+)(?::(\d\d))?)?\z/;
|
2012-09-27 23:24:30 -07:00
|
|
|
|
|
|
|
# If timezone doesn't exist, skip it
|
|
|
|
return unless defined $timezones{$name};
|
|
|
|
|
|
|
|
# Modifier can be skipped
|
|
|
|
$modifier //= 0;
|
|
|
|
|
|
|
|
# Minutes can be skipped too
|
|
|
|
$minutes //= 0;
|
2014-10-28 05:57:28 -07:00
|
|
|
|
2015-07-18 21:43:36 -07:00
|
|
|
my $hour = int( $timezones{$name} / 100 );
|
|
|
|
$minutes += $timezones{$name} % 100;
|
|
|
|
|
|
|
|
return ($timezone, $hour + $modifier + $minutes / 60);
|
2012-09-27 23:24:30 -07:00
|
|
|
}
|
|
|
|
|
2012-09-28 10:05:56 -07:00
|
|
|
sub to_time {
|
|
|
|
my ($hours, $american) = @_;
|
2014-10-28 06:49:15 -07:00
|
|
|
|
2012-09-28 10:05:56 -07:00
|
|
|
my $pm = "";
|
2012-09-27 23:24:30 -07:00
|
|
|
my $seconds = 3600 * fmod $hours, 1 / 60;
|
|
|
|
|
|
|
|
# I'm using floating point numbers. They sometimes don't do what I want.
|
|
|
|
if ( sprintf( '%.5f', $seconds ) == 60 ) {
|
|
|
|
$seconds = 0;
|
|
|
|
}
|
|
|
|
my $minutes
|
|
|
|
= ( $hours - int $hours ) * 60 - sprintf( '%.4f', $seconds ) / 60;
|
|
|
|
my $seconds_format = int $seconds ? ':%02.0f' : "";
|
2012-09-28 10:05:56 -07:00
|
|
|
if ($american) {
|
2012-09-30 10:35:11 -07:00
|
|
|
# Special case certain hours
|
|
|
|
return 'midnight' if $hours == 0;
|
|
|
|
return 'noon' if $hours == 12;
|
2014-10-28 06:05:56 -07:00
|
|
|
$pm = ' AM';
|
2012-09-30 10:35:11 -07:00
|
|
|
if ($hours > 12) {
|
2014-10-28 06:05:56 -07:00
|
|
|
$pm = ' PM';
|
2014-09-12 08:12:26 -07:00
|
|
|
$hours -= 12 if (int($hours) > 12);
|
2012-09-28 10:05:56 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
sprintf "%i:%02.0f$seconds_format$pm", $hours, $minutes, $seconds;
|
2012-09-27 23:24:30 -07:00
|
|
|
}
|
|
|
|
|
2014-09-12 07:12:00 -07:00
|
|
|
handle query => sub {
|
|
|
|
my $query = $_;
|
|
|
|
$query =~ m{
|
2012-09-27 23:24:30 -07:00
|
|
|
\A \s*
|
|
|
|
# Time
|
|
|
|
# Hours
|
2014-09-12 07:12:00 -07:00
|
|
|
(?<h>[01]?[0-9] | 2[0-3])
|
2012-09-27 23:24:30 -07:00
|
|
|
(?:
|
2015-02-06 12:42:14 -08:00
|
|
|
#Optional colon
|
|
|
|
:?
|
2012-09-27 23:24:30 -07:00
|
|
|
# Minutes
|
2015-02-06 12:42:14 -08:00
|
|
|
(?<m>[0-5] [0-9])
|
2012-09-27 23:24:30 -07:00
|
|
|
(?:
|
|
|
|
# Seconds
|
2014-09-12 07:12:00 -07:00
|
|
|
:(?<s>[0-5] [0-9])
|
2012-09-27 23:24:30 -07:00
|
|
|
)?
|
|
|
|
)?
|
|
|
|
# Optional spaces between tokens
|
|
|
|
\s*
|
|
|
|
# AM/PM
|
2014-09-12 07:12:00 -07:00
|
|
|
(?<american>(?:A|(?<pm>P))\.?M\.?)?
|
2012-09-27 23:24:30 -07:00
|
|
|
# Spaces between tokens
|
|
|
|
\s* \b
|
2014-09-12 08:12:26 -07:00
|
|
|
# Optional "from" indicator for input timezone
|
|
|
|
(?:\s+FROM\s+)?
|
2012-09-27 23:24:30 -07:00
|
|
|
# Optional input timezone
|
2014-09-12 07:12:00 -07:00
|
|
|
(?<from_tz>$timezone_re)
|
2012-09-27 23:24:30 -07:00
|
|
|
# Spaces
|
|
|
|
\s+
|
|
|
|
# in keywords
|
2012-09-28 01:17:20 -07:00
|
|
|
(?: IN (?: TO )? | TO )
|
2012-09-27 23:24:30 -07:00
|
|
|
# Spaces
|
|
|
|
\s+
|
|
|
|
# Output timezone
|
2014-09-12 07:12:00 -07:00
|
|
|
(?<to_tz>$timezone_re)
|
2012-09-27 23:24:30 -07:00
|
|
|
\s* \z
|
2014-09-12 07:12:00 -07:00
|
|
|
}ix or return;
|
2012-09-27 23:24:30 -07:00
|
|
|
|
2014-09-12 07:12:00 -07:00
|
|
|
my ($hours, $minutes, $seconds) = map { $_ // 0 } ($+{'h'}, $+{'m'}, $+{'s'});
|
|
|
|
my $american = $+{'american'};
|
2014-10-28 06:49:15 -07:00
|
|
|
my $pm = ($+{'pm'} && $hours != 12) ? 12 : (!$+{'pm'} && $hours == 12) ? -12 : 0;
|
2012-09-27 23:24:30 -07:00
|
|
|
|
2014-10-28 05:57:28 -07:00
|
|
|
# parse_timezone returns undef if the timezone cannot be parsed
|
|
|
|
my ($input_timezone, $gmt_input_timezone) = parse_timezone($+{'from_tz'});
|
2013-01-16 00:56:56 -08:00
|
|
|
return unless defined $gmt_input_timezone;
|
2014-10-28 05:57:28 -07:00
|
|
|
my ($output_timezone, $gmt_output_timezone) = parse_timezone($+{'to_tz'});
|
2014-02-08 09:41:09 -08:00
|
|
|
return unless defined($gmt_output_timezone);
|
2013-01-16 00:56:56 -08:00
|
|
|
|
2014-10-28 05:57:28 -07:00
|
|
|
my $modifier = $gmt_output_timezone - $gmt_input_timezone;
|
2015-07-18 21:43:36 -07:00
|
|
|
|
2012-09-27 23:24:30 -07:00
|
|
|
for ( $gmt_input_timezone, $gmt_output_timezone ) {
|
2012-09-28 10:05:56 -07:00
|
|
|
$_ = to_time $_;
|
2012-09-27 23:24:30 -07:00
|
|
|
s/\A\b/+/;
|
|
|
|
s/:00\z//;
|
|
|
|
}
|
|
|
|
|
2014-09-12 07:12:00 -07:00
|
|
|
my $input_time = $hours + $minutes / 60 + $seconds / 3600 + $pm;
|
2012-09-27 23:24:30 -07:00
|
|
|
my $output_time = $input_time + $modifier;
|
|
|
|
for ( $input_time, $output_time ) {
|
|
|
|
my $days = "";
|
|
|
|
if ( $_ < 0 ) {
|
|
|
|
my $s = $_ <= -24 ? 's' : "";
|
|
|
|
$days = sprintf ', %i day%s prior', $_ / -24 + 1, $s;
|
|
|
|
|
|
|
|
# fmod() doesn't do what I want, Perl's % operator doesn't
|
|
|
|
# support floating point numbers. Instead, I will use
|
|
|
|
# lamest hack ever to do things correctly.
|
|
|
|
$_ += int( $_ / -24 + 1 ) * 24;
|
|
|
|
}
|
|
|
|
elsif ( $_ >= 24 ) {
|
|
|
|
my $s = $_ >= 48 ? 's' : "";
|
|
|
|
$days = sprintf ', %i day%s after', $_ / 24, $s;
|
|
|
|
}
|
|
|
|
$_ = fmod $_, 24;
|
2012-09-28 10:05:56 -07:00
|
|
|
$_ = to_time($_, $american) . $days;
|
2012-09-27 23:24:30 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
my ( $input_format, $output_format ) = ('%s, UTC%s') x 2;
|
|
|
|
my @input_timezones = ( $input_timezone, $gmt_input_timezone );
|
|
|
|
my @output_timezones = ( $output_timezone, $gmt_output_timezone );
|
|
|
|
|
|
|
|
# I'm using @timezones because I need list context.
|
2015-07-18 21:43:36 -07:00
|
|
|
if ( @timezones{ $input_timezone =~ /(\A\w+)/ } !~ /[1-9]/ ) {
|
2012-09-27 23:24:30 -07:00
|
|
|
$input_format = '%s';
|
|
|
|
pop @input_timezones;
|
|
|
|
}
|
2015-07-18 21:43:36 -07:00
|
|
|
if ( @timezones{ $output_timezone =~ /(\A\w+)/ } !~ /[1-9]/ ) {
|
2012-09-27 23:24:30 -07:00
|
|
|
$output_format = '%s';
|
|
|
|
pop @output_timezones;
|
|
|
|
}
|
2014-10-15 12:26:57 -07:00
|
|
|
|
2014-10-15 12:19:31 -07:00
|
|
|
my $output_string = sprintf "%s ($input_format) is %s ($output_format).",
|
|
|
|
ucfirst $input_time, @input_timezones,
|
|
|
|
$output_time, @output_timezones;
|
2014-10-15 12:36:49 -07:00
|
|
|
my $output_html = sprintf "<div class='zci--timezone-converter text--secondary'><span class='text--primary'>%s</span> ($input_format) is <span class='text--primary'>%s</span> ($output_format).</div>",
|
2014-10-15 12:26:57 -07:00
|
|
|
ucfirst $input_time, @input_timezones,
|
|
|
|
$output_time, @output_timezones;
|
2014-10-15 12:19:31 -07:00
|
|
|
|
2014-10-15 12:26:57 -07:00
|
|
|
return $output_string, html => $output_html;
|
2012-09-27 23:24:30 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
1;
|