Category Archives: webmastering

webmastering

Wygrzebane z GitHuba (5) : CoffeeConsole

W piątym odcinku Waszego ulubio­nego cyklu pojawi się projekt z tygo­dnio­wym stażem na GitHu­bie pod tytu­łem Coffe­eCon­sole. Jest to inte­rak­tywna konsola języka Coffe­eScript dla prze­glą­darki Chrome. Do tej pory trzeba było używać okienka „TRY COFFEESCRIPT” na stro­nie http://coffeescript.org (brak podświe­tla­nej składni) lub insta­lo­wać Node.jsnpm.

Dla niewta­jem­ni­czo­nych, CS jest tzw. „lukrem skła­dnio­wym” na Java­Script, tzn. wyni­kiem kompi­la­cji CS jest czysty JS (który z kolei jesz­cze można ście­śnić jakimś „zmniej­sza­czem” — mini­fie­rem). Skład­nia CS przy­po­mina najbar­dziej Pythona (bloki kodu rozdzie­lane na podsta­wie wcięć, array compre­hen­sions). Domie­szano również nieco Ruby­ego (znak „?” przy zmien­nych będący spraw­dze­niem czy zmienna istnieje i „@” dla pól obieku).

Po ścią­gnię­ciu i rozpa­ko­wa­niu pakietu wystar­czy prze­cią­gnąć plik coffeeconsole.crx na okno otwar­tego Chroma i rozpocz­nie się insta­la­cja dodatku. Otrzy­mamy 2 pola edycji — lewy dla źródło­wego CS i prawy dla wyni­ko­wego kodu JS.

Poni­żej zade­mon­struję dzia­ła­nie tego cuda:

###
Przykladowy kod naskrobany w 5 min., nie zwracajcie na niego uwagi.
 
W ten sposob w wynikowym JS powstanie komentarz np. dotyczacy licencji.
###
 
# wartosc od ktorej humor jest dobry
goodMoodThrottle = 51
 
# tworze literal obiektowy
peopleMood = 
    'Dawid'  : 100
    'Marta'  : 78
    'Adam'   : 53
    'Marian' : 45
 
# tworze funkcje o 2 parametrach
getMoodString = (name, goodMood) ->
    # zwracam uwage na konstrukcje if-then-else
    # is zamieniane na ===
    # oraz "#{zmienna} cos tam dalej"
    if goodMood is true then "#{name} ma dobry humor" else "#{name} ma zły humor jak cholera"
 
# konstrukcja for-of dla petli po literalach obiektowych
for name, moodLevel of peopleMood
    # kompilator sam domysli sie, ze chodzi nam o alert(getMoodString(name,(bool)))
    alert getMoodString name, goodMoodThrottle <= moodLevel <= 100

W okienku z prawej strony otrzy­mamy nastę­pu­jący wynik kompilacji:

/*
Przykladowy kod naskrobany w 5 min., nie zwracajcie na niego uwagi.
 
W ten sposob w wynikowym JS powstanie komentarz np. dotyczacy licencji.
*/
 
var getMoodString, goodMoodThrottle, moodLevel, name, peopleMood;
 
goodMoodThrottle = 51;
 
peopleMood = {
  'Dawid': 100,
  'Marta': 78,
  'Adam': 53,
  'Marian': 45
};
 
getMoodString = function(name, goodMood) {
  if (goodMood === true) {
    return "" + name + " ma dobry humor";
  } else {
    return "" + name + " ma zły humor jak cholera";
  }
};
 
for (name in peopleMood) {
  moodLevel = peopleMood[name];
  alert(getMoodString(name, (goodMoodThrottle <= moodLevel && moodLevel <= 100)));
}

Patrząc na liczbę linii kodu mamy spory zysk w stosunku do wyni­ko­wego kodu JS. Przy okazji nie zajmu­jemy się zbęd­nymi śred­ni­kami i nawiasami.

Jeśli jesz­cze Was nie prze­ko­na­łem do użycia to propo­nuję zoba­czyć tworze­nie klas:

class Human
    constructor: (@name) ->
 
    whatIsYourName: ->
        "Mam na imie #{@name}"
 
people = []
 
people.push new Human 'Andrzej'
people.push new Human 'Marian'
people.push new Human 'Lucjan'
 
alert human.whatIsYourName() for human in people

Wynik w JS:

var Human, human, people, _i, _len;
 
Human = (function() {
 
  Human.name = 'Human';
 
  function Human(name) {
    this.name = name;
  }
 
  Human.prototype.whatIsYourName = function() {
    return "Mam na imie " + this.name;
  };
 
  return Human;
 
})();
 
people = [];
 
people.push(new Human('Andrzej'));
 
people.push(new Human('Marian'));
 
people.push(new Human('Lucjan'));
 
for (_i = 0, _len = people.length; _i < _len; _i++) {
  human = people[_i];
  alert(human.whatIsYourName());
}

Dla bardziej zain­te­re­so­wa­nych tema­tem Coffe­eScriptu pole­cam książkę The Little Book on Coffee Script dostępną online.

webmastering

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

webmastering

Reprezentacja bitowa znaków Unicode i UTF-8

Jakiś taki naukowy ten tytuł wyszedł. Ale inaczej się chyba nie da. Potrze­bo­wa­łem pretek­stu żeby się trochę poba­wić w mani­pu­lo­wa­nie bitami. Padło na Unicode z racji fajnego sposobu, w jaki wymy­ślono sam zapis znaków. Jeśli jest jesz­cze ktoś kto nie wie co to jest Unicode to zapra­szam do źródła.

Wszystko zostało tak prze­my­ślane, że im bardziej pokrę­cony język, tym więcej miej­sca potrzeba na jego zapi­sa­nie. Wszyst­kie liczby i litery bez ogon­ków damy radę zapi­sać w postaci 1 bajta. Chodziło o kompa­ty­bil­ność z forma­tem ASCII. Znaczki języka polskiego znaj­dują się w dziale Latin Extended-A. Zapis znaków polskich zajmie 2 bajty w forma­cie UTF-8.

Mając numer znaku z tabeli Unicode najła­twiej wyświe­tlić go za pomocą wbudo­wa­nej w PHP funk­cji html_entity_decode():

echo html_entity_decode('&#' . 0xA7 . ';', ENT_NOQUOTES, 'UTF-8');

Wywo­ła­nie powyż­szego wier­sza spowo­duje wyświe­tle­nie znaku o kodzie 0xA7 — para­grafu §. W jaki więc sposób wyko­nuje się czary-mary i znak Unicode staje się znakiem UTF-8? Weźmy na warsz­tat znak ę opisany w tablicy jako latin small letter e with ogonek. Ma numer 0119 (heksa­de­cy­mal­nie!), czyli można zapi­sać tak:
0×11916 = 28110 = 1000110012
Z powyż­szego widać, że liczbę dzie­siętną 281 możemy zapi­sać w postaci 9 bitów. Doku­men­ta­cja funk­cji utf8_encode() zawiera tabelkę ile bitów znaku zmie­ścimy w ilu bajtach UTF-8. Wygląda na to, że w jedno­baj­to­wym zapi­sie zmie­ścimy znaki o nume­rach od 0 do 12710 (7 bitów). Za to dyspo­nu­jąc 2 bajtami zapi­szemy liczby aż do 204710, czyli w zupeł­no­ści nam wystarczy.

Znak ę w UTF-8 zapi­su­jemy w 2 bajtach, czyli do liczby 110000002 musimy dodać prze­su­niętą o 6 bitów w prawo liczbę 281 (ponie­waż pójdą do drugiego bajtu znaku). Z drugiego wyrzu­camy wszystko poza 6 ostat­nimi bitami, a następ­nie doda­jemy do liczby 100000002.

$uniChar = 0x119;
$byte1 = $uniChar >> 6 | 0xC0;
$byte2 = $uniChar & 0x3F | 0x80

Wyszło na to, że ę w zapi­sie UTF-8 to będzie 11000100 10011001. Po ubra­niu tego wszyst­kiego w funkcję:

function hexToUTF8HexArray($hexNum)
{
   // konwertujemy na liczbe w razie czego
   if (is_string($hexNum)) $hexNum = hexdec($hexNum);
 
   $hexArray = array();
   if ($hexNum < 0x80) {
      $hexArray[0] = '0x' . dechex($hexNum);
      return $hexArray;
   } elseif ($hexNum < 0x800) {
      $hexArray[0] = '0x' . dechex($hexNum >> 6 | 0xC0);
      $hexArray[1] = '0x' . dechex($hexNum & 0x3F | 0x80);
      return $hexArray;
   } else {
      throw new Exception('Not supported');
   }
}

Wywo­łu­jąc hexToUTF8HexArray(0x119) da nam tablicę z warto­ściami 0xc4 i 0×99. No dobra, a co jak chcę odwró­cić proces? Teraz będzie przyjemniej:

function utf8ToUnicode(array $utf8Array)
{
   // konwertujemy na liczby
   $utf8ArrayChecked = array();
   foreach ($utf8Array as $utf8) {
      if (is_string($utf8)) $utf8 = hexdec($utf8);
      $utf8ArrayChecked[] = $utf8;
   }
 
   $bytesCount = count($utf8ArrayChecked);
   switch ($bytesCount) {
      case 1:
         return $utf8ArrayChecked[0];
      case 2:
         // wyrzucamy naglowki
         $b1 = $utf8ArrayChecked[0] & 0x1F;
         $b2 = $utf8ArrayChecked[1] & 0x3F;
 
         // tutaj cala magia
         $number = $b1 << 6 | $b2;
         return '0x' . dechex($number);
      default:
         throw new Exception('Not supported'); 
   }
}

Wywo­łu­jąc utf8ToUnicode(array(0xc4, 0x99)) otrzy­mamy 0×119, czyli gra jak trzeba.

Na koniec cieka­wostka: mając jakiś znak możemy łatwo wydo­być jego wartość UTF-8.

function charToUTF8HexArray($char)
{
   $i = 0;
   $hexArray = array();
   while (isset($char[$i])) {
      $hexArray[] = '0x' . dechex(ord($char[$i++]));
   }
   return $hexArray;
}

charToUTF8HexArray('ę') da nam tablicę 0xc4, 0×99.

Ten wpis to taka trochę notatka dla mnie, przez co może trochę chaotycz­nie napisany…