import rt 3.4.4
[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
230
231 Sets the date to midnight (at the beginning of the day) GMT
232 Returns the unixtime at midnight.
233
234 =cut
235
236 sub SetToMidnight {
237     my $self = shift;
238     
239     use Time::Local;
240     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime($self->Unix);
241     $self->Unix(timegm (0,0,0,$mday,$mon,$year,$wday,$yday));
242     
243     return ($self->Unix);
244     
245     
246 }
247
248
249 # }}}
250
251 # {{{ sub SetToNow
252 sub SetToNow {
253         my $self = shift;
254         return($self->Set(Format => 'unix', Value => time))
255 }
256 # }}}
257
258 # {{{ sub Diff
259
260 =head2 Diff
261
262 Takes either an RT::Date object or the date in unixtime format as a string
263
264 Returns the differnce between $self and that time as a number of seconds
265
266 =cut
267
268 sub Diff {
269     my $self = shift;
270     my $other = shift;
271
272     if (ref($other) eq 'RT::Date') {
273         $other=$other->Unix;
274     }
275     return ($self->Unix - $other);
276 }
277 # }}}
278
279 # {{{ sub DiffAsString
280
281 =head2 sub DiffAsString
282
283 Takes either an RT::Date object or the date in unixtime format as a string
284
285 Returns the differnce between $self and that time as a number of seconds as
286 as string fit for human consumption
287
288 =cut
289
290 sub DiffAsString {
291     my $self = shift;
292     my $other = shift;
293
294
295     if ($other < 1) {
296         return ("");
297     }
298     if ($self->Unix < 1) {
299         return("");
300     }
301     my $diff = $self->Diff($other);
302
303     return ($self->DurationAsString($diff));
304 }
305 # }}}
306
307 # {{{ sub DurationAsString
308
309
310 =head2 DurationAsString
311
312 Takes a number of seconds. returns a string describing that duration
313
314 =cut
315
316 sub DurationAsString {
317
318     my $self     = shift;
319     my $duration = shift;
320
321     my ( $negative, $s );
322
323     $negative = 1 if ( $duration < 0 );
324
325     $duration = abs($duration);
326
327     my $time_unit;
328     if ( $duration < $MINUTE ) {
329         $s         = $duration;
330         $time_unit = $self->loc("sec");
331     }
332     elsif ( $duration < ( 2 * $HOUR ) ) {
333         $s         = int( $duration / $MINUTE );
334         $time_unit = $self->loc("min");
335     }
336     elsif ( $duration < ( 2 * $DAY ) ) {
337         $s         = int( $duration / $HOUR );
338         $time_unit = $self->loc("hours");
339     }
340     elsif ( $duration < ( 2 * $WEEK ) ) {
341         $s         = int( $duration / $DAY );
342         $time_unit = $self->loc("days");
343     }
344     elsif ( $duration < ( 2 * $MONTH ) ) {
345         $s         = int( $duration / $WEEK );
346         $time_unit = $self->loc("weeks");
347     }
348     elsif ( $duration < $YEAR ) {
349         $s         = int( $duration / $MONTH );
350         $time_unit = $self->loc("months");
351     }
352     else {
353         $s         = int( $duration / $YEAR );
354         $time_unit = $self->loc("years");
355     }
356
357     if ($negative) {
358         return $self->loc( "[_1] [_2] ago", $s, $time_unit );
359     }
360     else {
361         return $self->loc( "[_1] [_2]", $s, $time_unit );
362     }
363 }
364
365 # }}}
366
367 # {{{ sub AgeAsString
368
369 =head2 sub AgeAsString
370
371 Takes nothing
372
373 Returns a string that's the differnce between the time in the object and now
374
375 =cut
376
377 sub AgeAsString {
378     my $self = shift;
379     return ($self->DiffAsString(time));
380     }
381 # }}}
382
383 # {{{ sub AsString
384
385 =head2 sub AsString
386
387 Returns the object\'s time as a string with the current timezone.
388
389 =cut
390
391 sub AsString {
392     my $self = shift;
393     return ($self->loc("Not set")) if ($self->Unix <= 0);
394
395     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($self->Unix);
396
397     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));
398 }
399 # }}}
400
401 # {{{ GetWeekday
402
403 =head2 GetWeekday DAY
404
405 Takes an integer day of week and returns a localized string for that day of week
406
407 =cut
408
409 sub GetWeekday {
410     my $self = shift;
411     my $dow = shift;
412     
413     return $self->loc('Mon.') if ($dow == 1);
414     return $self->loc('Tue.') if ($dow == 2);
415     return $self->loc('Wed.') if ($dow == 3);
416     return $self->loc('Thu.') if ($dow == 4);
417     return $self->loc('Fri.') if ($dow == 5);
418     return $self->loc('Sat.') if ($dow == 6);
419     return $self->loc('Sun.') if ($dow == 0);
420 }
421
422 # }}}
423
424 # {{{ GetMonth
425
426 =head2 GetMonth DAY
427
428 Takes an integer month and returns a localized string for that month 
429
430 =cut
431
432 sub GetMonth {
433     my $self = shift;
434    my $mon = shift;
435
436     # We do this rather than an array so that we don't call localize 12x what we need to
437     return $self->loc('Jan.') if ($mon == 0);
438     return $self->loc('Feb.') if ($mon == 1);
439     return $self->loc('Mar.') if ($mon == 2);
440     return $self->loc('Apr.') if ($mon == 3);
441     return $self->loc('May.') if ($mon == 4);
442     return $self->loc('Jun.') if ($mon == 5);
443     return $self->loc('Jul.') if ($mon == 6);
444     return $self->loc('Aug.') if ($mon == 7);
445     return $self->loc('Sep.') if ($mon == 8);
446     return $self->loc('Oct.') if ($mon == 9);
447     return $self->loc('Nov.') if ($mon == 10);
448     return $self->loc('Dec.') if ($mon == 11);
449 }
450
451 # }}}
452
453 # {{{ sub AddSeconds
454
455 =head2 sub AddSeconds
456
457 Takes a number of seconds as a string
458
459 Returns the new time
460
461 =cut
462
463 sub AddSeconds {
464     my $self = shift;
465     my $delta = shift;
466     
467     $self->Set(Format => 'unix', Value => ($self->Unix + $delta));
468     
469     return ($self->Unix);
470     
471
472 }
473
474 # }}}
475
476 # {{{ sub AddDays
477
478 =head2 AddDays $DAYS
479
480 Adds 24 hours * $DAYS to the current time
481
482 =cut
483
484 sub AddDays {
485     my $self = shift;
486     my $days = shift;
487     $self->AddSeconds($days * $DAY);
488     
489 }
490
491 # }}}
492
493 # {{{ sub AddDay
494
495 =head2 AddDay
496
497 Adds 24 hours to the current time
498
499 =cut
500
501 sub AddDay {
502     my $self = shift;
503     $self->AddSeconds($DAY);
504     
505 }
506
507 # }}}
508
509 # {{{ sub Unix
510
511 =head2 sub Unix [unixtime]
512
513 Optionally takes a date in unix seconds since the epoch format.
514 Returns the number of seconds since the epoch
515
516 =cut
517
518 sub Unix {
519     my $self = shift;
520     
521     $self->{'time'} = shift if (@_);
522     
523     return ($self->{'time'});
524 }
525 # }}}
526
527 # {{{ sub ISO
528
529 =head2 ISO
530
531 Takes nothing
532
533 Returns the object's date in ISO format
534
535 =cut
536
537 sub ISO {
538     my $self=shift;
539     my    ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst, $date) ;
540     
541     return ('1970-01-01 00:00:00') if ($self->Unix == -1);
542
543     #  0    1    2     3     4    5     6     7     8
544     ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime($self->Unix);
545     #make the year YYYY
546     $year+=1900;
547
548     #the month needs incrementing, as gmtime returns 0-11
549     $mon++;
550         
551     $date = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year,$mon,$mday, $hour,$min,$sec);
552     
553     return ($date);
554 }
555
556 # }}}
557
558 # {{{ sub W3CDTF
559
560 =head2 W3CDTF
561
562 Takes nothing
563
564 Returns the object's date in W3C DTF format
565
566 =cut
567
568 sub W3CDTF {
569     my $self = shift;
570     my $date = $self->ISO . 'Z';
571     $date =~ s/ /T/;
572     return $date;
573 };
574
575 # }}}
576
577 # {{{ sub LocalTimezone 
578
579 =head2 LocalTimezone
580
581   Returns the current timezone. For now, draws off a system timezone, RT::Timezone. Eventually, this may
582 pull from a 'Timezone' attribute of the CurrentUser
583
584 =cut
585
586 sub LocalTimezone {
587     my $self = shift;
588
589     return $self->CurrentUser->Timezone
590         if $self->CurrentUser and $self->CurrentUser->can('Timezone');
591
592     return ($RT::Timezone);
593 }
594
595 # }}}
596
597 eval "require RT::Date_Vendor";
598 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Date_Vendor.pm});
599 eval "require RT::Date_Local";
600 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Date_Local.pm});
601
602 1;