SplitTytuł dzisiejszej notki nie ma nic wspólnego z miejscowością wypoczynkową na południu Chorwacji, chociaż pewnie obecne temperatury nasuwają takie skojarzenia :) Zamiast tego chodzi o split łańcucha znaków, czyli bardzo często potrzebną w praktyce operację na stringach.
Danymi dla niej są najczęściej dwa napisy, zaś wynikiem jest tablica podciągów pierwszego z nich, powstała poprzez podzielenie go względem wystąpień drugiego (tzw. separatora). Ponieważ jak zwykle przykład będzie mówił najwięcej, niniejszym podaję nawet kilka:
Zwłaszcza ostatni pokazuje, że split jest rzeczywiście użyteczną funkcją, mogącą ułatwiać parsowanie formatów tekstowych (zwłaszcza, jeśli daje się ją zastosować wielokrotnie). Warto więc mieć takową w swoim języku/bibliotece. Jak więc przedstawia się jej dostępność na różnych platformach?
Tradycyjnie w .NET i Javie jest dobrze, a nawet lepiej. W obu przypadkach funkcja (a właściwie metoda) Split/split klasy String dodaje nawet trochę więcej możliwości niż to opisałem wyżej. I tak w .NET można podać więcej niż jeden oddzielacz, natomiast w Javie domyślnie może być nim również wyrażenie regularne.
Niektóre języki skryptowe i skryptopodobne też mają się w tym względnie całkiem dobrze. W Pythonie jest metoda split w klasie napisu, natomiast PHP ma funkcję explode, która mimo innej nazwy działa bardzo podobnie.
Ale nie wszędzie funkcja typu split jest od razu dostępna; niekiedy trzeba ją sobie samemu napisać. Przykładem języka, gdzie może być to koniecznie, jest Lua oraz - jakżeby inaczej - C++ :) Ze względu na użyteczność splita często znajdowałem się w sytuacji, gdzie konieczne/wygodne było jego napisanie. Po kilku(nastu?) takich przypadkach doszedłem wreszcie do czegoś podobnego do poniższego kodu:
std::string::size_type i = 0, j;
while (i <text.length()
&& (j = text.find(delim, i)) != std::string::npos)
{
res.push_back (text.substr(i, j - i));
i = j + delim.length();
}
res.push_back (text.substr(i));
return res;
}
Na koniec zwrócę jeszcze uwagę na to, że czasami trzeba ostrożnie postępować z rezultatem splitu. Zdecydowana większość wersji tej operacji dopuszcza, by w wynikowej tablicy występowały puste ciągi. Odpowiadają one kilku kolejnym wystąpieniom separatora lub jego obecnością na początku bądź końcu ciągu. Jeśli nie są one nam potrzebne (a rzadko są), to należy je zignorować lub usunąć.
Równoczesne zastępowanie kilku fraz w tekścieŁańcuchy znaków w językach programowania udostępniają zwykle operację zamiany wszystkich wystąpień jednego podciągu na inny. Nawet jeśli tak nie jest (co dotyczy chociażby C++, jak zwykle), możliwe jest jej łatwe zaimplementowanie przy pomocy prostszych funkcji (jak choćby std::string::find).
Gorzej jest z nieco bardziej skomplikowaną procedurą zastępowania kilku fraz naraz. Zaznaczam od razu, że nie jest to wcale równoważne wywołaniu funkcji typu Replace parę razy dla różnych argumentów. Jeśli bowiem zastąpimy w tekście X1 przez Y1, a w wyniku X2 przez Y2, to możemy otrzymać inny rezultat niż przy dokonywaniu obu zamian równocześnie - wystarczy, że Y1 będzie w sobie zawierał ciąg X2. W celu uzyskania poprawnego wyniku należy wszystkie zamiany przeprowadzać jednocześnie.
Prosta implementacja takiej operacji może się sprowadzać do przeglądania tekstu w poszukiwaniu któregoś z podciągów do zastąpienia, a następnie dokonania faktycznej zamiany dla tego z nich, który został wykryty najwcześniej w stosunku do aktualnej pozycji w tekście. Należy następnie przesunąć się tuż za znalezioną frazę i działanie powtórzyć - i tak aż do końca tekstu. W C++ wygląda to wszystko mniej więcej tak:
stringstream ss;
for (size_t i = 0; i <text.length(); )
{
StringMap::const_iterator phraseIt = phrases.end();
size_t minPhrasePos = 0;
for (StringMap::const_iterator it = phrases.begin();
it != phrases.end(); ++it)
{
size_t phrasePos = text.find(it->first, i);
if (phrasePos == string::npos) continue;
if (phraseIt == phrases.end() || phrasePos <minPhrasePos)
{ phraseIt = it; minPhrasePos = phrasePos; }
}
if (phraseIt == phrases.end()) { ss <<text.substr(i); break; }
ss <<text.substr(i, minPhrasePos - i);
ss <<phraseIt->second;
i = minPhrasePos + phraseIt->first.length();
}
return ss.str();
}
Jeśli tekst, na którym operujemy, ma długość n, a fraz do wyszukania i zastąpienia jest k, to w najgorszym przypadku całość może zabrać czas O(kn). Wymagałoby to jednak dość specyficznych danych, więc w praktyce nie musi być aż tak źle :) Dla lepszych rezultatów - zwłaszcza przy "obrabianiu" większej liczby tekstów tym samym słownikiem fraz - należałoby pewnie użyć specjalistycznych struktur danych, np. drzew prefiksowych (trie).
Argumenty w linii komendParametry przekazywane programom podczas uruchomienia mają najczęściej formę kilku przełączników (switches), po których opcjonalnie występują właściwe dla nich argumenty. Zwyczajowo rzeczone przełączniki są poprzedzone myślnikiem (styl uniksowy) lub slashem (styl MS-DOS), jak w przykładzie poniżej:
gcc -c main.c -o main.o -pedantic
Tak skonstruowany wiersz poleceń jest przekazywany przez runtime do aplikacji C/C++ jako tablica ciągów znaków, powstałych już po podzieleniu go na części według spacji i ew. cudzysłowów. Tradycyjnie pierwszy parametr (argc) funkcji main określa liczbę tych części, zaś drugi (argv) jest wspomnianą tablicą je zawierającą. Warto pamiętać, że - jeśli interesują nas tylko argumenty wywołania - jest to tablica indeksowana od jedynki, gdyż argv[0] zawiera zawsze samo polecenie uruchamiające program (w powyższym przykładzie byłby to ciąg "gcc"). Ostatnim elementem jest z kolei argv[argc - 1], zatem w sumie tablica zawiera argc - 1 znaczących parametrów programu. Wreszcie, gwarantowane jest, iż argv[argc] jest zawsze pustym wskaźnikiem, jeśli do czegokolwiek mogłoby się to przydać.
Bezpośrednia interpretacja wiersza poleceń, który używa wspomnianych na początku przełączników, może być jednak nieco kłopotliwa. Wydaje mi się, że lepiej jest przetworzyć ją do bardziej znośnej postaci słownika (mapy), pozwalającego odwoływać się do switchy i ich argumentów po nazwie. Można to zrobić choćby w poniższy sposób:
const char SWITCH_CHAR = '-'; // lub '/'
const CommandLine ParseCommandLine(int argc, const char* argv[])
{
CommandLine cl;
for (int i = 1; i <argc; )
if (*(argv[i]) == SWITCH_CHAR)
{
std::vector<std::string> p;
p.reserve (argc - i);
int j;
for (j = i + 1;
j <argc && (*(argv[j]) != SWITCH_CHAR || strstr(argv[j], " "));
++j)
p.push_back (argv[j]);
cl.insert (std::make_pair(argv[i] + 1, p));
i = j;
}
else
++i;
return cl;
}
W rezultacie otrzymamy mapę, w której wyszukiwanie (std::map::find) pozwoli nam określić, czy dany przełącznik był podany, zaś indeksowanie (operator []) da nam dostęp do jego parametrów w postaci wektora.
Unikalne łańcuchy znakówOkazjonalnie zachodzi potrzeba, aby do jakiegoś celu wygenerować niepowtarzalny ciąg znaków. Jednym z zastosowań może być tutaj nadanie plikowi nazwy gwarantującej brak kolizji z jakąkolwiek inną nazwą pliku w systemie albo chociaż w aktualnym katalogu. W przypadku gdy ów plik ma istnieć tylko przez chwilę - a więc być plikiem tymczasowym (tempfile), sprawa jest prosta i sprowadza się do wywołania GetTempFileName jako funkcji WinAPI lub metody klasy System.IO.Path z .NET.
Stworzona w ten sposób nazwa jest z definicji "krótko żyjąca". Na drugim biegunie w zakresie unikalności znajdują się tzw. UUID-y, czyli specjalnie ustandaryzowane 128-bitowe identyfikatory konstruowane tak, by zapewnić, że prawdopodobieństwo wygenerowania duplikatów jest mikroskopijnie małe. Każdy, kto grzebał trochę w Rejestrze Windows lub miał do czynienia z technologią COM, zna na pewno ich kuzynów, czyli GUID-y, opartych w gruncie rzeczy na podobnych zasadach.
Ponieważ tworzenie nowych GUID-ów/UUID-ów jest proste, można je wykorzystać w celu uzyskania niepowtarzalnych ciągów znaków. W czystym Windows API unikalny string oparty na UUID-zie można otrzymać np. tak:
char* uuidStr;
if (UuidToStringA(&uuid, (unsigned char**)&uuidStr) != RPC_S_OK)
return string();
string res(uuidStr);
RpcStringFree (&uuidStr);
return res;
}
Z kolei w .NET istnieje wyspecjalizowana klasa System.Guid, którą można wykorzystać w podobnym celu. W wyniku otrzymamy oczywiście teksty w stylu:
0DF44EAA-FF21-4412-828E-260A8728E7F1
co w wielu zastosowaniach może być strzelaniem z armaty do komara. Nie zawsze potrzebujemy przecież aż tak wielkiej unikalności, a UUID-y w formie tekstowej są długie i nieporęczne.
Są więc i inne rozwiązania, stosowalne jeśli nasz losowy ciąg musi być unikalny tylko przez stosunkowo niedługi czas, np. podczas jednego uruchomienia programu. W takim wypadku możemy użyć chociażby:
GetTickCount/QueryPerformanceCounter lub asemblerową instrukcję rdtsc. Jest to czas (lub liczba cykli procesora) od momentu rozruchu systemu, więc z definicji wartość taka nie powtórzy się w trakcie działania aplikacji. Warto wiedzieć aczkolwiek, że używanie rdtsc jest generalnie niezalecane.GetSystemTime z Windows API czy System.DateTime.Now z .NET - przekonwertowanego na string. Wadą jest tutaj wynikowa długość łańcucha i zbyt oczywisty format; prawdopodobnie więc należałoby jeszcze poddać go jakiemuś post-processingowi.Najważniejsze jest to, aby właściwie dobrać metodę do sytuacji, w której potrzebujemy unikalnego łańcucha znaków. Często wystarczy coś w stylu:
Nie myślmy nawet jednak, żeby tak stworzonego stringa używać do jakiejkolwiek formy komunikacji przez sieć. W takich przypadkach trzeba w zasadzie użyć UUID-ów.