Dt.php
<?php
namespace php_rutils;
use php_rutils\struct\TimeParams;
/**
* Russian dates without locales
* Class Dt
* @package php_rutils
*/
class Dt
{
public static $PREFIX_IN = "через"; //Prefix 'in' (i.e. B{in} three hours)
public static $SUFFIX_AGO = "назад"; //Prefix 'ago' (i.e. three hours B{ago})
private static $_DAY_NAMES = array(
array('пн', 'понедельник', 'понедельник', "в\xC2\xA0"),
array('вт', 'вторник', 'вторник', "во\xC2\xA0"),
array('ср', 'среда', 'среду', "в\xC2\xA0"),
array('чт', 'четверг', 'четверг', "в\xC2\xA0"),
array('пт', 'пятница', 'пятницу', "в\xC2\xA0"),
array('сб', 'суббота', 'субботу', "в\xC2\xA0"),
array('вск', 'воскресенье', 'воскресенье', "в\xC2\xA0")
); //Day alternatives (i.e. one day ago -> yesterday)
private static $_MONTH_NAMES = array(
array("янв", "январь", "января"),
array("фев", "февраль", "февраля"),
array("мар", "март", "марта"),
array("апр", "апрель", "апреля"),
array("май", "май", "мая"),
array("июн", "июнь", "июня"),
array("июл", "июль", "июля"),
array("авг", "август", "августа"),
array("сен", "сентябрь", "сентября"),
array("окт", "октябрь", "октября"),
array("ноя", "ноябрь", "ноября"),
array("дек", "декабрь", "декабря"),
); //Forms (1, 2, 5) for noun 'day'
private static $_PAST_ALTERNATIVES = array("вчера", "позавчера");
private static $_YEAR_VARIANTS = array("год", "года", "лет"); //Forms (1, 2, 5) for noun 'year'
private static $_MONTH_VARIANTS = array("месяц", "месяца", "месяцев");
private static $_DAY_VARIANTS = array("день", "дня", "дней");
private static $_HOUR_VARIANTS = array("час", "часа", "часов");
private static $_MINUTE_VARIANTS = array("минуту", "минуты", "минут");
private static $_DISTANCE_FIELDS = array('y', 'm', 'd', 'h', 'i');
/**
* Russian \DateTime::format
* @param array|\php_rutils\struct\TimeParams $params Params structure
* @return string Date/time string representation
*/
public function ruStrFTime($params = null)
{
//Params handle
if ($params === null) {
$params = new TimeParams();
} elseif (is_array($params)) {
$params = TimeParams::create($params);
} else {
$params = clone $params;
}
if ($params->date === null) {
$params->date = new \DateTime();
} else {
$params->date = $this->_processDateTime($params->date);
}
if (is_string($params->timezone)) {
$params->timezone = new \DateTimeZone($params->timezone);
}
if ($params->timezone) {
$params->date->setTimezone($params->timezone);
}
//Format processing
$weekday = $params->date->format('N') - 1;
$month = $params->date->format('n') - 1;
$prepos = $params->preposition ? self::$_DAY_NAMES[$weekday][3] : '';
$monthIdx = $params->monthInflected ? 2 : 1;
$dayIdx = ($params->dayInflected || $params->preposition) ? 2 : 1;
$search = array('D', 'l', 'M', 'F');
$replace = array(
$prepos.self::$_DAY_NAMES[$weekday][0],
$prepos.self::$_DAY_NAMES[$weekday][$dayIdx],
self::$_MONTH_NAMES[$month][0],
self::$_MONTH_NAMES[$month][$monthIdx],
);
//for russian typography standard,
//1 April 2007, but 01.04.2007
if (strpos($params->format, 'F') !== false || strpos($params->format, 'M') !== false) {
$search[] = 'd';
$replace[] = 'j';
}
$params->format = str_replace($search, $replace, $params->format);
//Create date/time string
return $params->date->format($params->format);
}
/**
* Process mixed format date
* @param mixed $dateTime
* @return \DateTime
* @throws \InvalidArgumentException
*/
private function _processDateTime($dateTime)
{
if (is_numeric($dateTime)) {
$timestamp = $dateTime;
$dateTime = new \DateTime();
$dateTime->setTimestamp($timestamp);
} elseif (empty($dateTime)) {
throw new \InvalidArgumentException('Date/time is empty');
} elseif (is_string($dateTime)) {
$dateTime = new \DateTime($dateTime);
}
if (!($dateTime instanceof \DateTime)) {
throw new \InvalidArgumentException('Incorrect date/time type');
}
return $dateTime;
}
/**
* Represents distance of time in words
* @param string|int|\DateTime $toTime Source time
* @param string|int|\DateTime $fromTime Target time
* @param int $accuracy Level of accuracy (year, month, day, hour, minute), default=year
* @throws \InvalidArgumentException
* @throws \RuntimeException
* @return string Distance of time in words
*/
public function distanceOfTimeInWords($toTime, $fromTime = null, $accuracy = RUtils::ACCURACY_YEAR)
{
$accuracy = (int)$accuracy;
if ($accuracy < 1 || $accuracy > 5) {
throw new \InvalidArgumentException('Wrong accuracy value (must be 1..5)');
}
/* @var $toTime \DateTime */
/* @var $fromTime \DateTime */
/* @var $timeZone \DateTimeZone */
/* @var $fromCurrent bool */
list($toTime, $fromTime, $timeZone, $fromCurrent) = $this->_processFunctionParams($toTime, $fromTime);
$interval = $toTime->diff($fromTime);
//if diff less than one minute
if ($interval->days == 0 && $interval->h == 0 && $interval->i == 0) {
if ($interval->invert) {
$result = 'менее чем через минуту';
} else {
$result = 'менее минуты назад';
}
return $result;
}
//create distance table
$distanceData = $this->_createDistanceData($interval);
$words = $this->_getResultWords($accuracy, $distanceData);
//check short result
if ($fromCurrent && min($accuracy, sizeof($words)) == 1) {
//if diff expressed in one word
$result = $this->_getOneWordResult($interval);
if ($result) {
return $result;
} elseif ($interval->days < 3) {
//if diff 1 or 2 days
$result = $this->_getTwoDaysResult($interval, $toTime, $timeZone);
if ($result) {
return $result;
}
}
}
//general case
$result = implode(', ', $words);
return $this->_addResultSuffix($interval, $result);
}
private function _processFunctionParams($toTime, $fromTime)
{
$toTime = $this->_processDateTime($toTime);
$timeZone = $toTime->getTimezone();
$fromCurrent = false;
if ($fromTime === null) {
$fromTime = new \DateTime('now', $timeZone);
$fromCurrent = true;
} else {
$fromTime = $this->_processDateTime($fromTime);
}
return array($toTime, $fromTime, $timeZone, $fromCurrent);
}
private function _createDistanceData(\DateInterval $interval)
{
$distanceData = array(); //table of word representations
$numeral = RUtils::numeral();
$years = $interval->y;
if ($years) {
$distanceData['y'] = $numeral->getPlural($years, self::$_YEAR_VARIANTS);
}
$months = $interval->m;
if ($months) {
$distanceData['m'] = $numeral->getPlural($months, self::$_MONTH_VARIANTS);
}
$days = $interval->d;
if ($days) {
$distanceData['d'] = $numeral->getPlural($days, self::$_DAY_VARIANTS);
}
$hours = $interval->h;
if ($hours) {
$distanceData['h'] = $numeral->getPlural($hours, self::$_HOUR_VARIANTS);
}
$minutes = $interval->i;
if ($minutes) {
$distanceData['i'] = $numeral->getPlural($minutes, self::$_MINUTE_VARIANTS);
}
return $distanceData;
}
private function _getYearResult(array $distanceData)
{
return $this->_getLevelResult('y', $distanceData);
}
private function _getMonthResult(array $distanceData)
{
list($words, $borderField) = $this->_getYearResult($distanceData);
return $this->_getLevelResult('m', $distanceData, $words, $borderField);
}
private function _getDaysResult(array $distanceData)
{
list($words, $borderField) = $this->_getMonthResult($distanceData);
return $this->_getLevelResult('d', $distanceData, $words, $borderField);
}
private function _getHoursResult(array $distanceData)
{
list($words, $borderField) = $this->_getDaysResult($distanceData);
return $this->_getLevelResult('h', $distanceData, $words, $borderField);
}
private function _getMinutesResult(array $distanceData)
{
list($words, $borderField) = $this->_getHoursResult($distanceData);
return $this->_getLevelResult('i', $distanceData, $words, $borderField);
}
private function _getLevelResult($fieldCode, array $distanceData, array $words = array(), $borderField = -1)
{
$curPos = array_search($fieldCode, self::$_DISTANCE_FIELDS);
if ($borderField >= $curPos) {
return array($words, $borderField);
}
$nextField = $borderField + 1;
$length = sizeof(self::$_DISTANCE_FIELDS);
for ($i = $nextField; $i < $length; ++$i) {
$field = self::$_DISTANCE_FIELDS[$i];
if ($borderField != -1 && $i > $curPos) {
break;
} elseif (isset($distanceData[$field])) {
$words[] = $distanceData[$field];
$borderField = $i;
break;
}
}
return array($words, $borderField);
}
private function _getOneWordResult(\DateInterval $interval)
{
$result = null;
if ($interval->days == 0 && $interval->h == 0 && $interval->i == 1) {
$result = 'минуту';
} elseif ($interval->days == 0 && $interval->h == 1) {
$result = 'час';
} elseif ($interval->y == 0 && $interval->m == 1) {
$result = 'месяц';
} elseif ($interval->y == 1) {
$result = 'год';
}
if ($result) {
$result = $this->_addResultSuffix($interval, $result);
}
return $result;
}
/**
* Add suffix or Postfix to string.
* @param \DateInterval $interval
* @param $result string
* @return string modified $result.
*/
private function _addResultSuffix(\DateInterval $interval, $result)
{
return $interval->invert ? self::$PREFIX_IN."\xC2\xA0".$result : $result."\xC2\xA0".self::$SUFFIX_AGO;
}
private function _getTwoDaysResult(\DateInterval $interval, \DateTime $toTime, \DateTimeZone $timeZone = null)
{
$result = null;
$days = $interval->days;
if ($interval->invert == 0 && ($days == 1 || $days == 2)) {
$variant = $days - 1;
$result = self::$_PAST_ALTERNATIVES[$variant];
} elseif ($interval->invert && ($days == 0 || $days == 1)) {
$tomorrow = new \DateTime('today', $timeZone);
$tomorrow->add(new \DateInterval('P1D'));
$afterTomorrow = new \DateTime('today', $timeZone);
$afterTomorrow->add(new \DateInterval('P2D'));
if ($toTime >= $tomorrow && $toTime < $afterTomorrow) {
$result = 'завтра';
} elseif ($days == 1 && $toTime >= $afterTomorrow) {
$result = 'послезавтра';
}
}
return $result;
}
private function _getResultWords($accuracy, $distanceData)
{
switch ($accuracy) {
case RUtils::ACCURACY_YEAR:
list($words,) = $this->_getYearResult($distanceData);
break;
case RUtils::ACCURACY_MONTH:
list($words,) = $this->_getMonthResult($distanceData);
break;
case RUtils::ACCURACY_DAY:
list($words,) = $this->_getDaysResult($distanceData);
break;
case RUtils::ACCURACY_HOUR:
list($words,) = $this->_getHoursResult($distanceData);
break;
case RUtils::ACCURACY_MINUTE:
list($words,) = $this->_getMinutesResult($distanceData);
break;
default:
throw new \RuntimeException("Unexpected accuracy level: $accuracy");
}
return $words;
}
/**
* Calculates age
* @param string|int|\DateTime $birthDate Date of birth
* @throws \InvalidArgumentException
* @return int Full years age
*/
public function getAge($birthDate)
{
$birthDate = $this->_processDateTime($birthDate);
$interval = $birthDate->diff(new \DateTime());
if ($interval->invert) {
throw new \InvalidArgumentException('Birth date is in future');
}
return $interval->y;
}
}