#
# COPYRIGHT:
#
-# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2017 Best Practical Solutions, LLC
# <sales@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
=head1 DESCRIPTION
-RT Date is a simple Date Object designed to be speedy and easy for RT to use
+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.
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 ) {
+ my $format = lc $args{'Format'};
+
+ if ( $format eq 'unix' ) {
return $self->Unix( $args{'Value'} );
}
- elsif ( $args{'Format'} =~ /^(sql|datemanip|iso)$/i ) {
+ elsif (
+ ($format eq 'sql' || $format eq 'iso')
+ && $args{'Value'} =~ /^(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/
+ ) {
+ local $@;
+ my $u = eval { Time::Local::timegm($6, $5, $4, $3, $2-1, $1) } || 0;
+ $RT::Logger->warning("Invalid date $args{'Value'}: $@") if $@ && !$u;
+ return $self->Unix( $u > 0 ? $u : 0 );
+ }
+ elsif ( $format =~ /^(sql|datemanip|iso)$/ ) {
$args{'Value'} =~ s!/!-!g;
if ( ( $args{'Value'} =~ /^(\d{4})?(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/ )
return $self->Unix(0);
}
}
- elsif ( $args{'Format'} =~ /^unknown$/i ) {
+ elsif ( $format eq 'unknown' ) {
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 = 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];
"RT::Date used Time::ParseDate to make '$args{'Value'}' $date\n"
);
- return $self->Set( Format => 'unix', Value => $date);
+ return $self->Unix($date || 0);
}
else {
$RT::Logger->error(
Takes a number of seconds. Returns a localized string describing
that duration.
+Takes optional named arguments:
+
+=over 4
+
+=item * Show
+
+How many elements to show, how precise it should be. Default is 1,
+most vague variant.
+
+=item * Short
+
+Turn on short notation with one character units, for example
+"3M 2d 1m 10s".
+
+=back
+
=cut
sub DurationAsString {
my $self = shift;
my $duration = int shift;
+ my %args = ( Show => 1, Short => 0, @_ );
+
+ unless ( $duration ) {
+ return $args{Short}? $self->loc("0s") : $self->loc("0 seconds");
+ }
- my ( $negative, $s, $time_unit );
+ my $negative;
$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");
+ my @res;
+
+ my $coef = 2;
+ my $i = 0;
+ while ( $duration > 0 && ++$i <= $args{'Show'} ) {
+
+ my ($locstr, $unit);
+ if ( $duration < $MINUTE ) {
+ $locstr = $args{Short}
+ ? '[_1]s' # loc
+ : '[quant,_1,second,seconds]'; # loc
+ $unit = 1;
+ }
+ elsif ( $duration < ( $coef * $HOUR ) ) {
+ $locstr = $args{Short}
+ ? '[_1]m' # loc
+ : '[quant,_1,minute,minutes]'; # loc
+ $unit = $MINUTE;
+ }
+ elsif ( $duration < ( $coef * $DAY ) ) {
+ $locstr = $args{Short}
+ ? '[_1]h' # loc
+ : '[quant,_1,hour,hours]'; # loc
+ $unit = $HOUR;
+ }
+ elsif ( $duration < ( $coef * $WEEK ) ) {
+ $locstr = $args{Short}
+ ? '[_1]d' # loc
+ : '[quant,_1,day,days]'; # loc
+ $unit = $DAY;
+ }
+ elsif ( $duration < ( $coef * $MONTH ) ) {
+ $locstr = $args{Short}
+ ? '[_1]W' # loc
+ : '[quant,_1,week,weeks]'; # loc
+ $unit = $WEEK;
+ }
+ elsif ( $duration < $YEAR ) {
+ $locstr = $args{Short}
+ ? '[_1]M' # loc
+ : '[quant,_1,month,months]'; # loc
+ $unit = $MONTH;
+ }
+ else {
+ $locstr = $args{Short}
+ ? '[_1]Y' # loc
+ : '[quant,_1,year,years]'; # loc
+ $unit = $YEAR;
+ }
+ my $value = int( $duration / $unit + ($i < $args{'Show'}? 0 : 0.5) );
+ $duration -= int( $value * $unit );
+
+ push @res, $self->loc($locstr, $value);
+
+ $coef = 1;
}
if ( $negative ) {
- return $self->loc( "[_1] [_2] ago", $s, $time_unit );
+ return $self->loc( "[_1] ago", join ' ', @res );
}
else {
- return $self->loc( "[_1] [_2]", $s, $time_unit );
+ return join ' ', @res;
}
}
=head2 AgeAsString
-Takes nothing. Returns a string that's the differnce between the
+Takes nothing. Returns a string that's the difference between the
time in the object and now.
=cut
=head2 AsString
-Returns the object's time as a localized string with curent user's prefered
+Returns the object's time as a localized string with curent user's preferred
format and timezone.
-If the current user didn't choose prefered format then system wide setting is
+If the current user didn't choose preferred format then system wide setting is
used or L</DefaultFormat> if the latter is not specified. See config option
C<DateTimeFormat>.
my $self = shift;
my %args = (@_);
- return $self->loc("Not set") unless $self->Unix > 0;
+ return $self->loc("Not set") unless $self->IsSet;
my $format = RT->Config->Get( 'DateTimeFormat', $self->CurrentUser ) || 'DefaultFormat';
$format = { Format => $format } unless ref $format;
sub AddDays {
my $self = shift;
- my $days = shift || 1;
+ my $days = shift;
+ $days = 1 unless defined $days;
return $self->AddSeconds( $days * $DAY );
}
sub Unix {
my $self = shift;
- $self->{'time'} = int(shift || 0) if @_;
+
+ if (@_) {
+ my $time = int(shift || 0);
+ if ($time < 0) {
+ RT->Logger->notice("Passed a unix time less than 0, forcing to 0: [$time]");
+ $time = 0;
+ }
+ $self->{'time'} = int $time;
+ }
return $self->{'time'};
}
=head2 DateTime
-Alias for L</Get> method. Arguments C<Date> and <Time>
+Alias for L</Get> method. Arguments C<Date> and C<Time>
are fixed to true values, other arguments could be used
as described in L</Get>.
=head2 Get
-Returnsa a formatted and localized string that represets time of
+Returns a formatted and localized string that represents the time of
the current object.
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 );
}
Formatters may also add own arguments to the list, for example
in RFC2822 format day of time in output is optional so it
-understand boolean argument C<DayOfTime>.
+understands boolean argument C<DayOfTime>.
=head3 Formatters
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
$self->Localtime($args{'Timezone'});
$wday = $self->GetWeekday($wday);
$mon = $self->GetMonth($mon);
- ($mday, $hour, $min, $sec) = map { sprintf "%02d", $_ } ($mday, $hour, $min, $sec);
+ $_ = sprintf "%02d", $_ foreach $mday, $hour, $min, $sec;
if( $args{'Date'} && !$args{'Time'} ) {
return $self->loc('[_1] [_2] [_3] [_4]',
}
}
+=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
-medium_time_format), C<AbbrDay> and C<AbbrMonth> which may be set to 0 if
+time format as specified in L<DateTime::Locale> (default to C<date_format_full> and
+C<time_format_medium>), 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');
+ # Require valid names for the format methods
+ my $date_format = $args{DateFormat} =~ /^\w+$/
+ ? $args{DateFormat} : 'date_format_full';
+ my $time_format = $args{TimeFormat} =~ /^\w+$/
+ ? $args{TimeFormat} : 'time_format_medium';
- my $date_format = $args{'DateFormat'};
- my $time_format = $args{'TimeFormat'};
-
- my $lang = $self->CurrentUser->UserObj->Lang;
- unless ($lang) {
- require I18N::LangTags::Detect;
- $lang = ( I18N::LangTags::Detect::detect(), 'en' )[0];
- }
-
-
- 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,
=head3 ISO
Returns the object's date in ISO format C<YYYY-MM-DD mm:hh:ss>.
-ISO format is locale independant, but adding timezone offset info
+ISO format is locale-independent, but adding timezone offset info
is not implemented yet.
Supports arguments: C<Timezone>, C<Date>, C<Time> and C<Seconds>.
-See </Output formatters> for description of arguments.
+See L</Output formatters> for description of arguments.
=cut
my $res = '';
$res .= sprintf("%04d-%02d-%02d", $year, $mon, $mday) if $args{'Date'};
$res .= sprintf(' %02d:%02d', $hour, $min) if $args{'Time'};
- $res .= sprintf(':%02d', $sec, $min) if $args{'Time'} && $args{'Seconds'};
+ $res .= sprintf(':%02d', $sec) if $args{'Time'} && $args{'Seconds'};
$res =~ s/^\s+//;
return $res;
Returns the object's date and time in W3C date time format
(L<http://www.w3.org/TR/NOTE-datetime>).
-Format is locale independand and is close enought to ISO, but
+Format is locale-independent and is close enough to ISO, but
note that date part is B<not optional> and output string
has timezone offset mark in C<[+-]hh:mm> format.
Supports arguments: C<Timezone>, C<Time> and C<Seconds>.
-See </Output formatters> for description of arguments.
+See L</Output formatters> for description of arguments.
=cut
$res .= sprintf("%04d-%02d-%02d", $year, $mon, $mday);
if ( $args{'Time'} ) {
$res .= sprintf('T%02d:%02d', $hour, $min);
- $res .= sprintf(':%02d', $sec, $min) if $args{'Seconds'};
+ $res .= sprintf(':%02d', $sec) if $args{'Seconds'};
if ( $offset ) {
$res .= sprintf "%s%02d:%02d", $self->_SplitOffset( $offset );
} else {
Returns the object's date and time in RFC2822 format,
for example C<Sun, 06 Nov 1994 08:49:37 +0000>.
-Format is locale independand as required by RFC. Time
+Format is locale-independent as required by RFC. Time
part always has timezone offset in digits with sign prefix.
Supports arguments: C<Timezone>, C<Date>, C<Time>, C<DayOfWeek>
-and C<Seconds>. See </Output formatters> for description of
+and C<Seconds>. See L</Output formatters> for description of
arguments.
=cut
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);
for example C<Sun, 06 Nov 1994 08:49:37 GMT>. While the RFC describes
version 1.1 of HTTP, but the same form date can be used in version 1.0.
-Format is fixed length, locale independand and always represented in GMT
-what makes it quite useless for users, but any date in HTTP transfers
+Format is fixed-length, locale-independent and always represented in GMT
+which makes it quite useless for users, but any date in HTTP transfers
must be presented using this format.
HTTP-date = rfc1123 | ...
Supports arguments: C<Date> and C<Time>, but you should use them only for
some personal reasons, RFC2616 doesn't define any optional parts.
-See </Output formatters> for description of arguments.
+See L</Output formatters> for description of arguments.
=cut
=head4 iCal
-Returns the object's date and time in iCalendar format,
+Returns the object's date and time in iCalendar format.
+If only date requested then user's timezone is used, otherwise
+it's UTC.
Supports arguments: C<Date> and C<Time>.
-See </Output formatters> for description of arguments.
+See L</Output formatters> for description of arguments.
=cut
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;
}
Returns object's date and time in the format provided by perl's
builtin functions C<localtime> and C<gmtime> with two exceptions:
-1) "Year" is a four-digit year, rather than "years since 1900"
+=over
+
+=item 1)
+
+"Year" is a four-digit year, rather than "years since 1900"
-2) The last element of the array returned is C<offset>, which
+=item 2)
+
+The last element of the array returned is C<offset>, which
represents timezone offset against C<UTC> in seconds.
+=back
+
=cut
sub Localtime
POSIX::tzset();
@local = localtime($unix);
}
- POSIX::tzset(); # return back previouse value
+ POSIX::tzset(); # return back previous value
}
$local[5] += 1900; # change year to 4+ digits format
my $offset = Time::Local::timegm_nocheck(@local) - $unix;
Takes argument C<$context>, which determines whether we should
treat C<@time> as "user local", "system" or "UTC" time.
-C<@time> is array returned by L<Localtime> functions. Only first
+C<@time> is array returned by L</Localtime> functions. Only first
six elements are mandatory - $sec, $min, $hour, $mday, $mon and $year.
You may pass $wday, $yday and $isdst, these are ignored.
If you pass C<$offset> as ninth argument, it's used instead of
C<$context>. It's done such way as code
-C<$self->Timelocal('utc', $self->Localtime('server'))> doesn't
-makes much sense and most probably would produce unexpected
-result, so the method ignore 'utc' context and uses offset
-returned by L<Localtime> method.
+C<< $self->Timelocal('utc', $self->Localtime('server')) >> doesn't
+make much sense and most probably would produce unexpected
+results, so the method ignores 'utc' context and uses the offset
+returned by the L</Localtime> method.
=cut
=head3 Timezone $context
-Returns the timezone name.
-
-Takes one argument, C<$context> argument which could be C<user>, C<server> or C<utc>.
+Returns the timezone name for the specified context. C<$context>
+should be one of these values:
=over
-=item user
+=item C<user>
-Default value is C<user> that mean it returns current user's Timezone value.
+The current user's Timezone value will be returned.
-=item server
+=item C<server>
-If context is C<server> it returns value of the C<Timezone> RT config option.
-
-=item utc
-
-If both server's and user's timezone names are undefined returns 'UTC'.
+The value of the C<Timezone> RT config option will be returned.
=back
+For any other value of C<$context>, or if the specified context has no
+defined timezone, C<UTC> is returned.
+
=cut
sub Timezone {
my $self = shift;
- my $context = lc(shift);
- $context = 'utc' unless $context =~ /^(?:utc|server|user)$/i;
+ if (@_ == 0) {
+ Carp::carp 'RT::Date->Timezone requires a context argument';
+ return undef;
+ }
+
+ my $context = lc(shift);
my $tz;
if( $context eq 'user' ) {
return $tz;
}
+=head3 IsSet
+
+Returns true if this Date is set in the database, otherwise returns a false value.
+
+This avoids needing to compare to 1970-01-01 in any of your code.
+
+=cut
+
+sub IsSet {
+ my $self = shift;
+ return $self->Unix ? 1 : 0;
+
+}
+
-eval "require RT::Date_Vendor";
-die $@ if ($@ && $@ !~ qr{^Can't locate RT/Date_Vendor.pm});
-eval "require RT::Date_Local";
-die $@ if ($@ && $@ !~ qr{^Can't locate RT/Date_Local.pm});
+RT::Base->_ImportOverlays();
1;