############################################################################### # # convert_date_time($date_time_string, $epoch) # # The function takes a date and time in ISO8601 "yyyy-mm-ddThh:mm:ss.ss" format # and converts it to a decimal number representing a valid Excel date. # # Dates and times in Excel are represented by real numbers. The integer part of # the number stores the number of days since the epoch and the fractional part # stores the percentage of the day in seconds. The epoch can be either 1900 or # 1904. # # Parameters: $date_time_string: String in one of the following formats: # yyyy-mm-ddThh:mm:ss.ss # Standard # yyyy-mm-ddT # Date only # Thh:mm:ss.ss # Time only # # $epoch: Boolean. Set the epoch as 1900 or 1904. Defaults to 1900. # # Returns: # A decimal number representing a valid Excel date, or # undef if the date is invalid. # sub convert_date_time { my $date_time = $_[0]; my $date_1904 = $_[1]; my $days = 0; # Number of days since epoch my $seconds = 0; # Time expressed as fraction of 24h hours in seconds my ($year, $month, $day); my ($hour, $min, $sec); # Strip leading and trailing whitespace. $date_time =~ s/^\s+//; $date_time =~ s/\s+$//; # Check for invalid date char. return if $date_time =~ /[^0-9T:\-\.Z]/; # Check for "T" after date or before time. return unless $date_time =~ /\dT|T\d/; # Strip trailing Z in ISO8601 date. $date_time =~ s/Z$//; # Split into date and time. my ($date, $time) = split /T/, $date_time; # We allow the time portion of the input DateTime to be optional. if ($time ne '') { # Match hh:mm:ss.sss+ where the seconds are optional if ($time =~ /^(\d\d):(\d\d)(:(\d\d(\.\d+)?))?/) { $hour = $1; $min = $2; $sec = $4 || 0; } else { return undef; # Not a valid time format. } # Some boundary checks return if $hour >= 24; return if $min >= 60; return if $sec >= 60; # Excel expresses seconds as a fraction of the number in 24 hours. $seconds = ($hour *60*60 + $min *60 + $sec) / (24 *60 *60); } # We allow the date portion of the input DateTime to be optional. return $seconds if $date eq ''; # Match date as yyyy-mm-dd. if ($date =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/) { $year = $1; $month = $2; $day = $3; } else { return undef; # Not a valid date format. } # Special cases for Excel. if (not $date_1904) { return $seconds if $date eq '1899-12-31'; # Excel 1900 epoch return $seconds if $date eq '1900-01-00'; # Excel 1900 epoch return 60 + $seconds if $date eq '1900-02-29'; # Excel false leapday } # We calculate the date by calculating the number of days since the epoch # and adjust for the number of leap days. We calculate the number of leap # days by normalising the year in relation to the epoch. Thus the year 2000 # becomes 100 for 4 and 100 year leapdays and 400 for 400 year leapdays. # my $epoch = $date_1904 ? 1904 : 1900; my $offset = $date_1904 ? 4 : 0; my $norm = 300; my $range = $year -$epoch; # Set month days and check for leap year. my @mdays = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); my $leap = 0; $leap = 1 if $year % 4 == 0 and $year % 100 or $year % 400 == 0; $mdays[1] = 29 if $leap; # Some boundary checks return if $year < $epoch or $year > 9999; return if $month < 1 or $month > 12; return if $day < 1 or $day > $mdays[$month -1]; # Accumulate the number of days since the epoch. $days = $day; # Add days for current month $days += $mdays[$_] for 0 .. $month -2; # Add days for past months $days += $range *365; # Add days for past years $days += int(($range) / 4); # Add leapdays $days -= int(($range +$offset) /100); # Subtract 100 year leapdays $days += int(($range +$offset +$norm)/400); # Add 400 year leapdays $days -= $leap; # Already counted above # Adjust for Excel erroneously treating 1900 as a leap year. $days++ if $date_1904 == 0 and $days > 59; return $days + $seconds; }