rt 4.2.16
[freeside.git] / rt / lib / RT / Date.pm
index db56cfe..6537fa3 100644 (file)
@@ -2,7 +2,7 @@
 #
 # COPYRIGHT:
 #
-# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
 #                                          <sales@bestpractical.com>
 #
 # (Except where explicitly superseded by other copyright notices)
@@ -56,7 +56,7 @@
 
 =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.
 
@@ -166,10 +166,21 @@ sub Set {
 
     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)$/ )
@@ -201,7 +212,7 @@ sub Set {
             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
@@ -230,7 +241,7 @@ sub Set {
             "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(
@@ -365,56 +376,105 @@ sub DiffAsString {
 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, @_ );
 
-    my ( $negative, $s, $time_unit );
+    unless ( $duration ) {
+        return $args{Short}? $self->loc("0s") : $self->loc("0 seconds");
+    }
+
+    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
@@ -425,10 +485,10 @@ sub AgeAsString { return $_[0]->DiffAsString }
 
 =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>.
 
@@ -438,7 +498,7 @@ sub AsString {
     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;
@@ -509,7 +569,8 @@ Returns new unix time.
 
 sub AddDays {
     my $self = shift;
-    my $days = shift || 1;
+    my $days = shift;
+    $days = 1 unless defined $days;
     return $self->AddSeconds( $days * $DAY );
 }
 
@@ -554,13 +615,21 @@ Returns the number of seconds since the epoch
 
 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>.
 
@@ -600,7 +669,7 @@ sub Time {
 
 =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.
 
 
@@ -638,7 +707,7 @@ Each method takes several arguments:
 
 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
 
@@ -686,7 +755,7 @@ sub DefaultFormat
                             $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]',
@@ -733,8 +802,8 @@ sub LocaleObj {
 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 L<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.
 
 =cut
@@ -795,11 +864,11 @@ sub LocalizedDateTime
 =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
 
@@ -821,7 +890,7 @@ sub ISO {
     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;
@@ -832,12 +901,12 @@ sub ISO {
 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
 
@@ -861,7 +930,7 @@ sub W3CDTF {
     $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 {
@@ -877,11 +946,11 @@ sub W3CDTF {
 
 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
@@ -919,8 +988,8 @@ Returns the object's date and time in RFC2616 (HTTP/1.1) format,
 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 | ...
@@ -935,7 +1004,7 @@ must be presented using this format.
 
 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
 
@@ -954,10 +1023,12 @@ sub RFC2616 {
 
 =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
 
@@ -1008,11 +1079,19 @@ argument unix C<$time>, default value is the current unix time.
 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"
+
+=item 2)
 
-2) The last element of the array returned is C<offset>, which
+The last element of the array returned is C<offset>, which
 represents timezone offset against C<UTC> in seconds.
 
+=back
+
 =cut
 
 sub Localtime
@@ -1034,7 +1113,7 @@ 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;
@@ -1046,16 +1125,16 @@ sub Localtime
 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
 
@@ -1086,40 +1165,36 @@ sub Timelocal {
 
 =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;
 
     if (@_ == 0) {
-        Carp::carp "RT::Date->Timezone is a setter only";
+        Carp::carp 'RT::Date->Timezone requires a context argument';
         return undef;
     }
 
     my $context = lc(shift);
 
-    $context = 'utc' unless $context =~ /^(?:utc|server|user)$/i;
-
     my $tz;
     if( $context eq 'user' ) {
         $tz = $self->CurrentUser->UserObj->Timezone;
@@ -1133,6 +1208,20 @@ sub Timezone {
     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;
+
+}
+
 
 RT::Base->_ImportOverlays();