4. díl - Knihovna StringUtils pro práci s textem v PHP

PHP Knihovny Knihovna StringUtils pro práci s textem v PHP American English version English version

V minulém dílu našeho seriálu tutoriálů o tvorbě knihoven pro PHP jsme dokončili knihovnu DateUtils, která usnadňuje formátování a parsování českého data a času. V dnešním dílu si naprogramujeme knihovnu StringUtils.

Motivace

PHP poskytuje bohatou škálu funkcí pro práci s textovými řetězci. Některé důležité funkce zde však zcela chybí a my je proto musíme implementovat sami a stále znovu a znovu.

StringUtils

Do knihovny StringUtils vložíme několik užitečných funkcí, které PHP postrádá. Třída je pomocného charakteru, nebude mít žádný vnitřní stav a její funkce budeme opět používat často a na mnoha místech našich aplikací. Proto budou všechny její metody statické.

class StringUtils
{

}

Třídu si můžete vložit do jmenného prostoru Utility. Ačkoli je to snad samozřejmé, raději připomenu, že pro vnitřní implementaci budeme používat výhradně PHP funkce s prefixem mb_, které podporují unicode.

startsWith() a endsWith()

Často bychom potřebovali vědět, zda nějaký text začíná nebo končí určitým podřetězcem. Např. parsujeme nějaké položky a právě podle částí jejich názvu se chceme orientovat. EndsWith() se hodí zejména pro autoloader, kde podle koncovky názvu třídy určujeme z jaké složky se má načíst.

public static function startsWith($haystack, $needle)
{
        return (mb_strpos($haystack, $needle) === 0);
}

public static function endsWith($haystack, $needle)
{
        return ((mb_strlen($haystack) >= mb_strlen($needle)) && ((mb_strpos($haystack, $needle, mb_strlen($haystack) - mb_strlen($needle))) !== false));
}

Metody berou v prvním parametru prohledávaný text a v druhém hledaný podřetězec. V anglické literatuře se při problémech prohledávání používají označení kupka sena (haystack) a jehla (needle). Asi tušíte proč :)

Na startsWith() není nic složitého, pouze musíme použít trojrovnítko, jelikož mb_strpos() vrací false při neúspěchu a 0 při nalezení na první pozici.

EndsWith() je již zajímavější. Kupka musí být delší nebo alespoň stejně dlouhá jako jehla, jinak v ní nemá smysl hledat. Dále necháme funkci mb_strpos() prohledat kupku až od té pozice, kde by mohla jehla začínat (délka kupky - délka jehly), předchozí výskyty nás nezajímají.

Funkce vyzkoušejme, nezapomeňte nastavit interní kódování na UTF-8:

require_once('models/System/Utility/StringUtils.php');
mb_internal_encoding('utf-8');
var_dump(StringUtils::startsWith('Čtvrtletní hodnocení', 'Čtvrtletní'));
var_dump(StringUtils::startsWith('Pololetní hodnocení', 'Čtvrtletní'));
var_dump(StringUtils::startsWith('Pololetní a čtvrtletní hodnocení', 'Čtvrtletní'));
var_dump(StringUtils::endsWith('ArticleController', 'Controller'));
var_dump(StringUtils::endsWith('ArticleHelperController', 'Helper'));

Výsledek:

PHP knihovna StringUtils – StartsWith a EndsWith

capitalize() a uncapitalize()

Výše uvedené funkce převedou počáteční písmeno řetězce na velký nebo na malý tvar. Capitalize() se nám hodí při výpisu např. tabulkových dat, kde chceme, aby uživatel viděl hodnoty s počátečním velkým písmenem, ale vnitřně se s nimi nějak pracuje a proto jsou malými písmeny. Uncapitalize() první písmeno naopak zmenší. Obě funkce budeme využívat ještě pro vnitřní potřeby knihovny.

Pozn. uncapitalize je pravděpodobně nasprávný anglický tvar, nicméně mi připadá zřejmější, než např. lowerFirst nebo něco podobného.

public static function capitalize($text)
{
        return mb_strtoupper(mb_substr($text, 0, 1)) . mb_substr($text, 1, mb_strlen($text));
}

public static function uncapitalize($text)
{
        return mb_strtolower(mb_substr($text, 0, 1)) . mb_substr($text, 1, mb_strlen($text));
}

Kód je zřejmý, pojďme ho tedy vyzkoušet:

echo(StringUtils::capitalize('žluťoučký kůň') . '<br />');
echo(StringUtils::uncapitalize('Žluťoučký kůň') . '<br />');

Výsledek:

PHP knihovna StringUtils – Capitalize a Uncapitalize

Zkrácení textu

Často vypisujeme nějaké popisky např. k položkám v eshopu a chceme, aby měl popisek nějaký maximální počet znaků, jelikož delší text se ke zboží v dané šabloně nevejde. Pokud je text delší, než určitá maximální délka, potřebujeme ho zkrátít a přidat na konec tři tečky, aby bylo jasné, že část chybí. Pokud byl kratší, nepřidáváme nic. Maximální délka se počítá již s případnými tečkami.

public static function shorten($text, $length)
{
        if (mb_strlen($text) - 3 > $length)
                $text = mb_substr($text, 0, $length - 3) . '...';
        return $text;
}

Metodu někdy naleznete ve frameworcích pod názvem truncate. Můžeme vyzkoušet:

echo(StringUtils::shorten('Notebook - Intel Pentium 2020M Ivy Bridge, 15.6" LED 1366x768 lesklý, RAM 4GB, Intel HD Graphics, HDD 500GB 5400 otáček, DVD, WiFi, Bluetooth, Webkamera, USB 3.0, Windows 8 64-bit', 71) . '<br />');
echo(StringUtils::shorten('Notebook - Intel Pentium 2020M', 71) . '<br />');

Výsledek jsem schválně usekl u znaku s diakritikou, vše funguje správně:

PHP knihovna StringUtils – Zkrácení textu

Odstranění diakritiky

Dnešní díl zakončeme odstraněním diakritiky. To je často potřeba, když vyrábíme např. ze jména emailovou adresu daného člověka, z názvu článku jeho URL adresu, pokud chceme ověřit, že uživatel nezadal do hesla diakritiku (heslo musí být shodné s heslem bez diakritiky, jinak je v něm diakritika) a podobně.

Přiznám se, že kód metody jsem vygooglil, přesněji ho vygooglil sczdavos :). Ukažme si ho:

public static function removeAccents($text) {
        $chars = array(
                // Decompositions for Latin-1 Supplement
                chr(195).chr(128) => 'A', chr(195).chr(129) => 'A',
                chr(195).chr(130) => 'A', chr(195).chr(131) => 'A',
                chr(195).chr(132) => 'A', chr(195).chr(133) => 'A',
                chr(195).chr(135) => 'C', chr(195).chr(136) => 'E',
                chr(195).chr(137) => 'E', chr(195).chr(138) => 'E',
                chr(195).chr(139) => 'E', chr(195).chr(140) => 'I',
                chr(195).chr(141) => 'I', chr(195).chr(142) => 'I',
                chr(195).chr(143) => 'I', chr(195).chr(145) => 'N',
                chr(195).chr(146) => 'O', chr(195).chr(147) => 'O',
                chr(195).chr(148) => 'O', chr(195).chr(149) => 'O',
                chr(195).chr(150) => 'O', chr(195).chr(153) => 'U',
                chr(195).chr(154) => 'U', chr(195).chr(155) => 'U',
                chr(195).chr(156) => 'U', chr(195).chr(157) => 'Y',
                chr(195).chr(159) => 's', chr(195).chr(160) => 'a',
                chr(195).chr(161) => 'a', chr(195).chr(162) => 'a',
                chr(195).chr(163) => 'a', chr(195).chr(164) => 'a',
                chr(195).chr(165) => 'a', chr(195).chr(167) => 'c',
                chr(195).chr(168) => 'e', chr(195).chr(169) => 'e',
                chr(195).chr(170) => 'e', chr(195).chr(171) => 'e',
                chr(195).chr(172) => 'i', chr(195).chr(173) => 'i',
                chr(195).chr(174) => 'i', chr(195).chr(175) => 'i',
                chr(195).chr(177) => 'n', chr(195).chr(178) => 'o',
                chr(195).chr(179) => 'o', chr(195).chr(180) => 'o',
                chr(195).chr(181) => 'o', chr(195).chr(182) => 'o',
                chr(195).chr(182) => 'o', chr(195).chr(185) => 'u',
                chr(195).chr(186) => 'u', chr(195).chr(187) => 'u',
                chr(195).chr(188) => 'u', chr(195).chr(189) => 'y',
                chr(195).chr(191) => 'y',
                // Decompositions for Latin Extended-A
                chr(196).chr(128) => 'A', chr(196).chr(129) => 'a',
                chr(196).chr(130) => 'A', chr(196).chr(131) => 'a',
                chr(196).chr(132) => 'A', chr(196).chr(133) => 'a',
                chr(196).chr(134) => 'C', chr(196).chr(135) => 'c',
                chr(196).chr(136) => 'C', chr(196).chr(137) => 'c',
                chr(196).chr(138) => 'C', chr(196).chr(139) => 'c',
                chr(196).chr(140) => 'C', chr(196).chr(141) => 'c',
                chr(196).chr(142) => 'D', chr(196).chr(143) => 'd',
                chr(196).chr(144) => 'D', chr(196).chr(145) => 'd',
                chr(196).chr(146) => 'E', chr(196).chr(147) => 'e',
                chr(196).chr(148) => 'E', chr(196).chr(149) => 'e',
                chr(196).chr(150) => 'E', chr(196).chr(151) => 'e',
                chr(196).chr(152) => 'E', chr(196).chr(153) => 'e',
                chr(196).chr(154) => 'E', chr(196).chr(155) => 'e',
                chr(196).chr(156) => 'G', chr(196).chr(157) => 'g',
                chr(196).chr(158) => 'G', chr(196).chr(159) => 'g',
                chr(196).chr(160) => 'G', chr(196).chr(161) => 'g',
                chr(196).chr(162) => 'G', chr(196).chr(163) => 'g',
                chr(196).chr(164) => 'H', chr(196).chr(165) => 'h',
                chr(196).chr(166) => 'H', chr(196).chr(167) => 'h',
                chr(196).chr(168) => 'I', chr(196).chr(169) => 'i',
                chr(196).chr(170) => 'I', chr(196).chr(171) => 'i',
                chr(196).chr(172) => 'I', chr(196).chr(173) => 'i',
                chr(196).chr(174) => 'I', chr(196).chr(175) => 'i',
                chr(196).chr(176) => 'I', chr(196).chr(177) => 'i',
                chr(196).chr(178) => 'IJ',chr(196).chr(179) => 'ij',
                chr(196).chr(180) => 'J', chr(196).chr(181) => 'j',
                chr(196).chr(182) => 'K', chr(196).chr(183) => 'k',
                chr(196).chr(184) => 'k', chr(196).chr(185) => 'L',
                chr(196).chr(186) => 'l', chr(196).chr(187) => 'L',
                chr(196).chr(188) => 'l', chr(196).chr(189) => 'L',
                chr(196).chr(190) => 'l', chr(196).chr(191) => 'L',
                chr(197).chr(128) => 'l', chr(197).chr(129) => 'L',
                chr(197).chr(130) => 'l', chr(197).chr(131) => 'N',
                chr(197).chr(132) => 'n', chr(197).chr(133) => 'N',
                chr(197).chr(134) => 'n', chr(197).chr(135) => 'N',
                chr(197).chr(136) => 'n', chr(197).chr(137) => 'N',
                chr(197).chr(138) => 'n', chr(197).chr(139) => 'N',
                chr(197).chr(140) => 'O', chr(197).chr(141) => 'o',
                chr(197).chr(142) => 'O', chr(197).chr(143) => 'o',
                chr(197).chr(144) => 'O', chr(197).chr(145) => 'o',
                chr(197).chr(146) => 'OE',chr(197).chr(147) => 'oe',
                chr(197).chr(148) => 'R',chr(197).chr(149) => 'r',
                chr(197).chr(150) => 'R',chr(197).chr(151) => 'r',
                chr(197).chr(152) => 'R',chr(197).chr(153) => 'r',
                chr(197).chr(154) => 'S',chr(197).chr(155) => 's',
                chr(197).chr(156) => 'S',chr(197).chr(157) => 's',
                chr(197).chr(158) => 'S',chr(197).chr(159) => 's',
                chr(197).chr(160) => 'S', chr(197).chr(161) => 's',
                chr(197).chr(162) => 'T', chr(197).chr(163) => 't',
                chr(197).chr(164) => 'T', chr(197).chr(165) => 't',
                chr(197).chr(166) => 'T', chr(197).chr(167) => 't',
                chr(197).chr(168) => 'U', chr(197).chr(169) => 'u',
                chr(197).chr(170) => 'U', chr(197).chr(171) => 'u',
                chr(197).chr(172) => 'U', chr(197).chr(173) => 'u',
                chr(197).chr(174) => 'U', chr(197).chr(175) => 'u',
                chr(197).chr(176) => 'U', chr(197).chr(177) => 'u',
                chr(197).chr(178) => 'U', chr(197).chr(179) => 'u',
                chr(197).chr(180) => 'W', chr(197).chr(181) => 'w',
                chr(197).chr(182) => 'Y', chr(197).chr(183) => 'y',
                chr(197).chr(184) => 'Y', chr(197).chr(185) => 'Z',
                chr(197).chr(186) => 'z', chr(197).chr(187) => 'Z',
                chr(197).chr(188) => 'z', chr(197).chr(189) => 'Z',
                chr(197).chr(190) => 'z', chr(197).chr(191) => 's',
                // Euro Sign
                chr(226).chr(130).chr(172) => 'E',
                // GBP (Pound) Sign
                chr(194).chr(163) => ''
        );
        return strtr($text, $chars);
}

Jde vlastně jen o výčet diakritických znaků, které jsou v UTF-8 reprezentovány jako několik ASCII znaků za sebou (např. háček a c). Výčet je zapsán jako pole, aby pomocí něj mohla následně PHP funkce strtr() provést nahrazení.

Kód je součástí metody nějakého modulu WordPressu (konkrétně zde: https://core.trac.wordpress.org/…rmatting.php), kde je výčet ještě mnohem delší, nám však bude tento stačit.

Metodu vyzkoušejme:

echo(StringUtils::removeAccents('Příliš žluťoučký kůň úpěl ďábelské ódy. PŘÍLIŠ ŽLUŤOUČKÝ KŮŇ ÚPĚL ĎÁBELSKÉ ÓDY. <br />'));
echo(StringUtils::removeAccents('Příliš žluťoučký kůň úpěl ďábelské ódy. PŘÍLIŠ ŽLUŤOUČKÝ KŮŇ ÚPĚL ĎÁBELSKÉ ÓDY. <br />'));

Výsledek:

PHP knihovna StringUtils – Odstranění diakritiky

To by bylo pro dnešek vše, příště StringUtils dokončíme.


 

  Aktivity (1)

Článek pro vás napsal David Čápka
Avatar
Autor pracuje jako softwarový architekt a pedagog na projektu ITnetwork.cz (a jeho zahraničních verzích). Velmi si váží svobody podnikání v naší zemi a věří, že když se člověk neštítí práce, tak dokáže úplně cokoli.
Unicorn College Autor se informační technologie naučil na Unicorn College - prestižní soukromé vysoké škole IT a ekonomie.

Jak se ti líbí článek?
Celkem (5 hlasů) :
4.84.84.84.84.8


 


Miniatura
Všechny články v sekci
Knihovny pro PHP
Miniatura
Následující článek
Dokončení knihovny StringUtils v PHP

 

 

Komentáře
Zobrazit starší komentáře (1)

Avatar
Zdeněk Pavlátka
Tým ITnetwork
Avatar
Zdeněk Pavlátka:

Nějak by to asi šlo, musel bys kontrolovat, jestli 1. znak v kombinaci je písmeno a 2. je diakritické znaménko (nebo jak to nazvat). Tahle možnost je ale jednodušší na pochopení.

Odpovědět 25.2.2014 18:51
Kolik jazyků umíš, tolikrát jsi programátor.
Avatar
David Čápka
Tým ITnetwork
Avatar
David Čápka:

Teoreticky by to mělo jít, ale nikde jsem to tak řešené neviděl, asi pro to bude nějaký důvod.

Odpovědět 25.2.2014 19:29
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
Jan Vargovský
Redaktor
Avatar
Jan Vargovský:
<?php
echo iconv("UTF-8", "ASCII//TRANSLIT", 'Příliš žluťoučký kůň úpěl
ďábelské ódy.');

Výsledek:

Prilis zlutoucky kun upel dabelske ody.
Editováno 25.2.2014 20:55
 
Odpovědět  +2 25.2.2014 20:54
Avatar
Odpovídá na Jan Vargovský
Michal Žůrek (misaz):

nakonec to není až zas tak těžké :D

Odpovědět 25.2.2014 20:56
Nesnáším {}, proto se jim vyhýbám.
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na Jan Vargovský
David Čápka:

Tohle nefunguje, pracovali jsme s tím a z nějakého důvodu jsme to vyhodili, v některém případě to selhávalo.

Odpovědět 25.2.2014 21:44
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
Jan Vargovský
Redaktor
Avatar
Odpovídá na David Čápka
Jan Vargovský:
<?php
$text = 'Příliš žluťoučký kůň úpěl ďábelské ódy za 10 €.';
setlocale(LC_CTYPE, 'cs_CZ.UTF-8');
echo iconv("UTF-8", "ASCII//TRANSLIT", $text);
 
Odpovědět 25.2.2014 22:16
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na Jan Vargovský
David Čápka:

Díky, zkusím to a v článku to případně zmíním. Nerad bych to ale dával jako výchozí řešení, ta setlocale není thread safe a typuji, že na freehostinzích to nepůjde, já jsem s tím tenkrát docela dlouho zápasil, mám dojem, že mi to jelo na localu a na produkci jsem to nemohl rozchodit. Spíše se nějak pokusím upravit tu nahrazovací funkci.

Odpovědět 26.2.2014 10:18
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
tribal.cz
Redaktor
Avatar
tribal.cz:

já používám funkci strtr() a nikdy jsem se nesetkal s odmítnutím

 
Odpovědět  +2 1.3.2014 9:05
Avatar
simek.zbynek
Člen
Avatar
simek.zbynek:

a to nejde použít pro převedení počátečního písmena funkci "ucfirst()"

 
Odpovědět  +1 25.3.2014 1:01
Avatar
Mazwor
Člen
Avatar
Mazwor:

Ahoj,
možná jsem jen momentálně mimo a něco mi uniká, ale proč je v té podmínce pro zkrácení textu brána jeho délka zmenšená o tři? Mám na mysli tuto část:

if (mb_strlen($text) - 3 > $length)

Vždyť zde není potřeba brát ty tři tečky v úvahu a naopak, pokud bych zadal maximální délku $length např. jako 20 a $text obsahoval 21 znaků, v podmínce by pak vyšlo FALSE (21-3 < 20) a text by těmito tečkami nebyl nahrazen, ačkoli přesahuje maximální zadanou délku...nemám pravdu? :)
Jak říkám, možná mi něco uniká, jen mám pořád pocit, že by zde to odčítání trojky být nemělo. Díky za případné vysvětlení ;).

Odpovědět  +1 5.9.2014 21:31
Pořádek je pro blbce, inteligent ovládá chaos. :D
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 11. Zobrazit vše