# BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # # This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC # # # (Except where explicitly superseded by other copyright notices) # # # LICENSE: # # This work is made available to you under the terms of Version 2 of # the GNU General Public License. A copy of that license should have # been provided with this software, but in any event can be snarfed # from www.gnu.org. # # This work 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, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 or visit their web page on the internet at # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. # # # CONTRIBUTION SUBMISSION POLICY: # # (The following paragraph is not intended to limit the rights granted # to you to modify and distribute this software under the terms of # the GNU General Public License and is only of importance to you if # you choose to contribute your changes and enhancements to the # community by submitting them to Best Practical Solutions, LLC.) # # By intentionally submitting any modifications, corrections or # derivatives to this work, or any other work intended for use with # Request Tracker, to Best Practical Solutions, LLC, you confirm that # you are the copyright holder for those contributions and you grant # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, # royalty-free, perpetual, license to use, copy, create derivative # works based on those contributions, and sublicense and distribute # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} =head1 NAME RT::Date - a simple Object Oriented date. =head1 SYNOPSIS use RT::Date =head1 DESCRIPTION RT Date is a simple Date Object designed to be speedy and easy for RT to use The fact that it assumes that a time of 0 means "never" is probably a bug. =head1 METHODS =cut package RT::Date; use strict; use warnings; use base qw/RT::Base/; use DateTime; use Time::Local; use POSIX qw(tzset); use vars qw($MINUTE $HOUR $DAY $WEEK $MONTH $YEAR); $MINUTE = 60; $HOUR = 60 * $MINUTE; $DAY = 24 * $HOUR; $WEEK = 7 * $DAY; $MONTH = 30.4375 * $DAY; $YEAR = 365.25 * $DAY; our @MONTHS = ( 'Jan', # loc 'Feb', # loc 'Mar', # loc 'Apr', # loc 'May', # loc 'Jun', # loc 'Jul', # loc 'Aug', # loc 'Sep', # loc 'Oct', # loc 'Nov', # loc 'Dec', # loc ); our @DAYS_OF_WEEK = ( 'Sun', # loc 'Mon', # loc 'Tue', # loc 'Wed', # loc 'Thu', # loc 'Fri', # loc 'Sat', # loc ); our @FORMATTERS = ( 'DefaultFormat', # loc 'ISO', # loc 'W3CDTF', # loc 'RFC2822', # loc 'RFC2616', # loc 'iCal', # loc 'LocalizedDateTime', # loc ); =head2 new Object constructor takes one argument C object. =cut sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = {}; bless ($self, $class); $self->CurrentUser(@_); $self->Unix(0); return $self; } =head2 Set Takes a param hash with the fields C, C and C. If $args->{'Format'} is 'unix', takes the number of seconds since the epoch. If $args->{'Format'} is ISO, tries to parse an ISO date. If $args->{'Format'} is 'unknown', require Time::ParseDate and make it figure things out. This is a heavyweight operation that should never be called from within RT's core. But it's really useful for something like the textbox date entry where we let the user do whatever they want. If $args->{'Value'} is 0, assumes you mean never. =cut sub Set { my $self = shift; my %args = ( Format => 'unix', Value => time, Timezone => 'user', @_ ); return $self->Unix(0) unless $args{'Value'} && $args{'Value'} =~ /\S/; if ( $args{'Format'} =~ /^unix$/i ) { return $self->Unix( $args{'Value'} ); } elsif ( $args{'Format'} =~ /^(sql|datemanip|iso)$/i ) { $args{'Value'} =~ s!/!-!g; if ( ( $args{'Value'} =~ /^(\d{4})?(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/ ) || ( $args{'Value'} =~ /^(\d{4})?(\d\d)(\d\d)(\d\d):(\d\d):(\d\d)$/ ) || ( $args{'Value'} =~ /^(?:(\d{4})-)?(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/ ) || ( $args{'Value'} =~ /^(?:(\d{4})-)?(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)\+00$/ ) ) { my ($year, $mon, $mday, $hours, $min, $sec) = ($1, $2, $3, $4, $5, $6); # use current year if string has no value $year ||= (localtime time)[5] + 1900; #timegm expects month as 0->11 $mon--; #now that we've parsed it, deal with the case where everything was 0 return $self->Unix(0) if $mon < 0 || $mon > 11; my $tz = lc $args{'Format'} eq 'datemanip'? 'user': 'utc'; $self->Unix( $self->Timelocal( $tz, $sec, $min, $hours, $mday, $mon, $year ) ); $self->Unix(0) unless $self->Unix > 0; } else { $RT::Logger->warning( "Couldn't parse date '$args{'Value'}' as a $args{'Format'} format" ); return $self->Unix(0); } } elsif ( $args{'Format'} =~ /^unknown$/i ) { require Time::ParseDate; # the module supports only legacy timezones like PDT or EST... # so we parse date as GMT and later apply offset, this only # should be applied to absolute times, so compensate shift in NOW my $now = time; $now += ($self->Localtime( $args{Timezone}, $now ))[9]; my ($date, $error) = Time::ParseDate::parsedate( $args{'Value'}, GMT => 1, NOW => $now, UK => RT->Config->Get('DateDayBeforeMonth'), PREFER_PAST => RT->Config->Get('AmbiguousDayInPast'), PREFER_FUTURE => RT->Config->Get('AmbiguousDayInFuture'), ); unless ( defined $date ) { $RT::Logger->warning( "Couldn't parse date '$args{'Value'}' by Time::ParseDate" ); return $self->Unix(0); } # apply timezone offset $date -= ($self->Localtime( $args{Timezone}, $date ))[9]; $RT::Logger->debug( "RT::Date used Time::ParseDate to make '$args{'Value'}' $date\n" ); return $self->Set( Format => 'unix', Value => $date); } else { $RT::Logger->error( "Unknown Date format: $args{'Format'}\n" ); return $self->Unix(0); } return $self->Unix; } =head2 SetToNow Set the object's time to the current time. Takes no arguments and returns unix time. =cut sub SetToNow { return $_[0]->Unix(time); } =head2 SetToMidnight [Timezone => 'utc'] Sets the date to midnight (at the beginning of the day). Returns the unixtime at midnight. Arguments: =over 4 =item Timezone Timezone context C, C or C. See also L. =back =cut sub SetToMidnight { my $self = shift; my %args = ( Timezone => '', @_ ); my $new = $self->Timelocal( $args{'Timezone'}, 0,0,0,($self->Localtime( $args{'Timezone'} ))[3..9] ); return $self->Unix( $new ); } =head2 SetToStart PERIOD[, Timezone => 'utc' ] Set to the beginning of the current PERIOD, which can be "year", "month", "day", "hour", or "minute". =cut sub SetToStart { my $self = shift; my $p = uc(shift); my %args = @_; my $tz = $args{'Timezone'} || ''; my @localtime = $self->Localtime($tz); #remove 'offset' so that DST is figured based on the resulting time. pop @localtime; # This is the cleanest way to implement it, I swear. { $localtime[0]=0; last if ($p eq 'MINUTE'); $localtime[1]=0; last if ($p eq 'HOUR'); $localtime[2]=0; last if ($p eq 'DAY'); $localtime[3]=1; last if ($p eq 'MONTH'); $localtime[4]=0; last if ($p eq 'YEAR'); $RT::Logger->warning("Couldn't find start date of '$p'."); return; } my $new = $self->Timelocal($tz, @localtime); return $self->Unix($new); } =head2 Diff Takes either an C object or the date in unixtime format as a string, if nothing is specified uses the current time. Returns the differnce between the time in the current object and that time as a number of seconds. Returns C if any of two compared values is incorrect or not set. =cut sub Diff { my $self = shift; my $other = shift; $other = time unless defined $other; if ( UNIVERSAL::isa( $other, 'RT::Date' ) ) { $other = $other->Unix; } return undef unless $other=~ /^\d+$/ && $other > 0; my $unix = $self->Unix; return undef unless $unix > 0; return $unix - $other; } =head2 DiffAsString Takes either an C object or the date in unixtime format as a string, if nothing is specified uses the current time. Returns the differnce between C<$self> and that time as a number of seconds as a localized string fit for human consumption. Returns empty string if any of two compared values is incorrect or not set. =cut sub DiffAsString { my $self = shift; my $diff = $self->Diff( @_ ); return '' unless defined $diff; return $self->DurationAsString( $diff ); } =head2 DurationAsString Takes a number of seconds. Returns a localized string describing that duration. =cut sub DurationAsString { my $self = shift; my $duration = int shift; my ( $negative, $s, $time_unit ); $negative = 1 if $duration < 0; $duration = abs $duration; if ( $duration < $MINUTE ) { $s = $duration; $time_unit = $self->loc("sec"); } elsif ( $duration < ( 2 * $HOUR ) ) { $s = int( $duration / $MINUTE + 0.5 ); $time_unit = $self->loc("min"); } elsif ( $duration < ( 2 * $DAY ) ) { $s = int( $duration / $HOUR + 0.5 ); $time_unit = $self->loc("hours"); } elsif ( $duration < ( 2 * $WEEK ) ) { $s = int( $duration / $DAY + 0.5 ); $time_unit = $self->loc("days"); } elsif ( $duration < ( 2 * $MONTH ) ) { $s = int( $duration / $WEEK + 0.5 ); $time_unit = $self->loc("weeks"); } elsif ( $duration < $YEAR ) { $s = int( $duration / $MONTH + 0.5 ); $time_unit = $self->loc("months"); } else { $s = int( $duration / $YEAR + 0.5 ); $time_unit = $self->loc("years"); } if ( $negative ) { return $self->loc( "[_1] [_2] ago", $s, $time_unit ); } else { return $self->loc( "[_1] [_2]", $s, $time_unit ); } } =head2 AgeAsString Takes nothing. Returns a string that's the differnce between the time in the object and now. =cut sub AgeAsString { return $_[0]->DiffAsString } =head2 AsString Returns the object's time as a localized string with curent user's prefered format and timezone. If the current user didn't choose prefered format then system wide setting is used or L if the latter is not specified. See config option C. =cut sub AsString { my $self = shift; my %args = (@_); return $self->loc("Not set") unless $self->Unix > 0; my $format = RT->Config->Get( 'DateTimeFormat', $self->CurrentUser ) || 'DefaultFormat'; $format = { Format => $format } unless ref $format; %args = (%$format, %args); return $self->Get( Timezone => 'user', %args ); } =head2 GetWeekday DAY Takes an integer day of week and returns a localized string for that day of week. Valid values are from range 0-6, Note that B<0 is sunday>. =cut sub GetWeekday { my $self = shift; my $dow = shift; return $self->loc($DAYS_OF_WEEK[$dow]) if $DAYS_OF_WEEK[$dow]; return ''; } =head2 GetMonth MONTH Takes an integer month and returns a localized string for that month. Valid values are from from range 0-11. =cut sub GetMonth { my $self = shift; my $mon = shift; return $self->loc($MONTHS[$mon]) if $MONTHS[$mon]; return ''; } =head2 AddSeconds SECONDS Takes a number of seconds and returns the new unix time. Negative value can be used to substract seconds. =cut sub AddSeconds { my $self = shift; my $delta = shift or return $self->Unix; $self->Set(Format => 'unix', Value => ($self->Unix + $delta)); return ($self->Unix); } =head2 AddDays [DAYS] Adds C<24 hours * DAYS> to the current time. Adds one day when no argument is specified. Negative value can be used to substract days. Returns new unix time. =cut sub AddDays { my $self = shift; my $days = shift || 1; return $self->AddSeconds( $days * $DAY ); } =head2 AddDay Adds 24 hours to the current time. Returns new unix time. =cut sub AddDay { return $_[0]->AddSeconds($DAY) } =head2 AddMonth Adds one month to the current time. Returns new unix time. =cut sub AddMonth { my $self = shift; my %args = @_; my @localtime = $self->Localtime($args{'Timezone'}); # remove offset, as with SetToStart pop @localtime; $localtime[4]++; #month if ( $localtime[4] == 12 ) { $localtime[4] = 0; $localtime[5]++; #year } my $new = $self->Timelocal($args{'Timezone'}, @localtime); return $self->Unix($new); } =head2 Unix [unixtime] Optionally takes a date in unix seconds since the epoch format. Returns the number of seconds since the epoch =cut sub Unix { my $self = shift; $self->{'time'} = int(shift || 0) if @_; return $self->{'time'}; } =head2 DateTime Alias for L method. Arguments C and