O modyfikatorach w Smarty i opisie słownym interwałów czasu

Pomysł na wpis dał mi kilka dni temu Face­book, a dokład­niej ich total­nie niekon­se­kwentne ozna­cze­nia co się kiedy wyda­rzyło na naszej tablicy. Raz jest to 23 minut(y) temu, raz 7 godz. temu, jesz­cze inaczej około godziny temu. Być może różne ekipy robią osobno opisy czasu i stąd różnice. Posta­no­wi­łem dla sportu zmie­rzyć się z proble­mem. Powstało takie coś jak Time­span Smarty Modi­fier.

Opis jak to zain­sta­lo­wać, wyma­ga­nia i sposób użycia może­cie sobie prze­czy­tać w moim kale­czo­nym angiel­skim w GitHu­bie. Tutaj chciał­bym się skupić jak to działa. Najwy­god­niej­szym sposo­bem korzy­sta­nia z biblio­teki jest poprzez mody­fi­ka­tor w szablo­nach Smarty, a więc {$jakaśZmienna|naszModyfikator}.

Przede wszyst­kim sama nazwa pliku musi nazy­wać się modifier.nazwa.php, za to funk­cja smarty_modifier_nazwa($argument). Pierw­szy argu­ment auto­ma­tycz­nie dostaje wartość zmien­nej, dla której wywo­łu­jemy mody­fi­ka­tor. Jeśli chcemy, następne poda­jemy po dwukropku. Drobna uwaga: nigdy niczego nie „echu­jemy”, tylko zwra­camy poprzez return. Chodzi o łańcu­chowe łącze­nie modyfikatorów.

Z uwagi na to, że całość rozro­sła się do ponad 400 linii kodu, nie ma sensu wszyst­kiego tutaj przy­ta­czać. Skupię się na najważ­niej­szych rzeczach.

Całość ma formę klasy abstrak­cyj­nej rozsze­rza­nej przez poszcze­gólne języki. Na podsta­wie zade­kla­ro­wa­nego języka skrypt próbuje znaleźć sobie właściwą klasę (fragm. modifier.timespan.php):

$className = 'Spiechu\TimeSpan\TimeSpan' . strtoupper($lang);
   if (class_exists($className)) {
      $timeSpan = new $className();
 
      // sprawdz czy klasa rozszerza AbstractTimeSpan
      if (!($timeSpan instanceof Spiechu\TimeSpan\AbstractTimeSpan)) {
         $timeSpan = new Spiechu\TimeSpan\TimeSpanEN();
      }
   } else {
 
      // jesli nie ma takiego jezyka lub klasa niewlasciwa uzywam angielskiego
      $timeSpan = new Spiechu\TimeSpan\TimeSpanEN();
   }

Potem tylko konfi­gu­ru­jemy klasę i zwra­camy wynik:

$timeSpan->setStartDate($date)->showSuffix($suffix);
return $timeSpan->getTimeSpan();

W klasie Abstract­Ti­me­Span z kolei mamy trochę logiki zwią­za­nej z obli­cza­niem interwału:

$curDate = new \DateTime('now');
$diff = $curDate->diff($this->_startDate); //otrzymujemt obiekt DateInterval

Począw­szy od najwięk­szej jednostki (rok) odpy­tu­jemy $diff która z jego zmien­nych publicz­nych jest więk­sza od 0 oraz za pomocą metod isHalfUnit($actualUnit, $fullUnit), almostFullUnit($actualUnit, $fullUnit) spraw­dzamy czy może prze­kro­czy­li­śmy połowę obec­nej jednostki, więk­szej jednostki oraz czy nie można jej zaokrą­glić do następ­nej całej.

Klasy rozsze­rza­jące imple­men­tują m.in. metodę getUnit($howMany, $unit­Sym­bol, $half). Liczeb­niki angiel­skie są tak trudne, że potrzeba na nie aż 1 linijki:

if ($howMany > 1) $howMany = 2;
return $this->_units[$howMany][$unitSymbol];

Jeśli mowa o $this->_units to wygląda to tak:

private $_units = array(
   -1 => array('s' => 'just now'),
   0 => array('i' => 'half minute',
      'h' => 'half hour',
      'd' => 'half day',
      'm' => 'half month',
      'y' => 'half year'),
   1 => array('s' => 'a second',
      'i' => 'a minute',
      'h' => 'an hour',
      'd' => 'a day',
      'm' => 'a month',
      'y' => 'a year'),
   2 => array('s' => 'seconds',
      'i' => 'minutes',
      'h' => 'hours',
      'd' => 'days',
      'm' => 'months',
      'y' => 'years')
);

Polskie jednostki z kolei prezen­tują się dużo bardziej okazale (3 odmiany):

private $_units = array(
   -1 => array('s' => 'przed chwilą'),
   0 => array('i' => 'pół minuty',
      'h' => 'pół godziny',
      'd' => 'pół dnia',
      'm' => 'pół miesiąca',
      'y' => 'pół roku'),
   1 => array('s' => 'sekundę',
      'i' => 'minutę',
      'h' => 'godzinę',
      'd' => 'dzień',
      'm' => 'miesiąc',
      'y' => 'rok'),
   2 => array('s' => 'sekundy',
      'i' => 'minuty',
      'h' => 'godziny',
      'd' => 'dni',
      'm' => 'miesiące',
      'y' => 'lata'),
   5 => array('s' => 'sekund',
      'i' => 'minut',
      'h' => 'godzin',
      'd' => 'dni',
      'm' => 'miesięcy',
      'y' => 'lat')
);

Mało tego, w polskiej klasie mamy jesz­cze jednostki specjalne :-) :

private $_specialUnits = array(
   'poltora' => array('s' => 'półtorej sekundy', // nigdy nie uzywane
      'i' => 'półtorej minuty',
      'h' => 'półtorej godziny',
      'd' => 'półtora dnia',
      'm' => 'półtora miesiąca',
      'y' => 'półtora roku')
);

Metodę getU­nit() przy­ta­czam w całości:

protected function getUnit($howMany, $unitSymbol, $half) {
 
   // kluczowa liczba, do ktorej wszystkie maja odmiane jak '5', potem tylko z jedynka na koncu
   if ($howMany > 21) {
 
      // badamy ostatnia cyfre 
      $howMany = substr($howMany, -1);
 
         // jesli to 1 to odmienia sie jak '5'
         if ($howMany <= 1) {
            $howMany = 5;
 
         // jesli nie to rekurencyjnie dla samej ostatniej cyfry
         } else {
            return $this->getUnit($howMany, $unitSymbol);
         }
   } elseif ($howMany >= 5) {
      $howMany = 5;
   } elseif ($howMany >= 2) {
      $howMany = 2;
 
   // jesli mamy '1 i pol' to odpytujemy specjalne jednostki
   } elseif ($howMany == 1 && $half == true) {
      return $this->_specialUnits['poltora'][$unitSymbol];
   }
   return $this->_units[$howMany][$unitSymbol];
}

Mamy już jednostki, teraz tylko skleić to w jeden ciąg znaków. Zajmuje się tym metoda getTi­me­Span() z klasy Abstract­Ti­me­Span:

public function getTimeSpan() {
   $interval = $this->getInterval();
   $timeUnit = $this->getUnit($interval['counter'], $interval['unit'], $interval['half']);
 
   // jesli gdziekolwiek dane byly szacowane to dorzucamy 'okolo'
   $prefix = ($interval['approx']) ? $this->getPrefix() . ' ' : '';
 
   // jesli mamy co najmniej 1 i pol
   $half = ($interval['half'] && $interval['counter'] > 0) ? $this->getHalf() . ' ' : '';
 
   // sprawdzamy czy chcemy 'temu'
   $suffix = ($this->_showSuffix) ? ' ' . $this->getSuffix() : '';
 
   // podajemy razem z liczba jednostkek
   if ($interval['counter'] > 1) {
      return $prefix . $interval['counter'] . ' ' . $half . $timeUnit . $suffix;
 
   // podajemy bez liczby jednostek
   } elseif ($interval['counter'] >= 0) {
      return $prefix . $timeUnit . ' ' . $half . $suffix;
 
   // zostala tylko mozliwosc, ze to 'przed chwila', czyli offset -1
   } else {
      return $timeUnit;
   }
}

Domyślne warto­ści to 10 sekund dla komu­ni­katu przed chwilą, 15% tole­ran­cji dla połowy jednostki (np. około 2 i pół godziny temu lub około pół godziny temu) i 15% tole­ran­cji dla cało­ści jednostki (np. około godzinę temu).

Myślę, że całość da się łatwo prze­nieść do innych syste­mów szablo­nów (od biedy w samym PHP też w końcu można). Póki co brakuje testów jednost­ko­wych, I’m working on it :-D

Podobne wpisy:

  1. Rozsze­rza­nie możli­wo­ści Smarty za pomocą pluginów
  2. 10 trików w Smarty
  3. PDO poprzez Depen­dency Injec­tion Conta­iner [cz. 2/2]
  4. Wali­duj z Harrym Potterem

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.

Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <p> <pre lang="" line="" escaped=""> <q cite=""> <strike> <strong>