Lekce 8 - Textové řetězce ve Swift podruhé - Práce se znaky

Swift Základní konstrukce Textové řetězce ve Swift podruhé - Práce se znaky

Unicorn College ONEbit hosting Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V milé lekci kurzu, Pole ve Swift, jsme se naučili pracovat s polem. Pokud jste vycítili ve String nějakou podobnost mezi polem a textovým řetězcem, tak jste vycítili správně. Pro ostatní může být překvapením, že String je v podstatě pole znaků (Characterů) a můžeme s ním i takto pracovat.

Nejprve si vyzkoušejme, že to všechno funguje. Rozcvičíme se na jednoduchém vypsání znaku na dané pozici. Bohužel nestačí pouze napsat index znaku jako číslo, ale musíme jej nejprve získat metodou index(). Ta vrací speciální typ String.Index a přes něj se již na znak dostaneme.

let s = "Ahoj ITnetwork"
print(s)
let index = s.index(s.startIndex, offsetBy: 2)
print(s[index])

Výstup:

Ahoj ITnetwork
o

Vidíme, že můžeme ke znakům v řetězci přistupovat přes hranatou závorku, jako tomu je i u pole. Zklamáním může být, že znaky na dané pozici jsou ve Swift read-only, nemůžeme tedy napsat:

// Tento kód nebude fungovat
var s = "Ahoj ITnetwork"
let index = s.index(s.startIndex, offsetBy: 2)
s[index] = "o"
print(s)

Samozřejmě to jde udělat jinak, později si to ukážeme, zatím se budeme věnovat pouze čtení jednotlivých znaků.

Analýza výskytu znaků ve větě

Napišme si jednoduchý program, který nám analyzuje zadanou větu. Bude nás zajímat počet samohlásek, souhlásek a počet nepísmenných znaků (např. mezera nebo !).

Daný textový řetězec si nejprve v programu zadáme napevno, abychom ho nemuseli při každém spuštění psát. Až bude program hotový, nahradíme ho readLine(). Řetězec budeme projíždět cyklem po jednom znaku. Rovnou zde říkám, že neapelujeme na rychlost programu a budeme volit názorná a jednoduchá řešení.

Nejprve si připravme kód, definujme si samohlásky a souhlásky. Počet ostatních znaků nemusíme počítat, bude to délka řetězce mínus samohlásky a souhlásky. Abychom nemuseli řešit velikost písmen, celý řetězec na začátku převedeme na malá písmena. Připravme si proměnné, do kterých budeme ukládat jednotlivé počty. Protože se jedná o složitější kód, nebudeme zapomínat na komentáře.

// řetězec, který chceme analyzovat
var s = "Programátor se zasekne ve sprše, protože instrukce na šampónu byly: Namydlit, omýt, opakovat."
print(s)
s = s.lowercased()

// inicializace počítadel
var pocetSamohlasek = 0
var pocetSouhlasek = 0

// definice typů znaků
let samohlasky = "aeiouyáéěíóúůý"
let souhlasky = "bcčdďfghjklmnpqrřsštťvwxzž"

// hlavní cyklus
for znak in s {

}

Zpočátku si připravíme řetězec a převedeme ho na malá písmena. Počítadla vynulujeme. Na definice znaků nám postačí obyčejné Stringy. Hlavní cyklus nám projede jednotlivé znaky v řetězci s, přičemž v každé iteraci cyklu bude v proměnné znak aktuální znak.

Pojďme plnit počítadla, pro jednoduchost již nebudu opisovat zbytek kódu a přesunu se jen k cyklu:

// hlavní cyklus
for znak in s {
    if samohlasky.contains(znak) {
        pocetSamohlasek += 1
    } else if souhlasky.contains(znak) {
        pocetSouhlasek += 1
    }
}

Metodu contains() na řetězci již známe, jako parametr ji lze předat jak podřetězec, tak přímo znak. Daný znak znak naší věty tedy nejprve zkusíme vyhledat v řetězci samohlasky a případně zvýšit jejich počítadlo. Pokud v samohláskách není, podíváme se do souhlásek a případně opětovně zvýšíme jejich počítadlo.

Nyní nám chybí již jen výpis na konec:

var s = "Programátor se zasekne ve sprše, protože instrukce na šampónu byly: Namydlit, omýt, opakovat."
print(s)
s = s.lowercased()

// inicializace počítadel
var pocetSamohlasek = 0
var pocetSouhlasek = 0

// definice typů znaků
let samohlasky = "aeiouyáéěíóúůý"
let souhlasky = "bcčdďfghjklmnpqrřsštťvwxzž"

// hlavní cyklus
for znak in s {
    if samohlasky.contains(znak) {
        pocetSamohlasek += 1
    } else if souhlasky.contains(znak) {
        pocetSouhlasek += 1
    }
}

print("Samohlásek: \(pocetSamohlasek)")
print("Souhlásek: \(pocetSouhlasek)")
print("Nepísmenných znaků: \(s.count - (pocetSamohlasek + pocetSouhlasek))")

Výsledek:

Programátor se zasekne ve sprše, protože instrukce na šampónu byly: Namydlit, omýt, opakovat.
Samohlásek: 31
Souhlásek: 45
Nepísmenných znaků: 17

A je to!

ASCII hodnota

Možná jste již někdy slyšeli o ASCII tabulce. Zejména v éře operačního systému MS-DOS prakticky nebyla jiná možnost, jak zaznamenávat text. Jednotlivé znaky byly uloženy jako čísla typu 1 bajt, tedy s rozsahem hodnot od 0 do 255. V systému byla uložena tzv. ASCII tabulka, která měla 256 znaků a každému ASCII kódu (číselnému kódu) přiřazovala jeden znak.

Asi je vám jasné, proč tento způsob nepřetrval dodnes. Do tabulky se jednoduše nevešly všechny znaky všech národních abeced. Nyní se používá Unicode (UTF-8) kódování, kde jsou znaky reprezentovány trochu jiným způsobem. Ve Swift máme možnost pracovat s ASCII hodnotami jednotlivých znaků. Hlavní výhoda je v tom, že znaky jsou uloženy v tabulce za sebou, podle abecedy. Např. na pozici 97 nalezneme "a", na 98 "b" a podobně. Podobně je to s čísly, diakritické znaky tam budou bohužel jen nějak rozházeny.

Zkusme si nyní převést znak do jeho ASCII hodnoty a naopak podle ASCII hodnoty daný znak vytvořit. Kód je trochu komplikovanější, za okamžik si jej popíšeme:

var c : Character // znak
c = "a"
// převedeme znak na jeho ASCII hodnotu
let optionalASCIIhodnota = c.unicodeScalars.filter{$0.isASCII}.first?.value
// dostali jsme Optional, ale už víme, co dělat
if let ASCIIhodnota = optionalASCIIhodnota {
    print("Znak \(c) jsme převedli na ASCII hodnotu \(ASCIIhodnota)")
}

// Převedeme ASCII hodnotu na znak
c = Character(UnicodeScalar(98))
print("ASCII hodnotu 98 jsme převedli na znak \(c)")

Protože zvlášť převod znaku na jeho ASCII hodnotu vypadá celkem děsivě, tak si alespoň stručně popíšeme, co vlastně tento řádek kódu dělá:

let optionalASCIIhodnota = c.unicodeScalars.filter{$0.isASCII}.first?.value

Znaky a textové řetězce jsou ve Swift reprezentovány pomocí tzv. Unicode Scalars, což je prostě 21bitová reprezentace jednoho znaku. To může být naše "a", číslice, speciální znaky či také emoji. Z vlastnosti unicodeScalars získáme tyto hodnoty pro Character nebo všechny znaky Stringu.

filter nám umožní vybrat jen ASCII znaky, jelikož Unicode Scalars obsahují i další speciální znaky. Do filtru předáváme $0, což v tomto případě zastupuje jeden Unicode Scalar. Pomocí vlastnosti isASCII se jednoduše ptáme, jestli tento znak patří do ASCII tabulky, která je omezenější než Unicode Scalars reprezentace. Potom se již jen zeptáme na první nalezený prvek a získáme jeho hodnotu. Ta je samozřejmě typu Optional, protože nemusíme nalézt nic.

Cézarova šifra

Vytvoříme si jednoduchý program pro šifrování textu. Pokud jste někdy slyšeli o Cézarově šifře, bude to přesně to, co si zde naprogramujeme. Šifrování textu spočívá v posouvání znaku v abecedě o určitý, pevně stanovený počet znaků. Například slovo "ahoj" se s posunem textu o 1 přeloží jako "bipk". Posun umožníme uživateli vybrat. Algoritmus zde máme samozřejmě opět vysvětlený a to v článku Cézarova šifra. Program si dokonce můžete vyzkoušet v praxi - Online cézarova šifra.

Vraťme se k programování a připravme si kód. Budeme potřebovat proměnné pro původní text, zašifrovanou zprávu a pro posun. Dále cyklus projíždějící jednotlivé znaky a výpis zašifrované zprávy. Zprávu si necháme zapsanou napevno v kódu, abychom ji nemuseli při každém spuštění programu psát. Po dokončení nahradíme obsah proměnné metodou readLine(). Šifra nepočítá s diakritikou, mezerami a interpunkčními znaménky. Diakritiku budeme bojkotovat a budeme předpokládat, že ji uživatel nebude zadávat. Ideálně bychom poté měli diakritiku před šifrováním odstranit, stejně tak cokoli kromě písmen.

// inicializace proměnných
let s = "cernediryjsoutamkdebuhdelilnulou"
print("Původní zpráva: \(s)")
var zprava : String = ""
var posun = 1

// cyklus projíždějící jednotlivé znaky
for c in s {

}

// výpis
print("Zašifrovaná zpráva: \(zprava)")

Nyní se přesuneme dovnitř cyklu, převedeme znak v c na ASCII hodnotu (neboli ordinální hodnotu), tuto hodnotu zvýšíme o posun a převedeme zpět na znak. Tento znak nakonec připojíme k výsledné zprávě:

// inicializace proměnných
let s = "cernediryjsoutamkdebuhdelilnulou"
print("Původní zpráva: \(s)")
var zprava : String = ""
var posun : UInt32 = 1

// cyklus projíždějící jednotlivé znaky
for c in s {
    let optionalASCIIhodnota = c.unicodeScalars.filter{$0.isASCII}.first?.value
    if let ASCIIhodnota = optionalASCIIhodnota {
        let novyZnak = Character(UnicodeScalar(ASCIIhodnota + posun)!)
        zprava += [novyZnak]
    }
}

// výpis
print("Zašifrovaná zpráva: \(zprava)")

Výsledek:

Původní zpráva: cernediryjsoutamkdebuhdelilnulou
Zašifrovaná zpráva: dfsofejszktpvubnlefcviefmjmovmpv

Můžete vidět, že jsme jednou Optional rozbalili "silou", ale víme, co děláme :-) Proč jsou okolo novyZnak hranaté závorky, když ho přidáváme do naší zprávy? Swift nám totiž dovolí pomocí += operátoru přidat do Stringu buď další String nebo pole znaků, ne však samostatný znak. Proto jsme vytvořili dočasné pole o jednom znaku.

Program si vyzkoušíme. Výsledek vypadá docela dobře. Zkusme si však zadat vyšší posun nebo napsat slovo "zebra". Vidíme, že znaky mohou po 'z' přetéct do ASCII hodnot dalších znaků, v textu tedy již nemáme jen písmena, ale další ošklivé znaky. Uzavřeme znaky do kruhu tak, aby posun plynule po 'z' přešel opět k 'a' a dále. Postačí nám k tomu jednoduchá podmínka, která od nové ASCII hodnoty odečte celou abecedu tak, abychom začínali opět na 'a'.

Nad cyklus vložíme novou proměnnou s ASCII hodnotou znaku 'z':

let zASCIIhodnota = "z".unicodeScalars.filter{$0.isASCII}.first!.value

A vnitřek cyklu upravíme do následující podoby:

let optionalASCIIhodnota = c.unicodeScalars.filter{$0.isASCII}.first?.value
if let ASCIIhodnota = optionalASCIIhodnota {
    var posunutyZnak = ASCIIhodnota + posun

    if posunutyZnak > zASCIIhodnota {
        posunutyZnak -= 26
    }
    let novyZnak = Character(UnicodeScalar(posunutyZnak)!)
    zprava += [novyZnak]
}

Pokud posunutyZnak přesáhne ASCII hodnotu 'z', snížíme ho o 26 znaků (tolik znaků má anglická abeceda). Operátor -= vykoná to samé, jako bychom napsali posunutyZnak = posunutyZnak - 26. Je to jednoduché a náš program je nyní funkční. Všimněme si, že nikde nepoužíváme přímé kódy znaků, v podmínce je námi dříve získaná hodnota 'z' znaku (stačí jednou, nemusíme ji získávat v každé iteraci cyklu), i když bychom tam mohli napsat rovnou 122. Je to z důvodu, aby byl náš program plně odstíněn od explicitních ASCII hodnot a bylo lépe viditelné, jak funguje. Cvičně si zkuste udělat dešifrování.

V příští lekci, Textové řetězce ve Swift do třetice - Split a joined, si ukážeme, že String umí přeci jen ještě něco navíc. Prozradím, že budeme dekódovat Morzeovu abecedu.


 

Stáhnout

Staženo 2x (72.41 kB)
Aplikace je včetně zdrojových kódů v jazyce Swift

 

 

Článek pro vás napsal Filip Němeček
Avatar
Jak se ti líbí článek?
Ještě nikdo nehodnotil, buď první!
Autor se příležitostně věnuje vývoji iOS aplikací či těch webových za pomocí frameworku Django. Aktuální projekt: hrejzdarma.cz Twitter: @nemecek_f (neprogramátorské tweety)
Miniatura
Předchozí článek
Pole ve Swift
Miniatura
Všechny články v sekci
Základní konstrukce jazyka Swift
Miniatura
Následující článek
Cvičení k 8. lekci Swift
Aktivity (9)

 

 

Komentáře

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.

Zatím nikdo nevložil komentář - buď první!