Radek Chalupa   konzultace a školení programování, vývoj software na zakázku

Databáze SQLite v C++ - třídění českých textů

22.11.2019

V databázi bude jednoduchá tabulka se sloupcem (kromě číselného primárního klíče) typu TEXT.

void vytvorit_databazi()
{
	std::string str_file = get_cesta_db();
	if (file_exists(str_file.c_str()))
	{
		if (remove(str_file.c_str()) !=0)
			throw std::runtime_error("Chyba smazání souboru");
	}
	int ires = sqlite3_open(str_file.c_str(), &_sqlite3);
	if (ires != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(ires));
	ires = sqlite3_exec(_sqlite3, "CREATE TABLE IF NOT EXISTS polozky (id INTEGER PRIMARY KEY,"
		" obsah TEXT)", nullptr, nullptr, nullptr);
	if (ires != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(ires));
}

Pro testování si tabulku naplníme několika řádky, na kterých bude vidět způsob řazení/třídění textů.

void naplnit_databazi()
{
	sqlite3_exec(_sqlite3, "INSERT INTO polozky (obsah) VALUES ('CC')",
		nullptr, nullptr, nullptr);
	sqlite3_exec(_sqlite3, "INSERT INTO polozky (obsah) VALUES ('cc')",
		nullptr, nullptr, nullptr);
	sqlite3_exec(_sqlite3, "INSERT INTO polozky (obsah) VALUES ('čč')",
		nullptr, nullptr, nullptr);
	sqlite3_exec(_sqlite3, "INSERT INTO polozky (obsah) VALUES ('ČČ')",
		nullptr, nullptr, nullptr);
	sqlite3_exec(_sqlite3, "INSERT INTO polozky (obsah) VALUES ('XX')",
		nullptr, nullptr, nullptr);
	sqlite3_exec(_sqlite3, "INSERT INTO polozky (obsah) VALUES ('xx')",
		nullptr, nullptr, nullptr);
	sqlite3_exec(_sqlite3, "INSERT INTO polozky (obsah) VALUES ('áá')",
		nullptr, nullptr, nullptr);
	sqlite3_exec(_sqlite3, "INSERT INTO polozky (obsah) VALUES ('aa')",
		nullptr, nullptr, nullptr);
	sqlite3_exec(_sqlite3, "INSERT INTO polozky (obsah) VALUES ('ch')",
		nullptr, nullptr, nullptr);
	sqlite3_exec(_sqlite3, "INSERT INTO polozky (obsah) VALUES ('CH')",
		nullptr, nullptr, nullptr);
	sqlite3_exec(_sqlite3, "INSERT INTO polozky (obsah) VALUES ('GG')",
		nullptr, nullptr, nullptr);
	sqlite3_exec(_sqlite3, "INSERT INTO polozky (obsah) VALUES ('jj')",
		nullptr, nullptr, nullptr);
	sqlite3_exec(_sqlite3, "INSERT INTO polozky (obsah) VALUES ('ii')",
		nullptr, nullptr, nullptr);
	sqlite3_exec(_sqlite3, "INSERT INTO polozky (obsah) VALUES ('íí')",
		nullptr, nullptr, nullptr);
}

Dále si vytvoříme funkci pro výpis všech textů v tabulce:

void vypsat_data()
{
	assert(_sqlite3);
	sqlite3_stmt* stmt = nullptr;
	char sz_sql[255];
	strcpy(sz_sql, "SELECT obsah FROM polozky ORDER BY obsah COLLATE cestina");
	int ires = sqlite3_prepare_v2(_sqlite3, sz_sql,
		static_cast(strlen(sz_sql)), &stmt, nullptr);
	if (ires != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(ires));
	int istep;
	istep = sqlite3_step(stmt);
	while (SQLITE_ROW == istep)
	{
		printf("%s\n", (char*)sqlite3_column_text(stmt, 0));
		istep = sqlite3_step(stmt);
		if (SQLITE_ERROR == istep)
			throw std::runtime_error(sqlite3_errstr(istep));
	}
	ires = sqlite3_reset(stmt);
	if (ires != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(ires));
	ires = sqlite3_finalize(stmt);
	if (ires != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(ires));
}

Když tento kód použijeme, dostaneme následující výsledek:

CC
CH
GG
XX
aa
cc
ch
ii
jj
xx
áá
íí
ČČ
čč

Vidíme tedy že z pohledu správného českého třídění to jistě není to pravé. Naštěstí SQLite podporuje použití vlastní porovnávací funkce v rámci vytvoření tzv. collation. Logika je obdobná jako například u standardní céčkovské funkce qsort.

Nejprve si připravíme vlastní porovnávací funkci, která bude volána zevnitř SQLite podle potřeby a v parametrech budou vždy dvě položky, které se mají porovnat. Logika návratové hodnoty této funkce je stejná jako u céčkovských funkcí skupiny strcmp, tedy pokud je první parametr (logicky) větší než druhý, vrátíme hodnotu větší než nula, pokud je větší druhý parametr, vrátíme hodnotu menší než nula a v případě shodnosti obou parametrů vrátíme 0. Pro porovnání textů použijeme funkci strcoll, která porovnává texty podle nastaveného locale. Proto musíme (nejlépe hned na začátku programu nastavit locale pro řazení textů pomocí funkce setlocale:

setlocale(LC_COLLATE, "cs_CZ.utf8");

Naše porovnávací funkce pak bude vypadat takto:

int porovnat_cesky(void*, int delka1, const void* text1, int delka2, const void* text2)
{
	return strcoll((const char*)text1, (const char*)text2);
}

Nyní ještě zbývá vytvořit (na existující otevřenou databázi) collation pomocí funkce sqlite3_create_collation:

sqlite3_create_collation(_sqlite3, "cestina", SQLITE_UTF8, nullptr, porovnat_cesky);

Jak je vidět, v této funkci kromě kódování zadáme adresu naší funkce a dále nějaký název pro toto collation. Stejný název pak použijeme v příkazu select, takže příslušný řádek kódu upravíme takto:

strcpy(sz_sql, "SELECT obsah FROM polozky ORDER BY obsah COLLATE cestina");

Když po výše uvedených úpravách spustíme výpis dat, dostaneme nyní následující výsledek:

aa
áá
cc
CC
čč
ČČ
GG
ch
CH
ii
íí
jj
xx
XX

Celý kód si můžete prohlédnout a stáhnout na mém githubu. Přeložit lze kompilátorem GCC příkazem:

g++ -O3 -Wall main.cpp -lsqlite3 -osqlite-cz