W3 Consorcium v roce 2001 zakládá skupinu Semantic Web Activity, která mimo jiné definuje sémantický web (semantic web) tak, že:
K tomuto účelu bylo nezbytné popisovat zdroje na webu, např.:
Tyto uvedené představy jso už dnes historie, přesto si tato myšlenka nalezla své místo v jiných oblastech:
Zkratka RDF odkazuje na Resource Description Framework, což můžeme přeložit jako rámec popisu zdrojů. Jeho autorem je W3 Consorcium a byl převážně navržen pro popis zdrojů na webu. RDF není jen rámec, ale také datový model či slovník. RDF je navržen maximálně obecně, aby mohl být čten a pochopen strojem (počítačem, ale zobrazení informace témto jazykem nebylo pochopitelné pro lidi.
Zápis RDF využívá pro zápis:
RDF má tyto silné stránky:
RDF je založeno na atomickém prvku označovaném trojice (triple). Trojice popisuje vlastnost zdroje. Trojice se skládá ze tří částí:
Trojice tvoří vždy jedno platné tvrzení. Máme-li více tvrzení zapisujeme je jako odpovídající počet trojic. Spojením trojic nám vzniká popis reálného světa v podobě orientovaného grafu:
Vše jsou trojice (triples) resp. čtveřice (quads). Datové úložiště označujeme jako:
Příklad tvrzení: Rozvrhovou akci v pondělí 9.20 vede Petr. lze zapsat do trojice:
Vidíme, že tam jsou i další tvrzení, které se vztahují k rozvrhové akci a blíže ji specifikují. Jak budou vypadat další tvrzení?
Další tvrzení přepsaná RDF trojicemi:
<zdroj> <vlastnost> <objekt> . ======================================================= <Rozvrhová akce v pondělí 9.20> <vede> <Petr> . <Rozvrhová akce v pondělí 9.20> <den> <pondělí> . <Rozvrhová akce v pondělí 9.20> <zacina> <9.20> . <9.20> <hodin> <9> . <9.20> <minut> <20> . ...
Výše uvedený zápis (bez prvních dvou řádek) se označuje N-TRIPLE notace a každá trojice je vždy na samostatném řádku ukončená znakem tečky. Zdroje jsou uzavřeny ve špičatých závorkách a text je v uvozovkách.
Výše uvedený zápis tvrzení byl ilustrační a v této podobě by neměl odpovídající přínos/význam. Zdroje musí být jednoznačně identifikovatelné buď jako URI (Uniform Resource Identifier) nebo jako IRI (Internationalized Resource Identifier). URI nebo IRI má tento obecný tvar:
scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]
IRI umožňuje oproti URI v řetězci použít Unicode znaky splňující pravidla dle RFC 3987. URI nebo IRI nabízí známé specializace:
scheme:path
Z důvodu možnosti propojení dat (v budoucnosti) na webu je doporučeno použití schéma/protokolu HTTP(S).
V našem příkladě použijeme např. jmenný prostor http://zcu.cz/rdf/
a zdroje i vlastnosti jím identifikujeme:
<zdroj> <vlastnost> <objekt> . ========================================================================= <http://zcu.cz/rdf/1> <http://zcu.cz/rdf/vede> "Petr" . <http://zcu.cz/rdf/1> <http://zcu.cz/rdf/den> "pondělí" . <http://zcu.cz/rdf/1> <http://zcu.cz/rdf/zacina> <http://zcu.cz/rdf/2> . <http://zcu.cz/rdf/2> <http://zcu.cz/rdf/hodin> "9" . <http://zcu.cz/rdf/2> <http://zcu.cz/rdf/minut> "20" . ...
Pro hodnoty lze definovat datový typ nebo jazyk. V notacích N-TRIPLE a Turtle:
"2"^^<http://www.w3.org/2001/XMLSchema#integer> "2012-04-18"^^<http://www.w3.org/2001/XMLSchema#date> "2012-09-19T23:20:00+0200"^^<http://www.w3.org/2001/XMLSchema#dateTime>
"address"@en "adresa"@cs
Ilustrační příklad ukazuje popis několika tvrzení, ale rozumíme mu (pravděpodobně) pouze my. Pro zajištění shody a pochopení ostatními lidmi i stroji je popis (zatím) nevhodný, resp. nedostatečný. Vlastnosti, které jsme si zavedli jsou naše a lokální. Kdokoliv jiný se na ně podívá, nemusí pochopit jejich správný význam nebo je správně interpretovat:
zcu:den
– den v měsíci? den v roce?zcu:hodin
– aktuální čas? čas události? 12/24 hod. – dopoledne nebo odpoledne?zcu:minut
– počet minut od/k čeho/čemu?zcu:vede
– vede projekt?zcu:zacina
– co/kde/proč začíná?Takto uvedené vlastnosti jsou „vytrženy z kontextu“, chybí nám kontext nebo spíše význam – sémantika vlastností. Zjednodušeně řečeno, proto vznikají slovníky nebo ontologie, které definují a současně dokumentují potřebný kontext, vztahy, doménu, obor hodnot, ...
Vytvoříme-li si odpovídající slovník (resp. ontologii) a zveřejníme jej – data a informace může využít už i někdo další a mohou se sdílet na webu. Problém?! Když si úplně každý vytvoří svůj vlastní slovník, pak bude výsledek nepoužitelný, protože data budou sdílená, ale možnosti využití a interpretace ostatními budou minimální.
Řešením tohoto problému jsou existující základní RDF slovníky a ontologie, které přináší rámec jak nad RDF popisovat zdroje jednotným způsobem:
rdf
,
rdf:type
– určení, že je popisovaný zdroj nějakého typu/třídy.rdfs
,
owl:sameAs
– pro popis, že nějaké dvě „věci“ jsou totéž
Závěr: RDF definuje jak psát popis a OWL definuje co psát.
Pro účely přehlednějšího a stručnějšího (úspornějšího) zápisu se používají prefixy i další notace. Pro stejný příklad popisu pondělní rozvrhové akce použijeme další zápisy.
Přímo výše uvedený příklad zapíšeme v notaci Turtle:
<http://zcu.cz/rdf/1> http://zcu.cz/rdf/:vede "Petr" ; http://zcu.cz/rdf/:den "pondělí" ; http://zcu.cz/rdf/:zacina http://zcu.cz/rdf/2 . <http://zcu.cz/rdf/2> http://zcu.cz/rdf/:hodin "9" ; http://zcu.cz/rdf/:minut "20" .
Tvrzení patřící stejnému subjektu oddělujeme středníkem, více hodnot u stejné vlastnosti oddělujeme čárkou a za posledním tvrzením je tečka.
Ke zvolenému jmennému prostoru nadefinujeme prefix zcu
a data ve výše uvedené notaci Turtle budou:
@prefix zcu: <http://zcu.cz/rdf/> . <http://zcu.cz/rdf/1> zcu:vede "Petr" ; zcu:den "pondělí" ; zcu:zacina zcu:2 . <http://zcu.cz/rdf/2> zcu:hodin "9" ; zcu:minut "20" .
Ve formátu Turtle může být na začátku deklarace prefixů začínající znakem zavináče a slovem prefix, za nímž následuje vlastní prefix a za dvojtečkou úplné URI/IRI jmenného prostoru za kterým je tečka.
Stejný zápis v provedení notace RDF/XML:
<rdf:RDF xmlns:zcu="http://zcu.cz/rdf/"> <rdf:Description rdf:about="http://zcu.cz/rdf/1"> <zcu:vede>Petr</zcu:vede> <zcu:den>pondělí</zcu:den> <zcu:zacina> <rdf:Description rdf:about="http://zcu.cz/rdf/2"> <zcu:hodin>9</zcu:hodin> <zcu:minut>20</zcu:minut> </rdf:Description> </zcu:zacina> </rdf:Description> </rdf:RDF>
Používá se jmenný prostor rdf
, který je rezervovaný. Prefixy jsou definovány v kořenovém elementu rdf:RDF
.
Příklad v základní RDF/XML notaci včetně použitého RDF jmenného prostoru:
<?xml version="1.0"?> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:zcu="http://zcu.cz/rdf/"> <rdf:Description rdf:about="http://zcu.cz/rdf/1"> <zcu:vede>Petr</zcu:vede> <zcu:den>pondělí</zcu:den> <zcu:zacina rdf:resource="http://zcu.cz/rdf/2" /> </rdf:Description> <rdf:Description rdf:about="http://zcu.cz/rdf/2"> <zcu:hodin>9</zcu:hodin> <zcu:minut>20</zcu:minut> </rdf:Description> </rdf:RDF>
Uvedené serializace RDF dat jsou vzájemně převoditelné – bezztrátově.
Nová tvrzení/trojice stačí k původním jen přidat.
<zdroj> <vlastnost> <objekt> . =========================================================== <Petr> <je> <Jméno> . <Petr> <je> <Osoba> . <Petr> <je> <Muž> . <pondělí> <je> <Den v týdnu> . ...
Tyto trojice mohou pocházet např. z jiného zdroje, slovníku nebo ontologie.
Lze se dotazovat u více zdrojů současně, a to prostřednictvím distribuované prostředí webu – SPARQL Endpoint.
Pokud zdroj používá jiné identifikátory, slovníky nebo ontologie, lze je propojit (merge) přidáním tvrzení s vlastnostmi
owl:sameAs
, owl:differentFrom
a owl:AllDifferent
.
Kocept RDF a OWL je velmi obecný, avšak lze najít podobnost s OOP přístupem.
rdf:type
) nějaké třídy nebo tříd.
Zásadní rozdíl je ve způsobu přístupu k datům.
Schéma databáze = datový model je pevně definován
Tabulka = entita
Sloupec tabulky = atribut entity
rdfs:domain
u RDF vlastnosti navíc znamená:
rdfs:domain
specifikuje.Datový typ atributu = povolený datový typ hodnot
rdfs:range
).
rdfs:range
u RDF vlastnosti navíc znamená, že všechny hodnoty, kde byl použit predikát s rdfs:range
,
bude datového typu, který definoval.
Záznam = řádek tabulky
RDF úložiště
Programovací jazyky
Seznam dalších nástrojů souvisejících s RDF: https://www.w3.org/RDF/
SPARQL 1.0 (2008)
SPARQL 1.1 (W3C Recommendation 21 March 2013)
Jazyk umožňuje:
SELECT
, DESCRIBE
, CONSTRUCT
)ASK
, DESCRIBE
),CONSTRUCT
).Výběrové typy dotazu:
ASK
CONSTRUCT
DESCRIBE
SELECT
Formát výsledku výběrových dotazů – závisí na typu dotazu:
ASK
– vrací boolean (hodnotu true/false),CONSTRUCT
a DESCRIBE
– vrací RDF graf,SELECT
– vrací tabulku – CSV/TSV, HTML, TXT, JSON, XML, … záleží na RDF úložišti/databázi, může poskytovat i např. XLS soubory.Aktualizace grafu:
INSERT DATA
– vložení trojic (triple)DELETE DATA
– odstranění trojic z grafu (quad)LOAD
– čte data ze zadaného IRI
LOAD [SILENT] <iri_ref> INTO GRAPH <graph_name>
CLEAR
– odstraní všechny trojice z daného/daných grafů (IRI, DEFAULT, NAMED, ALL).
Správa grafu:
CREATE
– vytvoření nového grafu (může-li existovat prázdný graf),DROP
– odstranění grafu i jeho obsahu,COPY
– kopíruje obsah grafu do jiného,MOVE
– přesune obsah grafu do jiného,ADD
– duplikuje obsah jednoho grafu do jiného.# deklarace prefixů PREFIX foo: <http://example.com/resources/> ... # definice datasetu/grafu/zdroje FROM ... # výsledek dotazu SELECT ... # podmínka — vzor dotazu (query pattern) WHERE { ... } # modifikátory dotazu ORDER BY ...
V části SELECT
:
WHERE
.
Proměnná začíná prefixem otazníku (dolaru): ?s ?p ?o.
V části WHERE
:
AND
mezi trojicemi (platí všechny současně),OPTIONAL
,WHERE
jsou spojeny s hodnotou odpovídající dané části trojice (zdroj, literál) a mohou být použity v části
SELECT
(CONSTRUCT
, DESCRIBE
)BIND
, které provede přiřazení hodnoty proměnné (explicitně zadané v dotazu např. v SELECT
):
BIND (?hodnota * (1 - ?sleva) AS ?cena)
Filtrování hodnot:
FILTER
pracuje s podmínkami typu boolean
.!
, &&
, ||
+
, -
, *
, /
=
, !=
, <
, >
, IN
, NOT IN
, ...isURI
, isBlank
, isLiteral
, isNumeric
, bound
, !bound
str, lang, datatype
sameTerm, langMatches, regex, REPLACE, ...
IF, COALESCE, EXISTS, NOT EXISTS
URI, BNODE, STRDT, STRLANG, UUID, STRUUID
STRLEN, SUBSTR, UCASE, LCASE, STRSTARTS, STRENDS, CONTAINS, STRBEFORE, STRAFTER, CONCAT, ENCODE_FOR_URI
abs, round, ceil, floor, rand
now, year, month, day, hours, minutes, seconds, timezone, tz
MD5, SHA1, SHA256, SHA384, SHA512
FILTER (?hodnota > 1000 && langMatches(lang(?nazev), "EN")) .
lang
získá jazyk specifikovaný u textového řetězcelangMatches
porovná s uvedeným jazykem nebo jejich výčtem# https://www.w3.org/TR/2013/REC-sparql11-query-20130321/#construct @prefix foaf: <http://xmlns.com/foaf/0.1/> . # Friend-of-a-Friend <abc> foaf:name "Alice" . <abc> foaf:mbox <mailto:alice@example.org> .
Typy dotazů jsou:
SELECT všechny nebo podmnožinu proměnných z podmínkové části
# příklad 1 - všechny trojice SELECT ?s ?p ?o WHERE { ?s ?p ?o . } # příklad 1 - všechny vlastnosti a jejich hodnoty SELECT ?p ?o WHERE { <abc> ?p ?o . }
CONSTRUCT vrací RDF graf sestavený dle šablony trojic
# https://www.w3.org/TR/2013/REC-sparql11-query-20130321/#construct PREFIX foaf: <http://xmlns.com/foaf/0.1/> PREFIX vcard: <http://www.w3.org/2001/vcard-rdf/3.0#> # příklad 1 - jen filtrování CONSTRUCT WHERE { ?x foaf:name ?name . } # příklad 2 - transformace schéma CONSTRUCT { ?x vcard:FN ?name } WHERE { ?x foaf:name ?name . }
ASK odpověď je datového typu boolean
;
# https://www.w3.org/TR/2013/REC-sparql11-query-20130321/#ask # příklad 1 - odpověď bude ’true’ nebo ’false’? PREFIX foaf: <http://xmlns.com/foaf/0.1/> ASK { ?x foaf:name "Alice" } # příklad 2 - odpověď bude ’true’ nebo ’false’? PREFIX vcard: <http://www.w3.org/2001/vcard-rdf/3.0#> ASK { ?x vcard:FN "Alice" }
true
, pokud vzor dotazu vyhovuje nějaké odpovědi,false
.
DESCRIBE vrací RDF graf popisující zdroj
# https://www.w3.org/TR/2013/REC-sparql11-query-20130321/#describe # příklad 1 - známe URI DESCRIBE <abc> # příklad 2 - neznáme konkrétní URI PREFIX foaf: <http://xmlns.com/foaf/0.1/> DESCRIBE ?x WHERE { ?x foaf:name "Alice" . }
Data vychází z datového formátu DASTA (DAtový STAndard) spravovaný Ministerstvem zdravotnictví ČR
V systému MRE používané ontologie jsou dokumentovány: https://mre.zcu.cz/ontology/ontologies.html V našem případě jsou:
dasta.owl
– http://mre.zcu.cz/ontology/dasta.owldscl.owl
http://mre.kiv.zcu.cz/ontology/dscl.owldicom.owl
http://mre.kiv.zcu.cz/ontology/dicom.owldcm:Patient
), k němuž se může vztahovat několik DICOM studií (dcm:Study
).dcm:Series
).dcm:CT_Image
) obrazového vyšetření,
např. počítačová tomografie (CT), magnetická rezonance (MR).
sits.owl
http://mre.kiv.zcu.cz/ontology/sits.owlnihss.owl
http://mre.kiv.zcu.cz/ontology/nihss.owlSchéma vybraných tříd a vlastností v systému MRE
patient1-medical
Anon_666,
patient6-medical
patient1-medical
,
patient7-medical
patient6-medical
,
patient7-imaging
patient7-medical
,Použité datové formáty (koncovka):
nt
N-TRIPLE
ttl
TURTLE
xml
RDF/XML
PREFIX id: <http://mre.zcu.cz/id/> PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> PREFIX dscl: <http://mre.zcu.cz/ontology/dscl.owl#> PREFIX dcm: <http://mre.zcu.cz/ontology/dcm.owl#> PREFIX sits: <http://mre.zcu.cz/ontology/sits.owl#> PREFIX nihss: <http://mre.zcu.cz/ontology/nihss.owl#> PREFIX mre: <http://mre.zcu.cz/ontology/mre.owl#> PREFIX acl: <http://www.w3.org/ns/auth/acl#> PREFIX dc: <http://purl.org/dc/elements/1.1/> PREFIX nfo: <http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#> PREFIX owl: <http://www.w3.org/2002/07/owl#> PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
Kompletní výčet a detaily viz https://mre.zcu.cz/ontology/ontologies.html.
Stáhněte si program z adresy https://jena.apache.org/download/, rozbalte jej a z rozbaleného adresáře spusťte
./fuseki-server
fuseki-server.bat
a následně v konzoli uvidíme start serveru. Server běží ve výchozím nastavení na portu 3030. Ve webovém prohlížeči zadáme adresu: http://localhost:3030 a vše je připraveno k nahrání dat a zkoušení příkladů.
id:cd3f0c85b158c08a2b113464991810cf2cdfc387 a ds:Patient , ds:Male ; ds:address id:3840aecb9edac9f7d7c9172f2f4be82b08ab3ddf ; ds:clinicalEvent id:be8d011f882326495f8d06c58f22db51d95cc7bf ; ds:datetimeBirth "1938-08-13"^^xsd:date ; ds:lastName "Anon_666" ; ds:patientID "666" ; ds:sex "M" ; ds:sexNCLPTPS dscl:NCLPTPS_M ; ds:sexPOHLAV dscl:POHLAV_1 ; dc:title "Anon_666 (M) * 1938-08-13" . id:3840aecb9edac9f7d7c9172f2f4be82b08ab3ddf a ds:PermanentAddress ; ds:addressCity "Město 1" ; ds:addressZIP "00001" ; dc:title "Město 1 (00001)" . id:be8d011f882326495f8d06c58f22db51d95cc7bf a ds:MedicalExamination ; ds:datetimeEvent "2012-09-19T23:20:00+0200"^^xsd:dateTime ; ds:diagnosis id:2c545600eb7a2722809d64c2753de714a5154b6b , id:2285692c932c88f8673a162ef7b5c997993da41c ; ds:dsclExaminationContent dscl:LZSOZ_NL ; ds:dsclExaminationOrigin dscl:LZTOZV_J ; ds:dsclExaminationRequest dscl:LZTZOV_D ; ds:dsclExaminationState dscl:LZSZZ_K ; ds:imagingStudyNumber "00000078" ; ds:originator id:44040e7024d5a4cc177bf0ed29683c2185dbd05b ; ds:reportText "CT mozku:..." ; ds:reportTitle "032/002 - CT mozku: s k.l. iv." ; dc:title "2012-09-19 23:20: 032/002 - CT mozku: s k.l. iv." . id:2c545600eb7a2722809d64c2753de714a5154b6b a ds:ActualDiagnosis ; ds:datetimeEvent "2012-04-18"^^xsd:date ; ds:diagCode dscl:MKN10_5_J180 ; ds:diagDetail "Bronchopneumonie NS" ; ds:diagOrder 1 ; ds:patient id:cd3f0c85b158c08a2b113464991810cf2cdfc387 ; dc:title "2012-04-18 (1) J18.0 - Bronchopneumonie NS" . id:2285692c932c88f8673a162ef7b5c997993da41c a ds:ActualDiagnosis ; ds:datetimeEvent "2012-04-18"^^xsd:date ; ds:diagCode dscl:MKN10_5_I639 ; ds:diagDetail "Mozkový infarkt" ; ds:diagOrder 2 ; ds:patient id:cd3f0c85b158c08a2b113464991810cf2cdfc387 ; dc:title "2012-04-18 (2) I63.9 - Mozkový infarkt" . id:44040e7024d5a4cc177bf0ed29683c2185dbd05b a ds:OriginatorDepartment ; ds:departmentName "Nemocnice na ..." ; dc:title "Nemocnice na ..." .
Níže uvedené dotazy budou aplikovány nad datovou sadou patient1-medical
.
Chceme-li získat všechny trojice, musíme v části WHERE
mít vzor trojice se třemi proměnnými.
Ty vyhovují všem trojicím. V části SELECT
je uvedeme.
# základní forma dotazu SELECT # 01a SELECT ?subject ?predicate ?object WHERE { ?subject ?predicate ?object . }
# na názvech proměnných nezáleží, jsou oddělovány jen mezerou # 01b SELECT ?s ?p ?o WHERE { ?s ?p ?o . }
Dotaz vrátí všechny trojice, tj. tři sloupce s hodnotami dle celkového počtu trojic v datasetu.
Omezení počtu lze provést použitím klíčového slova LIMIT
.
# 02 SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10
Dotaz vrátí 10 trojic ve třech sloupcích odpovídajících trojicím. Vedle LIMIT
, lze použít také OFFSET
.
# získání všech vlastností a jejich hodnot SELECTem # 03a SELECT ?p ?o WHERE { <http://mre.zcu.cz/id/cd3f0c85b158c08a2b113464991810cf2cdfc387> ?p ?o . }
# s využitím prefixu v zápisu # 03b PREFIX id: <http://mre.zcu.cz/id/> SELECT ?p ?o WHERE { id:cd3f0c85b158c08a2b113464991810cf2cdfc387 ?p ?o . }
Výsledkem dotazu je 11 dvojic (pár vlastnost a její hodnota) pro výše uvedené URI.
V podobě grafu lze prakticky totéž získat prostřednictvím DESCRIBE
:
# získání všech vlastností a jejich hodnot příkazem DESCRIBE # 03c PREFIX id: <http://mre.zcu.cz/id/> DESCRIBE id:cd3f0c85b158c08a2b113464991810cf2cdfc387
Vrátí RDF graf s 11 trojicemi, které mají jako subjekt uvedené URI.
ds:patientID
)# získání hodnot(y) vybrané vlastnosti # 04a PREFIX id: <http://mre.zcu.cz/id/> PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?o WHERE { id:cd3f0c85b158c08a2b113464991810cf2cdfc387 ds:patientID ?o . }
# přejmenování proměnné na ?rc # 04b PREFIX id: <http://mre.zcu.cz/id/> PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?rc WHERE { id:cd3f0c85b158c08a2b113464991810cf2cdfc387 ds:patientID ?rc . }
Dotaz vrátí jeden řádek s jednou hodnotou (jeden sloupec). V části WHERE
může být i celé URI bez zkrácení prefixem.
Hodnota vlastnosti ds:patientID
bude svázána (bind) s proměnnou ?rc
a dostupná jako výsledek v části SELECT
.
ds:patientID
).# získání subjectu (pacienta) na základě znalosti jeho rč # 05 PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?patient WHERE { ?patient ds:patientID "666" . }
Výsledkem dotazu je jedna hodnota URI.
Níže uvedené dotazy budou aplikovány nad datovou sadou patient6-medical
.
ds:patientID
.# získání identifikace pacienta a jeho rč # 06 PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?pacient ?rc WHERE { ?pacient ds:patientID ?rc . }
Výsledkem dotazu je šest hodnot URI a rodných čísel.
WHERE
.SELECT
.# každá vlastnost tvoří jednu RDF trojici ve WHERE # 07a PREFIX id: <http://mre.zcu.cz/id/> PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?rc ?jmeno WHERE { id:cd3f0c85b158c08a2b113464991810cf2cdfc387 ds:patientID ?rc . id:cd3f0c85b158c08a2b113464991810cf2cdfc387 ds:lastName ?jmeno . }
# vlastnosti ke stejnému zdroji jsou oddělené středníkem # 07b PREFIX id: <http://mre.zcu.cz/id/> PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?rc ?jmeno WHERE { id:cd3f0c85b158c08a2b113464991810cf2cdfc387 ds:patientID ?rc ; ds:lastName ?jmeno . }
V obou příkladech dostaneme stejný výsledek.
# v dalších dotazech budeme využívat zkráceného zápisu # 08a PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?pacient ?rc ?jmeno WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno . }
a použijeme hvězdičku (*
), chceme-li zobrazit všechny proměnné:
# použitá * pro vypsání všech proměnných # 08b PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno . }
ds:datetimeBirth
ds:datetimeDeath
# nejdříve dohledáme datum narození # 09a PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . }
# a potom datum úmrtí # 09b PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni ; ds:datetimeDeath ?umrti . }
Jaký je výsledek dotazu? Proč? Správné řešení:
# datum úmrtí je nepovinný - přidáme konstrukci OPTIONAL # 09c PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . OPTIONAL { ?pacient ds:datetimeDeath ?umrti . } }
Výsledek (počet řádek) dle zvoleného příkladu:
patient1-medical
,patient6-medical
,patient7-medical
.# pozor - pokud je proměnná prvně zavedena v konstrukci OPTIONAL # a až následně použita ve zbytku vzoru, výsledek se změní. # 09d PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT * WHERE { OPTIONAL { ?pacient ds:datetimeDeath ?umrti . } ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . }
Význam klíčového slova OPTIONAL
– ovlivní počet výsledků.
Filtrování hodnoty rodného čísla:
# filtrační podmínka - volání funkce FILTER # 10a PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . OPTIONAL { ?pacient ds:datetimeDeath ?umrti . } FILTER ( ?rc > 200 ) }
Všechny hodnoty rodného čísla porovná s filtrem. Funguje?
Hodnota ds:patientID
je string, nikoliv číslo, porovnáváme text s číslem.
# převedem hodnotu rc na číslo # 10b PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . OPTIONAL { ?pacient ds:datetimeDeath ?umrti . } FILTER ( ?rc > "200" ) }
Asi to funguje. Opravdu?
# změního hodnotu rc na 20 # 10c PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . OPTIONAL { ?pacient ds:datetimeDeath ?umrti . } FILTER ( ?rc > "20" ) }
Proč to přestalo fungovat? Odpovědí je lexikografické řazení. Řešení: musíme na číslo přetypovat hodnotu rc.
# nutné přetypování rc na číslo # 10d PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . OPTIONAL { ?pacient ds:datetimeDeath ?umrti . } FILTER ( xsd:int(?rc) > 200 ) }
Obdoba předchozích příkladů, jen je navíc požadováno řazení:
# řazení pacientů podle jejich rc - ORDER BY # 11a PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . OPTIONAL { ?pacient ds:datetimeDeath ?umrti . } } ORDER BY DESC (?rc)
# stejný problém jako u funkce FILTER # 11b PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . OPTIONAL { ?pacient ds:datetimeDeath ?umrti . } } ORDER BY DESC ( xsd:int(?rc) )
Vhodné použít data patient6-medical
nebo patient7-medical
. Výsledek dotazu (počet řádek) dle zvoleného příkladu:
patient1-medical
,patient6-medical
,patient7-medical
Porovnejte různé varianty části ORDER BY
– rozdíl mezi znakovým a číselným řazením:
ORDER BY ?rc ORDER BY ASC ( ?rc ) ORDER BY DESC ( ?rc ) ORDER BY DESC ( xsd:int(?rc) ) ORDER BY ?jmeno DESC ( ?rc )
Výsledek dotazu lze ovlivnit modifikátorem ORDER BY
pro řazení, ASC
, DESC
určují směr řazení.
V případě chybějícího datového typu se jedná obecně o string:
xsd:int(?rc).
Pacienti narozeni od 1. ledna 1930 do současnosti.
# pacienti narozeni 1.1.1930 a dříve # 12a PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . OPTIONAL { ?pacient ds:datetimeDeath ?umrti . } FILTER ( ?narozeni >= "1930-01-01" ) }
Ve filtru uvedeme i datový typ hodnoty:
# funguje s uvedením datového typu hodnoty # 12b PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . OPTIONAL { ?pacient ds:datetimeDeath ?umrti . } FILTER ( ?narozeni >= "1930-01-01"^^xsd:date ) }
Pacienti narozeni ve 30. letech 20. století.
# více FILTERů je spojeno implicitně AND # 13a PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . OPTIONAL { ?pacient ds:datetimeDeath ?umrti . } FILTER ( ?narozeni >= "1930-01-01"^^xsd:date ) FILTER ( ?narozeni <= "1939-12-31"^^xsd:date ) }
# složitější logická podmínka ve FILETRu # 13b PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . OPTIONAL { ?pacient ds:datetimeDeath ?umrti . } FILTER ( ?narozeni >= "1930-01-01"^^xsd:date && ?narozeni <= "1939-12-31"^^xsd:date ) }
Konstrukcí EXISTS
můžeme testovat, zda daný grafový vzor lze najít v datech. Chceme-li výsledek negovat, aplikujeme operátor NOT
.
# pacienti, kteří nemají uveden datum úmrtí - NOT EXISTS # 14a PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . FILTER ( NOT EXISTS { ?pacient ds:datetimeDeath ?umrti . } ) }
Srovnatelného výsledku dosáhneme voláním logické funkce BOUND
, která testuje, zda proměnná má hodnotu nebo ne.
Negaci funkce dosáhneme operátorem !
# pacienti, kteří mají uveden datum úmrtí - BOUND # 14b PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . OPTIONAL { ?pacient ds:datetimeDeath ?umrti . } FILTER (BOUND (?umrti)) }
# pacienti, kteří nemají uveden datum úmrtí - negace BOUND # 14c PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni . OPTIONAL { ?pacient ds:datetimeDeath ?umrti . } FILTER (! BOUND (?umrti)) }
Z předchozích dotazů víme, že 2 pacienti jsou již po smrti. Proto jim můžeme spočítat věk, kterého se dožili.
Každý výraz v klauzuli SELECT
musí být doplněn o alias a celý zápis odvozeného sloupce musí být uzavřen v závorce.
# u pacienta chceme spočítat, jakého věku se dožil # potřebujeme odvozený sloupec # 15a PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?rc ?jmeno ?narozeni ?umrti ((?umrti - ?narozeni) AS ?vek) WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni ; ds:datetimeDeath ?umrti . }
Vypočtený výsledek je ve dnech, chceme spočítat léta.
# spočítáme dožitý věk v letech # 15b PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?rc ?jmeno ?narozeni ?umrti (FLOOR (DAY (?umrti - ?narozeni) / 365) AS ?vek) WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni ; ds:datetimeDeath ?umrti . }
Pokud bychom chtěli filtrovat jen ty pacienty, kteří se dožili 80 let a více, tak máme smůlu.
Potom je potřeba využít funkce BIND
.
# u pacienta chceme spočítat, jakého věku se dožil # potřebujeme odvozený sloupec - BIND # 15c PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni ; ds:datetimeDeath ?umrti . BIND (FLOOR (DAY (?umrti - ?narozeni) / 365) AS ?vek) }
Filtrace 80 letých pacientů je snadná funkcí FILTER
.
# nad takto odvozenou hodotou můžeme stanovit filtrační podmínku # 15d PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT * WHERE { ?pacient ds:patientID ?rc ; ds:lastName ?jmeno ; ds:datetimeBirth ?narozeni ; ds:datetimeDeath ?umrti . BIND (FLOOR (DAY (?umrti - ?narozeni) / 365) AS ?vek) FILTER ( ?vek > 80 ) }
ds:Patient
.rdf:type
.# získání URI patřící jen pacientům # 16a PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> SELECT ?pacient WHERE { ?pacient rdf:type ds:Patient . }
Vlastnost rdf:type
lze zkracovat na prosté písmeno „a“.
# existuje zkratka, nevyžadující prefix rdf - a # 16 b PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?pacient WHERE { ?pacient a ds:Patient . }
ds:lastName
.ds:Patient
.# ověřme, do jakých tříd (typů) nám spadají všechny uzly (URI), které mají jméno # 17 PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?uzel ?trida WHERE { ?uzel a ?trida ; ds:lastName ?jmeno . }
Zjistili jsme, že každý pacient je navíc instancí třídy ds:Male
nebo ds:Female
, což odpovídá pohlaví pacienta.
Zjistěme, který pacient ma jaké pohlaví prostřednictvím dané třídy.
# získat identifikátory uzlů, které mají jméno a spadají do třídy ds:Male nebo ds:Female # 18a PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?uzel ?jmeno ?trida WHERE { ?uzel a ?trida ; ds:lastName ?jmeno . FILTER (?trida = ds:Female || ?trida = ds:Male) }
Konstrukcí VALUES
lze naplnit proměnnou nebo n-tici proměnných s přesně definovanou sadou hodnot.
Pokud je takto zavedená proměnná dále použita ve WHERE
klauzuli, nemůže nabývat jiných hodnot.
# to samé pomocí VALUES # 18b PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?uzel ?jmeno ?pohlavi WHERE { VALUES ?pohlavi { ds:Female ds:Male } ?uzel a ?pohlavi ; ds:lastName ?jmeno . }
# místo názvu třídy vypíšeme statický řetězec # 18c PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?uzel ?jmeno ?pohlavi WHERE { VALUES (?typ ?pohlavi) { (ds:Female "Žena") (ds:Male "Muž") } ?uzel a ?typ ; ds:lastName ?jmeno . }
Na základě znalosti, jaké pohlaví má který pacient, můžeme s využitím agregačních funkcí spočítat, kolik pacientů je žen a kolik je mužů.
# následně si uděláme malou statistiku pacientů podle jejich pohlaví - GROUP BY # 19 PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?pohlavi (COUNT (?uzel) AS ?pocet) WHERE { VALUES (?typ ?pohlavi) { (ds:Female "Žena") (ds:Male "Muž") } ?uzel a ?typ ; ds:lastName ?jmeno . } GROUP BY ?pohlavi
Použijte zejména data patient7-medical
a ověřte, že vaše dotazy vrací správné výsledky pro níže uvedené příklady.
dc:title
a současně i rdfs:label,
ds:Male
(20-2b).
Doporučená data: patient7-medical
.
# jméno a rč pacienta včetně data jeho lékařských vyšetření # 21a PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?rc ?jmeno ?datum WHERE { ?pacient a ds:Patient ; ds:patientID ?rc ; ds:lastName ?jmeno ; ds:clinicalEvent ?vysetreni . ?vysetreni ds:datetimeEvent ?datum . }
Dotaz vrací 1 013 výsledků. Je to správně? Není, opakují se nám stejné kombinace hodnot.
# ve výsledku se nám opakují hodnoty - použijeme DISTINCT # 21a PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT DISTINCT ?rc ?jmeno ?datum WHERE { ?pacient a ds:Patient ; ds:patientID ?rc ; ds:lastName ?jmeno ; ds:clinicalEvent ?vysetreni . ?vysetreni ds:datetimeEvent ?datum . }
Přidáním DISTINCT
za SELECT
omezíme počet duplicitních výsledků a dostaneme 47 výsledků. Je to už správně?
Nevíme, uděláme si kontrolu. Nejdříve zjistíme, kolik existuje instancí třídy ds:MedicalExamination
:
# 22a PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?uri WHERE { ?uri a ds:MedicalExamination . }
nebo prostřednictvím agregační funkce COUNT()
:
# 22b PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ( COUNT(?uri) as ?pocet ) WHERE { ?uri a ds:MedicalExamination . }
V obou případech je celkový počet 10 lékařských zpráv. Ve skutečnosti může být v ds:clinicalEvent
:
ds:MedicalExamination
),ds:LaboratoryReport
).Má-li být výstupem pouze datum a čas lékařských zpráv, musíme doplnit trojici, která specifikuje typ (třídu).
# problém je jinde, poskytuje to jak lékařská vyšetření, tak laboratorní vyšetření # 21b PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?rc ?jmeno ?datum WHERE { ?pacient a ds:Patient ; ds:patientID ?rc ; ds:lastName ?jmeno ; ds:clinicalEvent ?vysetreni . ?vysetreni ds:datetimeEvent ?datum ; a ds:MedicalExamination . }
Nyní dotaz vrací správně 10 výsledků (stejně to bude v tomto případě s DISTINCT
pro uvedená data).
Přidání trojice s určením typu třídy odstraní všechny ostatní instance ds:LaboratoryReport
, které nás zrovna nezajímají.
Diagnóza je vždy uvedena u lékařské zprávy pacienta:
# zjistíme, jaké diagnózy byly určeny během lékařských vyšetření # diagnózy jsou dostupné jen přes lékařská vyšetření # 23a PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?rc ?jmeno ?popis WHERE { ?pacient a ds:Patient ; ds:patientID ?rc ; ds:lastName ?jmeno ; ds:clinicalEvent ?vysetreni . ?vysetreni ds:diagnosis ?diagnoza . ?diagnoza ds:diagDetail ?popis . } ORDER BY ?jmeno ?popis
Ve výsledku je patrné, že se nám některé diagnózy u stejného pacieta opakují. To je dáno skutečností, že stejná diagnóza byla u pacienta znova detekována na dalším vyšetření. Zjistíme, kolikrát byla každá diagnóza detekována u každého pacienta:
# kolikrát byla která diagnóza určena během lékařského vyšetření # 23b PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?rc ?jmeno (COUNT (?diagnoza) AS ?pocet) ?popis WHERE { ?pacient a ds:Patient ; ds:patientID ?rc ; ds:lastName ?jmeno ; ds:clinicalEvent ?vysetreni . ?vysetreni ds:diagnosis ?diagnoza . ?diagnoza ds:diagDetail ?popis . } GROUP BY ?rc ?jmeno ?popis ORDER BY ?jmeno ?popis
Výsledek dotazu je 34 diagnóz pro data z patient7-medical
. V dotazu musíme projít od pacienta ?patient
(získáme jeho rodné číslo a jméno) na klinickou událost (?vysetreni
), z klinické události ?vysetreni
projdeme
k diagnóze ?diagnoza
a z diagnózy nás zajímá její popis.
Ke stejnému výsledku dojdeme také při využití Property Path ve SPARQL 1.1:
# získání popisu diagnózy pomocí Property Path # 23c PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT ?rc ?jmeno (COUNT (?diagnoza) AS ?pocet) ?popis WHERE { ?pacient a ds:Patient ; ds:patientID ?rc ; ds:lastName ?jmeno ; ds:clinicalEvent/ds:diagnosis ?diagnoza . ?diagnoza ds:diagDetail ?popis . } GROUP BY ?rc ?jmeno ?popis ORDER BY ?jmeno ?popis
ds:MedicalExamination
)?patient7-medical
instancí ds:LaboratoryReport
?ds:diagOrder
).FILTER
?
Dotazovací jazyk SPARQL nabízí kromě příkazu SELECT
ještě další dva příkazy pro dotazování: DESCRIBE
a ASK
.
Příkazy SELECT
a ASK
vyžadují konstrukci FROM
, u příkazu DESCRIBE
je tato konstrukce nepovinná.
Příkaz DESCRIBE
pro všechny proměnné mající tvar URI vrací všechny RDF troji těchto identifikátorů.
Chceme-li znát všechny vlastnosti a jejich hodnoty daného identifikároru, použijeme k tomu tento příkaz.
Naproti tomu příkaz ASK
poskytuje jen odpovědi True nebo False na otázku, zda dané trojice jsou v datové sadě dostupné či nikoliv.
V uvedených trojicích se mohou vyskytovat proměnné, pak probíhá dosazování různých hodnot za tyto proměnné.
Porovnejte výsledky níže uvedených dotazů, které mají shodnou konstrukci WHERE
:
# příkaz SELECT PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> SELECT * WHERE { ?pacient a ds:Patient . ?pacient ds:patientID "666" . ?pacient ds:clinicalEvent ?vysetreni . }
# příkaz DESCRIBE PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> DESCRIBE * WHERE { ?pacient a ds:Patient . ?pacient ds:patientID "666" . ?pacient ds:clinicalEvent ?vysetreni . }
# příkaz ASK PREFIX ds: <http://mre.zcu.cz/ontology/dasta.owl#> ASK WHERE { ?pacient a ds:Patient . ?pacient ds:patientID "666" . ?pacient ds:clinicalEvent ?vysetreni . }
DISTINCT
,FILTER
,VALUES
,GROUP BY
,HAVING
,
Pro následující úlohy použijte současně data patient7-medical
a patient7-imaging
.
Můžete je nahrát:
FROM
.Úlohy
ds:DiagCode
) a abecedně je seřaďte.
dcm:Study
)?
ds:patientID
a číslo studie dcm:Study_ID
.
dcm:Series
)?
dcm:CT_Image
)?
dcm:CT_Image
) se každá ze sérií skládá, a seřaďte je od největší k nejmenší?
Vypište číslo série (dcm:Series_Number
), popis (dcm:Series_Description)
a počet souborů v sérii.
dcm:CT_Image
) se každá ze sérií skládá, a jaký objem dat na disku zabírá?
Vypište číslo série (dcm:Series_Number
), popis (dcm:Series_Description
), počet souborů v sérii, celkovou velikost série v B i současně v MB.
Hodnotu velikosti v MB zaokrouhlete. Série seřaďte dle velikosti v Bytech.
ds:labNumberValue
) jsou mimo stanovené referenční meze pro daného pacienta:
ds:labScale4
je referenční mez dolní,ds:labScale5
je referenční mez horní,ds:labLocalName
), hodnotu, a obě referenční meze.
Data budou řazena vzestupně dle názvu, dolní meze, hodnoty a horní meze.
ds:labLocalName
) mimo stanovené meze.
Zobrazit pouze ty, které se opakují (alespoň 2x) a seřadit dle počtu (sestupně) a názvů (vzestupně).
Copyright © 2023 Petr Včelák a Martin Zíma