#!/usr/bin/perl
###########################################
#
# Simple Time Zone Converter (c) Arjun Roychowdhury, arjunrc@gmail.com
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#################################################
# compilation for executable:
# pp -p tz.pl -c -o tz.par
# open par with 7zip, paste in perl\site\lib
# pp tz.par -o tz.cgi
use DateTime;
use Date::Parse;
use DateTime::TimeZone;
use Text::Capitalize;
use DateTime::Locale;
use File::Slurp;
my %cities; # will be populated with convenience names from file
my %tzmaps; # will contain full timezone and shortname hash
my %revtzmaps;
$glob_tzlist ="tzlist.read";
$glob_tzmap="tzmap.read";
$glob_revtzmap = "revtzmap.read";
#-------------------------------------------------------------------------------#
# Simple Date/Time and Timezone converer
#
# I needed a flexible no-nonsense Date/Time TZ converter that
# allowed me to enter free form text. Did not find any simple
# enough to use, so wrote this.
#
# - Arjun Roychowdhury
# ------------------------------------------------------------------------------#
#
# CHANGELOG:
# April 23 2008: Version 1.3
# - used file::slurp to decrease starup time
# - moved tzmaps, revtzmaps to files for faster processing
# Mar 30 2008: Version 1.2
# - added common convenience names
# - added option to edit convenience names at runtime
# - hashed tz/shot name list for faster processing (but slower start)
# Mar 08 2008: Version 1.1
# - added comma separated dest
# Nov 29/2006: Version 1.0
# - original version
#
#---------------------------------------------------------
# Error handler for browser environment
#---------------------------------------------------------
use CGI;
use CGI::Carp qw(fatalsToBrowser set_message);
BEGIN
{
sub handle_errors
{
my $msg = shift;
print "<p style='color:white; background-color:red'>ERROR: $msg</p>";
}
set_message(\&handle_errors);
}
# if the user decides to select a TZ from the dropdown list,
# the timezone text input is automatically updated
$JSCRIPT=<<EOF;
function updateVar(elem,x)
{
var v = elem.options[elem.selectedIndex].text;
if (x == '1')
{
document.timezone.form_stz.value = v;
}
else
{
document.timezone.form_dtz.value = v;
}
}
EOF
;
#---------------------------------------------------------
# returns version
#---------------------------------------------------------
sub version()
{
return "1.3";
}
#---------------------------------------------------------
# Debug printing
#---------------------------------------------------------
sub print_dbg {
print "<small><font color=blue><i>DBG::", @_, "</i></font></small><br>\n" if ($x_dbg);
}
#---------------------------------------------------------
# remove leading and trailing ws
#---------------------------------------------------------
sub trim($)
{
my $string = shift;
$string =~ s/^\s+//;
$string =~ s/\s+$//;
return $string;
}
#---------------------------------------------------------
# Given a convenience name, converts to
# proper TZ value
#---------------------------------------------------------
sub convenience_convert($)
{
my $sstring = shift;
return $sstring if (!$sstring);
$sstring=lc($sstring);
my $conv = "";
$conv = $cities{$sstring};
$conv = $sstring if (!$conv);
print_dbg ("Converted ".$sstring. " to ".$conv) if ($sstring ne $conv);
return $conv;
}
#---------------------------------------------------------
# MAIN
#---------------------------------------------------------
$glob_first=0;
#grab all well formatted timezones
@tzs=DateTime::TimeZone->all_names;
unshift(@tzs,""); # add a blank entry on top - just for form display
if (-r $glob_tzmap)
{
my $text = read_file($glob_tzmap);
%tzmaps = $text =~ /^(.+)=(.+)$/mg ;
my $text2 = read_file($glob_revtzmap);
%revtzmaps = $text2 =~ /^(.+)=(.+)$/mg ;
#while (@temp=each(%tzmaps)) {print "KEY:VALUE is @temp\n";}
#while (@temp2=each(%revtzmaps)) {print "REVKEY:VALUE is @temp2\n";}
#exit;
}
else
{
my $ddt = DateTime->now;
#populate tz/shortname hash for faster search
open (FH1,">$glob_tzmap");
open (FH2,">$glob_revtzmap");
for my $ndx (@tzs)
{
next if (!$ndx);
$ddt->set_time_zone($ndx);
my $sname = $ddt->time_zone_short_name;
$tzmaps{$sname}=$ndx;
#user can enter long names in mixed cases, so store key as all ucase so i can compare with uc
$revtzmaps{uc($ndx)}=uc($sname);
print FH1 "$sname=$ndx\n";
$rsname=uc($sname);
$rndx=uc($ndx);
print FH2 "$rndx=$rsname\n";
}
close(FH1);
close(FH2);
$glob_first++;
}
#exit if ($glob_first);
# read convenience mappings before each query
open (FH,"<mycities.inc") || die "Cannot find convenience mappings";
@cdd = ();
while (<FH>)
{
s/#.*//;
next if /^(\s)*$/;
chomp;
($key,$value)=split(':',$_);
$key = trim ($key);
$value = trim ($value);
$cities{$key}=$value;
push (@cdd, join(' is mapped to ',$key, $value));
}
# print_dbg("A total of ".keys(%cities)." convenience mappings have been detected in mycities.inc");
$ver=version();
$q = new CGI;
print $q->header(),
$q->start_html(-title=>'No-Nonsense TimeZoneConverter',-script=>$JSCRIPT,-style=>{-src=>'/tz.css'},),
$q->h3('No-Nonsense TimeZone Converter v'.$ver),
# start defining the form that will ask for values
$q->start_form(-name=>'timezone'),
"Time (free form text) ",$q->textfield('form_time','',50), "<a href='/tzhelp.html' target='_blank'> help</a>",$q->br,
"<small><i> enter time in whole or part, like 'nov 17 4:45p paris to india,london,oulu' or '12:20a ist to america/new_york' etc.</i></small>"
,$q->p,
"<span id='arc_note'>List of convenience mappings you can use above:", $q->popup_menu(-name=>'conv_dropdown', -values=>\@cdd),
$q->br,"To add to the list of convenience mappings, click <a href='/cgi-bin/cv.cgi'>here</a></span>",
$q->p,
"<a href='#' onclick=\"document.getElementById('arc_options').style.display='inline';\">Show</a>",
" or <a href='#' onclick=\"document.getElementById('arc_options').style.display='none';\">Hide</a> more options",
$q->br,
"<span id='arc_options'>",
"Optional: (use this only if you have not entered all the information above)",$q->p,
"Source Timezone ",$q->textfield('form_stz','',20)," or ",
$q->popup_menu(-name=>'stz_dropdown',-values=>\@tzs, -onChange=>"updateVar(this,1)"),
"<small><i>either select from a list or type it in - short forms work too, like cst,est...</i></small>",$q->br(),
"Target Timezone ",$q->textfield('form_dtz','',20)," or ",
$q->popup_menu(-name=>'dtz_dropdown',-values=>\@tzs,-onChange=>"updateVar(this,2)"),
"<small><i>in simplest form, leave everything blank and just fill in dst</i></small>",
$q->p,
"</span>",
$q->p,
$q->submit('submit','submit'),
$q->defaults('reset'), $q->p,
$q->checkbox('form_dbg',1,1,'debug'),
$q->end_form,
"<p><small><i>Tried many world times,none did what I needed easily...</i></small>",
$q->hr,"\n";
if ($q->param())
{
my $s_time = $q->param('form_time');
$x_dbg = $q->param('form_dbg'); # if checked, this will display useful debug output
($a,$b) = split (' to ',$s_time); # if user typed ' to ' that means the time box has both source and destination inputs
$a=trim($a);
$b=trim($b);
# now see if user has specified multiple :0
# timezones
@destinations=split(',',$b);
my $list_dest = $q->param('form_dtz');
push @destinations, $list_dest if ($list_dest); # if there is a dest in the dropdown, add it
$original_a = $a;
foreach $b (@destinations)
{
$a = $original_a; # since we strip tz from $a, we need the original back for a comma separated list
$b=trim($b);
my @atz=($q->param('form_stz'), $q->param('form_dtz'));
# resolve EST ambiguity - bias towards US/EST here - since EST is also used for other timezones in the world
if (lc($atz[0]) eq "est") {$atz[0]="EST5EDT";print_dbg("Source:Converted EST to EST5EDT (I assumed you meant EST of USA)");}
if (lc($atz[1]) eq "est") {$atz[1]="EST5EDT";print_dbg("Dest:Converted EST to EST5EDT (I assumed you meant EST of USA)");}
$b = convenience_convert ($b);
if ($b)
{
print_dbg("Found all values in Time box..");
$atz[1]=$b;
} #if b
else
{
print_dbg("looks like you did not specify destination in time box - so I will check the other boxes...");
}
# now we need to check if $a also has TZ
# logic is we check for last word. If it ends with 't' and does not
# begin with 'a', it is a timezone, since otherwise it may be august
# Alternately, if it had a '/' then it is also a timezone
$olda=$a;
$a =~s/(\S+)$//; #get last word in $1, remove last word from time
$etz=$1;
# now that we have extracted the last word, let us see if it is really a timezone
#if ( ((lc(substr($etz,-1,1)) eq "t") && (lc(substr($etz,0,1)) ne "a")) || ($etz =~ m:/:) || ($etz ne convenience_convert($etz)))
#{
$s_time = $a;
$atz[0] = $etz;
#print_dbg ("IT IT S ATZ");
#}
#else # last word was not a timezone
#{
# $s_time=$olda; # so, put it back to where it belongs
# print_dbg ("Last word not FQTZ reverting back to $s_time");
#}
if (!$atz[0])
{
$atz[0]="America/New_York";
print_dbg("You did not specify a source timezone, so I am defaulting to America/New_York");
}
if (!$atz[1])
{
$atz[1]="America/New_York";
print_dbg("You did not specify a destination timezone, so I am defaulting to America/New_York");
}
if ((!($atz[0]=~m:/:)) || (!($atz[1]=~m:/:)))
{
print_dbg("You are using shortcodes in timezones. Remember that the same shortcode can represent different time zones");
print_dbg("So I am going to try a best match. If it is not what you want, I suggest you use the full Timezone name from the dropdown-list");
}
$atz[0] = convenience_convert($atz[0]);
$atz[1] = convenience_convert($atz[1]);
# flexible parser for freeform date/time entries
($xss,$xmin,$xhr,$xday,$xmonth,$xyear,) = strptime($s_time);
# in windows, perl could not figure out my local time with 'local',
# did not bother investigating...
my $oopsie=0;
foreach $elem (@atz)
{
if ($tzmaps{uc($elem)})
{
print_dbg("Found ". uc($elem).", replacing with".$tzmaps{uc($elem)}
." (which supposedly also uses a timezone shortcode of ".uc($elem).")");
$elem = $tzmaps{uc($elem)};
}
elsif (!$revtzmaps{uc($elem)})
{
print "<p style='color:black; background-color:yellow'>Oops. I don't recognize '$elem'..</p>";
$oopsie=1;
}
}
next if $oopsie;
my $s_tz = $atz[0];
my $d_tz = $atz[1];
$s_tz=capitalize($s_tz);
$d_tz=capitalize($d_tz);
# the above does not capitalize the character after _ so let us do it manually
# otherwise set_time_zone barfs - it needs exact capitalization
$s_tz =~ s/_(.)/"_".uc($1)/eg;
$d_tz =~ s/_(.)/"_".uc($1)/eg;
print_dbg("Final values: STZ=$s_tz and DTZ=$d_tz");
my $source = DateTime->now->set_time_zone($s_tz);
# we start with current time, and then replace with whatever the user enters.
if ($s_time)
{
$source->set_hour($xhr) if (defined $xhr);
$source->set_minute($xmin) if (defined $xmin);
$source->set_day($xday) if (defined $xday);
$source->set_month($xmonth+1) if (defined $xmonth);
$source->set_year($xyear+1900) if (defined $xyear);
}
# make a copy with the dest. tz
my $result = $source->clone()
->set_time_zone($d_tz);
print "<h3>",$source->strftime(" %a, %b %d %Y: "),"<font color=blue>",$source->strftime("%I:%M%P %Z"),
"</font> is ". $result->strftime(" %a, %b %d %Y: ")."<font color=red>",
$result->strftime("%I:%M%P %Z"),"</font></h3>";
} # end foreach dest
} #q->param
# print "<img src=\"../GMT2.jpg\" />"; # just a pretty picture..
print $q->end_html;