#
# COPYRIGHT:
#
-# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
# <sales@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
package RT::Date;
-use Time::Local;
-use POSIX qw(tzset);
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;
);
our @FORMATTERS = (
- 'DefaultFormat', # loc
- 'ISO', # loc
- 'W3CDTF', # loc
- 'RFC2822', # loc
- 'RFC2616', # loc
- 'iCal', # loc
+ 'DefaultFormat', # loc
+ 'ISO', # loc
+ 'W3CDTF', # loc
+ 'RFC2822', # loc
+ 'RFC2616', # loc
+ 'iCal', # loc
+ 'LocalizedDateTime', # loc
);
-if ( eval 'use DateTime qw(); 1;' && eval 'use DateTime::Locale qw(); 1;' &&
- DateTime->can('format_cldr') && DateTime::Locale::root->can('date_format_full') ) {
- push @FORMATTERS, 'LocalizedDateTime'; # loc
-}
=head2 new
@_
);
- return $self->Unix(0) unless $args{'Value'};
+ return $self->Unix(0) unless $args{'Value'} && $args{'Value'} =~ /\S/;
if ( $args{'Format'} =~ /^unix$/i ) {
return $self->Unix( $args{'Value'} );
# should be applied to absolute times, so compensate shift in NOW
my $now = time;
$now += ($self->Localtime( $args{Timezone}, $now ))[9];
- my $date = Time::ParseDate::parsedate(
+ my ($date, $error) = Time::ParseDate::parsedate(
$args{'Value'},
GMT => 1,
NOW => $now,
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];
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<RT::Date> object or the date in unixtime format as a string,
sub AddDays {
my $self = shift;
- my $days = shift || 1;
+ my $days = shift;
+ $days = 1 unless defined $days;
return $self->AddSeconds( $days * $DAY );
}
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.
my $self = shift;
my %args = (Format => 'ISO', @_);
my $formatter = $args{'Format'};
+ unless ( $self->ValidFormatter($formatter) ) {
+ RT->Logger->warning("Invalid date formatter '$formatter', falling back to ISO");
+ $formatter = 'ISO';
+ }
$formatter = 'ISO' unless $self->can($formatter);
return $self->$formatter( %args );
}
return @FORMATTERS;
}
+=head3 ValidFormatter FORMAT
+
+Returns a true value if C<FORMAT> is a known formatter. Otherwise returns
+false.
+
+=cut
+
+sub ValidFormatter {
+ my $self = shift;
+ my $format = shift;
+ return (grep { $_ eq $format } $self->Formatters and $self->can($format))
+ ? 1 : 0;
+}
+
=head3 DefaultFormat
=cut
}
}
+=head2 LocaleObj
+
+Returns the L<DateTime::Locale> object representing the current user's locale.
+
+=cut
+
+sub LocaleObj {
+ my $self = shift;
+
+ my $lang = $self->CurrentUser->UserObj->Lang;
+ unless ($lang) {
+ require I18N::LangTags::Detect;
+ $lang = ( I18N::LangTags::Detect::detect(), 'en' )[0];
+ }
+
+ return DateTime::Locale->load($lang);
+}
+
=head3 LocalizedDateTime
Returns date and time as string, with user localization.
Supports arguments: C<DateFormat> and C<TimeFormat> which may contains date and
-time format as specified in DateTime::Locale (default to full_date_format and
+time format as specified in L<DateTime::Locale> (default to full_date_format and
medium_time_format), C<AbbrDay> and C<AbbrMonth> which may be set to 0 if
you want full Day/Month names instead of abbreviated ones.
-Require optionnal DateTime::Locale module.
-
=cut
sub LocalizedDateTime
my %args = ( Date => 1,
Time => 1,
Timezone => '',
- DateFormat => 'date_format_full',
- TimeFormat => 'time_format_medium',
+ DateFormat => '',
+ TimeFormat => '',
AbbrDay => 1,
AbbrMonth => 1,
@_,
);
- return $self->loc("DateTime module missing") unless ( eval 'use DateTime qw(); 1;' );
- return $self->loc("DateTime::Locale module missing") unless ( eval 'use DateTime::Locale qw(); 1;' );
- return $self->loc("DateTime doesn't support format_cldr, you must upgrade to use this feature")
- unless can DateTime::('format_cldr');
-
-
- my $date_format = $args{'DateFormat'};
- my $time_format = $args{'TimeFormat'};
+ # Require valid names for the format methods
+ my $date_format = $args{DateFormat} =~ /^\w+$/
+ ? $args{DateFormat} : 'date_format_full';
- my $lang = $self->CurrentUser->UserObj->Lang;
- unless ($lang) {
- require I18N::LangTags::Detect;
- $lang = ( I18N::LangTags::Detect::detect(), 'en' )[0];
- }
-
+ my $time_format = $args{TimeFormat} =~ /^\w+$/
+ ? $args{TimeFormat} : 'time_format_medium';
- my $formatter = DateTime::Locale->load($lang);
- return $self->loc("DateTime::Locale doesn't support date_format_full, you must upgrade to use this feature")
- unless $formatter->can('date_format_full');
+ my $formatter = $self->LocaleObj;
$date_format = $formatter->$date_format;
$time_format = $formatter->$time_format;
$date_format =~ s/EEEE/EEE/g if ( $args{'AbbrDay'} );
# FIXME : another way to call this module without conflict with local
# DateTime method?
- my $dt = new DateTime::( locale => $lang,
+ my $dt = DateTime::->new( locale => $formatter,
time_zone => $tz,
year => $year,
month => $mon,
my ($date, $time) = ('','');
$date .= "$DAYS_OF_WEEK[$wday], " if $args{'DayOfWeek'} && $args{'Date'};
- $date .= "$mday $MONTHS[$mon] $year" if $args{'Date'};
+ $date .= sprintf("%02d %s %04d", $mday, $MONTHS[$mon], $year) if $args{'Date'};
if ( $args{'Time'} ) {
$time .= sprintf("%02d:%02d", $hour, $min);
Date => 1, Time => 1,
@_,
);
- my ($sec,$min,$hour,$mday,$mon,$year,$wday,$ydaym,$isdst,$offset) =
- $self->Localtime( 'utc' );
-
- #the month needs incrementing, as gmtime returns 0-11
- $mon++;
my $res;
if ( $args{'Date'} && !$args{'Time'} ) {
- $res = sprintf( '%04d%02d%02d', $year, $mon, $mday );
- }
- elsif ( !$args{'Date'} && $args{'Time'} ) {
+ my (undef, undef, undef, $mday, $mon, $year) =
+ $self->Localtime( 'user' );
+ $res = sprintf( '%04d%02d%02d', $year, $mon+1, $mday );
+ } elsif ( !$args{'Date'} && $args{'Time'} ) {
+ my ($sec, $min, $hour) =
+ $self->Localtime( 'utc' );
$res = sprintf( 'T%02d%02d%02dZ', $hour, $min, $sec );
- }
- else {
- $res = sprintf( '%04d%02d%02dT%02d%02d%02dZ', $year, $mon, $mday, $hour, $min, $sec );
+ } else {
+ my ($sec, $min, $hour, $mday, $mon, $year) =
+ $self->Localtime( 'utc' );
+ $res = sprintf( '%04d%02d%02dT%02d%02d%02dZ', $year, $mon+1, $mday, $hour, $min, $sec );
}
return $res;
}
sub Timezone {
my $self = shift;
+
+ if (@_ == 0) {
+ Carp::carp "RT::Date->Timezone is a setter only";
+ return undef;
+ }
+
my $context = lc(shift);
$context = 'utc' unless $context =~ /^(?:utc|server|user)$/i;