cc66e0f5d50f4d96a1c061264f11f8065fc8aa36
[freeside.git] / rt / lib / RT / Date.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 =head1 NAME
50
51   RT::Date - a simple Object Oriented date.
52
53 =head1 SYNOPSIS
54
55   use RT::Date
56
57 =head1 DESCRIPTION
58
59 RT Date is a simple Date Object designed to be speedy and easy for RT to use
60
61 The fact that it assumes that a time of 0 means "never" is probably a bug.
62
63
64 =head1 METHODS
65
66 =cut
67
68
69 package RT::Date;
70
71 use Time::Local;
72 use POSIX qw(tzset);
73
74 use strict;
75 use warnings;
76 use base qw/RT::Base/;
77
78 use vars qw($MINUTE $HOUR $DAY $WEEK $MONTH $YEAR);
79
80 $MINUTE = 60;
81 $HOUR   = 60 * $MINUTE;
82 $DAY    = 24 * $HOUR;
83 $WEEK   = 7 * $DAY;
84 $MONTH  = 30.4375 * $DAY;
85 $YEAR   = 365.25 * $DAY;
86
87 our @MONTHS = (
88     'Jan', # loc
89     'Feb', # loc
90     'Mar', # loc
91     'Apr', # loc
92     'May', # loc
93     'Jun', # loc
94     'Jul', # loc
95     'Aug', # loc
96     'Sep', # loc
97     'Oct', # loc
98     'Nov', # loc
99     'Dec', # loc
100 );
101
102 our @DAYS_OF_WEEK = (
103     'Sun', # loc
104     'Mon', # loc
105     'Tue', # loc
106     'Wed', # loc
107     'Thu', # loc
108     'Fri', # loc
109     'Sat', # loc
110 );
111
112 our @FORMATTERS = (
113     'DefaultFormat', # loc
114     'ISO',           # loc
115     'W3CDTF',        # loc
116     'RFC2822',       # loc
117     'RFC2616',       # loc
118     'iCal',          # loc
119 );
120 if ( eval 'use DateTime qw(); 1;' && eval 'use DateTime::Locale qw(); 1;' && 
121      DateTime->can('format_cldr') && DateTime::Locale::root->can('date_format_full') ) {
122     push @FORMATTERS, 'LocalizedDateTime'; # loc
123 }
124
125 =head2 new
126
127 Object constructor takes one argument C<RT::CurrentUser> object.
128
129 =cut
130
131 sub new {
132     my $proto = shift;
133     my $class = ref($proto) || $proto;
134     my $self  = {};
135     bless ($self, $class);
136     $self->CurrentUser(@_);
137     $self->Unix(0);
138     return $self;
139 }
140
141 =head2 Set
142
143 Takes a param hash with the fields C<Format>, C<Value> and C<Timezone>.
144
145 If $args->{'Format'} is 'unix', takes the number of seconds since the epoch.
146
147 If $args->{'Format'} is ISO, tries to parse an ISO date.
148
149 If $args->{'Format'} is 'unknown', require Time::ParseDate and make it figure
150 things out. This is a heavyweight operation that should never be called from
151 within RT's core. But it's really useful for something like the textbox date
152 entry where we let the user do whatever they want.
153
154 If $args->{'Value'} is 0, assumes you mean never.
155
156 =cut
157
158 sub Set {
159     my $self = shift;
160     my %args = (
161         Format   => 'unix',
162         Value    => time,
163         Timezone => 'user',
164         @_
165     );
166
167     return $self->Unix(0) unless $args{'Value'};
168
169     if ( $args{'Format'} =~ /^unix$/i ) {
170         return $self->Unix( $args{'Value'} );
171     }
172     elsif ( $args{'Format'} =~ /^(sql|datemanip|iso)$/i ) {
173         $args{'Value'} =~ s!/!-!g;
174
175         if (   ( $args{'Value'} =~ /^(\d{4})?(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/ )
176             || ( $args{'Value'} =~ /^(\d{4})?(\d\d)(\d\d)(\d\d):(\d\d):(\d\d)$/ )
177             || ( $args{'Value'} =~ /^(?:(\d{4})-)?(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/ )
178             || ( $args{'Value'} =~ /^(?:(\d{4})-)?(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)\+00$/ )
179           ) {
180
181             my ($year, $mon, $mday, $hours, $min, $sec)  = ($1, $2, $3, $4, $5, $6);
182
183             # use current year if string has no value
184             $year ||= (localtime time)[5] + 1900;
185
186             #timegm expects month as 0->11
187             $mon--;
188
189             #now that we've parsed it, deal with the case where everything was 0
190             return $self->Unix(0) if $mon < 0 || $mon > 11;
191
192             my $tz = lc $args{'Format'} eq 'datemanip'? 'user': 'utc';
193             $self->Unix( $self->Timelocal( $tz, $sec, $min, $hours, $mday, $mon, $year ) );
194
195             $self->Unix(0) unless $self->Unix > 0;
196         }
197         else {
198             $RT::Logger->warning(
199                 "Couldn't parse date '$args{'Value'}' as a $args{'Format'} format"
200             );
201             return $self->Unix(0);
202         }
203     }
204     elsif ( $args{'Format'} =~ /^unknown$/i ) {
205         require Time::ParseDate;
206         # the module supports only legacy timezones like PDT or EST...
207         # so we parse date as GMT and later apply offset, this only
208         # should be applied to absolute times, so compensate shift in NOW
209         my $now = time;
210         $now += ($self->Localtime( $args{Timezone}, $now ))[9];
211         my $date = Time::ParseDate::parsedate(
212             $args{'Value'},
213             GMT           => 1,
214             NOW           => $now,
215             UK            => RT->Config->Get('DateDayBeforeMonth'),
216             PREFER_PAST   => RT->Config->Get('AmbiguousDayInPast'),
217             PREFER_FUTURE => RT->Config->Get('AmbiguousDayInFuture'),
218         );
219         # apply timezone offset
220         $date -= ($self->Localtime( $args{Timezone}, $date ))[9];
221
222         $RT::Logger->debug(
223             "RT::Date used Time::ParseDate to make '$args{'Value'}' $date\n"
224         );
225
226         return $self->Set( Format => 'unix', Value => $date);
227     }
228     else {
229         $RT::Logger->error(
230             "Unknown Date format: $args{'Format'}\n"
231         );
232         return $self->Unix(0);
233     }
234
235     return $self->Unix;
236 }
237
238 =head2 SetToNow
239
240 Set the object's time to the current time. Takes no arguments
241 and returns unix time.
242
243 =cut
244
245 sub SetToNow {
246     return $_[0]->Unix(time);
247 }
248
249 =head2 SetToMidnight [Timezone => 'utc']
250
251 Sets the date to midnight (at the beginning of the day).
252 Returns the unixtime at midnight.
253
254 Arguments:
255
256 =over 4
257
258 =item Timezone
259
260 Timezone context C<user>, C<server> or C<UTC>. See also L</Timezone>.
261
262 =back
263
264 =cut
265
266 sub SetToMidnight {
267     my $self = shift;
268     my %args = ( Timezone => '', @_ );
269     my $new = $self->Timelocal(
270         $args{'Timezone'},
271         0,0,0,($self->Localtime( $args{'Timezone'} ))[3..9]
272     );
273     return $self->Unix( $new );
274 }
275
276 =head2 SetToStart PERIOD[, Timezone => 'utc' ]
277
278 Set to the beginning of the current PERIOD, which can be 
279 "year", "month", "day", "hour", or "minute".
280
281 =cut
282
283 sub SetToStart {
284     my $self = shift;
285     my $p = uc(shift);
286     my %args = @_;
287     my $tz = $args{'Timezone'} || '';
288     my @localtime = $self->Localtime($tz);
289
290     # This is the cleanest way to implement it, I swear.
291     {
292         $localtime[0]=0;
293         last if ($p eq 'MINUTE');
294         $localtime[1]=0;
295         last if ($p eq 'HOUR');
296         $localtime[2]=0;
297         last if ($p eq 'DAY');
298         $localtime[3]=1;
299         last if ($p eq 'MONTH');
300         $localtime[4]=0;
301         last if ($p eq 'YEAR');
302         $RT::Logger->warning("Couldn't find start date of '$p'.");
303         return;
304     }
305     my $new = $self->Timelocal($tz, @localtime);
306     return $self->Unix($new);
307 }
308
309 =head2 Diff
310
311 Takes either an C<RT::Date> object or the date in unixtime format as a string,
312 if nothing is specified uses the current time.
313
314 Returns the differnce between the time in the current object and that time
315 as a number of seconds. Returns C<undef> if any of two compared values is
316 incorrect or not set.
317
318 =cut
319
320 sub Diff {
321     my $self = shift;
322     my $other = shift;
323     $other = time unless defined $other;
324     if ( UNIVERSAL::isa( $other, 'RT::Date' ) ) {
325         $other = $other->Unix;
326     }
327     return undef unless $other=~ /^\d+$/ && $other > 0;
328
329     my $unix = $self->Unix;
330     return undef unless $unix > 0;
331
332     return $unix - $other;
333 }
334
335 =head2 DiffAsString
336
337 Takes either an C<RT::Date> object or the date in unixtime format as a string,
338 if nothing is specified uses the current time.
339
340 Returns the differnce between C<$self> and that time as a number of seconds as
341 a localized string fit for human consumption. Returns empty string if any of
342 two compared values is incorrect or not set.
343
344 =cut
345
346 sub DiffAsString {
347     my $self = shift;
348     my $diff = $self->Diff( @_ );
349     return '' unless defined $diff;
350
351     return $self->DurationAsString( $diff );
352 }
353
354 =head2 DurationAsString
355
356 Takes a number of seconds. Returns a localized string describing
357 that duration.
358
359 =cut
360
361 sub DurationAsString {
362     my $self     = shift;
363     my $duration = int shift;
364
365     my ( $negative, $s, $time_unit );
366     $negative = 1 if $duration < 0;
367     $duration = abs $duration;
368
369     if ( $duration < $MINUTE ) {
370         $s         = $duration;
371         $time_unit = $self->loc("sec");
372     }
373     elsif ( $duration < ( 2 * $HOUR ) ) {
374         $s         = int( $duration / $MINUTE + 0.5 );
375         $time_unit = $self->loc("min");
376     }
377     elsif ( $duration < ( 2 * $DAY ) ) {
378         $s         = int( $duration / $HOUR + 0.5 );
379         $time_unit = $self->loc("hours");
380     }
381     elsif ( $duration < ( 2 * $WEEK ) ) {
382         $s         = int( $duration / $DAY + 0.5 );
383         $time_unit = $self->loc("days");
384     }
385     elsif ( $duration < ( 2 * $MONTH ) ) {
386         $s         = int( $duration / $WEEK + 0.5 );
387         $time_unit = $self->loc("weeks");
388     }
389     elsif ( $duration < $YEAR ) {
390         $s         = int( $duration / $MONTH + 0.5 );
391         $time_unit = $self->loc("months");
392     }
393     else {
394         $s         = int( $duration / $YEAR + 0.5 );
395         $time_unit = $self->loc("years");
396     }
397
398     if ( $negative ) {
399         return $self->loc( "[_1] [_2] ago", $s, $time_unit );
400     }
401     else {
402         return $self->loc( "[_1] [_2]", $s, $time_unit );
403     }
404 }
405
406 =head2 AgeAsString
407
408 Takes nothing. Returns a string that's the differnce between the
409 time in the object and now.
410
411 =cut
412
413 sub AgeAsString { return $_[0]->DiffAsString }
414
415
416
417 =head2 AsString
418
419 Returns the object's time as a localized string with curent user's prefered
420 format and timezone.
421
422 If the current user didn't choose prefered format then system wide setting is
423 used or L</DefaultFormat> if the latter is not specified. See config option
424 C<DateTimeFormat>.
425
426 =cut
427
428 sub AsString {
429     my $self = shift;
430     my %args = (@_);
431
432     return $self->loc("Not set") unless $self->Unix > 0;
433
434     my $format = RT->Config->Get( 'DateTimeFormat', $self->CurrentUser ) || 'DefaultFormat';
435     $format = { Format => $format } unless ref $format;
436     %args = (%$format, %args);
437
438     return $self->Get( Timezone => 'user', %args );
439 }
440
441 =head2 GetWeekday DAY
442
443 Takes an integer day of week and returns a localized string for
444 that day of week. Valid values are from range 0-6, Note that B<0
445 is sunday>.
446
447 =cut
448
449 sub GetWeekday {
450     my $self = shift;
451     my $dow = shift;
452     
453     return $self->loc($DAYS_OF_WEEK[$dow])
454         if $DAYS_OF_WEEK[$dow];
455     return '';
456 }
457
458 =head2 GetMonth MONTH
459
460 Takes an integer month and returns a localized string for that month.
461 Valid values are from from range 0-11.
462
463 =cut
464
465 sub GetMonth {
466     my $self = shift;
467     my $mon = shift;
468
469     return $self->loc($MONTHS[$mon])
470         if $MONTHS[$mon];
471     return '';
472 }
473
474 =head2 AddSeconds SECONDS
475
476 Takes a number of seconds and returns the new unix time.
477
478 Negative value can be used to substract seconds.
479
480 =cut
481
482 sub AddSeconds {
483     my $self = shift;
484     my $delta = shift or return $self->Unix;
485     
486     $self->Set(Format => 'unix', Value => ($self->Unix + $delta));
487  
488     return ($self->Unix);
489 }
490
491 =head2 AddDays [DAYS]
492
493 Adds C<24 hours * DAYS> to the current time. Adds one day when
494 no argument is specified. Negative value can be used to substract
495 days.
496
497 Returns new unix time.
498
499 =cut
500
501 sub AddDays {
502     my $self = shift;
503     my $days = shift || 1;
504     return $self->AddSeconds( $days * $DAY );
505 }
506
507 =head2 AddDay
508
509 Adds 24 hours to the current time. Returns new unix time.
510
511 =cut
512
513 sub AddDay { return $_[0]->AddSeconds($DAY) }
514
515 =head2 AddMonth
516
517 Adds one month to the current time. Returns new 
518 unix time.
519
520 =cut
521
522 sub AddMonth {
523     require Time::ParseDate;
524     my $self = shift;
525     my $date = ( 
526         Time::ParseDate::parsedate(
527             '1 month',
528             NOW => $self->Unix
529         )
530     );
531     return $self->Unix($date);
532 }
533
534 =head2 Unix [unixtime]
535
536 Optionally takes a date in unix seconds since the epoch format.
537 Returns the number of seconds since the epoch
538
539 =cut
540
541 sub Unix {
542     my $self = shift; 
543     $self->{'time'} = int(shift || 0) if @_;
544     return $self->{'time'};
545 }
546
547 =head2 DateTime
548
549 Alias for L</Get> method. Arguments C<Date> and <Time>
550 are fixed to true values, other arguments could be used
551 as described in L</Get>.
552
553 =cut
554
555 sub DateTime {
556     my $self = shift;
557     unless (defined $self) {
558         use Carp; Carp::confess("undefined $self");
559     }
560     return $self->Get( @_, Date => 1, Time => 1 );
561 }
562
563 =head2 Date
564
565 Takes Format argument which allows you choose date formatter.
566 Pass throught other arguments to the formatter method.
567
568 Returns the object's formatted date. Default formatter is ISO.
569
570 =cut
571
572 sub Date {
573     my $self = shift;
574     return $self->Get( @_, Date => 1, Time => 0 );
575 }
576
577 =head2 Time
578
579
580 =cut
581
582 sub Time {
583     my $self = shift;
584     return $self->Get( @_, Date => 0, Time => 1 );
585 }
586
587 =head2 Get
588
589 Returnsa a formatted and localized string that represets time of
590 the current object.
591
592
593 =cut
594
595 sub Get
596 {
597     my $self = shift;
598     my %args = (Format => 'ISO', @_);
599     my $formatter = $args{'Format'};
600     $formatter = 'ISO' unless $self->can($formatter);
601     return $self->$formatter( %args );
602 }
603
604 =head2 Output formatters
605
606 Fomatter is a method that returns date and time in different configurable
607 format.
608
609 Each method takes several arguments:
610
611 =over 1
612
613 =item Date
614
615 =item Time
616
617 =item Timezone - Timezone context C<server>, C<user> or C<UTC>
618
619 =back
620
621 Formatters may also add own arguments to the list, for example
622 in RFC2822 format day of time in output is optional so it
623 understand boolean argument C<DayOfTime>.
624
625 =head3 Formatters
626
627 Returns an array of available formatters.
628
629 =cut
630
631 sub Formatters
632 {
633     my $self = shift;
634
635     return @FORMATTERS;
636 }
637
638 =head3 DefaultFormat
639
640 =cut
641
642 sub DefaultFormat
643 {
644     my $self = shift;
645     my %args = ( Date => 1,
646                  Time => 1,
647                  Timezone => '',
648                  Seconds => 1,
649                  @_,
650                );
651     
652        #  0    1    2     3     4    5     6     7      8      9
653     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$ydaym,$isdst,$offset) =
654                             $self->Localtime($args{'Timezone'});
655     $wday = $self->GetWeekday($wday);
656     $mon = $self->GetMonth($mon);
657     ($mday, $hour, $min, $sec) = map { sprintf "%02d", $_ } ($mday, $hour, $min, $sec);
658
659     if( $args{'Date'} && !$args{'Time'} ) {
660         return $self->loc('[_1] [_2] [_3] [_4]',
661                           $wday,$mon,$mday,$year);
662     } elsif( !$args{'Date'} && $args{'Time'} ) {
663         if( $args{'Seconds'} ) {
664             return $self->loc('[_1]:[_2]:[_3]',
665                               $hour,$min,$sec);
666         } else {
667             return $self->loc('[_1]:[_2]',
668                               $hour,$min);
669         }
670     } else {
671         if( $args{'Seconds'} ) {
672             return $self->loc('[_1] [_2] [_3] [_4]:[_5]:[_6] [_7]',
673                               $wday,$mon,$mday,$hour,$min,$sec,$year);
674         } else {
675             return $self->loc('[_1] [_2] [_3] [_4]:[_5] [_6]',
676                               $wday,$mon,$mday,$hour,$min,$year);
677         }
678     }
679 }
680
681 =head3 LocalizedDateTime
682
683 Returns date and time as string, with user localization.
684
685 Supports arguments: C<DateFormat> and C<TimeFormat> which may contains date and
686 time format as specified in DateTime::Locale (default to full_date_format and
687 medium_time_format), C<AbbrDay> and C<AbbrMonth> which may be set to 0 if
688 you want full Day/Month names instead of abbreviated ones.
689
690 Require optionnal DateTime::Locale module.
691
692 =cut
693
694 sub LocalizedDateTime
695 {
696     my $self = shift;
697     my %args = ( Date => 1,
698                  Time => 1,
699                  Timezone => '',
700                  DateFormat => 'date_format_full',
701                  TimeFormat => 'time_format_medium',
702                  AbbrDay => 1,
703                  AbbrMonth => 1,
704                  @_,
705                );
706
707     return $self->loc("DateTime module missing") unless ( eval 'use DateTime qw(); 1;' );
708     return $self->loc("DateTime::Locale module missing") unless ( eval 'use DateTime::Locale qw(); 1;' );
709     return $self->loc("DateTime doesn't support format_cldr, you must upgrade to use this feature") 
710         unless can DateTime::('format_cldr');
711
712
713     my $date_format = $args{'DateFormat'};
714     my $time_format = $args{'TimeFormat'};
715
716     my $lang = $self->CurrentUser->UserObj->Lang;
717     unless ($lang) {
718         require I18N::LangTags::Detect;
719         $lang = ( I18N::LangTags::Detect::detect(), 'en' )[0];
720     }
721     
722
723     my $formatter = DateTime::Locale->load($lang);
724     return $self->loc("DateTime::Locale doesn't support date_format_full, you must upgrade to use this feature") 
725         unless $formatter->can('date_format_full');
726     $date_format = $formatter->$date_format;
727     $time_format = $formatter->$time_format;
728     $date_format =~ s/EEEE/EEE/g if ( $args{'AbbrDay'} );
729     $date_format =~ s/MMMM/MMM/g if ( $args{'AbbrMonth'} );
730
731     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$ydaym,$isdst,$offset) =
732                             $self->Localtime($args{'Timezone'});
733     $mon++;
734     my $tz = $self->Timezone($args{'Timezone'});
735
736     # FIXME : another way to call this module without conflict with local
737     # DateTime method?
738     my $dt = new DateTime::( locale => $lang,
739                             time_zone => $tz,
740                             year => $year,
741                             month => $mon,
742                             day => $mday,
743                             hour => $hour,
744                             minute => $min,
745                             second => $sec,
746                             nanosecond => 0,
747                           );
748
749     if ( $args{'Date'} && !$args{'Time'} ) {
750         return $dt->format_cldr($date_format);
751     } elsif ( !$args{'Date'} && $args{'Time'} ) {
752         return $dt->format_cldr($time_format);
753     } else {
754         return $dt->format_cldr($date_format) . " " . $dt->format_cldr($time_format);
755     }
756 }
757
758 =head3 ISO
759
760 Returns the object's date in ISO format C<YYYY-MM-DD mm:hh:ss>.
761 ISO format is locale independant, but adding timezone offset info
762 is not implemented yet.
763
764 Supports arguments: C<Timezone>, C<Date>, C<Time> and C<Seconds>.
765 See </Output formatters> for description of arguments.
766
767 =cut
768
769 sub ISO {
770     my $self = shift;
771     my %args = ( Date => 1,
772                  Time => 1,
773                  Timezone => '',
774                  Seconds => 1,
775                  @_,
776                );
777        #  0    1    2     3     4    5     6     7      8      9
778     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$ydaym,$isdst,$offset) =
779                             $self->Localtime($args{'Timezone'});
780
781     #the month needs incrementing, as gmtime returns 0-11
782     $mon++;
783
784     my $res = '';
785     $res .= sprintf("%04d-%02d-%02d", $year, $mon, $mday) if $args{'Date'};
786     $res .= sprintf(' %02d:%02d', $hour, $min) if $args{'Time'};
787     $res .= sprintf(':%02d', $sec, $min) if $args{'Time'} && $args{'Seconds'};
788     $res =~ s/^\s+//;
789
790     return $res;
791 }
792
793 =head3 W3CDTF
794
795 Returns the object's date and time in W3C date time format
796 (L<http://www.w3.org/TR/NOTE-datetime>).
797
798 Format is locale independand and is close enought to ISO, but
799 note that date part is B<not optional> and output string
800 has timezone offset mark in C<[+-]hh:mm> format.
801
802 Supports arguments: C<Timezone>, C<Time> and C<Seconds>.
803 See </Output formatters> for description of arguments.
804
805 =cut
806
807 sub W3CDTF {
808     my $self = shift;
809     my %args = (
810         Time => 1,
811         Timezone => '',
812         Seconds => 1,
813         @_,
814         Date => 1,
815     );
816        #  0    1    2     3     4    5     6     7      8      9
817     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$ydaym,$isdst,$offset) =
818                             $self->Localtime( $args{'Timezone'} );
819
820     #the month needs incrementing, as gmtime returns 0-11
821     $mon++;
822
823     my $res = '';
824     $res .= sprintf("%04d-%02d-%02d", $year, $mon, $mday);
825     if ( $args{'Time'} ) {
826         $res .= sprintf('T%02d:%02d', $hour, $min);
827         $res .= sprintf(':%02d', $sec, $min) if $args{'Seconds'};
828         if ( $offset ) {
829             $res .= sprintf "%s%02d:%02d", $self->_SplitOffset( $offset );
830         } else {
831             $res .= 'Z';
832         }
833     }
834
835     return $res;
836 };
837
838
839 =head3 RFC2822 (MIME)
840
841 Returns the object's date and time in RFC2822 format,
842 for example C<Sun, 06 Nov 1994 08:49:37 +0000>.
843 Format is locale independand as required by RFC. Time
844 part always has timezone offset in digits with sign prefix.
845
846 Supports arguments: C<Timezone>, C<Date>, C<Time>, C<DayOfWeek>
847 and C<Seconds>. See </Output formatters> for description of
848 arguments.
849
850 =cut
851
852 sub RFC2822 {
853     my $self = shift;
854     my %args = ( Date => 1,
855                  Time => 1,
856                  Timezone => '',
857                  DayOfWeek => 1,
858                  Seconds => 1,
859                  @_,
860                );
861
862        #  0    1    2     3     4    5     6     7      8     9
863     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$ydaym,$isdst,$offset) =
864                             $self->Localtime($args{'Timezone'});
865
866     my ($date, $time) = ('','');
867     $date .= "$DAYS_OF_WEEK[$wday], " if $args{'DayOfWeek'} && $args{'Date'};
868     $date .= "$mday $MONTHS[$mon] $year" if $args{'Date'};
869
870     if ( $args{'Time'} ) {
871         $time .= sprintf("%02d:%02d", $hour, $min);
872         $time .= sprintf(":%02d", $sec) if $args{'Seconds'};
873         $time .= sprintf " %s%02d%02d", $self->_SplitOffset( $offset );
874     }
875
876     return join ' ', grep $_, ($date, $time);
877 }
878
879 =head3 RFC2616 (HTTP)
880
881 Returns the object's date and time in RFC2616 (HTTP/1.1) format,
882 for example C<Sun, 06 Nov 1994 08:49:37 GMT>. While the RFC describes
883 version 1.1 of HTTP, but the same form date can be used in version 1.0.
884
885 Format is fixed length, locale independand and always represented in GMT
886 what makes it quite useless for users, but any date in HTTP transfers
887 must be presented using this format.
888
889     HTTP-date = rfc1123 | ...
890     rfc1123   = wkday "," SP date SP time SP "GMT"
891     date      = 2DIGIT SP month SP 4DIGIT
892                 ; day month year (e.g., 02 Jun 1982)
893     time      = 2DIGIT ":" 2DIGIT ":" 2DIGIT
894                 ; 00:00:00 - 23:59:59
895     wkday     = "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" | "Sun"
896     month     = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun"
897               | "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec"
898
899 Supports arguments: C<Date> and C<Time>, but you should use them only for
900 some personal reasons, RFC2616 doesn't define any optional parts.
901 See </Output formatters> for description of arguments.
902
903 =cut
904
905 sub RFC2616 {
906     my $self = shift;
907     my %args = ( Date => 1, Time => 1,
908                  @_,
909                  Timezone => 'utc',
910                  Seconds => 1, DayOfWeek => 1,
911                );
912
913     my $res = $self->RFC2822( %args );
914     $res =~ s/\s*[+-]\d\d\d\d$/ GMT/ if $args{'Time'};
915     return $res;
916 }
917
918 =head4 iCal
919
920 Returns the object's date and time in iCalendar format,
921
922 Supports arguments: C<Date> and C<Time>.
923 See </Output formatters> for description of arguments.
924
925 =cut
926
927 sub iCal {
928     my $self = shift;
929     my %args = (
930         Date => 1, Time => 1,
931         @_,
932     );
933     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$ydaym,$isdst,$offset) =
934         $self->Localtime( 'utc' );
935
936     #the month needs incrementing, as gmtime returns 0-11
937     $mon++;
938
939     my $res;
940     if ( $args{'Date'} && !$args{'Time'} ) {
941         $res = sprintf( '%04d%02d%02d', $year, $mon, $mday );
942     }
943     elsif ( !$args{'Date'} && $args{'Time'} ) {
944         $res = sprintf( 'T%02d%02d%02dZ', $hour, $min, $sec );
945     }
946     else {
947         $res = sprintf( '%04d%02d%02dT%02d%02d%02dZ', $year, $mon, $mday, $hour, $min, $sec );
948     }
949     return $res;
950 }
951
952 # it's been added by mistake in 3.8.0
953 sub iCalDate { return (shift)->iCal( Time => 0, @_ ) }
954
955 sub _SplitOffset {
956     my ($self, $offset) = @_;
957     my $sign = $offset < 0? '-': '+';
958     $offset = int( (abs $offset) / 60 + 0.001 );
959     my $mins = $offset % 60;
960     my $hours = int( $offset/60 + 0.001 );
961     return $sign, $hours, $mins; 
962 }
963
964 =head2 Timezones handling
965
966 =head3 Localtime $context [$time]
967
968 Takes one mandatory argument C<$context>, which determines whether
969 we want "user local", "system" or "UTC" time. Also, takes optional
970 argument unix C<$time>, default value is the current unix time.
971
972 Returns object's date and time in the format provided by perl's
973 builtin functions C<localtime> and C<gmtime> with two exceptions:
974
975 1) "Year" is a four-digit year, rather than "years since 1900"
976
977 2) The last element of the array returned is C<offset>, which
978 represents timezone offset against C<UTC> in seconds.
979
980 =cut
981
982 sub Localtime
983 {
984     my $self = shift;
985     my $tz = $self->Timezone(shift);
986
987     my $unix = shift || $self->Unix;
988     $unix = 0 unless $unix >= 0;
989     
990     my @local;
991     if ($tz eq 'UTC') {
992         @local = gmtime($unix);
993     } else {
994         {
995             local $ENV{'TZ'} = $tz;
996             ## Using POSIX::tzset fixes a bug where the TZ environment variable
997             ## is cached.
998             POSIX::tzset();
999             @local = localtime($unix);
1000         }
1001         POSIX::tzset(); # return back previouse value
1002     }
1003     $local[5] += 1900; # change year to 4+ digits format
1004     my $offset = Time::Local::timegm_nocheck(@local) - $unix;
1005     return @local, $offset;
1006 }
1007
1008 =head3 Timelocal $context @time
1009
1010 Takes argument C<$context>, which determines whether we should
1011 treat C<@time> as "user local", "system" or "UTC" time.
1012
1013 C<@time> is array returned by L<Localtime> functions. Only first
1014 six elements are mandatory - $sec, $min, $hour, $mday, $mon and $year.
1015 You may pass $wday, $yday and $isdst, these are ignored.
1016
1017 If you pass C<$offset> as ninth argument, it's used instead of
1018 C<$context>. It's done such way as code 
1019 C<$self->Timelocal('utc', $self->Localtime('server'))> doesn't
1020 makes much sense and most probably would produce unexpected
1021 result, so the method ignore 'utc' context and uses offset
1022 returned by L<Localtime> method.
1023
1024 =cut
1025
1026 sub Timelocal {
1027     my $self = shift;
1028     my $tz = shift;
1029     if ( defined $_[9] ) {
1030         return timegm(@_[0..5]) - $_[9];
1031     } else {
1032         $tz = $self->Timezone( $tz );
1033         if ( $tz eq 'UTC' ) {
1034             return Time::Local::timegm(@_[0..5]);
1035         } else {
1036             my $rv;
1037             {
1038                 local $ENV{'TZ'} = $tz;
1039                 ## Using POSIX::tzset fixes a bug where the TZ environment variable
1040                 ## is cached.
1041                 POSIX::tzset();
1042                 $rv = Time::Local::timelocal(@_[0..5]);
1043             };
1044             POSIX::tzset(); # switch back to previouse value
1045             return $rv;
1046         }
1047     }
1048 }
1049
1050
1051 =head3 Timezone $context
1052
1053 Returns the timezone name.
1054
1055 Takes one argument, C<$context> argument which could be C<user>, C<server> or C<utc>.
1056
1057 =over
1058
1059 =item user
1060
1061 Default value is C<user> that mean it returns current user's Timezone value.
1062
1063 =item server
1064
1065 If context is C<server> it returns value of the C<Timezone> RT config option.
1066
1067 =item  utc
1068
1069 If both server's and user's timezone names are undefined returns 'UTC'.
1070
1071 =back
1072
1073 =cut
1074
1075 sub Timezone {
1076     my $self = shift;
1077     my $context = lc(shift);
1078
1079     $context = 'utc' unless $context =~ /^(?:utc|server|user)$/i;
1080
1081     my $tz;
1082     if( $context eq 'user' ) {
1083         $tz = $self->CurrentUser->UserObj->Timezone;
1084     } elsif( $context eq 'server') {
1085         $tz = RT->Config->Get('Timezone');
1086     } else {
1087         $tz = 'UTC';
1088     }
1089     $tz ||= RT->Config->Get('Timezone') || 'UTC';
1090     $tz = 'UTC' if lc $tz eq 'gmt';
1091     return $tz;
1092 }
1093
1094
1095 eval "require RT::Date_Vendor";
1096 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Date_Vendor.pm});
1097 eval "require RT::Date_Local";
1098 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Date_Local.pm});
1099
1100 1;