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