Login   Register  
PHP Classes
elePHPant
Icontem

File: Time_When.php

Recommend this page to a friend!
Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  Classes of Martin  >  Time_When  >  Time_When.php  >  Download  
File: Time_When.php
Role: Class source
Content type: text/plain
Description: class
Class: Time_When
Convert relative times into English expressions
Author: By
Last change: Changed to version 0.2
- Added so its possible to - 2.5 day = 3 days, 2.4 days = 2 days
- Can now also get times from the future
- Cleaned everything a bit
Date: 4 years ago
Size: 18,210 bytes
 

Contents

Class file image Download
<?php
/**
 * @package Time
 * @copyright Copyright (c) 2009, Martin Aarhof
 * @author Martin Aarhof <martin at aarhof dot eu>
 */

/**
 * A class to get those nice "just a moment ago", "1 day ago", "6 hours ago" or "in about 2 months", instead of a simple timestamp.
 * This can also be reconfigured for own use.
 *
 * @package Time_When
 * @version 0.2
 *
 * @changelog
 * - Version 0.2
 *      - Added so its possible to - 2.5 day = 3 days, 2.4 days = 2 days
 *      - Can now also get times from the future
 *      - Cleaned everything a bit
 * - Version 0.1
 *      - Initial version
 *
 * @todo
 * - Better way to count time diff.
 * - GET RID OF THAT SWITCH {@link createAssignments()}
 *
 */
class Time_When
{

    const EQUALS = '==';
    const LESS_THAN = '<';
    const HIGHER_THAN = '>';

    /**
     * Placeholder for our types (with times in the past), can be added at {@link addType()} and removed by {@link removeType()}
     * @var array
     */
    protected $types = array();

    /**
     * Placeholder for our types (with times in the future), can be added at {@link addType()} and removed by {@link removeType()}
     * @var array
     */
    protected $typesFuture = array();

    /**
     * Placeholder for our timers can be added at {@link addTimer()}
     * @var array
     */
    protected $timer = array();

    /**
     * Identifier to text in type
     * @var string
     */
    public $textAssigner = '?';

    /**
     * Text to replace with the time from the type
     * @var string
     */
    public $textReplacer = '[NUM]';

    /**
     * Use round or floor (round 2.5 day = 3 days, 2.4 days = 2 days, floor - always 2 days in this scenario)
     * @var boolean
     */
    public $useRound = true;

    /**
     * Instance
     * @var Time_When
     */
    static $_instance;

    /**
     * Constructor, in this you can set some default types.
     * Initiate using {@link getIntance()}
     * @access protected
     */
    protected function __construct()
    {

        $this->types = array(
                'now' => array('time' => 0, 'text' => 'Just now!'),
                '-sec' => array('time' => -60, 'text' => 'moments ago'),
                '-min' => array('time' => -(60*60), 'text' => $this->textAssigner . ' ' . $this->textAssigner . ' ago', 'assign' => array($this->textReplacer, array(self::LESS_THAN, 2, 'minute', 'minutes'))),
                '-hour' => array('time' => -(60*60*60), 'text' => $this->textAssigner . ' ' . $this->textAssigner . ' ago', 'assign' => array($this->textReplacer, array(self::EQUALS, 1, 'hour', 'hours'))),
                '-day' => array('time' => -(60*60*60*24), 'text' => $this->textAssigner . ' ' . $this->textAssigner . ' ago', 'assign' => array($this->textReplacer, array(self::HIGHER_THAN, 1, 'days', 'day'))),
                '-month' => array('time' => -(60*60*60*24*30), 'text' => $this->textAssigner . ' ' . $this->textAssigner . ' ago', 'assign' => array($this->textReplacer, array(self::EQUALS, 1, 'month', 'months'))),
                '-year' => array('time' => -(60*60*60*24*30*12), 'text' => $this->textAssigner . ' ' . $this->textAssigner . ' ago', 'assign' => array($this->textReplacer, array(self::EQUALS, 1, 'year', 'years'))),
                '-galaxy' => array('time' => -(60*60*60*24*30*12*200), 'text' => 'Long, long time ago, in a far far galaxy'),
        );

        $this->typesFuture = array(
            'now' => array('time' => 0, 'text' => 'Just now!'),
            '+sec' => array('time' => 60, 'text' => 'almost right away'),
            '+min' => array('time' => (60*60), 'text' => 'In about: ' . $this->textAssigner . ' ' . $this->textAssigner, 'assign' => array('[NUM]', array(self::LESS_THAN, 2, 'minute', 'minutes'))),
            '+hour' => array('time' => (60*60*60), 'text' => 'In about: ' . $this->textAssigner . ' ' . $this->textAssigner, 'assign' => array('[NUM]', array(self::EQUALS, 1, 'hour', 'hours'))),
            '+day' => array('time' => (60*60*60*24), 'text' => 'In about: ' . $this->textAssigner . ' ' . $this->textAssigner, 'assign' => array('[NUM]', array(self::HIGHER_THAN, 1, 'days', 'day'))),
            '+month' => array('time' => (60*60*60*24*30), 'text' => 'In about: ' . $this->textAssigner . ' ' . $this->textAssigner, 'assign' => array('[NUM]', array(self::EQUALS, 1, 'month', 'months'))),
            '+year' => array('time' => (60*60*60*24*30*12), 'text' => 'In about: ' . $this->textAssigner . ' ' . $this->textAssigner, 'assign' => array('[NUM]', array(self::EQUALS, 1, 'year', 'years'))),
            '+galaxy' => array('time' => (60*60*60*24*30*12*200), 'text' => 'We were landed on Mars for 10 years ago'),
        );

        $this->timer = array();

    }

    /**
     * Disallow cloning - this is a singleton!
     * @access private
     */
    private function __clone()
    {
    }

    /**
     * Singleton instance
     * @access public
     * @return Time_When
     */
    public static function getInstance()
    {
        if (null === self::$_instance)
            self::$_instance = new self();

        return self::$_instance;
    }

    /**
     * Resetting everything
     * @access public
     * @return Time_When
     */
    public function reset()
    {
        $this->__construct();
        return $this;
    }

    /**
     * Method to add new types {@link types} to our conversion.
     * $name HAS TO BE UNIQUE!
     *
     * @access public
     * @example example.php 0 38
     * @param string $name a unique identifier
     * @param array $options - $options['time'] and $options['text'] is required! - see example for options
     * @throws Exception
     * @return Time_When
     */
    public function addType($name, array $options)
    {
        if (!isset($options['time']) || !isset($options['text']))
            throw new Exception('addType requires $options["time"] and $options["text"]');

        if (isset($this->types[$name]) || isset($this->typesFuture[$name]))
            throw new Exception('$this->types / $this->typesFuture already have a ' . $name . ', please use a unique identifier');

        if ($options['time'] > 0)
            $this->typesFuture[$name] = $options;
        else
            $this->types[$name] = $options;

        return $this;
    }

    /**
     * Method to remove a type from {@link types} using the name
     *
     * @access public
     * @example example.php 0 38
     * @param string $name
     * @return bool
     */
    public function removeType($name)
    {
        if (isset($this->types[$name]))
        {
            unset($this->types[$name]);
            return true;
        }

        if (isset($this->typesFuture[$name]))
        {
            unset($this->typesFuture[$name]);
            return true;
        }

        return false;
    }

    /**
     * Method to add timers to the conversion
     *
     * @access public
     * @example example.php 38 111
     * @param timestamp $endtime timestamp of the endtime
     * @param timestamp $starttime timestamp of the starttime ( null = time() )
     * @param mixed $id a unique identifier if you convert many at one time
     * @return Time_When
     */
    public function addTimer($endtime, $starttime = null, $id = null)
    {
        $starttime = ($starttime === null ? time() : $starttime);
        
        $array = array('starttimer' => $starttime, 'endtimer' => $endtime, 'timer' => $starttime-$endtime);

        if ($id !== null)
            $this->timer[$id] = $array;
        else
            $this->timer[] = $array;

        return $this;
    }

    /**
     * Method to remove one or all timers
     *
     * @access public
     * @example example.php 111
     * @param mixed $id a unique identifier if you want to remove only one
     * @return integer numbers of deleted timers
     */
    public function removeTimer($id = null)
    {
        if ($id === null)
        {
            $count = count($this->timer);
            $this->timer = array();
            return $count;
        }

        if (isset($this->timer[$id]))
        {
            unset($this->timer[$id]);
            return 1;
        } else
            return 0;

    }

    /**
     * To get our results, and only get the results.
     * If more than one {@link timer} this will return array otherwise just a string
     *
     * @access public
     * @example example.php 38 111
     * @return mixed
     */
    public function __toString()
    {

        $results = $this->getResult();
        $returns = array();
        $count = count($this->timer);
        foreach($this->timer AS $id => $timer)
        {
            if ($count == 1)
                return $timer['result']['translated'];
            else
                $returns[$id] = $timer['result']['translated'];
        }

        $this->removeTimer();
        return $returns;

    }

    /**
     * @access public
     * @see {@link __toString()}
     * @return mixed
     */
    public function toString()
    {
        return $this->__toString();
    }

    /**
     * To get our results, with all information about the conversion tool used.
     *
     * @access public
     * @example example.php 38 111
     * @return array
     */
    public function getResult()
    {

        $last = null;

        // Which should we use?
        foreach ($this->timer AS $timerid => $timer)
        {
            if ($timer['timer'] > 0) { // Times for the future
                uasort($this->typesFuture, array(self, 'sorttypesFuture'));

                $last = null;
                foreach (array_reverse($this->typesFuture, true) AS $name => $options)
                {

                    if ($last === null && ($timer['timer'] >= $options['time']))
                    {
                        $this->timer[$timerid]['result'] = $this->getUnit($timerid, $name, $options);
                        break;
                    }

                    // If the time in the types are smaller than the timer, we dont want that,
                    // but we will store the name in the $last
                    if ($options['time'] <= $timer['timer'])
                    {
                        $last = $name;
                        continue;
                    }

                    // Aha, here the timer is larger than types timer, so we will pick the last one found
                    // But if we havent found a $last, we will get the name from the loop
                    $this->timer[$timerid]['result'] = $this->getUnit($timerid, ($last === null ? $name : $last), $options[($last === null ? $name : $last)] );

                }

                if (! $this->timer[$timerid]['result'])
                {
                    // Here the timer is larger than all the types timer, so we will use the last found
                    $this->timer[$timerid]['result'] = $this->getUnit($timerid, $last, $options[$last]);
                }

            } else { // Times for the past

                // Lets start by sorting the types array
                uasort($this->types, array(self, 'sorttypesPast'));

                #echo '<pre>';
                #var_dump($this->types);
                #exit;

                $last = null;
                foreach ($this->types AS $name => $options)
                {

                    if ($last === null && ($options['time'] <= $timer['timer']))
                    {
                        $this->timer[$timerid]['result'] = $this->getUnit($timerid, $name, $this->types[$name]);
                        break;
                    }

                    // If the time in the types are bigger than the timer, we dont want that,
                    // but we will store the name in the $last
                    if ($options['time'] >= $timer['timer'])
                    {
                        $last = $name;
                        continue;
                    }

                    // Aha, here the timer is larger than types timer, so we will pick the last one found
                    // But if we havent found a $last, we will get the name from the loop
                    $this->timer[$timerid]['result'] = $this->getUnit($timerid, ($last === null ? $name : $last), $this->types[($last === null ? $name : $last)] );

                }

                if (! $this->timer[$timerid]['result'])
                {
                    // Here the timer is larger than all the types timer, so we will use the last found
                    $this->timer[$timerid]['result'] = $this->getUnit($timerid, $last, $this->types[$last]);
                }
            }
        }

        return $this->timer;

    }

    /**
     * Our main method which convert our {@link timer} using {@link types} to get our conversion
     *
     * @access protected
     * @param string $timerId
     * @param string $name
     * @return array
     * @throws Exception
     */
    protected function getUnit($timerId, $name, $options)
    {
        $timer = $this->timer[$timerId];

        $options['time'] = ($options['time'] == 0 ? 1 : $options['time']);
        if ($timer['timer'] > 0)
            $timestring = $timer['timer']/$options['time']; // Future times
        elseif ($timer['timer'] < 0)
            $timestring = $timer['timer']/$options['time']; // Past times
        else
            $timestring = 0; // Zero :)

        if ($this->useRound)
            $floored = round($timestring);
        elseif ($timer['timer'] >= 0)
            $floored = ceil($timestring);
        else
            $floored = floor($timestring);

        $assignments = self::createAssignments($options, $floored, $this->textAssigner, $this->textReplacer);
        if ($assignments)
        {
            $options['timestring'] = array('floored' => $floored, 'normal' => $timestring);
            $options['assignments'] = $assignments;
            $options['text'] = ($this->textAssigner != '%s' ? str_replace($this->textAssigner, '%s', $options['text']) : $options['text']);
            $text = call_user_func_array('sprintf', array_merge((array)$options['text'], $assignments));
        }
        else
        {
            $text = $options['text'];
        }

        return array('converter' => array($name => $options), 'translated' => $text);

    }

    /**
     * Method which returns a array of needles found in haystack
     *
     * @access private
     * @param string $haystack
     * @param string $needle
     * @param integer $offset
     * @return array
     */
    static private function strallpos($haystack,$needle,$offset = 0)
    {
        $result = array();
        for($i = $offset; $i<strlen($haystack); $i++)
        {
            $pos = strpos($haystack,$needle,$i);
            if($pos !== FALSE)
            {
                $offset =  $pos;
                if($offset >= $i)
                {
                    $i = $offset;
                    $result[] = $offset;
                }
            }
        }
        return $result;
    }

    /**
     * Private function to create our text translation
     *
     * @access private
     * @param array $assignments
     * @param integer $number
     * @return array
     */
    static private function createAssignments($options, $number, $textassign, $textreplacer)
    {

        $assignments = (isset($options['assign']) ? count($options['assign']) : 0);
        $questionmarks = count(self::strallpos($options['text'], $textassign));
        if ($questionmarks > $assignments)
            throw new Exception('There are more ' . $textassign . ' (' . $questionmarks . ') in the text than in the assignments (' . $assignments. ')');
        if ($questionmarks < $assignments)
            throw new Exception('There are less ' . $textassign . ' (' . $questionmarks . ') in the text than in the assignments (' . $assignments. ')');

        $ret = array();
        if ($assignments) {
            foreach ($options['assign'] AS $assign)
            {
                if (is_array($assign))
                {
                    list($sep, $num, $true, $false) = $assign;

                    /**
                     * @todo get rid of the switch!
                     * $assignments[] = ($timer $sep $num ? $true : $false);
                     * would be the best!
                     */
                    switch ($sep)
                    {
                        case self::LESS_THAN:
                            $ret[] = ($number < (int)$num ? $true : $false);
                            break;
                        case self::HIGHER_THAN:
                            $ret[] = ($number > (int)$num ? $true : $false);
                            break;
                        case self::EQUALS:
                            $ret[] = ($number == (int)$num ? $true : $false);
                            break;
                    }

                } else
                    $ret[] = str_replace($textreplacer, $number, $assign);
            }
        }

        return $ret;

    }

    /**
     * Just a method to sort our array for the times from the past
     *
     * @access private
     * @param array $a
     * @param array $b
     * @return int
     */
    static private function sorttypesPast($a, $b)
    {
        if ($a['time'] == $b['time'])
            return 0;

        return ($a['time'] > $b['time']) ? -1 : 1;
    }

    /**
     * Just a method to sort our array for the times to the future
     *
     * @access private
     * @param array $a
     * @param array $b
     * @return int
     */
    static private function sorttypesFuture($a, $b)
    {
        if ($a['time'] == $b['time'])
            return 0;

        return ($a['time'] < $b['time']) ? -1 : 1;
    }

}