1use warnings; 2use strict; 3 4package Jifty::DateTime; 5 6=head1 NAME 7 8Jifty::DateTime - a DateTime subclass that knows about Jifty users 9 10=head1 SYNOPSIS 11 12 use Jifty::DateTime; 13 14 # Get the current date and time 15 my $dt = Jifty::DateTime->now; 16 17 # Print out the pretty date (i.e., today, tomorrow, yesterday, or 2007-09-11) 18 Jifty->web->out( $dt->friendly_date ); 19 20 # Better date parsing 21 my $dt_from_human = Jifty::DateTime->new_from_string("next Saturday"); 22 23=head1 DESCRIPTION 24 25Jifty natively stores timestamps in the database in GMT. Dates are 26stored without timezone. This class loads and parses dates and sets 27them into the proper timezone. 28 29To use this DateTime class to it's fullest ability, you'll need to add 30a C<time_zone> method to your application's user object class. This is 31the class returned by L<Jifty::CurrentUser/user_object>. It must 32return a value valid for using as an argument to L<DateTime>'s 33C<set_time_zone()> method. 34 35=cut 36 37BEGIN { 38 # we spent about 30% of the time in validate during 'require 39 # DateTime::Locale' which isn't necessary at all 40 require Params::Validate; 41 no warnings 'redefine'; 42 local *Params::Validate::validate = sub { pop @_, return @_ }; 43 require DateTime::Locale; 44} 45 46use base qw(Jifty::Object DateTime); 47 48use Jifty::DBI::Schema; 49Jifty::DBI::Schema->register_types( 50 timestamp => sub { 51 encode_on_select is 1, 52 type is 'timestamp', 53 filters are qw( Jifty::Filter::DateTime Jifty::DBI::Filter::DateTime ), 54 }, 55); 56 57=head2 new ARGS 58 59See L<DateTime/new>. If we get what appears to be a date, then we keep this in 60the floating datetime. Otherwise, set this object's timezone to the current 61user's time zone, if the current user's user object has a method called 62C<time_zone>. 63 64=cut 65 66sub new { 67 my $class = shift; 68 my %args = ( 69 current_user => undef, 70 time_zone => undef, 71 input_time_zone => undef, 72 output_time_zone => undef, 73 @_, 74 ); 75 76 my ($input_time_zone, $output_time_zone); 77 $input_time_zone = delete($args{input_time_zone}) || $args{time_zone}; 78 $output_time_zone = delete($args{output_time_zone}) || $args{time_zone}; 79 80 my $current_user = delete $args{current_user}; 81 82 my $self = $class->SUPER::new(%args, time_zone => $input_time_zone); 83 84 my $is_date = $self->hms eq '00:00:00' 85 && $self->time_zone->name eq 'floating'; 86 87 # The output time_zone is the *current user's* time zone. It's okay that 88 # $current_user can be undef; we'll still find and set the right current 89 # user then set the time zone. 90 $self->current_user($current_user); 91 92 if ($output_time_zone) { 93 $self->set_time_zone($output_time_zone); 94 } 95 # If we were given a date, then we need to make sure its output time zone 96 # is Floating and it's set to 00:00:00. 97 # This sucks when you want a timestamp (not just a datestamp) at midnight 98 # in the floating time zone but we don't have any better way to make this 99 # work. 100 elsif ($is_date) { 101 $self->set_time_zone('floating'); 102 103 # Without this check we loop infinitely, because set_hour constructs 104 # a new Jifty::DateTime object. 105 if ($self->hms ne '00:00:00') { 106 $self->set_hour(0); 107 $self->set_minute(0); 108 $self->set_second(0); 109 } 110 } 111 112 return $self; 113} 114 115=head2 now ARGS 116 117See L<DateTime/now>. If a time_zone argument is passed in, then this wrapper 118is effectively a no-op. 119 120OTHERWISE this will always set this object's timezone to the current user's 121timezone. Without this, DateTime's C<now> will set the timezone to UTC always 122(by passing C<< time_zone => 'UTC' >> to C<Jifty::DateTime::new>. We want 123Jifty::DateTime to always reflect the current 124user's timezone (unless otherwise requested, of course). 125 126=cut 127 128sub now { 129 my $class = shift; 130 my %args = ( 131 current_user => undef, 132 #time_zone => undef, # DateTime doesn't like undef time_zone 133 @_, 134 ); 135 136 my $current_user = delete $args{current_user}; 137 my $self = $class->SUPER::now(%args); 138 139 $self->current_user($current_user); 140 141 # We set time_zone here since saying 142 # "Jifty::DateTime->now(time_zone => 'UTC')" is obviously referring the 143 # output time zone; the input time zone doesn't matter at all. 144 $self->set_time_zone($args{time_zone}) if $args{time_zone}; 145 146 return $self; 147} 148 149=head2 from_epoch ARGS 150 151See L<DateTime/from_epoch> and L<Jifty::DateTime/now>. This handles the common 152mistake of C<from_epoch($epoch)> as well. 153 154=cut 155 156sub from_epoch { 157 my $class = shift; 158 159 # from_epoch(100) should dwim 160 unshift @_, 'epoch' if @_ == 1; 161 162 my %args = ( 163 current_user => undef, 164 #time_zone => undef, # DateTime doesn't like undef time_zone 165 @_, 166 ); 167 168 my $current_user = delete $args{current_user}; 169 my $self = $class->SUPER::from_epoch(%args); 170 171 $self->current_user($current_user); 172 173 # We set time_zone here since saying 174 # "Jifty::DateTime->now(time_zone => 'UTC')" is obviously referring the 175 # output time zone; the input time zone doesn't matter at all. 176 $self->set_time_zone($args{time_zone}) if $args{time_zone}; 177 178 return $self; 179} 180 181=head2 current_user [CURRENTUSER] 182 183When setting the current user, update the timezone appropriately. 184 185If an C<undef> current user is passed, this method will find the correct 186current user and set the time zone. 187 188=cut 189 190sub current_user { 191 my $self = shift; 192 return $self->SUPER::current_user unless @_; 193 194 # $date->current_user(undef) will not remove the current user, but it will 195 # calculate who the current user is for setting the time zone 196 if (@_ == 1 && !defined($_[0])) { 197 shift; 198 $self->_get_current_user; 199 } 200 201 my $ret = $self->SUPER::current_user(@_); 202 203 $self->set_current_user_timezone(); 204 return $ret; 205} 206 207=head2 current_user_has_timezone 208 209Return timezone if the current user has one. This is determined by 210checking to see if the current user has a user object. If it has a 211user object, then it checks to see if that user object has a 212C<time_zone> method and uses that to determine the value. 213 214=cut 215 216sub current_user_has_timezone { 217 my $self = shift; 218 219 # make this work as Jifty::DateTime->current_user_has_timezone 220 my $dt = ref($self) ? $self : $self->now; 221 222 $dt->_get_current_user(); 223 224 # Can't continue if we have no notion of a user_object 225 $dt->current_user->can('user_object') or return; 226 227 # Can't continue unless the user object is defined 228 my $user_obj = $dt->current_user->user_object or return; 229 230 # Check for a time_zone method and then use it if it exists 231 my $f = $user_obj->can('time_zone') || $user_obj->can('timezone') 232 or return; 233 234 return $f->($user_obj); 235} 236 237=head2 set_current_user_timezone [DEFAULT_TZ] 238 239=head2 set_current_user_time_zone [DEFAULT_TZ] 240 241Set this Jifty::DateTime's timezone to the current user's timezone. If that's 242not available, then use the passed in DEFAULT_TZ (or GMT if not passed in). 243Returns the Jifty::DateTime object itself. 244 245If your subclass changes this method, please override 246C<set_current_user_timezone> not C<set_current_user_time_zone>, since the 247latter is merely an alias for the former. 248 249=cut 250 251sub set_current_user_timezone { 252 my $self = shift; 253 my $default = shift || Jifty->config->framework('Timezone') || 'UTC'; 254 my $tz = $self->current_user_has_timezone || $default; 255 256 $self->set_time_zone($tz); 257 return $self; 258} 259 260sub set_current_user_time_zone { shift->set_current_user_timezone(@_) } 261 262=head2 new_from_string STRING[, ARGS] 263 264Take some user defined string like "tomorrow" and turn it into a 265C<Jifty::Datetime> object. If a C<time_zone> argument is passed in, that is 266used for the B<input> time zone. 267 268If the string appears to be a _date_, the B<output> time zone will be floating. 269Otherwise, the B<output> time zone will be the current user's time zone. 270 271As of this writing, this uses L<Date::Manip> along with some internal 272hacks to alter the way L<Date::Manip> normally interprets week day 273names. This may change in the future. 274 275=cut 276 277sub new_from_string { 278 my $class = shift; 279 my $string = shift; 280 return unless $string; 281 my %args = ( 282 time_zone => undef, 283 @_, 284 ); 285 286 my $epoch; 287 288 # Hack to use Date::Manip to flexibly scan dates from strings 289 { 290 # Date::Manip interprets days of the week (eg, ''Monday'') as 291 # days within the *current* week. Detect these and prepend 292 # ``next'' 293 # XXX TODO: Find a real solution (better date-parsing library?) 294 if($string =~ /^\s* (?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)$/xi) { 295 $string = "next $string"; 296 } 297 298 my $offset = $class->get_tz_offset( 299 $args{time_zone} ? (time_zone => $args{time_zone}) : (), 300 ); 301 302 require Date::Manip; 303 304 # ForceDate forces the current date to be now in the user's timezone, 305 # if we don't set it then DM uses the machine's timezone 306 Date::Manip::Date_Init("ForceDate=now,$offset"); 307 $epoch = Date::Manip::UnixDate( $string, "%o" ); 308 } 309 310 # Stop here if Date::Manip couldn't figure it out 311 return undef unless $epoch; 312 313 # Build a DateTime object from the Date::Manip value and setup the TZ 314 my $self = $class->from_epoch( epoch => $epoch, time_zone => 'UTC' ); 315 if (my $tz = $self->current_user_has_timezone) { 316 if ($self->hms(':') ne '00:00:00') { 317 $self->set_time_zone($tz); 318 } 319 else { 320 $self->set_time_zone("floating"); 321 } 322 } 323 324 return $self; 325} 326 327=head2 friendly_date 328 329Returns the date given by this C<Jifty::DateTime> object. It will display "today" 330for today, "tomorrow" for tomorrow, or "yesterday" for yesterday. Any other date 331will be displayed in C<ymd> format. 332 333We currently shift by "24 hours" to detect yesterday and tomorrow, rather than 334"1 day" because of daylight saving issues. "1 day" can result in invalid local 335time errors. 336 337=cut 338 339sub friendly_date { 340 my $self = shift; 341 my $ymd = $self->ymd; 342 343 # Use the current user's time zone on the date 344 my $tz = $self->current_user_has_timezone || $self->time_zone; 345 my $rel = DateTime->now( time_zone => $tz ); 346 347 # Is it today? 348 if ($ymd eq $rel->ymd) { 349 return "today"; 350 } 351 352 # Is it yesterday? 353 my $yesterday = $rel->clone->subtract(hours => 24); 354 if ($ymd eq $yesterday->ymd) { 355 return "yesterday"; 356 } 357 358 # Is it tomorrow? 359 my $tomorrow = $rel->clone->add(hours => 24); 360 if ($ymd eq $tomorrow->ymd) { 361 return "tomorrow"; 362 } 363 364 # None of the above, just spit out the date 365 return $ymd; 366} 367 368=head2 is_date 369 370Returns whether or not this C<Jifty::DateTime> object represents a date 371(without a specific time). Dates in Jifty are in the floating time zone and 372are set to midnight. 373 374=cut 375 376sub is_date { 377 my $self = shift; 378 379 # all dates are in the floating time zone 380 return 0 unless $self->time_zone->name eq 'floating'; 381 382 # all dates are set to midnight 383 return 0 unless $self->hms eq '00:00:00'; 384 385 return 1; 386} 387 388=head2 get_tz_offset 389 390Returns the offset for a time zone. If there is no current 391user, or the current user's time zone is unset, then UTC will be used. 392 393The optional datetime argument lets you calculate an offset for some time other 394than "right now". 395 396=cut 397 398sub get_tz_offset { 399 my $self = shift; 400 my %args = ( 401 datetime => DateTime->now, 402 time_zone => $self->current_user_has_timezone || 'UTC', 403 @_, 404 ); 405 406 my $dt = $args{datetime}->clone; 407 408 $dt->set_time_zone($args{time_zone}); 409 return $dt->strftime("%z"); 410} 411 412=head2 jifty_serialize_format 413 414This returns a DateTime (or string) consistent with Jifty's date format. 415 416=cut 417 418sub jifty_serialize_format { 419 my $dt = shift; 420 421 # if it looks like just a date, then return just the date portion 422 return $dt->ymd if $dt->is_date; 423 424 # otherwise let stringification take care of it 425 return $dt; 426} 427 428=head1 WHY? 429 430There are other ways to do some of these things and some of the 431decisions here may seem arbitrary, particularly if you read the 432code. They are. 433 434These things are valuable to applications built by Best Practical 435Solutions, so it's here. If you disagree with the policy or need to do 436it differently, then you probably need to implement something yourself 437using a DateTime::Format::* class or your own code. 438 439Parts may be cleaned up and the API cleared up a bit more in the future. 440 441=head1 SEE ALSO 442 443L<DateTime>, L<DateTime::TimeZone>, L<Jifty::CurrentUser> 444 445=head1 LICENSE 446 447Jifty is Copyright 2005-2010 Best Practical Solutions, LLC. 448Jifty is distributed under the same terms as Perl itself. 449 450=cut 451 4521; 453