1<?php 2/** 3 * @package Habari 4 * 5 */ 6 7/** 8 * HabariDateTime class to wrap dates in. 9 * 10 * @property-read HabariDateTime $clone Returns a clonned object. 11 * @property-read string $sql Returns a unix timestamp for inserting into DB. 12 * @property-read int $int Returns a unix timestamp as integer. 13 * @property-read string $time Returns the time formatted according to the blog's settings. 14 * @property-read string $date Returns the date formatted according to the blog's settings. 15 * @property-read string $friendly Returns the time as a friendly string (ie: 4 months, 3 days ago, etc.). 16 * @property-read string $fuzzy Returns the time as a short "fuzzy" string (ie: "just now", "yesterday", "2 weeks ago", etc.). 17 */ 18class HabariDateTime extends DateTime 19{ 20 private static $default_timezone; 21 private static $default_datetime_format = 'c'; 22 private static $default_date_format; 23 private static $default_time_format; 24 25 // various time increments in seconds 26 const YEAR = 31556926; 27 const MONTH = 2629744; 28 const WEEK = 604800; 29 const DAY = 86400; 30 const HOUR = 3600; 31 const MINUTE = 60; 32 33 /** 34 * Set default timezone to system default on init. 35 * 36 * @static 37 */ 38 public static function __static() 39 { 40 if ( Options::get( 'timezone' ) ) { 41 self::set_default_timezone( Options::get( 'timezone' ) ); 42 } 43 44 self::$default_timezone = date_default_timezone_get(); 45 46 self::$default_date_format = Options::get( 'dateformat' ); 47 self::$default_time_format = Options::get( 'timeformat' ); 48 49 if ( self::$default_date_format || self::$default_time_format ) { 50 self::set_default_datetime_format( self::$default_date_format . ' ' . self::$default_time_format ); 51 } 52 } 53 54 /** 55 * Set default date/time format. The format is the same as the 56 * internal php {@link http://ca.php.net/date date() function}. 57 * 58 * @static 59 * @param string $format The date format. 60 */ 61 public static function set_default_datetime_format( $format ) 62 { 63 self::$default_datetime_format = $format; 64 } 65 66 /** 67 * Get the default date/time format set. 68 * 69 * @static 70 * @see set_default_datetime_format() 71 * @return string The date format set. 72 */ 73 public static function get_default_datetime_format() 74 { 75 $user_datetime_format = User::identify()->info->locale_date_format . ' ' . User::identify()->info->locale_time_format; 76 if ( $user_datetime_format != self::$default_datetime_format ) { 77 self::set_default_datetime_format( $user_datetime_format ); 78 } 79 return self::$default_datetime_format; 80 } 81 82 /** 83 * Sets the timezone for Habari and PHP. 84 * 85 * @static 86 * @param string $timezone A timezone name, not an abbreviation, for example 'America/New York' 87 */ 88 public static function set_default_timezone( $timezone ) 89 { 90 self::$default_timezone = $timezone; 91 date_default_timezone_set( self::$default_timezone ); 92 } 93 94 /** 95 * Get the timezone for Habari and PHP. 96 * Defaults to system timezone if not set. 97 * 98 * @static 99 * @see set_default_timezone() 100 * @param string The deafult timezone. 101 */ 102 public static function get_default_timezone() 103 { 104 return self::$default_timezone; 105 } 106 107 /** 108 * Helper function to create a HabariDateTime object for the given 109 * time and timezone. If no time is given, defaults to 'now'. If no 110 * timezone given defaults to timezone set in {@link set_default_timezone()} 111 * 112 * @static 113 * @see DateTime::__construct() 114 * @param string $time String in a format accepted by 115 * {@link http://ca.php.net/strtotime strtotime()}, defaults to "now". 116 * @param string $timezone A timezone name, not an abbreviation. 117 */ 118 public static function date_create( $time = null, $timezone = null ) 119 { 120 if ( $time instanceOf HabariDateTime ) { 121 return $time; 122 } 123 elseif ( $time instanceOf DateTime ) { 124 $time = $time->format( 'U' ); 125 } 126 elseif ( $time === null ) { 127 $time = 'now'; 128 } 129 elseif ( is_numeric( $time ) ) { 130 $time = '@' . $time; 131 } 132 133 if ( $timezone === null ) { 134 $timezone = self::$default_timezone; 135 } 136 137 // passing the timezone to construct doesn't seem to do anything. 138 $datetime = new HabariDateTime( $time ); 139 $datetime->set_timezone( $timezone ); 140 return $datetime; 141 } 142 143 /** 144 * Set the date of this object 145 * 146 * @see DateTime::setDate() 147 * @param int $year Year of the date 148 * @param int $month Month of the date 149 * @param int $day Day of the date 150 */ 151 public function set_date( $year, $month, $day ) 152 { 153 parent::setDate( $year, $month, $day ); 154 return $this; 155 } 156 157 /** 158 * Sets the ISO date 159 * 160 * @see DateTime::setISODate() 161 * @param int $year Year of the date 162 * @param int $month Month of the date 163 * @param int $day Day of the date 164 */ 165 public function set_isodate( $year, $week, $day = null ) 166 { 167 parent::setISODate( $year, $week, $day ); 168 return $this; 169 } 170 171 /** 172 * Set the time of this object 173 * 174 * @see DateTime::setTime() 175 * @param int $hour Hour of the time 176 * @param int $minute Minute of the time 177 * @param int $second Second of the time 178 */ 179 public function set_time( $hour, $minute, $second = null ) 180 { 181 parent::setTime( $hour, $minute, $second ); 182 return $this; 183 } 184 185 /** 186 * Set the timezone for this datetime object. Can be either string 187 * timezone identifier, or DateTimeZone object. 188 * 189 * @see DateTime::setTimezone() 190 * @param mixed The timezone to use. 191 * @return HabariDateTime $this object. 192 */ 193 public function set_timezone( $timezone ) 194 { 195 if ( ! $timezone instanceof DateTimeZone ) { 196 $timezone = new DateTimeZone( $timezone ); 197 } 198 parent::setTimezone( $timezone ); 199 return $this; 200 } 201 202 /** 203 * Get the timezone identifier that is set for this datetime object. 204 * 205 * @return DateTimeZone The timezone object. 206 */ 207 public function get_timezone() 208 { 209 return parent::getTimezone(); 210 } 211 212 /** 213 * Returns date formatted according to given format. 214 * 215 * @see DateTime::format() 216 * @param string $format Format accepted by {@link http://php.net/date date()}. 217 * @return string The formatted date, false on failure. 218 */ 219 public function format( $format = null ) 220 { 221 $day_months = array( 222 'January' => _t( 'January' ), 223 'February' => _t( 'February' ), 224 'March' => _t( 'March' ), 225 'April' => _t( 'April' ), 226 'May' => _t( 'May' ), 227 'June' => _t( 'June' ), 228 'July' => _t( 'July' ), 229 'August' => _t( 'August' ), 230 'September' => _t( 'September' ), 231 'October' => _t( 'October' ), 232 'November' => _t( 'November' ), 233 'December' => _t( 'December' ), 234 'Jan' => _t( 'Jan' ), 235 'Feb' => _t( 'Feb' ), 236 'Mar' => _t( 'Mar' ), 237 'Apr' => _t( 'Apr' ), 238 'May' => _t( 'May' ), 239 'Jun' => _t( 'Jun' ), 240 'Jul' => _t( 'Jul' ), 241 'Aug' => _t( 'Aug' ), 242 'Sep' => _t( 'Sep' ), 243 'Oct' => _t( 'Oct' ), 244 'Nov' => _t( 'Nov' ), 245 'Dec' => _t( 'Dec' ), 246 'Sunday' => _t( 'Sunday' ), 247 'Monday' => _t( 'Monday' ), 248 'Tuesday' => _t( 'Tuesday' ), 249 'Wednesday' => _t( 'Wednesday' ), 250 'Thursday' => _t( 'Thursday' ), 251 'Friday' => _t( 'Friday' ), 252 'Saturday' => _t( 'Saturday' ), 253 'Sun' => _t( 'Sun' ), 254 'Mon' => _t( 'Mon' ), 255 'Tue' => _t( 'Tue' ), 256 'Wed' => _t( 'Wed' ), 257 'Thu' => _t( 'Thu' ), 258 'Fri' => _t( 'Fri' ), 259 'Sat' => _t( 'Sat' ), 260 'am' => _t( 'am' ), 261 'pm' => _t( 'pm' ), 262 'AM' => _t( 'AM' ), 263 'PM' => _t( 'PM' ), 264 ); 265 266 if ( $format === null ) { 267 $format = self::$default_datetime_format; 268 } 269 270 $result = parent::format( $format ); 271 272 if ( ! $result ) { 273 return false; 274 } 275 276 $result = Multibyte::str_replace( array_keys( $day_months ), array_values( $day_months ), $result ); 277 278 return $result; 279 } 280 281 /** 282 * Returns date components inserted into a string 283 * 284 * Example: 285 * echo HabariDateTime::date_create('2010-01-01')->text_format('The year was {Y}.'); 286 * // Expected output: The year was 2010. 287 * 288 * @param string $format A string with single-character date format codes {@link http://php.net/date date()} surrounded by braces 289 * @return string The string with date components inserted 290 */ 291 public function text_format( $format ) 292 { 293 return preg_replace_callback( '%\{(\w)\}%iu', array( $this, 'text_format_callback' ), $format ); 294 } 295 296 /** 297 * Callback method for supplying replacements for HabariDatTime::text_format() 298 * 299 * @param array $matches The matches found in the regular expression. 300 * @return string The date component value for the matched character. 301 */ 302 private function text_format_callback( $matches ) 303 { 304 return $this->format( $matches[1] ); 305 } 306 307 /** 308 * Alters the timestamp 309 * 310 * @param string $format A format accepted by {@link http://php.net/strtotime strtotime()}. 311 * @return HabariDateTime $this object. 312 */ 313 public function modify( $args ) 314 { 315 parent::modify( $args ); 316 return $this; 317 } 318 319 /** 320 * @see format() 321 */ 322 public function get( $format = null ) 323 { 324 return $this->format( $format ); 325 } 326 327 /** 328 * Echos date formatted according to given format. 329 * 330 * @see format() 331 * @param string $format Format accepted by {@link http://php.net/date date()}. 332 */ 333 public function out( $format = null ) 334 { 335 echo $this->format( $format ); 336 } 337 338 /** 339 * Magic method called when this object is cast to string. Returns the 340 * unix timestamp of this object. 341 * 342 * @return string The unix timestamp 343 */ 344 public function __toString() 345 { 346 return $this->format( 'U' ); 347 } 348 349 /** 350 * Magic method to get magic ponies... properties, I mean. 351 */ 352 public function __get( $property ) 353 { 354 // if you add more cases to this list, please also add the repsective @property to the top of the class so it shows up propertly in IDEs! 355 switch ( $property ) { 356 case 'clone': 357 return clone $this; 358 359 case 'sql': 360 return $this->format( 'U' ); 361 break; 362 363 case 'int': 364 return intval( $this->format( 'U' ) ); 365 break; 366 367 case 'time': 368 return $this->format( self::get_default_time_format() ); 369 break; 370 371 case 'date': 372 return $this->format( self::get_default_date_format() ); 373 break; 374 375 case 'friendly': 376 return $this->friendly(); 377 break; 378 379 case 'fuzzy': 380 return $this->fuzzy(); 381 break; 382 383 default: 384 $info = getdate( $this->format( 'U' ) ); 385 $info['mon0'] = substr( '0' . $info['mon'], -2, 2 ); 386 $info['mday0'] = substr( '0' . $info['mday'], -2, 2 ); 387 if ( isset( $info[$property] ) ) { 388 return $info[$property]; 389 } 390 return $this->$property; 391 } 392 } 393 394 /** 395 * Return the default date format, as set in the Options table 396 * 397 * @return The default date format 398 **/ 399 public static function get_default_date_format() 400 { 401 if ( User::identify()->info->locale_date_format != Options::get( 'dateformat' ) ) { 402 self::set_default_date_format( User::identify()->info->locale_date_format ); 403 } 404 return self::$default_date_format; 405 } 406 407 /** 408 * Set default date format. The format is the same as the 409 * internal php {@link http://ca.php.net/date date() function}. 410 * 411 * @static 412 * @param string $format The date format. 413 */ 414 public static function set_default_date_format( $format ) 415 { 416 self::$default_date_format = $format; 417 } 418 419 /** 420 * Return the default time format, as set in the Options table 421 * 422 * @return The default time format 423 **/ 424 public static function get_default_time_format() 425 { 426 if ( User::identify()->info->locale_time_format != Options::get( 'timeformat' ) ) { 427 self::set_default_time_format( User::identify()->info->locale_time_format ); 428 } 429 return self::$default_time_format; 430 } 431 432 /** 433 * Set default time format. The format is the same as the 434 * internal php {@link http://ca.php.net/date date() function}. 435 * 436 * @static 437 * @param string $format The time format. 438 */ 439 public static function set_default_time_format( $format ) 440 { 441 self::$default_time_format = $format; 442 } 443 444 /** 445 * Returns an associative array containing the date information for 446 * this HabariDateTime object, as per {@link http://php.net/getdate getdate()} 447 * 448 * @return array Associative array containing the date information 449 */ 450 public function getdate() 451 { 452 $info = getdate( $this->format( 'U' ) ); 453 $info['mon0'] = substr( '0' . $info['mon'], -2, 2 ); 454 $info['mday0'] = substr( '0' . $info['mday'], -2, 2 ); 455 return $info; 456 } 457 458 /** 459 * Returns a friendlier string version of the time, ie: 3 days, 1 hour, and 5 minutes ago 460 * 461 * @param int $precision Only display x intervals. Note that this does not round, it only limits the display length. 462 * @param boolean $include_suffix Include the 'ago' or 'from now' suffix? 463 * @return string Time passed in the specified units. 464 */ 465 public function friendly ( $precision = 7, $include_suffix = true ) 466 { 467 468 $difference = self::difference( self::date_create(), $this ); 469 470 471 $result = array(); 472 473 if ( $difference['y'] ) { 474 $result[] = sprintf( '%d %s', $difference['y'], _n( 'year', 'years', $difference['y'] ) ); 475 } 476 477 if ( $difference['m'] ) { 478 $result[] = sprintf( '%d %s', $difference['m'], _n( 'month', 'months', $difference['m'] ) ); 479 } 480 481 if ( $difference['w'] ) { 482 $result[] = sprintf( '%d %s', $difference['w'], _n( 'week', 'weeks', $difference['w'] ) ); 483 } 484 485 if ( $difference['d'] ) { 486 $result[] = sprintf( '%d %s', $difference['d'], _n( 'day', 'days', $difference['d'] ) ); 487 } 488 489 if ( $difference['h'] ) { 490 $result[] = sprintf( '%d %s', $difference['h'], _n( 'hour', 'hours', $difference['h'] ) ); 491 } 492 493 if ( $difference['i'] ) { 494 $result[] = sprintf( '%d %s', $difference['i'], _n( 'minute', 'minutes', $difference['i'] ) ); 495 } 496 497 if ( $difference['s'] ) { 498 $result[] = sprintf( '%d %s', $difference['s'], _n( 'second', 'seconds', $difference['s'] ) ); 499 } 500 501 // limit the precision 502 $result = array_slice( $result, 0, $precision ); 503 504 $result = Format::and_list( $result ); 505 506 if ( $include_suffix ) { 507 508 if ( $difference['invert'] == true ) { 509 $result = _t( '%s from now', array( $result ) ); 510 } 511 else { 512 $result = _t( '%s ago', array( $result ) ); 513 } 514 515 } 516 517 return $result; 518 519 } 520 521 /** 522 * Similar to friendly(), but much more... fuzzy. 523 * 524 * Returns a very short version of the difference in time between now and the current HDT object. 525 */ 526 public function fuzzy () 527 { 528 $difference = self::date_create()->int - $this->int; 529 530 if ( $difference < self::MINUTE ) { 531 $result = _t( 'just now' ); 532 } 533 else if ( $difference < self::HOUR ) { 534 $minutes = round( $difference / self::MINUTE ); 535 $result = sprintf( _n( '%d minute ago', '%d minutes ago', $minutes ), $minutes ); 536 } 537 else if ( $difference < self::DAY ) { 538 $hours = round( $difference / self::HOUR ); 539 $result = sprintf( _n( '%d hour ago', '%d hours ago', $hours ), $hours ); 540 } 541 else if ( $difference < self::WEEK ) { 542 $days = round( $difference / self::DAY ); 543 $result = sprintf( _n( 'yesterday', '%d days ago', $days ), $days ); 544 } 545 else if ( $difference < self::MONTH ) { 546 $weeks = round( $difference / self::WEEK ); 547 $result = sprintf( _n( 'last week', '%d weeks ago', $weeks ), $weeks ); 548 } 549 else if ( $difference < self::YEAR ) { 550 $months = round( $difference / self::MONTH ); 551 $result = sprintf( _n( 'last month', '%d months ago', $months ), $months ); 552 } 553 else { 554 $years = round( $difference / self::YEAR ); 555 $result = sprintf( _n( 'last year', '%d years ago', $years ), $years ); 556 } 557 558 return $result; 559 560 } 561 562 /** 563 * Returns an array representing the difference between two times by interval. 564 * 565 * <code> 566 * print_r( HabariDateTime::difference( 'now', 'January 1, 2010' ) ); 567 * // output (past): Array ( [invert] => [y] => 0 [m] => 9 [w] => 3 [d] => 5 [h] => 22 [i] => 33 [s] => 5 ) 568 * print_r( HabariDateTime::difference( 'now', 'January 1, 2011' ) ); 569 * // output (future): Array ( [invert] => 1 [y] => 0 [m] => 2 [w] => 0 [d] => 3 [h] => 5 [i] => 33 [s] => 11 ) 570 * </code> 571 * 572 * If 'invert' is true, the time is in the future (ie: x from now). If it is false, the time is in the past (ie: x ago). 573 * 574 * For more information, see PHP's DateInterval class, which this and friendly() attempt to emulate for < PHP 5.3 575 * 576 * @todo Add total_days, total_years, etc. values? 577 * 578 * @param mixed $start_date The start date, as a HDT object or any format accepted by HabariDateTime::date_create(). 579 * @param mixed $end_date The end date, as a HDT object or any format accepted by HabariDateTime::date_create(). 580 * @return array Array of each interval and whether the interval is inverted or not. 581 */ 582 public static function difference( $start_date, $end_date ) 583 { 584 585 // if the dates aren't HDT objects, try to convert them to one. this lets you pass in just about any format 586 if ( !$start_date instanceof HabariDateTime ) { 587 $start_date = HabariDateTime::date_create( $start_date ); 588 } 589 590 if ( !$end_date instanceof HabariDateTime ) { 591 $end_date = HabariDateTime::date_create( $end_date ); 592 } 593 594 $result = array(); 595 596 // calculate the difference, in seconds 597 $difference = $end_date->int - $start_date->int; 598 599 if ( $difference < 0 ) { 600 // if it's negative, time AGO 601 $result['invert'] = false; 602 } 603 else { 604 // if it's positive, time UNTIL 605 $result['invert'] = true; 606 } 607 608 $difference = abs( $difference ); 609 610 // we'll progressively subtract from the seconds left, so initialize it 611 $seconds_left = $difference; 612 613 $result['y'] = floor( $seconds_left / self::YEAR ); 614 $seconds_left = $seconds_left - ( $result['y'] * self::YEAR ); 615 616 $result['m'] = floor( $seconds_left / self::MONTH ); 617 $seconds_left = $seconds_left - ( $result['m'] * self::MONTH ); 618 619 $result['w'] = floor( $seconds_left / self::WEEK ); 620 $seconds_left = $seconds_left - ( $result['w'] * self::WEEK ); 621 622 $result['d'] = floor( $seconds_left / self::DAY ); 623 $seconds_left = $seconds_left - ( $result['d'] * self::DAY ); 624 625 $result['h'] = floor( $seconds_left / self::HOUR ); 626 $seconds_left = $seconds_left - ( $result['h'] * self::HOUR ); 627 628 $result['i'] = floor( $seconds_left / self::MINUTE ); 629 $seconds_left = $seconds_left - ( $result['i'] * self::MINUTE ); 630 631 $result['s'] = $seconds_left; 632 633 return $result; 634 635 } 636 637} 638 639?> 640