import rt 3.4.6
[freeside.git] / rt / lib / RT / Date.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC 
6 #                                          <jesse@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., 675 Mass Ave, Cambridge, MA 02139, USA.
26
27
28 # CONTRIBUTION SUBMISSION POLICY:
29
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
35
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
44
45 # END BPS TAGGED BLOCK }}}
46
47 =head1 NAME
48
49   RT::Date - a simple Object Oriented date.
50
51 =head1 SYNOPSIS
52
53   use RT::Date
54
55 =head1 DESCRIPTION
56
57 RT Date is a simple Date Object designed to be speedy and easy for RT to use
58
59 The fact that it assumes that a time of 0 means "never" is probably a bug.
60
61 =begin testing
62
63 ok (require RT::Date);
64
65 =end testing
66
67 =head1 METHODS
68
69 =cut
70
71
72 package RT::Date;
73
74 use Time::Local;
75
76 use RT::Base;
77
78 use strict;
79 use vars qw/@ISA/;
80 @ISA = qw/RT::Base/;
81
82 use vars qw($MINUTE $HOUR $DAY $WEEK $MONTH $YEAR);
83
84 $MINUTE = 60;
85 $HOUR   = 60 * $MINUTE;
86 $DAY    = 24 * $HOUR;
87 $WEEK   = 7 * $DAY;
88 $MONTH  = 4 * $WEEK;
89 $YEAR   = 365 * $DAY;
90
91 # {{{ sub new 
92
93 sub new  {
94   my $proto = shift;
95   my $class = ref($proto) || $proto;
96   my $self  = {};
97   bless ($self, $class);
98   $self->CurrentUser(@_);
99   $self->Unix(0);
100   return $self;
101 }
102
103 # }}}
104
105 # {{{ sub Set
106
107 =head2 sub Set
108
109 takes a param hash with the fields 'Format' and 'Value'
110
111 if $args->{'Format'} is 'unix', takes the number of seconds since the epoch 
112
113 If $args->{'Format'} is ISO, tries to parse an ISO date.
114
115 If $args->{'Format'} is 'unknown', require Time::ParseDate and make it figure
116 things out. This is a heavyweight operation that should never be called from
117 within RT's core. But it's really useful for something like the textbox date
118 entry where we let the user do whatever they want.
119
120 If $args->{'Value'}  is 0, assumes you mean never.
121
122 =begin testing
123
124 use_ok(RT::Date);
125 my $date = RT::Date->new($RT::SystemUser);
126 $date->Set(Format => 'unix', Value => '0');
127 ok ($date->ISO eq '1970-01-01 00:00:00', "Set a date to midnight 1/1/1970 GMT");
128
129 =end testing
130
131 =cut
132
133 sub Set {
134     my $self = shift;
135     my %args = ( Format => 'unix',
136                  Value  => time,
137                  @_ );
138     if ( !$args{'Value'}
139          || ( ( $args{'Value'} =~ /^\d*$/ ) and ( $args{'Value'} == 0 ) ) ) {
140         $self->Unix(-1);
141         return ( $self->Unix() );
142     }
143
144     if ( $args{'Format'} =~ /^unix$/i ) {
145         $self->Unix( $args{'Value'} );
146     }
147
148     elsif ( $args{'Format'} =~ /^(sql|datemanip|iso)$/i ) {
149         $args{'Value'} =~ s!/!-!g;
150
151         if (( $args{'Value'} =~ /^(\d{4}?)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/ )
152             || ( $args{'Value'} =~
153                  /^(\d{4}?)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/ )
154             || ( $args{'Value'} =~
155                  /^(\d{4}?)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)\+00$/ )
156             || ($args{'Value'} =~ /^(\d{4}?)(\d\d)(\d\d)(\d\d):(\d\d):(\d\d)$/ )
157           ) {
158
159             my $year  = $1;
160             my $mon   = $2;
161             my $mday  = $3;
162             my $hours = $4;
163             my $min   = $5;
164             my $sec   = $6;
165
166             #timegm expects month as 0->11
167             $mon--;
168
169             #now that we've parsed it, deal with the case where everything
170             #was 0
171             if ( $mon == -1 ) {
172                 $self->Unix(-1);
173             }
174             else {
175
176                 #Dateamnip strings aren't in GMT.
177                 if ( $args{'Format'} =~ /^datemanip$/i ) {
178                     $self->Unix(
179                           timelocal( $sec, $min, $hours, $mday, $mon, $year ) );
180                 }
181
182                 #ISO and SQL dates are in GMT
183                 else {
184                     $self->Unix(
185                              timegm( $sec, $min, $hours, $mday, $mon, $year ) );
186                 }
187
188                 $self->Unix(-1) unless $self->Unix;
189             }
190         }
191         else {
192             use Carp;
193             Carp::cluck;
194             $RT::Logger->debug(
195                      "Couldn't parse date $args{'Value'} as a $args{'Format'}");
196
197         }
198     }
199     elsif ( $args{'Format'} =~ /^unknown$/i ) {
200         require Time::ParseDate;
201
202         #Convert it to an ISO format string
203
204         my $date = Time::ParseDate::parsedate($args{'Value'},
205                         UK => $RT::DateDayBeforeMonth,
206                         PREFER_PAST => $RT::AmbiguousDayInPast,
207                         PREFER_FUTURE => !($RT::AmbiguousDayInPast));
208
209         #This date has now been set to a date in the _local_ timezone.
210         #since ISO dates are known to be in GMT (for RT's purposes);
211
212         $RT::Logger->debug( "RT::Date used date::parse to make "
213                             . $args{'Value'}
214                             . " $date\n" );
215
216         return ( $self->Set( Format => 'unix', Value => "$date" ) );
217     }
218     else {
219         die "Unknown Date format: " . $args{'Format'} . "\n";
220     }
221
222     return ( $self->Unix() );
223 }
224
225 # }}}
226
227 # {{{ sub SetToMidnight 
228
229 =head2 SetToMidnight [Timezone => 'utc']
230
231 Sets the date to midnight (at the beginning of the day).
232 Returns the unixtime at midnight.
233
234 Arguments:
235
236 =over 4
237
238 =item Timezone - Timezone context C<server> or C<UTC>
239
240 =cut
241
242 sub SetToMidnight {
243     my $self = shift;
244     my %args = ( Timezone => 'UTC', @_ );
245     if ( lc $args{'Timezone'} eq 'server' ) {
246         $self->Unix( Time::Local::timelocal( 0,0,0,(localtime $self->Unix)[3..7] ) );
247     } else {
248         $self->Unix( Time::Local::timegm( 0,0,0,(gmtime $self->Unix)[3..7] ) );
249     }
250     return ($self->Unix);
251 }
252
253
254 # }}}
255
256 # {{{ sub SetToNow
257 sub SetToNow {
258         my $self = shift;
259         return($self->Set(Format => 'unix', Value => time))
260 }
261 # }}}
262
263 # {{{ sub Diff
264
265 =head2 Diff
266
267 Takes either an RT::Date object or the date in unixtime format as a string
268
269 Returns the differnce between $self and that time as a number of seconds
270
271 =cut
272
273 sub Diff {
274     my $self = shift;
275     my $other = shift;
276
277     if (ref($other) eq 'RT::Date') {
278         $other=$other->Unix;
279     }
280     return ($self->Unix - $other);
281 }
282 # }}}
283
284 # {{{ sub DiffAsString
285
286 =head2 sub DiffAsString
287
288 Takes either an RT::Date object or the date in unixtime format as a string
289
290 Returns the differnce between $self and that time as a number of seconds as
291 as string fit for human consumption
292
293 =cut
294
295 sub DiffAsString {
296     my $self = shift;
297     my $other = shift;
298
299
300     if ($other < 1) {
301         return ("");
302     }
303     if ($self->Unix < 1) {
304         return("");
305     }
306     my $diff = $self->Diff($other);
307
308     return ($self->DurationAsString($diff));
309 }
310 # }}}
311
312 # {{{ sub DurationAsString
313
314
315 =head2 DurationAsString
316
317 Takes a number of seconds. returns a string describing that duration
318
319 =cut
320
321 sub DurationAsString {
322
323     my $self     = shift;
324     my $duration = shift;
325
326     my ( $negative, $s );
327
328     $negative = 1 if ( $duration < 0 );
329
330     $duration = abs($duration);
331
332     my $time_unit;
333     if ( $duration < $MINUTE ) {
334         $s         = $duration;
335         $time_unit = $self->loc("sec");
336     }
337     elsif ( $duration < ( 2 * $HOUR ) ) {
338         $s         = int( $duration / $MINUTE );
339         $time_unit = $self->loc("min");
340     }
341     elsif ( $duration < ( 2 * $DAY ) ) {
342         $s         = int( $duration / $HOUR );
343         $time_unit = $self->loc("hours");
344     }
345     elsif ( $duration < ( 2 * $WEEK ) ) {
346         $s         = int( $duration / $DAY );
347         $time_unit = $self->loc("days");
348     }
349     elsif ( $duration < ( 2 * $MONTH ) ) {
350         $s         = int( $duration / $WEEK );
351         $time_unit = $self->loc("weeks");
352     }
353     elsif ( $duration < $YEAR ) {
354         $s         = int( $duration / $MONTH );
355         $time_unit = $self->loc("months");
356     }
357     else {
358         $s         = int( $duration / $YEAR );
359         $time_unit = $self->loc("years");
360     }
361
362     if ($negative) {
363         return $self->loc( "[_1] [_2] ago", $s, $time_unit );
364     }
365     else {
366         return $self->loc( "[_1] [_2]", $s, $time_unit );
367     }
368 }
369
370 # }}}
371
372 # {{{ sub AgeAsString
373
374 =head2 sub AgeAsString
375
376 Takes nothing
377
378 Returns a string that's the differnce between the time in the object and now
379
380 =cut
381
382 sub AgeAsString {
383     my $self = shift;
384     return ($self->DiffAsString(time));
385     }
386 # }}}
387
388 # {{{ sub AsString
389
390 =head2 sub AsString
391
392 Returns the object\'s time as a string with the current timezone.
393
394 =cut
395
396 sub AsString {
397     my $self = shift;
398     return ($self->loc("Not set")) if ($self->Unix <= 0);
399
400     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($self->Unix);
401
402     return $self->loc("[_1] [_2] [_3] [_4]:[_5]:[_6] [_7]", $self->GetWeekday($wday), $self->GetMonth($mon), map {sprintf "%02d", $_} ($mday, $hour, $min, $sec), ($year+1900));
403 }
404 # }}}
405
406 # {{{ GetWeekday
407
408 =head2 GetWeekday DAY
409
410 Takes an integer day of week and returns a localized string for that day of week
411
412 =cut
413
414 sub GetWeekday {
415     my $self = shift;
416     my $dow = shift;
417     
418     return $self->loc('Mon.') if ($dow == 1);
419     return $self->loc('Tue.') if ($dow == 2);
420     return $self->loc('Wed.') if ($dow == 3);
421     return $self->loc('Thu.') if ($dow == 4);
422     return $self->loc('Fri.') if ($dow == 5);
423     return $self->loc('Sat.') if ($dow == 6);
424     return $self->loc('Sun.') if ($dow == 0);
425 }
426
427 # }}}
428
429 # {{{ GetMonth
430
431 =head2 GetMonth DAY
432
433 Takes an integer month and returns a localized string for that month 
434
435 =cut
436
437 sub GetMonth {
438     my $self = shift;
439    my $mon = shift;
440
441     # We do this rather than an array so that we don't call localize 12x what we need to
442     return $self->loc('Jan.') if ($mon == 0);
443     return $self->loc('Feb.') if ($mon == 1);
444     return $self->loc('Mar.') if ($mon == 2);
445     return $self->loc('Apr.') if ($mon == 3);
446     return $self->loc('May.') if ($mon == 4);
447     return $self->loc('Jun.') if ($mon == 5);
448     return $self->loc('Jul.') if ($mon == 6);
449     return $self->loc('Aug.') if ($mon == 7);
450     return $self->loc('Sep.') if ($mon == 8);
451     return $self->loc('Oct.') if ($mon == 9);
452     return $self->loc('Nov.') if ($mon == 10);
453     return $self->loc('Dec.') if ($mon == 11);
454 }
455
456 # }}}
457
458 # {{{ sub AddSeconds
459
460 =head2 sub AddSeconds
461
462 Takes a number of seconds as a string
463
464 Returns the new time
465
466 =cut
467
468 sub AddSeconds {
469     my $self = shift;
470     my $delta = shift;
471     
472     $self->Set(Format => 'unix', Value => ($self->Unix + $delta));
473     
474     return ($self->Unix);
475     
476
477 }
478
479 # }}}
480
481 # {{{ sub AddDays
482
483 =head2 AddDays $DAYS
484
485 Adds 24 hours * $DAYS to the current time
486
487 =cut
488
489 sub AddDays {
490     my $self = shift;
491     my $days = shift;
492     $self->AddSeconds($days * $DAY);
493     
494 }
495
496 # }}}
497
498 # {{{ sub AddDay
499
500 =head2 AddDay
501
502 Adds 24 hours to the current time
503
504 =cut
505
506 sub AddDay {
507     my $self = shift;
508     $self->AddSeconds($DAY);
509     
510 }
511
512 # }}}
513
514 # {{{ sub Unix
515
516 =head2 sub Unix [unixtime]
517
518 Optionally takes a date in unix seconds since the epoch format.
519 Returns the number of seconds since the epoch
520
521 =cut
522
523 sub Unix {
524     my $self = shift;
525     
526     $self->{'time'} = shift if (@_);
527     
528     return ($self->{'time'});
529 }
530 # }}}
531
532 # {{{ sub ISO
533
534 =head2 ISO
535
536 Takes nothing
537
538 Returns the object's date in ISO format
539
540 =cut
541
542 sub ISO {
543     my $self=shift;
544     my    ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst, $date) ;
545     
546     return ('1970-01-01 00:00:00') if ($self->Unix == -1);
547
548     #  0    1    2     3     4    5     6     7     8
549     ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime($self->Unix);
550     #make the year YYYY
551     $year+=1900;
552
553     #the month needs incrementing, as gmtime returns 0-11
554     $mon++;
555         
556     $date = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year,$mon,$mday, $hour,$min,$sec);
557     
558     return ($date);
559 }
560
561 # }}}
562
563 # {{{ sub W3CDTF
564
565 =head2 W3CDTF
566
567 Takes nothing
568
569 Returns the object's date in W3C DTF format
570
571 =cut
572
573 sub W3CDTF {
574     my $self = shift;
575     my $date = $self->ISO . 'Z';
576     $date =~ s/ /T/;
577     return $date;
578 };
579
580 # }}}
581
582 # {{{ sub LocalTimezone 
583
584 =head2 LocalTimezone
585
586   Returns the current timezone. For now, draws off a system timezone, RT::Timezone. Eventually, this may
587 pull from a 'Timezone' attribute of the CurrentUser
588
589 =cut
590
591 sub LocalTimezone {
592     my $self = shift;
593
594     return $self->CurrentUser->Timezone
595         if $self->CurrentUser and $self->CurrentUser->can('Timezone');
596
597     return ($RT::Timezone);
598 }
599
600 # }}}
601
602 eval "require RT::Date_Vendor";
603 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Date_Vendor.pm});
604 eval "require RT::Date_Local";
605 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Date_Local.pm});
606
607 1;