ES6 – strings

javascript

Kolejnym przystankiem na drodze do poznania ECMAScript2015 jest zapoznanie się z nowościami dotyczącymi łańcuchów znaków. Następna wersja języka JavaScript, wprowadza kilka bardzo ważnych udoskonaleń, które postaram się opisać w niniejszym wpisie.


Zobacz całą serię: Let-s talk about ECMAScript 2015


template strings

W oparciu o interpolację możemy wreszcie tworzyć szablony, które uzupełniane będą wartościami zmiennych. Można rzec: w końcu!

let x = 1;
let y = 2;
let sumTpl = `${x} + ${y} = ${x + y}`;
 
console.log(sumTpl); // 1 + 2 = 3

Jak widać, wyłuskiwanie wartości w takim szablonie następuje poprzez użycie konstrukcji ${value}. Poza tym całe wyrażenie musi być zbudowane z użyciem znaku akcentu (`), by całość rozpoznana była jako szablon.

Powyższy przykład w ES5 przedstawia się następująco:

var x = 1;
var y = 2;
var sumTpl = "" + x + " + " + y + " = " + (x + y);
console.log(sumTpl); // 1 + 2 = 3

Znacznie lepiej wygląda sprawa z ECMAScript2015. Nie potrzebujemy już żadnych narzędzi pozwalających budować szablony.

Łańcuchy znaków są od teraz jeszcze bardziej użyteczne. Pozwalają na definiowanie wartości w wielu liniach, bez użycia znaków konkatenacji:

let types = `Number
String
Array
Object`;
console.log(types); // Number
                    // String
                    // Array
                    // Object

Z wykorzystaniem składni ES5 napisalibyśmy pewnie coś takiego:

var types = "Number\\nString\\nArray\\nObject";
console.log(types); // Number
                    // String
                    // Array
                    // Object

Obiekt String posiada od teraz również nową własność – raw. Pozwala ona pominąć interpretację znaków \\ w wyrażeniu typu multi-line:

let interpreted = 'raw\\nstring';
let esaped = 'raw\\\\nstring';
let raw = String.raw`raw\\nstring`;
console.log(interpreted);    // raw
                             // string
console.log(raw === esaped); // true

Unicode

Wraz z ES6, otrzymujemy pełne wsparcie dla standardu Unicode, mającego obejmować wszystkie znaki znane na całym świecie.

Skrótu ES6 używam w zasadzie naprzemiennie z ECMAScript2015. Kolejna wersja języka JavaScript oznaczona jest numerem 6, który przyjął się w środowisku deweloperów znacznie szybciej niż pojawiła się pełna nazwa nowego standardu.

Spójrzmy na przykład:

let str = '𠮷';
console.log(str.length);             // 2
console.log(str === '\\uD842\\uDFB7'); // true

Widzimy, że znak 𠮷 reprezentowany jest przez dwa 16-bitowe znaki kodowe. Tak więc mamy znak, który potrzebuje aż dwóch znaków kodowych do poprawnego zapisania. Mimo iż widzimy tylko jeden znak, jego długość wynosi 2.

Termin surrogate pairs jest ściśle związany z zapisem tego typu znaków. W ramach UTF-16 pozwala on na zapisanie wartości powyżej U+FFFF:

console.log(str.charCodeAt(0)); // 55362
console.log(str.charCodeAt(1)); // 57271

Metoda charCodeAt() zwraca 16-bitowy numer reprezentujący daną jednostkę znaku. W ramach standardu ECMAScript2015 otrzymujemy do dyspozycji nową metodę – codePointAt(), która zamiast poszczególnych jednostek (jak ma to miejsce w przypadku metody charCodeAt()) zwraca kody w ramach standardu Unicode UTF-8.

console.log(str.codePointAt(0)); // 134071
console.log(str.codePointAt(1)); // 57271
console.log(str.codePointAt(0) === 0x20BB7); // true

Metoda codePointAt() zwraca więc dokładnie takie same wartości, poza znakami BMP.

BMP — Basic Multilingual Plane — the first 2^16 code points.

Kolejna nowe metoda w ES6 związana z Unicode, to fromCodePoint(), czyli operacja odwrotna do funkcji codePointAt():

console.log(String.fromCodePoint(134071));  // "𠮷"
console.log(String.fromCodePoint(0x20BB7)); // "𠮷"

Kody znaków w Unicode reprezentowane są przez 6 znaków, z czego dwa pierwsze to \u, a kolejne 4, to cyfry w zapisie szesnastkowym.

W przypadku UTF-16 sytuacja wygląda nieco inaczej, bo tego typu wartości reprezentowane są przez co najmniej 5 znaków i maksymalnie 10. Ich format to:

\u{1-6 liczb w zapisie szesnastkowym}

Spójrzmy zatem na przykłady z wykorzystaniem składni ES6 oraz porównanie ze składnią ES5:

// ES6
console.log('\u{20BB7}'); // 𠮷
console.log('\u{20BB7}' === '\\uD842\\uDFB7'); // true

// ES5
console.log('\u20BB7); // 7!
console.log('\u20BB7' === '\\uD842\\uDFB7'); // false

W przypadku, gdy będziemy próbowali dopasować jeden znak poprzez wyrażenie regularne, w ES5 otrzymamy nieoczekiwany rezultat:

console.log(/^.$/.test(str)); // false

str ma bowiem długość 2, a nie 1.

Dzięki ES6 mamy możliwość wykorzystać w tym wypadku nową flagę (u), która jest odpowiedzialna za poprawne interpretowanie znaków Unicode.

console.log(/^.$/u.test(str)); // true

pętla for-of i konstrukcja spread

Przy okazji łańcuchów znaków wspomnę także o nowym sposobie iteracji za pomocą metody for-of. Napiszę o niej więcej w kolejnym artykule, ale już teraz nadmienię, że pozwala ona na iterowanie po łańcuchach znaków z pełnym wsparciem dla Unicode w standardzie UTF-16:

let str = 'abc\\uD842\\uDFB7';
console.log(str.length); // 5
for (let c of str) {
  console.log(c); // a
                  // b
                  // c
                  // 𠮷
}

Kolejna ciekawostka to fakt, że również konstrukcja spread pozwala nam bardzo prosto przetransformować łańcuch znaków do postaci tablicy i to z pełnym wsparciem dla UTF-16:

let str = 'abc\\uD842\\uDFB7';
let chars = […str];
console.log(chars); // ['a', 'b', 'c', '𠮷']

kolejne nowe metody

Na koniec zostawiłem sobie opis kilku nowych, ciekawych metod, dzięki którym możemy w prosty sposób operować na łańcuchach znaków.

repeat(n) – powtórzenie łańcucha znaków n razy

console.log('abc|'.repeat(3)); // 'abc|abc|abc|'

startsWith(str, starts = 0) : boolean – sprawdzenie, czy łańcuch znaków zaczyna się od str, zaczynając sprawdzanie od starts

console.log('ecmascript'.startsWith('ecma'));      // true
console.log('ecmascript'.startsWith('script', 4)); // true

endsWith(str, ends = str.length) : boolean – sprawdzenie, czy łańcuch znaków kończy się na str, gdzie parametr ends definiuje nam koniec

console.log('ecmascript'.endsWith('script'));  // true
console.log('ecmascript'.endsWith('ecma', 4)); // true

includes(str, starts = 0) : boolean – sprawdzenie, czy łańcuch znaków zawiera w sobie wartość str, zaczynając sprawdzanie od starts

onsole.log('ecmascript'.includes('ecma'));      // true
console.log('ecmascript'.includes('script', 4)); // true

Losowy łańcuch znaków (string) w PHP

random_string

Kolejny wpis dotyczący języka PHP i zarazem kolejny, który dotyczy operacji na łańcuchach znaków (string) w tymże języku. Tym razem będzie to prosta klasa, która pozwoli nam wygenerować losowy łańcuch znaków, który może nam posłużyć jako kod do weryfikacji, hash, czy losowo wygenerowane hasło.

W tym celu posłużymy się statyczną funkcją generate($length), która przyjmować będzie tylko jeden argument $length, który odpowiadać będzie za długość generowanego ciągu znaków.

public static function generate($length) {
    // TODO
}

Poza tym – musimy sobie stworzyć łańcuch znaków $chars, który zawierać będzie wszystkie dostępne znaki jakie będą mogły wystąpić w wygenerowanym stringu.

private $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

Ostatecznie wykorzystamy pętlę for oraz funkcję rand(), która losowo wybierze nam poszczególne wartości ze zdefiniowanego wcześniej łańcucha znaków $chars.

$size = strlen($this->chars);
for ($i = 0; $i < $length; $i++) {
	$randomString .= $this->chars[rand(0, $size - 1)];
}

Cała klasa prezentuje się następująco:

<?php

class RandomString {

	private $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

	public static function generate($length) {
		$randomString = '';
		$size = strlen($this->chars);
		for ($i = 0; $i < $length; $i++) {
			$randomString .= $this->chars[rand(0, $size - 1)];
		}

		return $randomString;
	}

}

Użycie:

$randomString = RandomString::generate(5);
// $randomString = PAozU

Lepsze formatowanie stringów w PHP

php-string

Formatowanie łańcuchów znaków w języku PHP często jest mało użyteczne, a sam zapis takiego kodu niezbyt przejrzysty.

W Pythonie realizowane jest to znacznie lepiej i przykładowy kod takiego rozwiązania wygląda tak:

print "Hello %(name)s. Your %(name)s has just been created!" % { 'name' : 'world' }
# Hello world. Your world has just been created!

W powyższym przykładzie widać, że argumenty, które mają zostać zamienione na podany przez nas łańcuch znaków są nazwane i wielokrotne ich występowanie nie przysparza żadnego problemu.

W PHP by osiągnąć podobny efekt musielibyśmy napisać coś takiego:

sprintf("Hello %s. Your %s has just been created!", 'world', 'world');
// Hello world. Your world has just been created!

Zgodzicie się pewnie ze mną, że gdyby powyższy przykład zawierał kilka zdań więcej i wielokrotne występowanie różnych argumentów, to tworzenie takiego kodu oraz jego czytelność pozostawiałyby wiele do życzenia, a na pewno sprawiały trudności podczas późniejszej modyfikacji.

Możemy wprawdzie użyć takiej konstrukcji:

printf('Hello %1$s. Your %1$s has just been created!', 'world');

.., ale tutaj także nie mamy argumentów nazwanych.

Jednak takie rozwiązanie jest osiągalne i przykład takiego kodu znalazłem w oficjalnej dokumentacji PHP na stronie: http://www.php.net/manual/en/function.vsprintf.php

W tym wypadku wykorzystana zostanie funkcja vsprintf, która różni się tym od funkcji sprintf, że zamiast podawania bezpośredniego argumentów po przecinku, przyjmuje ona tablicę takich argumentów.

function dsprintf() {
  $data = func_get_args(); // pobieramy wszystkie argumenty przekazane do funkcji
  $string = array_shift($data); // nasz lancuch znakow jest pierwszym argumentem
  if (is_array(func_get_arg(1))) { // jezeli drugim argumentem jest tablica - uzywamy jej
    $data = func_get_arg(1);
  }
  $used_keys = array();
  // pobieramy argumenty z lancucha znakow i przekazujemy je do naszej kolejnej funkcji
  $string = preg_replace('/\%\((.*?)\)(.)/e',
    'dsprintfMatch(\\'$1\\',\\'$2\\',\\$data,$used_keys)', $string);
  $data = array_diff_key($data,$used_keys); // diff the data with the used_keys
  return vsprintf($string,$data); // yeah!
}

function dsprintfMatch($m1,$m2,&$data,&$used_keys) {
  if (isset($data[$m1])) { // sprawdzamy czy dany klucz istnieje w przekazanej tablicy argumentow
    $str = $data[$m1];
    $used_keys[$m1] = $m1; // nie usuwamy wystapienia - moze wystepowac wiele razy
    return sprintf("%".$m2,$str); // uzywamy sprintf na lancuchu znakow, wiec %s lub %d dzialaja tak jak powinny
  } else {
    return "%".$m2; // w przeciwnym wypadku uzywamy %s lub %d - w zaleznosci od wykorzystania
  }
}

$str = <<<HITHERE
Hello, %(firstName)s, I know your favorite funcion is %(functionName)s.
HITHERE;

$dataArray = array(
 'firstName'   => 'PHP',
 'functionName' => 'var_dump()',
);
echo dsprintf($str, $dataArray);
// Hello, PHP, I know your favorite function is var_dump().