Tytuł 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:
typedef std::vector
StringArray Split(const std::string& text, const std::string& delim)
{
StringArray res;
if (delim.empty()) { res.push_back(text); return res; }
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;
}[/cpp]
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ąć.
Operując na C-stringach, często (oczywiście nie zawsze) przydaje się split in-situ – czyli nie kopiujemy zawartości tablicy znaków, a jedynie zastępujemy wszystkie wystąpienia delimitera :) No i jeszcze zwykle zwracam tablicę wskaźników tychże stringów, no ale to raczej dodatek.
Jeśli chodzi o końcową uwagę o dwóch delimiterach pod rząd generujących puste stringi to w Javie jest fajnie bo można ustawić regex jako delimiter i ja zazwyczaj robie coś w stylu: “\s+”.
No i zanim splituje string to jeszcze trimm() na nim i wówczas mam pewność, że nie ma pustych elementów.
Zawsze mnie jednak zastanawiało czy zaprzęganie wyrażeń regularnych do tego to nie jest przypadkiem armata na muchę…
W C++ są dwa standardowe sposoby, których ja używam
Pierwszy łatwo przewidzieć – boost http://www.boost.org/doc/libs/1_43_0/doc/html/string_algo/usage.html#id
1761650
Drugi jest ciekawszy, ale działa tylko w przypadku prostych delimiterów. Można stworzyć stringstream z łańcucha i wykorzystać funkcję getline :-)
W Lua można zrobić split przy pomocy string.gsub np. tak:
string.gsub(“witaj swiecie”, “[^%s]+”, function(x) print(x) end);
Funkcję z ostatniego warunku warto zrobić jako local przed wywołaniem gsub ze względów wydajnościowych
Łojej, jak możesz zwracać z funkcji coś takiego jak wektor stringów przez wartość! Przecież to angażuje dodatkowe kopiowanie całego wektora, co najmniej raz!!!
Sama idea Split jest słuszna i fajnie, że zwróciłeś uwagę na to zagadnienie. Przydałaby się tylko jeszcze wersja inkrementacyjna, która zamiast budować wektor stringów to z kolejnymi wywołaniami jakiejś funkcji czy metody zwracałaby kolejne stringi.
Czasami aż się zastanawiam, co jest takiego trudnego w zaimplementowaniu RVO w tak oczywistych przypadkach… No ale co ja mogę wiedzieć o budowie kompilatorów :)
Co do funkcji inkrementacyjnej, to strtok() ze standardowej biblioteki C robi właśnie coś takiego, tyle że ograniczeniem jest jednoznakowy separator (ale za to można podać kilka separatorów naraz).