Podatkovne tabele (angl. Data Frames)

Podatkovne tabele (ali pa v dobesednem prevodu okviri) so pomembna nadgradnja osnovnega tipa seznamov: tabela je pravzaprav predstavljena kot seznam njenih stolpcev (tudi vsak stolpec tabele je seznam). Namenjene so shranjevanju množice podatkov (angl. data set), ki so predmet analize.

Pogosta navada pri analizi podatkov je, da vrstice ustrezajo opazovanim enotam (angl. units). Enote lahko imenujemo tudi primerki oz. primeri (angl. examples). V tem primeru stolpci ustrezajo opazovanim ali izmerjenim lastnostim (angl. attributes) enot, ki jih imenujemo spremenljivke (angl. variables). Spremenljivka torej podaja in tudi vsebuje vse opazovane ali izmerjene vrednosti ustreznega atributa enote.

Poglejmo si primer preproste podatkovne tabele v kateri so enote osebe:

osebe_df = data.frame(
  ime = c("Mojca", "Janez", "Špela", "Vito"),
  spol = factor(c("ž", "m", "ž", "m"), levels=c("m", "ž")),
  starost = c(30, 48, 63, 27)
)
osebe_df

Podatkovna tabela vsebuje podatke o štirih opazovanih enotah (osebah). Vsaka oseba je opisana s tremi lastnostmi, ime, spol in starost, torej podatkovna množica ima tri spremenljivke z istimi imeni, ime, spol in starost.

Domena spremenljivke je množica možnih vrednosti te spremenljivke. Tako je, na primer, domena spremenljivke spol množica {m, ž}, domena spremenljivke starost pa množica naravnih števil. Že takoj je jasno, da so lahko spremenljivke dveh tipov:

Vrednosti kvantitativnih spremenljivk so tipa integer ali dobule, vrednosti kvalitativnih pa so tipa factor ali string.

Preverimo sedaj kakšnega tipa je podatkovna tabela v R-ju in kakšne so lastnosti tega tipa:

typeof(osebe_df)
[1] "list"
attributes(osebe_df)
$names
[1] "ime"     "spol"    "starost"

$class
[1] "data.frame"

$row.names
[1] 1 2 3 4

Dva pomembna atributa podatkovne tabele sta:

Vrednosti teh atributov lahko dobimo s funkcijami rownames in colnames (slednjo lahko zamenjamo z names, a temu se načrtno izogibamo, da bo koda bolj berljiva). Število vrstic in stolpcev dobimo s funkcijama nrow in ncol:

paste("Dimenzije tabele osebe_df:",
    nrow(osebe_df), "vrstice in",
    ncol(osebe_df), "stolpci."
)
[1] "Dimenzije tabele osebe_df: 4 vrstice in 3 stolpci."
rownames(osebe_df)
[1] "1" "2" "3" "4"
colnames(osebe_df)
[1] "ime"     "spol"    "starost"

Stolpci v tabeli so seznami, kar omogoča veliko fleksibilnost pri tipih elementov tabele: vsak element je lahko svojega tipa. Kljub temu, za lažjo analizo in vizualizacijo podatkov se običajno omejimo na take tabele, kjer ima vsak stolpec elemente istega tipa.

Podatkovne tabele tipa tibble

Osnovni podatkovni tip data.frame v R-ju je lahko nadležen, predvsem zato, ker upošteva vse navade R-ja glede prisile in dinamičnega prilagajanja tipov. Tako, na primer, če je rezultat indeksiranja stolpcev v tabeli en sam stolpec, ga bo R avtomatsko spremenil v vektor ali seznam - v primeru dveh ali več stolpcev bo pa rezultat še vedno tabela.

Zato pogosto uporabljamo podatkovne tabele tipa tibble, ki jih implementira istoimenski CRAN paket:

library(tibble)

osebe_dft = as_tibble(osebe_df)
osebe_dft

V nadaljevanju si oglejmo nekaj razlik med tibble in data.frame.

Imena stolpcev v tibble so bolj fleksibilna, kot tista v data.frame, predvsem pa se ne spremenijo kar samodejno brez opozoril:

colnames(data.frame(`1` = c(1,2,3)))
[1] "X1"
colnames(tibble(`1` = c(1,2,3)))
[1] "1"

Pri kreiranju tabele tipa tibble so stolpci, ki vsebujejo nize znakov, samodejno pretvorijo v faktorje, kar je lahko prednost ali slabost. Preverite še enkrat primer tabele zgoraj in opazili boste, da smo za pretvorbo stolpca v faktor poskrbeli sami med ustvarjanjem podatkovne tabele.

Nadalje, podatkovni tip tibble ne omogoča poimenovanje vrstic v tabeli. Kot smo zapisali zgoraj je poimenovanje stolpcev itak nezaželeno.

Nenazadnje, tibble omogoča sklicevanje na prejšnje stolpce pri tvorjenju novih:

starost_v_letih = function(datum_rojstva) {
  as.integer(
    as.numeric(difftime(Sys.Date(), datum_rojstva, units="weeks")) / 52.25
  )
}

osebe_dft = tibble(
  ime = c("Mojca", "Janez", "Špela", "Vito"),
  spol = factor(c("ž", "m", "ž", "m"), levels=c("ž", "m")),
  datum.rojstva = as.Date(c("1991-09-15", "1973-02-04", "1958-07-30", "1995-05-27")),
  starost = starost_v_letih(datum.rojstva)
)
osebe_dft

Analizirajte pozorno funkcijo za izračun starosti osebe v letih na osnovi podanega datuma rojstva.

Indeksiranje podatkovnih tabel

Podatkovne tabele, tako kot matrike, lahko indeksiramo z navajanjem indeksa vrstice i in nato indeksa stolpca j, torej df[i, j]. Če kakšnega izmed teh dveh indeksov pustimo praznega, dobimo kot rezultat vse vrstice oz. stolpce podatkovne tabele. Pozor: rezultat indeksiranja je tabela, le če uporabljamo podatkovne tabele tipa tibble ali pa nastavimo argument drop=FALSE:

osebe_dft[c(1,2), ]
osebe_dft[c(1,1,1,2), ]

osebe_dft[, -3]


osebe_dft[c(1,2), "spol"]
osebe_df[c(1,2), "spol"] # opazuj razliko s prejšnjim rezultatom
[1] ž m
Levels: m ž
osebe_df[c(1,2), "spol", drop=FALSE]

Če drop=FALSE ne nastavimo in je df podatkovna tabela tipa data frame, se bo tip rezultata (prisilno) prilagodil v vektor (če je rezultat indeksiranja ena vrstica ali en stolpec tabele) oziroma skalar (če je rezultat indeksiranja vrednost ene celice v tabeli).

Za indeksiranje podatkovnih tabel lahko uporabljamo vse načine indeksiranja, ki jih poznamo iz indeksiranja vektorjev in seznamov. Če si zraven zapomnimo, da je tabela pravzaprav seznam stolpcev, vemo kako pridemo do vsebine enega stolpca na tri različne načine (najbolj pogosto uporabljamo zadnjega, ki je kratek in hkrati berljiv):

osebe_dft[[1]]
[1] "Mojca" "Janez" "Špela" "Vito" 
osebe_dft[["starost"]]
[1] 31 49 64 27
osebe_dft$datum.rojstva
[1] "1991-09-15" "1973-02-04" "1958-07-30" "1995-05-27"

V nadaljevanju si oglejmo še nekaj načinov indeksiranja, ki jih poznamo še od prej, a tukaj jih ponazorimo na podatkovnih tabelah.

Lahko indeksiramo z logičnim pogojem zato, da dobimo podmnožico vrstic, ki pogoju ustrezajo:

osebe_dft[osebe_dft$starost > 45, ]
osebe_dft$starost > 45 # da ugotovimo zakaj indeksiranje zgoraj pravilno deluje
[1] FALSE  TRUE  TRUE FALSE

Z indeksiranjem si lahko pomagamo tudi pri brisanju stolpcev (ali vrstic) tabele:

osebe_dft1 = osebe_dft[, -3]
osebe_dft1

Z indeksiranjem lahko tudi urejamo vrstice v tabeli v naraščajočem ali padajočem vrstnem redu, tako da v indksu uporabimo funkcijo order:

order(osebe_dft$starost)
[1] 4 1 2 3
osebe_dft[order(osebe_dft$starost), ]

osebe_dft[order(osebe_dft$starost, decreasing = TRUE), ]

osebe_dft[order(osebe_dft$spol), ]
osebe_dft[order(osebe_dft$spol, -osebe_dft$starost), ]

osebe_dft[, order(colnames(osebe_dft))]

Z indeksiranjem tudi enostavno vzorčimo primere s ponavaljanjem ali brez

osebe_dft[sample(nrow(osebe_dft), 2), ]
osebe_dft[sample(nrow(osebe_dft), 10, replace=T), ]

Od podatkov do podatkovnih tabel

Podatki o opazovanih enotah lahko zbiramo na različne načine. Lahko jih pripravimo iz lastnih meritev in jih sami vnašamo v R v obliki vektorjev in seznamov. Lahko jih pridobimo v obliki preglednic Excel iz nekega standardnega vira statističnih podatkov, kot je podatkovna baza SiStat Statističnega urada Republike Slovenije ali pa podatkovna baza Eurostat, evropskega statističnega urada. Spletna enciklopedija Wikipedia je tudi bogat vir podatkovnih tabel, kot je na primer tabela s podatki o bruto domačem proizvodu držav.

Kot bomo spoznali v nadaljevanju, R ponuja priročne funkcije za branje podatkov iz datotek s preglednicami Excel ali iz podatkovnih tabel v spletnih enciklopediji Wikipedia. Pridobivanje takih podatkov in njihovo branje oziroma transformacija v R-jevske podatkovne tabele je zato relativno enostavno. Ker se običajno zaplete je, da imajo podatki pridobljeni iz različnih podatkovnih virov različno strukturo.

Struktura in pomen podatkov

Poglejmo si preprosti primer. Denimo, da opazujemo nekaj objektov, pravokotnikov in kvadratov in pri tem merimo njihove dimenzije. Rezultate meritev smo si zapisali v naslednji seznam:

objekti = list(
  o1 = list("kvadrat", 3),
  o2 = list("pravokotnik", c(2, 7)),
  o3 = list("pravokotnik", c(9, 4)),
  o4 = list("kvadrat", 8),
  o5 = list("pravokotnik", c(6, 5))
)

Preden se lotimo analize podatkov o opisanih objektih, bi jih morali pretvoriti v podatkovno tabelo. A podatkovne tabele so lahko različnih oblik, ki jim rečemo tudi podatkovne strukture (angl. data structures) in vsaka ta oblika oziroma struktura bi lahko bila sprejemljiv način zapisa podatkov v podatkovno tabelo. Poglejmo si nekaj primerov podatovnih tabel za podane objekte.

Prva podatkovna tabela umesti objekte v vrstice tabele, tri njihove lastnosti v stolpce:

oblika_objekta = function(o) {
  o[[1]]
}

dimenzija_objekta = function(o) {
  paste0(o[[2]], collapse="-")
}

objekti_df1 = data.frame(
  ime = names(objekti),
  oblika = sapply(objekti, FUN=oblika_objekta),
  dimenzija = sapply(objekti, FUN=dimenzija_objekta)
)
objekti_df1

Ta podatkovna struktura določa tudi pomen podatkov (angl. data semantics): enota opazovanja je objekt, njegove lastnosti merimo s tremi spremenljivkami, ime, oblika in dimenzija.

Predstaviti dimenzijo posameznega objekta kot niz znakov ni ravno dobra popotnica za morebitne nadaljnje izračune pri analizi podatkov. Predstavljajte si, da bi si želeli v naslednjem koraku analize izračunati površino posameznega objekta. Zato bi potrebovali širino in višino objekta posebej. Da to naredimo, bi lahko podatkom pripisali drugo strukturo oziroma podatkovno tabelo:

visina_objekta = function(o) {
  ifelse(length(o[[2]]) == 1, o[[2]], o[[2]][1])
}

sirina_objekta = function(o) {
  ifelse(length(o[[2]]) == 1, o[[2]], o[[2]][2])
}

objekti_df2 = data.frame(
  ime = names(objekti),
  oblika = sapply(objekti, FUN=oblika_objekta),
  visina = sapply(objekti, FUN=visina_objekta),
  sirina = sapply(objekti, FUN=sirina_objekta)
)
objekti_df2

Tako podatkovno tabelo bi po vsej verjetnosti pripravili, če bi uporabljali program Excel. Pomen podatkov v tej podatkovni tabeli je podoben kot pomen v prejšnji, le da tokrat ima vsaka enota štiri lastnosti oziroma spremenljivke ime, oblika, visina in sirina.

Obe podatkovni tabeli pripravljeni zgoraj domnevajo, da so objekti zapisane po vrsticah, kar je običajna domneva pri načrtovanju strukture podatkov: vrstice v tabeli ustrezajo posameznim enotam (objektom). Vsekakor bi pa lahko pripravili podatkovno tabelo, kjer bi objekti bili spravljeni v stolpce, njihove lastnosti pa v vrstice:

objekti_df3 = data.frame(
  "X" = c("ime", "oblika", "visina", "sirina"),
  o1 = c("o1", "kvadrat", 3, 3),
  o2 = c("o2", "pravokotnik", 2, 7),
  o3 = c("o3", "pravokotnik", 9, 4),
  o4 = c("o4", "kvadrat", 8, 8),
  o5 = c("o5", "pravokotnik", 6, 5)
)
objekti_df3

S tem vprašanjem se soočamo na začetku vsakega projekta analize podatkov. Odgovorov na to vprašanje, kot kaže zgornji primer, veliko. Čeprav je primer zelo enostaven. Še več bi jih bilo, če bi obravnavali realističen primer bolj zapletenih podatkov.

Ali obstaja standarden odgovor na to vprašanje? Obstaja. Morebiti ne ravno standard, temveč dogovor, ki mu rečemo urejeni podatki (angl. tidy data). To je dogovor o standardni obliki zapisovanja podatkov v podatkovne tabele, ki omogoča njihovo enostavno analizo s pomočjo funkcij iz zbirke paketov, ki jih vsebuje knjižnica tidyverse.

Urejeni podatki (angl. tidy data)

Dogovor o urejenih podatkih določa tri enostavna pravila za podatkovne tabele.

  1. Vsaka spremenljivka tvori stolpec (rečemo lahko tudi ustreza stolpcu).
  2. Vsako opazovanje tvori vrstico.
  3. Vsak tip opazovane enote tvori tabelo.

Pri načrtovanju naših podatkovnih tabel začnemo s premislekom o tretjem pravilu. Ključno vprašanje pri tem premisleku je Kateri tipi opazovanih enot se pojavljajo v naših podatkih? Glede na to, kakšen je odgovor na to vprašanje, lahko za objekte iz našega primera sestavimo tabele urjenih podatkov na dva načina.

Prvi način: dva tipa opazovanih enot

V našem primeru je očiten tip opazovane enote objekt. A njega opazujemo na dva načina, po eni strani opazujemo kvalitativno lastnost objekta oblika. Drugi način opazovanja je kvantitativna dimenzija objekta, ki jo merimo skozi dva različna vidika širine in višine. Ker imamo dva načina opazovanja objektov, lahko pripravimo dve tabeli:

  • tabela oblika bo vsakemu objektu pripisala obliko;
  • tabela dimenzija bo vsakemu paru objekta in vidika dimenzije (širine ali višine) pripisala izmerjeno vrednost tega vidika dimenzije.

Tabela oblika ima torej dve kvalitativni spremenljivki:

  • objekt, ki določi ime objekta in ima torej domeno {o1, o2, o3, o4 in o5};
  • vrednost, ki določi obliko objekta in ima domeno {kvadrat, pravokotnik}.

Med njima obstaja pomembna razlika. Prva je fiksna spremenljivka, katere vrednost določa predmet opazovanja, v našem primeru objekt. Imenujemo jo tudi dimenzijska spremenljivka. Druga je merjena spremenljivka: njeno vrednost določimo z opazovanjem ali merjenjem izbranega predmeta opazovanja, v našem primeru z opazovanjem (ugotavljanjem) oblike objekta. Imenujemo jo tudi vrednostna spremenljivka.

Sestavimo torej prvo tabelo oblika:

oblika_df = data.frame(
  objekt = names(objekti),
  vrednost = sapply(objekti, FUN=oblika_objekta)
)
oblika_df

Druga tabela dimenzija ima dve kvalitativni in eno kvantitativno spremenljivko:

  • objekt, ki ima enako domeno kot zgoraj, torej {o1, o2, o3, o4 in o5};
  • vidik, ki ima domeno {širina, višina};
  • vrednost, ki poda vrednost izbranega vidika dimenzije (višine ali širine) z opazovani objekt.

V tej tabeli sta prvi dve spremenljivki dimenzijski (fiksni), saj njihovi vrednosti določata za kateri objekt in katero meritev opazovanega objekta gre, ne beleži pa vrednosti merjenja. Zadnja spremenljivka je pa merjena (vrednostna), saj beleži vrednosti opravljenih merjenj izbranega vidika dimenzije objekta.

dimenzija_df = expand.grid(
  objekt = names(objekti),
  vidik = c("širina", "višina"),
  vrednost = NA
)
dimenzija_df$vrednost = c(
  sapply(objekti, FUN=sirina_objekta),
  sapply(objekti, FUN=visina_objekta)
)
dimenzija_df

Funkcija expand.grid v kodi zgoraj poskrbi za to, da sestavimo podatkovno tabelo kot kartezični produkt domen dimenzijskih spremenljivk objekt in vidik, kjer so vse vrednosti merjene spremenljivke enake NA. Ker je moč domene spremenljivke objekt enaka 5, moč domene vidik 2, in je moč (začasne) domene merjene spremenljivke vrednost 1, ima podatkovna tabela, ki je rezultat klica funkcije expand.grid 5 * 2 * 1 = 10 vrstic.

Takoj zatem izračunamo še vrednosti zadnje spremenljivke vrednost v tabeli. Pri tem upoštevamo, da se prvih pet vrstic v kartezičnem produktu nanaša na širino, preostalih pet na višino objektov.

Običajni vrstni red stolpcev v tabeli z urejenimi podatki je tak, da dimenzijskim spremenljivkam sledijo merjene. Vrstice so pa običajno urejene po naraščajoči vrednosti prve spremenljivke, nato druge, tretje in tako naprej.

Stolpci v tabeli dimeznija sledijo običajnemu vrstnemu redu, vrstice pa ne. Lahko jih uredimo takole:

dimenzija_df = dimenzija_df[order(dimenzija_df$objekt, dimenzija_df$vidik), ]
dimenzija_df

POZOR! V primeru, ko so podatki razdeljeni v več podatkovnih tabel, moramo poskrbeti, da si te tabele delijo vsaj kakšno dimenzijsko spremenljivko, ki nam bo omogočila, da zberemo podatke o eni opazovani enoti skupaj. V zgornjem primeru je taka spremenljivka objekt, ki nam omogoča da povežemo podatek o obliki objekta (iz tabele oblika) s podatki o njegovih dimenzijah (zapisanimi v tabeli dimenzija).

Drugi način: trije tipi opazovanih enot

Pri sestavljanju tabele z urejenimi podatki dimenzija smo domnevali, da sta širina in višina le dva različna vidika meritve dimenzije objekta. Če bi širino in višino določili kot dva različna tipa opazovanih enot, bi končali s tremi tabelami urejenih podatkov. Prva tabela oblika bi ostala enaka kot prej, tabelo dimenzija bi zamenjali z dvema tabelama sirina in visina. Obe tabeli bi imeli enak nabor dveh spremenljivk objekt in vrednost:

sirina_df = data.frame(
  objekt = names(objekti),
  vrednost = sapply(objekti, FUN=sirina_objekta)
)
sirina_df

visina_df = data.frame(
  objekt = names(objekti),
  vrednost = sapply(objekti, FUN=visina_objekta)
)
visina_df

Za vsako podatkovno tabelo z urejenimi podatki, ki ima \(d+1\) stolpcev, velja, da prvih \(d\) stolpcev ustreza dimenzijskim (fiksnim) spremenljivkam z domenami \(D_1, D_2, \ldots, D_{d}\), zadnji stolpec pa merjeni (vrednostni) spremenljivki z domeno \(D_{d+1}\). Lahko torej rečemo, da urejeni podatki podajo tabelarično definicijo funkcije \(f: D_1 \times D_2 \times \ldots \times D_{d} \to D_{d+1}\). Funkcija \(f\) za podane vrednosti dimenzijskih spremenljivk vrne vrednost merjene spremenljivke.

Naj na koncu omenimo še eno možnost za podatkovne tabele z urejenimi podatki. Če imamo \(m\) tabel z istimi dimenzijskimi spremenljivkami z domenami \(D_1, D_2, \ldots, D_d\), jih lahko združimo v eno podatkovno tabelo z urejenimi podatki, ki ima \(d\) dimenzijskih in \(m\) merjenih spremenljivk. Taka tabela definira funkcijo \(g: D_1 \times D_2 \times \ldots \times D_{d} \to D_{d+1} \times D_{d+2} \times \ldots \times D_{d+m}\), ki za podane vrednosti dimenzijskih spremenljivk vrne vrednosti vseh \(m\)-tih merjenih spremenljivk hkrati.

V zgornjem primeru z objekti, vse tri podatkovne tabele z urejenimi podatki oblika, visina in sirina imajo isto dimenzijsko spremenljivko objekt in jih torej lahko združimo v eno podatkovno tabelo objekti z eno dimenzijsko spremenljivko objekt in tremi merjenimi spremenljivkami oblika, sirina in visina takole:

objekti_df4 = data.frame(
  objekt = names(objekti),
  oblika = sapply(objekti, FUN=oblika_objekta),
  sirina = sapply(objekti, FUN=sirina_objekta),
  visina = sapply(objekti, FUN=visina_objekta)
)
objekti_df4

Ta zadnja različica podatkovne tabele tudi sledi standardom urejenih podatkov tidy data, pri čemer predpostavimo, da imamo opravka z enim tipom opazovane enote objekt, različne načine opazovanja in meritev enot (objektov) pa določimo kot različne lastnosti opazovanih enot oziroma spremenljivke.

Standardi poimenovanj

Imena tabel in spremenljivk (stolpcev) naj bodo zapisane z malimi črkami. Če je ime sestavljeno iz več besed, vmesne presledke zamenjamo z _, na primer ime_in_priimek, ne pa ime in priimek. V imenih tabel in spremenljivk se izogibamo šumnikom: č, š in ž zamenjamo z c, s in z, na primer, zarisce namesto žarišče.

Vrednosti v tabeli zapišemo tako, kot želimo oziroma tako, kot so podane, z velikimi in malimi črkami, s šumniki in presledki. Pri tem moramo biti pozorni na kodiranje znakov, ki ga uporablja R. Preverimo ga lahko s klicem funkcije localeToCharset(): zaželena nastavitev je "UTF-8". Pri branju podatkov iz besedilnih ali Excel-ovih datotek moramo poskrbeti, da se podatkovne vrednosti med uvozom pretvorijo v kodiranje znakov, ki ga uporablja R, sicer bodo šumniki (in kakšni drugi znaki) zapisani napačno.


Osnove tidyverse: Uvoz in izvoz podatkovnih tabel

Paket, ki omogoča pripravo in analizo urejenih podatkov, opisanih v prejšnjem poglavju omogoča zbirka paketov, ki jih vsebuje knjižnica tidyverse. V tem poglavju bomo spoznali funkcije za uvoz in izvoz podatkovnih tabel, ki jih ponuja ta knjižnica. Najprej bomo uvozili knjižnico:

library(tidyverse)
Registered S3 methods overwritten by 'dbplyr':
  method         from
  print.tbl_lazy     
  print.tbl_sql      
── Attaching packages ───────────────────────────────────────── tidyverse 1.3.1 ──
✓ ggplot2 3.3.5     ✓ dplyr   1.0.7
✓ tidyr   1.1.4     ✓ stringr 1.4.0
✓ readr   2.1.2     ✓ forcats 0.5.1
✓ purrr   0.3.4     
Warning: package ‘readr’ was built under R version 4.0.5── Conflicts ──────────────────────────────────────────── tidyverse_conflicts() ──
x dplyr::filter() masks stats::filter()
x dplyr::lag()    masks stats::lag()

Format CSV

Datoteka v formatu CSV vsebuje besedilni zapis podatkovne tabele, kjer je vsaka vrstica iz tabele zapisna kot vrstica v datoteki, posamezne vrednosti v stolpcih (celicah) tabele so ločene z vejico ,.

Funkcija, ki omogoča shranjevanje podatkovne tabele v formatu CSV (angl. Comma-Separated Values) je write_csv. Za podano podatkovno tabelo in ime datoteke, funkcija shrani podatke iz podatkovne tabele v datoteko formata CSV s podanim imenom:

write_csv(objekti_df4, "objekti.csv")

Nekaj koristnih argumentov funkcije write_csv:

  • na s privzeto vrednostjo "NA" poda niz znakov za zapisovanje neznanih vrednosti.
  • append s privzeto vrednostjo FALSE je logična vrednost, ki določi ali naj podatki povozijo prejšnjo vsebino obstoječe datoteke (vrednost FALSE) ali naj se podatki zapišejo v nadaljevanju obstoječih vrstic v datoteki (vrednost TRUE).
  • col_names s privzeto vrednostjo !append je logična vrednost, ki določi ali naj se v prvi vrstici datoteke zapišejo imena stolpcev.

Funkcija read_csv prebere podatkovno tabelo iz datoteke podanega imena, kjer so podatki zapisani v formatu CSV. Koristen argument funkcije je col_names s privzeto vrednostjo TRUE, ki določi ali prvi vrsti datoteke vsebuje imena stolpcev.

Drugi pogosto uporabljeni argumenti funkcije read_csv vplivajo na tipe stolpcev v podatkovni tabeli z uvoženimi stolpci. Funkcija namreč samodejno ugotovi katerega tipa so podatki v stolpcu tako, da pregleda prvih 1000 (tisoč) vrstic preglednice. To lahko spremenimo na dva načina, opisana v nadaljevanju.

  • Z nastavitvijo argumenta guess_max (privzeta vrednost 1000), ki določi število vrstic, ki jih pregleda funkcija preden se odloči o tipu vrednosti v stolpcu. Če želimo, da funkcija pregleda vse razpoložljive vrednosti, uporabimo guess_max=Inf.

  • Z nastavitvijo argumenta col_types, ki lahko povozi samodejno ugotavljanje tipov stolpcev. Vrednost je lahko vektor nizov znakov z naslednjimi vrednostmi:

    • "skip": vrednosti stolpca ne uvažamo;
    • "guess": tip vrednosti določi funkcija samodejno;
    • "list": tip vrednosti v stoplcu se spreminja;
    • "logical", "numeric", "text" ali "date": vrednosti v stolpcu so logične, numerične, nizi znakov ali datumi.

    V primeru, da za vrednost argumenta col_types podamo niz znakov, potem tip posamezne kolone ustreza enemu znaku v nizu. Pomen znakov je - ali _ za skip, ? za guess, n za numeric, c za text in D za datum. Celoten seznam možnih tipov in okrajšav dobimo z ?read_csv in ?readr::cols.

Excel-ove preglednice

V R-ju lahko beremo podatke iz Excel-ovih preglednic z uporabo ukaza read_excel v formatu XLS ali XLSX, ki se določi na osnovi podaljška podanega imena datoteke. Če hočemo ugotoviti kateri listi (angl. sheets) so na voljo v datoteki, uporabimo funkcijo excel_sheets. Obe funkciji kot prvi argument sprejmeta ime datoteke z Excel preglednico.

Nekaj koristnih parametrov funkcije read_excel:

  • sheet s privzeto vrednostjo NULL določi list preglednice, ki ga hočemo prebrati. Vrednost je lahko celoštevilčna (podamo pozicijo lista v preglednici) ali niz znakov (podamo ime lista v preglednici). Če podana vrednost ne določi lista, funkcija prebere podatke iz prvega lista preglednice.
  • range s privzeto vrednostjo NULL določi obseg celic preglednice, katere vrednosti hočemo uvoziti v R. Uporabljamo Excel-ov način določanja obsega, npr. "A1:C11" ali pa "Rezultati!A1:Q1024". V primeru podanega obsega, funkcija uvozi tudi prazne vrstice in stolpce iz tega obsega.
  • col_names, guess_max in col_types imajo isti pomen kot istoimenski argumenti funkcije read_csv opisane v prejšnjem razdelku.

Pregled funkcij za uvoz in izvoz podatkovnih tabel, ki jih ponuja tidyverse, je na voljo v plonk listu Data import with the tidyverse.


Naloge

  1. Denimo, da smo Alešu, Barbari, Cirilu in Darji izmerili višine v centimetrih (180, 165, 160, 193) in teže v kilogramih (87, 58, 65, 100). Nato smo za vsako osebo izračunali indeks telesne mase (ITM) po formuli \[ \text{ITM} = \frac{\text{teža v kilogramih}}{(\text{višina v metrih})^2}. \] Iz izmerjenih iz izračunanih podatke sestavi podatkovno tabelo z urejenimi podatki tipa tidy data. Pri tem dobro premisli kakšne možnosti imaš na voljo: kaj so tipi enot, spremenljivke in vrstice v tabelah z urejenimi podatki.

  2. V datoteki izidi.xlsx je preglednica s podatki o numeričnih izidih uporabe dveh različnih terapij na treh pacientih. Prazne celice ustrezajo neznanim vrednostim izidov. Napiši program v R-ju, ki prebere podatke iz preglednice in iz njih sestavi podatkovno tabelo z urejenimi podatki. Namig: rabiš sestaviti eno tabelo z dvema dimenzijskima in eno merjeno spremenljivko.

  3. Profesorica, ki na FMF izvaja dve-semestrski predmet, si je v datoteki rezultati-kolokvijev.xlsx zapisovala rezultate kolokvijev v prvem in drugem semestru. Napiši program v R-ju, ki prebere podatke iz vseh listov preglednice: naj program sam ugotovi koliko listov ima preglednica.

    Nato premisli katere vse enote opazovanja rabimo zato, da profesorici pomagamo korektno pretvoriti podatke o rezultatih kolokvijev v podatkovne tabele z urejenimi podatki tipa tidy data, ki bi ji omogočale boljšo analizo podatkov. Ročno ali s programom v R-ju sestavi te podatkovne tabele.


Zvezek sem oktobra 2022 pripravil Ljupčo Todorovski v jeziku R Markdown. Ideje za primere sem si v vekiki meri izposojal iz učbenika (Wickham 2019), vse morebitne napake v primerih so izključno moje.

Več o pravilih urejanja podatkov v podatkovne tabele lahko preberete v članku Wigham, H. (2014) Tidy Data. Jorunal of Statistical Software 59(10): 1-23. DOI: 10.18637/jss.v059.i10.

LS0tCnRpdGxlOiAnUG9kYXRrb3ZuZSB0YWJlbGUgaW4gdXJlamVuaSBwb2RhdGtpJwpvdXRwdXQ6CiAgaHRtbF9ub3RlYm9vazogZGVmYXVsdAogIHBkZl9kb2N1bWVudDogZGVmYXVsdAotLS0KCiMgUG9kYXRrb3ZuZSB0YWJlbGUgKGFuZ2wuIF9EYXRhIEZyYW1lc18pCgpQb2RhdGtvdm5lIHRhYmVsZSAoYWxpIHBhIHYgZG9iZXNlZG5lbSBwcmV2b2R1IG9rdmlyaSkgc28gcG9tZW1ibmEgbmFkZ3JhZG5qYSBvc25vdm5lZ2EgdGlwYSBzZXpuYW1vdjogdGFiZWxhIGplIHByYXZ6YXByYXYgcHJlZHN0YXZsamVuYSBrb3Qgc2V6bmFtIG5qZW5paCBzdG9scGNldiAodHVkaSB2c2FrIHN0b2xwZWMgdGFiZWxlIGplIHNlem5hbSkuIE5hbWVuamVuZSBzbyBzaHJhbmpldmFuanUgbW5vxb5pY2UgcG9kYXRrb3YgKGFuZ2wuIF9kYXRhIHNldF8pLCBraSBzbyBwcmVkbWV0IGFuYWxpemUuCgpQb2dvc3RhIG5hdmFkYSBwcmkgYW5hbGl6aSBwb2RhdGtvdiBqZSwgZGEgdnJzdGljZSB1c3RyZXpham8gb3Bhem92YW5pbSAqKmVub3RhbSoqIChhbmdsLiBfdW5pdHNfKS4gRW5vdGUgbGFoa28gaW1lbnVqZW1vIHR1ZGkgcHJpbWVya2kgb3ouIHByaW1lcmkgKGFuZ2wuIF9leGFtcGxlc18pLiBWIHRlbSBwcmltZXJ1IHN0b2xwY2kgdXN0cmV6YWpvIG9wYXpvdmFuaW0gYWxpIGl6bWVyamVuaW0gKipsYXN0bm9zdGltKiogKGFuZ2wuIF9hdHRyaWJ1dGVzXykgKiplbm90KiosIGtpIGppaCBpbWVudWplbW8gKipzcHJlbWVubGppdmtlKiogKGFuZ2wuIF92YXJpYWJsZXNfKS4gU3ByZW1lbmxqaXZrYSB0b3JlaiBwb2RhamEgaW4gdHVkaSB2c2VidWplIHZzZSBvcGF6b3ZhbmUgYWxpIGl6bWVyamVuZSB2cmVkbm9zdGkgdXN0cmV6bmVnYSBhdHJpYnV0YSBlbm90ZS4KClBvZ2xlam1vIHNpIHByaW1lciBwcmVwcm9zdGUgcG9kYXRrb3ZuZSB0YWJlbGUgdiBrYXRlcmkgc28gZW5vdGUgb3NlYmU6CmBgYHtyfQpvc2ViZV9kZiA9IGRhdGEuZnJhbWUoCiAgaW1lID0gYygiTW9qY2EiLCAiSmFuZXoiLCAixaBwZWxhIiwgIlZpdG8iKSwKICBzcG9sID0gZmFjdG9yKGMoIsW+IiwgIm0iLCAixb4iLCAibSIpLCBsZXZlbHM9YygibSIsICLFviIpKSwKICBzdGFyb3N0ID0gYygzMCwgNDgsIDYzLCAyNykKKQpvc2ViZV9kZgpgYGAKClBvZGF0a292bmEgdGFiZWxhIHZzZWJ1amUgcG9kYXRrZSBvIMWhdGlyaWggb3Bhem92YW5paCBlbm90YWggKG9zZWJhaCkuIFZzYWthIG9zZWJhIGplIG9waXNhbmEgcyB0cmVtaSBsYXN0bm9zdG1pLCBfaW1lXywgX3Nwb2xfIGluIF9zdGFyb3N0XywgdG9yZWogcG9kYXRrb3ZuYSBtbm/FvmljYSBpbWEgdHJpIHNwcmVtZW5saml2a2UgeiBpc3RpbWkgaW1lbmksIGBpbWVgLCBgc3BvbGAgaW4gYHN0YXJvc3RgLgoKKipEb21lbmEgc3ByZW1lbmxqaXZrZSoqIGplIG1ub8W+aWNhIG1vxb5uaWggdnJlZG5vc3RpIHRlIHNwcmVtZW5saml2a2UuIFRha28gamUsIG5hIHByaW1lciwgZG9tZW5hIHNwcmVtZW5saml2a2UgX3Nwb2xfIG1ub8W+aWNhIHtgbWAsIGDFvmB9LCBkb21lbmEgc3ByZW1lbmxqaXZrZSBgc3Rhcm9zdGAgcGEgbW5vxb5pY2EgbmFyYXZuaWggxaF0ZXZpbC4gxb1lIHRha29qIGplIGphc25vLCBkYSBzbyBsYWhrbyBzcHJlbWVubGppdmtlIGR2ZWggdGlwb3Y6CgogICogKmt2YW50aXRhdGl2bmUgc3ByZW1lbmxqdmlrZSogaW1ham8gKGxhaGtvIHR1ZGkgbmVza29uxI1ubykgZG9tZW5vIMWhdGV2aWzEjW5paCB2cmVkbm9zdGksCiAgKiAqa3ZhbGl0YXRpdm5lIHNwcmVtZW5saml2a2UqIGltYWpvIGtvbsSNbm8gZG9tZW5vLCB0LmouLCBtbm/FvmljbyB2bmFwcmVqIHpuYW5paCB2cmVkbm9zdGkuCiAgICAKVnJlZG5vc3RpIGt2YW50aXRhdGl2bmloIHNwcmVtZW5saml2ayBzbyB0aXBhIF9pbnRlZ2VyXyBhbGkgX2RvYnVsZV8sIHZyZWRub3N0aSBrdmFsaXRhdGl2bmloIHBhIHNvIHRpcGEgX2ZhY3Rvcl8gYWxpIF9zdHJpbmdfLgoKUHJldmVyaW1vIHNlZGFqIGtha8WhbmVnYSB0aXBhIGplIHBvZGF0a292bmEgdGFiZWxhIHYgUi1qdSBpbiBrYWvFoW5lIHNvIGxhc3Rub3N0aSB0ZWdhIHRpcGE6CmBgYHtyfQp0eXBlb2Yob3NlYmVfZGYpCmF0dHJpYnV0ZXMob3NlYmVfZGYpCmBgYAoKRHZhIHBvbWVtYm5hIGF0cmlidXRhIHBvZGF0a292bmUgdGFiZWxlIHN0YToKCiogYG5hbWVzYCBqZSB2ZWt0b3Igbml6b3Ygem5ha292LCBraSB1c3RyZXpham8gaW1lbm9tIHN0b2xwY2V2LgoqIGByb3cubmFtZXNgamUgb2JpxI1ham5vIHZla3RvciBjZWxpaCDFoXRldmlsIG9ibGlrZSBgMTpuYCwga2plciBqZSBgbmAgxaF0ZXZpbG8gdnJzdGljIHYgdGFiZWxpLiBMYWhrbyBiaSBiaWwgdHVkaSB2ZWt0b3Igbml6b3Ygem5ha292LCBrYXIgb21vZ2/EjWEgcG9pbWVub3ZhbmplIHZyc3RpYy4gQSBvYmnEjWFqbm8gc2UgdGVnYSBuZSBwb3NsdcW+dWplbW86IMSNZSBpbWFqbyB2cnN0aWNlIGltZW5hIGluIHRvcmVqIGltYWpvIGltZW5hIG5la2FrxaFlbiBwb2RhdGtvdm5pIHBvbWVuLCBwb3RlbSBpbWVub20gbmFtZW5pbW8gKHBydmkpIHN0b2xwZWMgdiB0YWJlbGkuCgpWcmVkbm9zdGkgdGVoIGF0cmlidXRvdiBsYWhrbyBkb2JpbW8gcyBmdW5rY2lqYW1pIGByb3duYW1lc2AgaW4gYGNvbG5hbWVzYCAoc2xlZG5qbyBsYWhrbyB6YW1lbmphbW8geiBgbmFtZXNgLCBhIHRlbXUgc2UgbmHEjXJ0bm8gaXpvZ2liYW1vLCBkYSBibyBrb2RhIGJvbGogYmVybGppdmEpLiDFoHRldmlsbyB2cnN0aWMgaW4gc3RvbHBjZXYgZG9iaW1vIHMgZnVua2NpamFtYSBgbnJvd2AgaW4gYG5jb2xgOgpgYGB7cn0KcGFzdGUoIkRpbWVuemlqZSB0YWJlbGUgb3NlYmVfZGY6IiwKICAgIG5yb3cob3NlYmVfZGYpLCAidnJzdGljZSBpbiIsCiAgICBuY29sKG9zZWJlX2RmKSwgInN0b2xwY2kuIgopCnJvd25hbWVzKG9zZWJlX2RmKQpjb2xuYW1lcyhvc2ViZV9kZikKYGBgCgpTdG9scGNpIHYgdGFiZWxpIHNvIHNlem5hbWksIGthciBvbW9nb8SNYSB2ZWxpa28gZmxla3NpYmlsbm9zdCBwcmkgdGlwaWggZWxlbWVudG92IHRhYmVsZTogdnNhayBlbGVtZW50IGplIGxhaGtvIHN2b2plZ2EgdGlwYS4gS2xqdWIgdGVtdSwgemEgbGHFvmpvIGFuYWxpem8gaW4gdml6dWFsaXphY2lqbyBwb2RhdGtvdiBzZSBvYmnEjWFqbm8gb21lamltbyBuYSB0YWtlIHRhYmVsZSwga2plciBpbWEgdnNhayBzdG9scGVjIGVsZW1lbnRlIGlzdGVnYSB0aXBhLgoKIyMgUG9kYXRrb3ZuZSB0YWJlbGUgdGlwYSBgdGliYmxlYAoKT3Nub3ZuaSBwb2RhdGtvdm5pIHRpcCBgZGF0YS5mcmFtZWAgdiBSLWp1IGplIGxhaGtvIG5hZGxlxb5lbiwgcHJlZHZzZW0gemF0bywga2VyIHVwb8WhdGV2YSB2c2UgbmF2YWRlIFItamEgZ2xlZGUgcHJpc2lsZSBpbiBkaW5hbWnEjW5lZ2EgcHJpbGFnYWphbmphIHRpcG92LiBUYWtvLCBuYSBwcmltZXIsIMSNZSBqZSByZXp1bHRhdCBpbmRla3NpcmFuamEgc3RvbHBjZXYgdiB0YWJlbGkgZW4gc2FtIHN0b2xwZWMsIGdhIGJvIFIgYXZ0b21hdHNrbyBzcHJlbWVuaWwgdiB2ZWt0b3IgYWxpIHNlem5hbSAtIHYgcHJpbWVydSBkdmVoIGFsaSB2ZcSNIHN0b2xwY2V2IGJvIHBhIHJlenVsdGF0IMWhZSB2ZWRubyB0YWJlbGEuCgpaYXRvIHBvZ29zdG8gdXBvcmFibGphbW8gcG9kYXRrb3ZuZSB0YWJlbGUgdGlwYSBgdGliYmxlYCwga2kgamloIGltcGxlbWVudGlyYSBbaXN0b2ltZW5za2kgQ1JBTiBwYWtldF0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL3RpYmJsZS9pbmRleC5odG1sKToKYGBge3J9CmxpYnJhcnkodGliYmxlKQoKb3NlYmVfZGZ0ID0gYXNfdGliYmxlKG9zZWJlX2RmKQpvc2ViZV9kZnQKYGBgCgpWIG5hZGFsamV2YW5qdSBzaSBvZ2xlam1vIG5la2FqIHJhemxpayBtZWQgYHRpYmJsZWAgaW4gYGRhdGEuZnJhbWVgLgoKSW1lbmEgc3RvbHBjZXYgdiBgdGliYmxlYCBzbyBib2xqIGZsZWtzaWJpbG5hLCBrb3QgdGlzdGEgdiBgZGF0YS5mcmFtZWAsIHByZWR2c2VtIHBhIHNlIG5lIHNwcmVtZW5pam8ga2FyIHNhbW9kZWpubyBicmV6IG9wb3pvcmlsOgpgYGB7cn0KY29sbmFtZXMoZGF0YS5mcmFtZShgMWAgPSBjKDEsMiwzKSkpCmNvbG5hbWVzKHRpYmJsZShgMWAgPSBjKDEsMiwzKSkpCmBgYAoKUHJpIGtyZWlyYW5qdSB0YWJlbGUgdGlwYSBgdGliYmxlYCBzbyBzdG9scGNpLCBraSB2c2VidWplam8gbml6ZSB6bmFrb3YsIHNhbW9kZWpubyBwcmV0dm9yaWpvIHYgZmFrdG9yamUsIGthciBqZSBsYWhrbyBwcmVkbm9zdCBhbGkgc2xhYm9zdC4gUHJldmVyaXRlIMWhZSBlbmtyYXQgcHJpbWVyIHRhYmVsZSB6Z29yYWogaW4gb3BhemlsaSBib3N0ZSwgZGEgc21vIHphIHByZXR2b3JibyBzdG9scGNhIHYgZmFrdG9yIHBvc2tyYmVsaSBzYW1pIG1lZCB1c3R2YXJqYW5qZW0gcG9kYXRrb3ZuZSB0YWJlbGUuCgpOYWRhbGplLCBwb2RhdGtvdm5pIHRpcCBgdGliYmxlYCBuZSBvbW9nb8SNYSBwb2ltZW5vdmFuamUgdnJzdGljIHYgdGFiZWxpLiBLb3Qgc21vIHphcGlzYWxpIHpnb3JhaiBqZSBwb2ltZW5vdmFuamUgc3RvbHBjZXYgaXRhayBuZXphxb5lbGVuby4KCk5lbmF6YWRuamUsIGB0aWJibGVgIG9tb2dvxI1hIHNrbGljZXZhbmplIG5hIHByZWrFoW5qZSBzdG9scGNlIHByaSB0dm9yamVuanUgbm92aWg6CmBgYHtyfQpzdGFyb3N0X3ZfbGV0aWggPSBmdW5jdGlvbihkYXR1bV9yb2pzdHZhKSB7CiAgYXMuaW50ZWdlcigKICAgIGFzLm51bWVyaWMoZGlmZnRpbWUoU3lzLkRhdGUoKSwgZGF0dW1fcm9qc3R2YSwgdW5pdHM9IndlZWtzIikpIC8gNTIuMjUKICApCn0KCm9zZWJlX2RmdCA9IHRpYmJsZSgKICBpbWUgPSBjKCJNb2pjYSIsICJKYW5leiIsICLFoHBlbGEiLCAiVml0byIpLAogIHNwb2wgPSBmYWN0b3IoYygixb4iLCAibSIsICLFviIsICJtIiksIGxldmVscz1jKCLFviIsICJtIikpLAogIGRhdHVtLnJvanN0dmEgPSBhcy5EYXRlKGMoIjE5OTEtMDktMTUiLCAiMTk3My0wMi0wNCIsICIxOTU4LTA3LTMwIiwgIjE5OTUtMDUtMjciKSksCiAgc3Rhcm9zdCA9IHN0YXJvc3Rfdl9sZXRpaChkYXR1bS5yb2pzdHZhKQopCm9zZWJlX2RmdApgYGAKCkFuYWxpemlyYWp0ZSBwb3pvcm5vIGZ1bmtjaWpvIHphIGl6cmHEjXVuIHN0YXJvc3RpIG9zZWJlIHYgbGV0aWggbmEgb3Nub3ZpIHBvZGFuZWdhIGRhdHVtYSByb2pzdHZhLgoKIyMgSW5kZWtzaXJhbmplIHBvZGF0a292bmloIHRhYmVsCgpQb2RhdGtvdm5lIHRhYmVsZSwgdGFrbyBrb3QgbWF0cmlrZSwgbGFoa28gaW5kZWtzaXJhbW8geiBuYXZhamFuamVtIGluZGVrc2EgdnJzdGljZSBgaWAgaW4gbmF0byBpbmRla3NhIHN0b2xwY2EgYGpgLCB0b3JlaiBgYGRmW2ksIGpdYGAuIMSMZSBrYWvFoW5lZ2EgaXptZWQgdGVoIGR2ZWggaW5kZWtzb3YgcHVzdGltbyBwcmF6bmVnYSwgZG9iaW1vIGtvdCByZXp1bHRhdCB2c2UgdnJzdGljZSBvei4gc3RvbHBjZSBwb2RhdGtvdm5lIHRhYmVsZS4gUG96b3I6IHJlenVsdGF0IGluZGVrc2lyYW5qYSBqZSB0YWJlbGEsIGxlIMSNZSB1cG9yYWJsamFtbyBwb2RhdGtvdm5lIHRhYmVsZSB0aXBhIGB0aWJibGVgIGFsaSBwYSBuYXN0YXZpbW8gYXJndW1lbnQgYGRyb3A9RkFMU0VgOgpgYGB7cn0Kb3NlYmVfZGZ0W2MoMSwyKSwgXQpvc2ViZV9kZnRbYygxLDEsMSwyKSwgXQoKb3NlYmVfZGZ0WywgLTNdCgoKb3NlYmVfZGZ0W2MoMSwyKSwgInNwb2wiXQpvc2ViZV9kZltjKDEsMiksICJzcG9sIl0gIyBvcGF6dWogcmF6bGlrbyBzIHByZWrFoW5qaW0gcmV6dWx0YXRvbQpvc2ViZV9kZltjKDEsMiksICJzcG9sIiwgZHJvcD1GQUxTRV0KYGBgCgrEjGUgYGRyb3A9RkFMU0VgIG5lIG5hc3RhdmltbyBpbiBqZSBgZGZgIHBvZGF0a292bmEgdGFiZWxhIHRpcGEgX2RhdGEgZnJhbWVfLCBzZSBibyB0aXAgcmV6dWx0YXRhIChwcmlzaWxubykgcHJpbGFnb2RpbCB2IHZla3RvciAoxI1lIGplIHJlenVsdGF0IGluZGVrc2lyYW5qYSBlbmEgdnJzdGljYSBhbGkgZW4gc3RvbHBlYyB0YWJlbGUpIG96aXJvbWEgc2thbGFyICjEjWUgamUgcmV6dWx0YXQgaW5kZWtzaXJhbmphIHZyZWRub3N0IGVuZSBjZWxpY2UgdiB0YWJlbGkpLgoKWmEgaW5kZWtzaXJhbmplIHBvZGF0a292bmloIHRhYmVsIGxhaGtvIHVwb3JhYmxqYW1vIHZzZSBuYcSNaW5lIGluZGVrc2lyYW5qYSwga2kgamloIHBvem5hbW8gaXogaW5kZWtzaXJhbmphIHZla3RvcmpldiBpbiBzZXpuYW1vdi4gxIxlIHNpIHpyYXZlbiB6YXBvbW5pbW8sIGRhIGplIHRhYmVsYSBwcmF2emFwcmF2IHNlem5hbSBzdG9scGNldiwgdmVtbyBrYWtvIHByaWRlbW8gZG8gdnNlYmluZSBlbmVnYSBzdG9scGNhIG5hIHRyaSByYXpsacSNbmUgbmHEjWluZSAobmFqYm9saiBwb2dvc3RvIHVwb3JhYmxqYW1vIHphZG5qZWdhLCBraSBqZSBrcmF0ZWsgaW4gaGtyYXRpIGJlcmxqaXYpOgpgYGB7cn0Kb3NlYmVfZGZ0W1sxXV0Kb3NlYmVfZGZ0W1sic3Rhcm9zdCJdXQpvc2ViZV9kZnQkZGF0dW0ucm9qc3R2YQpgYGAKClYgbmFkYWxqZXZhbmp1IHNpIG9nbGVqbW8gxaFlIG5la2FqIG5hxI1pbm92IGluZGVrc2lyYW5qYSwga2kgamloIHBvem5hbW8gxaFlIG9kIHByZWosIGEgdHVrYWogamloIHBvbmF6b3JpbW8gbmEgcG9kYXRrb3ZuaWggdGFiZWxhaC4KCkxhaGtvIGluZGVrc2lyYW1vIHogbG9nacSNbmltIHBvZ29qZW0gemF0bywgZGEgZG9iaW1vIHBvZG1ub8W+aWNvIHZyc3RpYywga2kgcG9nb2p1IHVzdHJlemFqbzoKYGBge3J9Cm9zZWJlX2RmdFtvc2ViZV9kZnQkc3Rhcm9zdCA+IDQ1LCBdCm9zZWJlX2RmdCRzdGFyb3N0ID4gNDUgIyBkYSB1Z290b3ZpbW8gemFrYWogaW5kZWtzaXJhbmplIHpnb3JhaiBwcmF2aWxubyBkZWx1amUKYGBgCgpaIGluZGVrc2lyYW5qZW0gc2kgbGFoa28gcG9tYWdhbW8gdHVkaSBwcmkgYnJpc2FuanUgc3RvbHBjZXYgKGFsaSB2cnN0aWMpIHRhYmVsZToKYGBge3J9Cm9zZWJlX2RmdDEgPSBvc2ViZV9kZnRbLCAtM10Kb3NlYmVfZGZ0MQpgYGAKClogaW5kZWtzaXJhbmplbSBsYWhrbyB0dWRpIHVyZWphbW8gdnJzdGljZSB2IHRhYmVsaSB2IG5hcmHFocSNYWpvxI1lbSBhbGkgcGFkYWpvxI1lbSB2cnN0bmVtIHJlZHUsIHRha28gZGEgdiBpbmRrc3UgdXBvcmFiaW1vIGZ1bmtjaWpvIGBvcmRlcmA6CmBgYHtyfQpvcmRlcihvc2ViZV9kZnQkc3Rhcm9zdCkKb3NlYmVfZGZ0W29yZGVyKG9zZWJlX2RmdCRzdGFyb3N0KSwgXQoKb3NlYmVfZGZ0W29yZGVyKG9zZWJlX2RmdCRzdGFyb3N0LCBkZWNyZWFzaW5nID0gVFJVRSksIF0KCm9zZWJlX2RmdFtvcmRlcihvc2ViZV9kZnQkc3BvbCksIF0Kb3NlYmVfZGZ0W29yZGVyKG9zZWJlX2RmdCRzcG9sLCAtb3NlYmVfZGZ0JHN0YXJvc3QpLCBdCgpvc2ViZV9kZnRbLCBvcmRlcihjb2xuYW1lcyhvc2ViZV9kZnQpKV0KYGBgCgpaIGluZGVrc2lyYW5qZW0gdHVkaSBlbm9zdGF2bm8gdnpvcsSNaW1vIHByaW1lcmUgcyBwb25hdmFsamFuamVtIGFsaSBicmV6CmBgYHtyfQpvc2ViZV9kZnRbc2FtcGxlKG5yb3cob3NlYmVfZGZ0KSwgMiksIF0Kb3NlYmVfZGZ0W3NhbXBsZShucm93KG9zZWJlX2RmdCksIDEwLCByZXBsYWNlPVQpLCBdCmBgYAoKCi0tLQoKIyBPZCBwb2RhdGtvdiBkbyBwb2RhdGtvdm5paCB0YWJlbAoKUG9kYXRraSBvIG9wYXpvdmFuaWggZW5vdGFoIGxhaGtvIHpiaXJhbW8gbmEgcmF6bGnEjW5lIG5hxI1pbmUuIExhaGtvIGppaCBwcmlwcmF2aW1vIGl6IGxhc3RuaWggbWVyaXRldiBpbiBqaWggc2FtaSB2bmHFoWFtbyB2IFIgdiBvYmxpa2kgdmVrdG9yamV2IGluIHNlem5hbW92LiBMYWhrbyBqaWggcHJpZG9iaW1vIHYgb2JsaWtpIHByZWdsZWRuaWMgRXhjZWwgaXogbmVrZWdhIHN0YW5kYXJkbmVnYSB2aXJhIHN0YXRpc3RpxI1uaWggcG9kYXRrb3YsIGtvdCBqZSBbcG9kYXRrb3ZuYSBiYXphIFNpU3RhdF0oaHR0cHM6Ly9weHdlYi5zdGF0LnNpLykgU3RhdGlzdGnEjW5lZ2EgdXJhZGEgUmVwdWJsaWtlIFNsb3ZlbmlqZSBhbGkgcGEgW3BvZGF0a292bmEgYmF6YSBFdXJvc3RhdF0oaHR0cHM6Ly9lYy5ldXJvcGEuZXUvZXVyb3N0YXQvZGF0YS9kYXRhYmFzZSksIGV2cm9wc2tlZ2Egc3RhdGlzdGnEjW5lZ2EgdXJhZGEuIFNwbGV0bmEgZW5jaWtsb3BlZGlqYSBbV2lraXBlZGlhXShodHRwczovL3d3dy53aWtpcGVkaWEub3JnLykgamUgdHVkaSBib2dhdCB2aXIgcG9kYXRrb3ZuaWggdGFiZWwsIGtvdCBqZSBuYSBwcmltZXIgW3RhYmVsYSBzIHBvZGF0a2kgbyBicnV0byBkb21hxI1lbSBwcm9penZvZHUgZHLFvmF2XShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9MaXN0X29mX2NvdW50cmllc19ieV9HRFBfKG5vbWluYWwpKS4KCktvdCBib21vIHNwb3puYWxpIHYgbmFkYWxqZXZhbmp1LCBSIHBvbnVqYSBwcmlyb8SNbmUgZnVua2NpamUgemEgYnJhbmplIHBvZGF0a292IGl6IGRhdG90ZWsgcyBwcmVnbGVkbmljYW1pIEV4Y2VsIGFsaSBpeiBwb2RhdGtvdm5paCB0YWJlbCB2IHNwbGV0bmloIGVuY2lrbG9wZWRpamkgV2lraXBlZGlhLiBQcmlkb2JpdmFuamUgdGFraWggcG9kYXRrb3YgaW4gbmppaG92byBicmFuamUgb3ppcm9tYSB0cmFuc2Zvcm1hY2lqYSB2IFItamV2c2tlIHBvZGF0a292bmUgdGFiZWxlIGplIHphdG8gcmVsYXRpdm5vIGVub3N0YXZuby4gS2VyIHNlIG9iacSNYWpubyB6YXBsZXRlIGplLCBkYSBpbWFqbyBwb2RhdGtpIHByaWRvYmxqZW5pIGl6IHJhemxpxI1uaWggcG9kYXRrb3ZuaWggdmlyb3YgcmF6bGnEjW5vIF9zdHJ1a3R1cm9fLgoKIyMgU3RydWt0dXJhIGluIHBvbWVuIHBvZGF0a292CgpQb2dsZWptbyBzaSBwcmVwcm9zdGkgcHJpbWVyLiBEZW5pbW8sIGRhIG9wYXp1amVtbyBuZWthaiBvYmpla3RvdiwgcHJhdm9rb3RuaWtvdiBpbiBrdmFkcmF0b3YgaW4gcHJpIHRlbSBtZXJpbW8gbmppaG92ZSBkaW1lbnppamUuIFJlenVsdGF0ZSBtZXJpdGV2IHNtbyBzaSB6YXBpc2FsaSB2IG5hc2xlZG5qaSBzZXpuYW06CmBgYHtyfQpvYmpla3RpID0gbGlzdCgKICBvMSA9IGxpc3QoImt2YWRyYXQiLCAzKSwKICBvMiA9IGxpc3QoInByYXZva290bmlrIiwgYygyLCA3KSksCiAgbzMgPSBsaXN0KCJwcmF2b2tvdG5payIsIGMoOSwgNCkpLAogIG80ID0gbGlzdCgia3ZhZHJhdCIsIDgpLAogIG81ID0gbGlzdCgicHJhdm9rb3RuaWsiLCBjKDYsIDUpKQopCmBgYAoKUHJlZGVuIHNlIGxvdGltbyBhbmFsaXplIHBvZGF0a292IG8gb3Bpc2FuaWggb2JqZWt0aWgsIGJpIGppaCBtb3JhbGkgcHJldHZvcml0aSB2IHBvZGF0a292bm8gdGFiZWxvLiBBIHBvZGF0a292bmUgdGFiZWxlIHNvIGxhaGtvIHJhemxpxI1uaWggb2JsaWssIGtpIGppbSByZcSNZW1vIHR1ZGkgX3BvZGF0a292bmUgc3RydWt0dXJlXyAoYW5nbC4gX2RhdGEgc3RydWN0dXJlc18pIGluIHZzYWthIHRhIG9ibGlrYSBvemlyb21hIHN0cnVrdHVyYSBiaSBsYWhrbyBiaWxhIHNwcmVqZW1saml2IG5hxI1pbiB6YXBpc2EgcG9kYXRrb3YgdiBwb2RhdGtvdm5vIHRhYmVsby4gUG9nbGVqbW8gc2kgbmVrYWogcHJpbWVyb3YgcG9kYXRvdm5paCB0YWJlbCB6YSBwb2RhbmUgb2JqZWt0ZS4KClBydmEgcG9kYXRrb3ZuYSB0YWJlbGEgdW1lc3RpIG9iamVrdGUgdiB2cnN0aWNlIHRhYmVsZSwgdHJpIG5qaWhvdmUgbGFzdG5vc3RpIHYgc3RvbHBjZToKYGBge3J9Cm9ibGlrYV9vYmpla3RhID0gZnVuY3Rpb24obykgewogIG9bWzFdXQp9CgpkaW1lbnppamFfb2JqZWt0YSA9IGZ1bmN0aW9uKG8pIHsKICBwYXN0ZTAob1tbMl1dLCBjb2xsYXBzZT0iLSIpCn0KCm9iamVrdGlfZGYxID0gZGF0YS5mcmFtZSgKICBpbWUgPSBuYW1lcyhvYmpla3RpKSwKICBvYmxpa2EgPSBzYXBwbHkob2JqZWt0aSwgRlVOPW9ibGlrYV9vYmpla3RhKSwKICBkaW1lbnppamEgPSBzYXBwbHkob2JqZWt0aSwgRlVOPWRpbWVuemlqYV9vYmpla3RhKQopCm9iamVrdGlfZGYxCmBgYAoKVGEgcG9kYXRrb3ZuYSBzdHJ1a3R1cmEgZG9sb8SNYSB0dWRpICoqcG9tZW4gcG9kYXRrb3YqKiAoYW5nbC4gX2RhdGEgc2VtYW50aWNzXyk6IGVub3RhIG9wYXpvdmFuamEgamUgb2JqZWt0LCBuamVnb3ZlIGxhc3Rub3N0aSBtZXJpbW8gcyB0cmVtaSBzcHJlbWVubGppdmthbWksIGBpbWVgLCBgb2JsaWthYCBpbiBgZGltZW56aWphYC4KClByZWRzdGF2aXRpIGRpbWVuemlqbyBwb3NhbWV6bmVnYSBvYmpla3RhIGtvdCBuaXogem5ha292IG5pIHJhdm5vIGRvYnJhIHBvcG90bmljYSB6YSBtb3JlYml0bmUgbmFkYWxqbmplIGl6cmHEjXVuZSBwcmkgYW5hbGl6aSBwb2RhdGtvdi4gUHJlZHN0YXZsamFqdGUgc2ksIGRhIGJpIHNpIMW+ZWxlbGkgdiBuYXNsZWRuamVtIGtvcmFrdSBhbmFsaXplIGl6cmHEjXVuYXRpIHBvdnLFoWlubyBwb3NhbWV6bmVnYSBvYmpla3RhLiBaYXRvIGJpIHBvdHJlYm92YWxpIMWhaXJpbm8gaW4gdmnFoWlubyBvYmpla3RhIHBvc2ViZWouIERhIHRvIG5hcmVkaW1vLCBiaSBsYWhrbyBwb2RhdGtvbSBwcmlwaXNhbGkgZHJ1Z28gc3RydWt0dXJvIG96aXJvbWEgcG9kYXRrb3ZubyB0YWJlbG86CmBgYHtyfQp2aXNpbmFfb2JqZWt0YSA9IGZ1bmN0aW9uKG8pIHsKICBpZmVsc2UobGVuZ3RoKG9bWzJdXSkgPT0gMSwgb1tbMl1dLCBvW1syXV1bMV0pCn0KCnNpcmluYV9vYmpla3RhID0gZnVuY3Rpb24obykgewogIGlmZWxzZShsZW5ndGgob1tbMl1dKSA9PSAxLCBvW1syXV0sIG9bWzJdXVsyXSkKfQoKb2JqZWt0aV9kZjIgPSBkYXRhLmZyYW1lKAogIGltZSA9IG5hbWVzKG9iamVrdGkpLAogIG9ibGlrYSA9IHNhcHBseShvYmpla3RpLCBGVU49b2JsaWthX29iamVrdGEpLAogIHZpc2luYSA9IHNhcHBseShvYmpla3RpLCBGVU49dmlzaW5hX29iamVrdGEpLAogIHNpcmluYSA9IHNhcHBseShvYmpla3RpLCBGVU49c2lyaW5hX29iamVrdGEpCikKb2JqZWt0aV9kZjIKYGBgCgpUYWtvIHBvZGF0a292bm8gdGFiZWxvIGJpIHBvIHZzZWogdmVyamV0bm9zdGkgcHJpcHJhdmlsaSwgxI1lIGJpIHVwb3JhYmxqYWxpIHByb2dyYW0gRXhjZWwuIFBvbWVuIHBvZGF0a292IHYgdGVqIHBvZGF0a292bmkgdGFiZWxpIGplIHBvZG9iZW4ga290IHBvbWVuIHYgcHJlasWhbmppLCBsZSBkYSB0b2tyYXQgaW1hIHZzYWthIGVub3RhIMWhdGlyaSBsYXN0bm9zdGkgb3ppcm9tYSBzcHJlbWVubGppdmtlIGBpbWVgLCBgb2JsaWthYCwgYHZpc2luYWAgaW4gYHNpcmluYWAuCgpPYmUgcG9kYXRrb3ZuaSB0YWJlbGkgcHJpcHJhdmxqZW5pIHpnb3JhaiBkb21uZXZham8sIGRhIHNvIG9iamVrdGkgemFwaXNhbmUgcG8gdnJzdGljYWgsIGthciBqZSBvYmnEjWFqbmEgZG9tbmV2YSBwcmkgbmHEjXJ0b3Zhbmp1IHN0cnVrdHVyZSBwb2RhdGtvdjogdnJzdGljZSB2IHRhYmVsaSB1c3RyZXpham8gcG9zYW1lem5pbSBlbm90YW0gKG9iamVrdG9tKS4gVnNla2Frb3IgYmkgcGEgbGFoa28gcHJpcHJhdmlsaSBwb2RhdGtvdm5vIHRhYmVsbywga2plciBiaSBvYmpla3RpIGJpbGkgc3ByYXZsamVuaSB2IHN0b2xwY2UsIG5qaWhvdmUgbGFzdG5vc3RpIHBhIHYgdnJzdGljZToKYGBge3J9Cm9iamVrdGlfZGYzID0gZGF0YS5mcmFtZSgKICAiWCIgPSBjKCJpbWUiLCAib2JsaWthIiwgInZpc2luYSIsICJzaXJpbmEiKSwKICBvMSA9IGMoIm8xIiwgImt2YWRyYXQiLCAzLCAzKSwKICBvMiA9IGMoIm8yIiwgInByYXZva290bmlrIiwgMiwgNyksCiAgbzMgPSBjKCJvMyIsICJwcmF2b2tvdG5payIsIDksIDQpLAogIG80ID0gYygibzQiLCAia3ZhZHJhdCIsIDgsIDgpLAogIG81ID0gYygibzUiLCAicHJhdm9rb3RuaWsiLCA2LCA1KQopCm9iamVrdGlfZGYzCmBgYAoKUyB0ZW0gdnByYcWhYW5qZW0gc2Ugc29vxI1hbW8gbmEgemHEjWV0a3UgdnNha2VnYSBwcm9qZWt0YSBhbmFsaXplIHBvZGF0a292LiBPZGdvdm9yb3YgbmEgdG8gdnByYcWhYW5qZSwga290IGthxb5lIHpnb3JuamkgcHJpbWVyLCB2ZWxpa28uIMSMZXByYXYgamUgcHJpbWVyIHplbG8gZW5vc3RhdmVuLiDFoGUgdmXEjSBiaSBqaWggYmlsbywgxI1lIGJpIG9icmF2bmF2YWxpIHJlYWxpc3RpxI1lbiBwcmltZXIgYm9saiB6YXBsZXRlbmloIHBvZGF0a292LgoKQWxpIG9ic3RhamEgc3RhbmRhcmRlbiBvZGdvdm9yIG5hIHRvIHZwcmHFoWFuamU/IE9ic3RhamEuIE1vcmViaXRpIG5lIHJhdm5vIHN0YW5kYXJkLCB0ZW12ZcSNIGRvZ292b3IsIGtpIG11IHJlxI1lbW8gKnVyZWplbmkgcG9kYXRraSogKGFuZ2wuIF90aWR5IGRhdGFfKS4gVG8gamUgZG9nb3ZvciBvIHN0YW5kYXJkbmkgb2JsaWtpIHphcGlzb3ZhbmphIHBvZGF0a292IHYgcG9kYXRrb3ZuZSB0YWJlbGUsIGtpIG9tb2dvxI1hIG5qaWhvdm8gZW5vc3Rhdm5vIGFuYWxpem8gcyBwb21vxI1qbyBmdW5rY2lqIGl6IHpiaXJrZSBwYWtldG92LCBraSBqaWggdnNlYnVqZSBrbmppxb5uaWNhIFtgdGlkeXZlcnNlYF0oaHR0cHM6Ly93d3cudGlkeXZlcnNlLm9yZy8pLgoKIyMgVXJlamVuaSBwb2RhdGtpIChhbmdsLiBfdGlkeSBkYXRhXykKCkRvZ292b3IgbyB1cmVqZW5paCBwb2RhdGtpaCBkb2xvxI1hICoqdHJpIGVub3N0YXZuYSBwcmF2aWxhIHphIHBvZGF0a292bmUgdGFiZWxlKiouCgoxLiBWc2FrYSBzcHJlbWVubGppdmthIHR2b3JpIHN0b2xwZWMgKHJlxI1lbW8gbGFoa28gdHVkaSB1c3RyZXphIHN0b2xwY3UpLgoxLiBWc2FrbyBvcGF6b3ZhbmplIHR2b3JpIHZyc3RpY28uCjEuIFZzYWsgdGlwIG9wYXpvdmFuZSBlbm90ZSB0dm9yaSB0YWJlbG8uCgpQcmkgbmHEjXJ0b3Zhbmp1IG5hxaFpaCBwb2RhdGtvdm5paCB0YWJlbCB6YcSNbmVtbyBzIHByZW1pc2xla29tIG8gdHJldGplbSBwcmF2aWx1LiBLbGp1xI1ubyB2cHJhxaFhbmplIHByaSB0ZW0gcHJlbWlzbGVrdSBqZSAqKkthdGVyaSB0aXBpIG9wYXpvdmFuaWggZW5vdCBzZSBwb2phdmxqYWpvIHYgbmHFoWloIHBvZGF0a2loPyoqIEdsZWRlIG5hIHRvLCBrYWvFoWVuIGplIG9kZ292b3IgbmEgdG8gdnByYcWhYW5qZSwgbGFoa28gemEgb2JqZWt0ZSBpeiBuYcWhZWdhIHByaW1lcmEgc2VzdGF2aW1vIHRhYmVsZSB1cmplbmloIHBvZGF0a292IG5hIGR2YSBuYcSNaW5hLgoKIyMjIFBydmkgbmHEjWluOiBkdmEgdGlwYSBvcGF6b3ZhbmloIGVub3QKClYgbmHFoWVtIHByaW1lcnUgamUgb8SNaXRlbiB0aXAgb3Bhem92YW5lIGVub3RlIG9iamVrdC4gQSBuamVnYSBvcGF6dWplbW8gbmEgZHZhIG5hxI1pbmEsIHBvIGVuaSBzdHJhbmkgb3BhenVqZW1vIGt2YWxpdGF0aXZubyBsYXN0bm9zdCBvYmpla3RhIF9vYmxpa2FfLiBEcnVnaSBuYcSNaW4gb3Bhem92YW5qYSBqZSBrdmFudGl0YXRpdm5hIF9kaW1lbnppamFfIG9iamVrdGEsIGtpIGpvIG1lcmltbyBza296aSBkdmEgcmF6bGnEjW5hIHZpZGlrYSDFoWlyaW5lIGluIHZpxaFpbmUuIEtlciBpbWFtbyBkdmEgbmHEjWluYSBvcGF6b3ZhbmphIG9iamVrdG92LCBsYWhrbyBwcmlwcmF2aW1vIGR2ZSB0YWJlbGk6CgoqIHRhYmVsYSBgb2JsaWthYCBibyB2c2FrZW11IG9iamVrdHUgcHJpcGlzYWxhIG9ibGlrbzsKKiB0YWJlbGEgYGRpbWVuemlqYWAgYm8gdnNha2VtdSBwYXJ1IG9iamVrdGEgaW4gdmlkaWthIGRpbWVuemlqZSAoxaFpcmluZSBhbGkgdmnFoWluZSkgcHJpcGlzYWxhIGl6bWVyamVubyB2cmVkbm9zdCB0ZWdhIHZpZGlrYSBkaW1lbnppamUuCgpUYWJlbGEgYG9ibGlrYWAgaW1hIHRvcmVqIGR2ZSBrdmFsaXRhdGl2bmkgc3ByZW1lbmxqaXZraToKCiogYG9iamVrdGAsIGtpIGRvbG/EjWkgaW1lIG9iamVrdGEgaW4gaW1hIHRvcmVqIGRvbWVubyB7YG8xYCwgYG8yYCwgYG8zYCwgYG80YCBpbiBgbzVgfTsKKiBgdnJlZG5vc3RgLCBraSBkb2xvxI1pIG9ibGlrbyBvYmpla3RhIGluIGltYSBkb21lbm8ge2BrdmFkcmF0YCwgYHByYXZva290bmlrYH0uCgpNZWQgbmppbWEgb2JzdGFqYSBwb21lbWJuYSByYXpsaWthLiBQcnZhIGplICoqZmlrc25hIHNwcmVtZW5saml2a2EqKiwga2F0ZXJlIHZyZWRub3N0IGRvbG/EjWEgcHJlZG1ldCBvcGF6b3ZhbmphLCB2IG5hxaFlbSBwcmltZXJ1IG9iamVrdC4gSW1lbnVqZW1vIGpvIHR1ZGkgKipkaW1lbnppanNrYSBzcHJlbWVubGppdmthKiouIERydWdhIGplICoqbWVyamVuYSBzcHJlbWVubGppdmthKio6IG5qZW5vIHZyZWRub3N0IGRvbG/EjWltbyB6IG9wYXpvdmFuamVtIGFsaSBtZXJqZW5qZW0gaXpicmFuZWdhIHByZWRtZXRhIG9wYXpvdmFuamEsIHYgbmHFoWVtIHByaW1lcnUgeiBvcGF6b3ZhbmplbSAodWdvdGF2bGphbmplbSkgb2JsaWtlIG9iamVrdGEuIEltZW51amVtbyBqbyB0dWRpICoqdnJlZG5vc3RuYSBzcHJlbWVubGppdmthKiouCgpTZXN0YXZpbW8gdG9yZWogcHJ2byB0YWJlbG8gYG9ibGlrYWA6CmBgYHtyfQpvYmxpa2FfZGYgPSBkYXRhLmZyYW1lKAogIG9iamVrdCA9IG5hbWVzKG9iamVrdGkpLAogIHZyZWRub3N0ID0gc2FwcGx5KG9iamVrdGksIEZVTj1vYmxpa2Ffb2JqZWt0YSkKKQpvYmxpa2FfZGYKYGBgCgpEcnVnYSB0YWJlbGEgYGRpbWVuemlqYWAgaW1hIGR2ZSBrdmFsaXRhdGl2bmkgaW4gZW5vIGt2YW50aXRhdGl2bm8gc3ByZW1lbmxqaXZrbzoKCiogYG9iamVrdGAsIGtpIGltYSBlbmFrbyBkb21lbm8ga290IHpnb3JhaiwgdG9yZWoge2BvMWAsIGBvMmAsIGBvM2AsIGBvNGAgaW4gYG81YH07CiogYHZpZGlrYCwga2kgaW1hIGRvbWVubyB7YMWhaXJpbmFgLCBgdmnFoWluYWB9OwoqIGB2cmVkbm9zdGAsIGtpIHBvZGEgdnJlZG5vc3QgaXpicmFuZWdhIHZpZGlrYSBkaW1lbnppamUgKHZpxaFpbmUgYWxpIMWhaXJpbmUpIHogb3Bhem92YW5pIG9iamVrdC4KClYgdGVqIHRhYmVsaSBzdGEgcHJ2aSBkdmUgc3ByZW1lbmxqaXZraSBkaW1lbnppanNraSAoZmlrc25pKSwgc2FqIG5qaWhvdmkgdnJlZG5vc3RpIGRvbG/EjWF0YSB6YSBrYXRlcmkgb2JqZWt0IGluIGthdGVybyBtZXJpdGV2IG9wYXpvdmFuZWdhIG9iamVrdGEgZ3JlLCBuZSBiZWxlxb5pIHBhIHZyZWRub3N0aSBtZXJqZW5qYS4gWmFkbmphIHNwcmVtZW5saml2a2EgamUgcGEgbWVyamVuYSAodnJlZG5vc3RuYSksIHNhaiBiZWxlxb5pIHZyZWRub3N0aSBvcHJhdmxqZW5paCBtZXJqZW5qIGl6YnJhbmVnYSB2aWRpa2EgZGltZW56aWplIG9iamVrdGEuCgpgYGB7cn0KZGltZW56aWphX2RmID0gZXhwYW5kLmdyaWQoCiAgb2JqZWt0ID0gbmFtZXMob2JqZWt0aSksCiAgdmlkaWsgPSBjKCLFoWlyaW5hIiwgInZpxaFpbmEiKSwKICB2cmVkbm9zdCA9IE5BCikKZGltZW56aWphX2RmJHZyZWRub3N0ID0gYygKICBzYXBwbHkob2JqZWt0aSwgRlVOPXNpcmluYV9vYmpla3RhKSwKICBzYXBwbHkob2JqZWt0aSwgRlVOPXZpc2luYV9vYmpla3RhKQopCmRpbWVuemlqYV9kZgpgYGAKCkZ1bmtjaWphIGBleHBhbmQuZ3JpZGAgdiBrb2RpIHpnb3JhaiBwb3NrcmJpIHphIHRvLCBkYSBzZXN0YXZpbW8gcG9kYXRrb3ZubyB0YWJlbG8ga290IGthcnRlemnEjW5pIHByb2R1a3QgZG9tZW4gZGltZW56aWpza2loIHNwcmVtZW5saml2ayBgb2JqZWt0YCBpbiBgdmlkaWtgLCBramVyIHNvIHZzZSB2cmVkbm9zdGkgbWVyamVuZSBzcHJlbWVubGppdmtlIGVuYWtlIGBOQWAuIEtlciBqZSBtb8SNIGRvbWVuZSBzcHJlbWVubGppdmtlIGBvYmpla3RgIGVuYWthIDUsIG1vxI0gZG9tZW5lIGB2aWRpa2AgMiwgaW4gamUgbW/EjSAoemHEjWFzbmUpIGRvbWVuZSBtZXJqZW5lIHNwcmVtZW5saml2a2UgYHZyZWRub3N0YCAxLCBpbWEgcG9kYXRrb3ZuYSB0YWJlbGEsIGtpIGplIHJlenVsdGF0IGtsaWNhIGZ1bmtjaWplIGBleHBhbmQuZ3JpZGAgYDUgKiAyICogMSA9IDEwYCB2cnN0aWMuCgpUYWtvaiB6YXRlbSBpenJhxI11bmFtbyDFoWUgdnJlZG5vc3RpIHphZG5qZSBzcHJlbWVubGppdmtlIGB2cmVkbm9zdGAgdiB0YWJlbGkuIFByaSB0ZW0gdXBvxaF0ZXZhbW8sIGRhIHNlIHBydmloIHBldCB2cnN0aWMgdiBrYXJ0ZXppxI1uZW0gcHJvZHVrdHUgbmFuYcWhYSBuYSDFoWlyaW5vLCBwcmVvc3RhbGloIHBldCBuYSB2acWhaW5vIG9iamVrdG92LgoKT2JpxI1ham5pICoqdnJzdG5pIHJlZCBzdG9scGNldiB2IHRhYmVsaSB6IHVyZWplbmltaSBwb2RhdGtpKiogamUgdGFrLCBkYSAqKmRpbWVuemlqc2tpbSBzcHJlbWVubGppdmthbSBzbGVkaWpvIG1lcmplbmUqKi4gKipWcnN0aWNlKiogc28gcGEgb2JpxI1ham5vICoqdXJlamVuZSBwbyBuYXJhxaHEjWFqb8SNaSB2cmVkbm9zdGkgcHJ2ZSBzcHJlbWVubGppdmtlKiosIG5hdG8gZHJ1Z2UsIHRyZXRqZSBpbiB0YWtvIG5hcHJlai4KClN0b2xwY2kgdiB0YWJlbGkgYGRpbWV6bmlqYWAgc2xlZGlqbyBvYmnEjWFqbmVtdSB2cnN0bmVtdSByZWR1LCB2cnN0aWNlIHBhIG5lLiBMYWhrbyBqaWggdXJlZGltbyB0YWtvbGU6CmBgYHtyfQpkaW1lbnppamFfZGYgPSBkaW1lbnppamFfZGZbb3JkZXIoZGltZW56aWphX2RmJG9iamVrdCwgZGltZW56aWphX2RmJHZpZGlrKSwgXQpkaW1lbnppamFfZGYKYGBgCgpQT1pPUiEgViBwcmltZXJ1LCBrbyBzbyBwb2RhdGtpIHJhemRlbGplbmkgdiB2ZcSNIHBvZGF0a292bmloIHRhYmVsLCBtb3JhbW8gcG9za3JiZXRpLCBkYSBzaSB0ZSB0YWJlbGUgZGVsaWpvIHZzYWoga2FrxaFubyBkaW1lbnppanNrbyBzcHJlbWVubGppdmtvLCBraSBuYW0gYm8gb21vZ2/EjWlsYSwgZGEgemJlcmVtbyBwb2RhdGtlIG8gZW5pIG9wYXpvdmFuaSBlbm90aSBza3VwYWouIFYgemdvcm5qZW0gcHJpbWVydSBqZSB0YWthIHNwcmVtZW5saml2a2EgYG9iamVrdGAsIGtpIG5hbSBvbW9nb8SNYSBkYSBwb3Zlxb5lbW8gcG9kYXRlayBvIG9ibGlraSBvYmpla3RhIChpeiB0YWJlbGUgYG9ibGlrYWApIHMgcG9kYXRraSBvIG5qZWdvdmloIGRpbWVuemlqYWggKHphcGlzYW5pbWkgdiB0YWJlbGkgYGRpbWVuemlqYWApLgoKIyMjIERydWdpIG5hxI1pbjogdHJpamUgdGlwaSBvcGF6b3ZhbmloIGVub3QKClByaSBzZXN0YXZsamFuanUgdGFiZWxlIHogdXJlamVuaW1pIHBvZGF0a2kgYGRpbWVuemlqYWAgc21vIGRvbW5ldmFsaSwgZGEgc3RhIMWhaXJpbmEgaW4gdmnFoWluYSBsZSBkdmEgcmF6bGnEjW5hIHZpZGlrYSBtZXJpdHZlIGRpbWVuemlqZSBvYmpla3RhLiDEjGUgYmkgxaFpcmlubyBpbiB2acWhaW5vIGRvbG/EjWlsaSBrb3QgZHZhIHJhemxpxI1uYSB0aXBhIG9wYXpvdmFuaWggZW5vdCwgYmkga29uxI1hbGkgcyB0cmVtaSB0YWJlbGFtaSB1cmVqZW5paCBwb2RhdGtvdi4gUHJ2YSB0YWJlbGEgYG9ibGlrYWAgYmkgb3N0YWxhIGVuYWthIGtvdCBwcmVqLCB0YWJlbG8gYGRpbWVuemlqYWAgYmkgemFtZW5qYWxpIHogZHZlbWEgdGFiZWxhbWEgYHNpcmluYWAgaW4gYHZpc2luYWAuIE9iZSB0YWJlbGkgYmkgaW1lbGkgZW5hayBuYWJvciBkdmVoIHNwcmVtZW5saml2ayBgb2JqZWt0YCBpbiBgdnJlZG5vc3RgOgpgYGB7cn0Kc2lyaW5hX2RmID0gZGF0YS5mcmFtZSgKICBvYmpla3QgPSBuYW1lcyhvYmpla3RpKSwKICB2cmVkbm9zdCA9IHNhcHBseShvYmpla3RpLCBGVU49c2lyaW5hX29iamVrdGEpCikKc2lyaW5hX2RmCgp2aXNpbmFfZGYgPSBkYXRhLmZyYW1lKAogIG9iamVrdCA9IG5hbWVzKG9iamVrdGkpLAogIHZyZWRub3N0ID0gc2FwcGx5KG9iamVrdGksIEZVTj12aXNpbmFfb2JqZWt0YSkKKQp2aXNpbmFfZGYKYGBgCgpaYSB2c2FrbyBwb2RhdGtvdm5vIHRhYmVsbyB6IHVyZWplbmltaSBwb2RhdGtpLCBraSBpbWEgJGQrMSQgc3RvbHBjZXYsIHZlbGphLCBkYSBwcnZpaCAkZCQgc3RvbHBjZXYgdXN0cmV6YSBkaW1lbnppanNraW0gKGZpa3NuaW0pIHNwcmVtZW5saml2a2FtIHogZG9tZW5hbWkgJERfMSwgRF8yLCBcbGRvdHMsIERfe2R9JCwgemFkbmppIHN0b2xwZWMgcGEgbWVyamVuaSAodnJlZG5vc3RuaSkgc3ByZW1lbmxqaXZraSB6IGRvbWVubyAkRF97ZCsxfSQuIExhaGtvIHRvcmVqIHJlxI1lbW8sIGRhIHVyZWplbmkgcG9kYXRraSBwb2Rham8gdGFiZWxhcmnEjW5vIGRlZmluaWNpam8gZnVua2NpamUgJGY6IERfMSBcdGltZXMgRF8yIFx0aW1lcyBcbGRvdHMgXHRpbWVzIERfe2R9IFx0byBEX3tkKzF9JC4gRnVua2NpamEgJGYkIHphIHBvZGFuZSB2cmVkbm9zdGkgZGltZW56aWpza2loIHNwcmVtZW5saml2ayB2cm5lIHZyZWRub3N0IG1lcmplbmUgc3ByZW1lbmxqaXZrZS4KCk5haiBuYSBrb25jdSBvbWVuaW1vIMWhZSBlbm8gbW/Fvm5vc3QgemEgcG9kYXRrb3ZuZSB0YWJlbGUgeiB1cmVqZW5pbWkgcG9kYXRraS4gxIxlIGltYW1vICRtJCB0YWJlbCB6IGlzdGltaSBkaW1lbnppanNraW1pIHNwcmVtZW5saml2a2FtaSB6IGRvbWVuYW1pICREXzEsIERfMiwgXGxkb3RzLCBEX2QkLCBqaWggbGFoa28gemRydcW+aW1vIHYgZW5vIHBvZGF0a292bm8gdGFiZWxvIHogdXJlamVuaW1pIHBvZGF0a2ksIGtpIGltYSAkZCQgZGltZW56aWpza2loIGluICRtJCBtZXJqZW5paCBzcHJlbWVubGppdmsuIFRha2EgdGFiZWxhIGRlZmluaXJhIGZ1bmtjaWpvICRnOiBEXzEgXHRpbWVzIERfMiBcdGltZXMgXGxkb3RzIFx0aW1lcyBEX3tkfSBcdG8gRF97ZCsxfSBcdGltZXMgRF97ZCsyfSBcdGltZXMgXGxkb3RzIFx0aW1lcyBEX3tkK219JCwga2kgemEgcG9kYW5lIHZyZWRub3N0aSBkaW1lbnppanNraWggc3ByZW1lbmxqaXZrIHZybmUgdnJlZG5vc3RpIHZzZWggJG0kLXRpaCBtZXJqZW5paCBzcHJlbWVubGppdmsgaGtyYXRpLgoKViB6Z29ybmplbSBwcmltZXJ1IHogb2JqZWt0aSwgdnNlIHRyaSBwb2RhdGtvdm5lIHRhYmVsZSB6IHVyZWplbmltaSBwb2RhdGtpIGBvYmxpa2FgLCBgdmlzaW5hYCBpbiBgc2lyaW5hYCBpbWFqbyBpc3RvIGRpbWVuemlqc2tvIHNwcmVtZW5saml2a28gYG9iamVrdGAgaW4gamloIHRvcmVqIGxhaGtvIHpkcnXFvmltbyB2IGVubyBwb2RhdGtvdm5vIHRhYmVsbyBgb2JqZWt0aWAgeiBlbm8gZGltZW56aWpza28gc3ByZW1lbmxqaXZrbyBgb2JqZWt0YCBpbiB0cmVtaSBtZXJqZW5pbWkgc3ByZW1lbmxqaXZrYW1pIGBvYmxpa2FgLCBgc2lyaW5hYCBpbiBgdmlzaW5hYCB0YWtvbGU6CmBgYHtyfQpvYmpla3RpX2RmNCA9IGRhdGEuZnJhbWUoCiAgb2JqZWt0ID0gbmFtZXMob2JqZWt0aSksCiAgb2JsaWthID0gc2FwcGx5KG9iamVrdGksIEZVTj1vYmxpa2Ffb2JqZWt0YSksCiAgc2lyaW5hID0gc2FwcGx5KG9iamVrdGksIEZVTj1zaXJpbmFfb2JqZWt0YSksCiAgdmlzaW5hID0gc2FwcGx5KG9iamVrdGksIEZVTj12aXNpbmFfb2JqZWt0YSkKKQpvYmpla3RpX2RmNApgYGAKClRhIHphZG5qYSByYXpsacSNaWNhIHBvZGF0a292bmUgdGFiZWxlIHR1ZGkgc2xlZGkgc3RhbmRhcmRvbSB1cmVqZW5paCBwb2RhdGtvdiBfdGlkeSBkYXRhXywgcHJpIMSNZW1lciBwcmVkcG9zdGF2aW1vLCBkYSBpbWFtbyBvcHJhdmthIHogZW5pbSB0aXBvbSBvcGF6b3ZhbmUgZW5vdGUgb2JqZWt0LCByYXpsacSNbmUgbmHEjWluZSBvcGF6b3ZhbmphIGluIG1lcml0ZXYgZW5vdCAob2JqZWt0b3YpIHBhIGRvbG/EjWltbyBrb3QgcmF6bGnEjW5lIGxhc3Rub3N0aSBvcGF6b3ZhbmloIGVub3Qgb3ppcm9tYSBzcHJlbWVubGppdmtlLgoKCiMjIFN0YW5kYXJkaSBwb2ltZW5vdmFuagoKSW1lbmEgdGFiZWwgaW4gc3ByZW1lbmxqaXZrIChzdG9scGNldikgbmFqIGJvZG8gemFwaXNhbmUgeiBtYWxpbWkgxI1ya2FtaS4gxIxlIGplIGltZSBzZXN0YXZsamVubyBpeiB2ZcSNIGJlc2VkLCB2bWVzbmUgcHJlc2xlZGtlIHphbWVuamFtbyB6IGBfYCwgbmEgcHJpbWVyIGBpbWVfaW5fcHJpaW1la2AsIG5lIHBhIGBpbWUgaW4gcHJpaW1la2AuIFYgaW1lbmloIHRhYmVsIGluIHNwcmVtZW5saml2ayBzZSBpem9naWJhbW8gxaF1bW5pa29tOiDEjSwgxaEgaW4gxb4gemFtZW5qYW1vIHogYywgcyBpbiB6LCBuYSBwcmltZXIsIGB6YXJpc2NlYCBuYW1lc3RvIGDFvmFyacWhxI1lYC4KClZyZWRub3N0aSB2IHRhYmVsaSB6YXBpxaFlbW8gdGFrbywga290IMW+ZWxpbW8gb3ppcm9tYSB0YWtvLCBrb3Qgc28gcG9kYW5lLCB6IHZlbGlraW1pIGluIG1hbGltaSDEjXJrYW1pLCBzIMWhdW1uaWtpIGluIHByZXNsZWRraS4gUHJpIHRlbSBtb3JhbW8gYml0aSBwb3pvcm5pIG5hIGtvZGlyYW5qZSB6bmFrb3YsIGtpIGdhIHVwb3JhYmxqYSBSLiBQcmV2ZXJpbW8gZ2EgbGFoa28gcyBrbGljZW0gZnVua2NpamUgYGxvY2FsZVRvQ2hhcnNldCgpYDogemHFvmVsZW5hIG5hc3Rhdml0ZXYgamUgYCJVVEYtOCJgLiBQcmkgYnJhbmp1IHBvZGF0a292IGl6IGJlc2VkaWxuaWggYWxpIEV4Y2VsLW92aWggZGF0b3RlayBtb3JhbW8gcG9za3JiZXRpLCBkYSBzZSBwb2RhdGtvdm5lIHZyZWRub3N0aSBtZWQgdXZvem9tIHByZXR2b3Jpam8gdiBrb2RpcmFuamUgem5ha292LCBraSBnYSB1cG9yYWJsamEgUiwgc2ljZXIgYm9kbyDFoXVtbmlraSAoaW4ga2FrxaFuaSBkcnVnaSB6bmFraSkgemFwaXNhbmkgbmFwYcSNbm8uCgoKLS0tCgojIE9zbm92ZSBgdGlkeXZlcnNlYDogVXZveiBpbiBpenZveiBwb2RhdGtvdm5paCB0YWJlbAoKUGFrZXQsIGtpIG9tb2dvxI1hIHByaXByYXZvIGluIGFuYWxpem8gdXJlamVuaWggcG9kYXRrb3YsIG9waXNhbmloIHYgcHJlasWhbmplbSBwb2dsYXZqdSBvbW9nb8SNYSB6Ymlya2EgcGFrZXRvdiwga2kgamloIHZzZWJ1amUga25qacW+bmljYSBbYHRpZHl2ZXJzZWBdKGh0dHBzOi8vd3d3LnRpZHl2ZXJzZS5vcmcvKS4gViB0ZW0gcG9nbGF2anUgYm9tbyBzcG96bmFsaSBmdW5rY2lqZSB6YSB1dm96IGluIGl6dm96IHBvZGF0a292bmloIHRhYmVsLCBraSBqaWggcG9udWphIHRhIGtuamnFvm5pY2EuIE5hanByZWogYm9tbyB1dm96aWxpIGtuamnFvm5pY286CmBgYHtyfQpsaWJyYXJ5KHRpZHl2ZXJzZSkKYGBgCgojIyBGb3JtYXQgQ1NWCgpEYXRvdGVrYSB2IGZvcm1hdHUgQ1NWIHZzZWJ1amUgYmVzZWRpbG5pIHphcGlzIHBvZGF0a292bmUgdGFiZWxlLCBramVyIGplIHZzYWthIHZyc3RpY2EgaXogdGFiZWxlIHphcGlzbmEga290IHZyc3RpY2EgdiBkYXRvdGVraSwgcG9zYW1lem5lIHZyZWRub3N0aSB2IHN0b2xwY2loIChjZWxpY2FoKSB0YWJlbGUgc28gbG/EjWVuZSB6IHZlamljbyBgLGAuCgpGdW5rY2lqYSwga2kgb21vZ2/EjWEgc2hyYW5qZXZhbmplIHBvZGF0a292bmUgdGFiZWxlIHYgZm9ybWF0dSBDU1YgKGFuZ2wuIF9Db21tYS1TZXBhcmF0ZWQgVmFsdWVzXykgamUgYHdyaXRlX2NzdmAuIFphIHBvZGFubyBwb2RhdGtvdm5vIHRhYmVsbyBpbiBpbWUgZGF0b3Rla2UsIGZ1bmtjaWphIHNocmFuaSBwb2RhdGtlIGl6IHBvZGF0a292bmUgdGFiZWxlIHYgZGF0b3Rla28gZm9ybWF0YSBDU1YgcyBwb2RhbmltIGltZW5vbToKYGBge3J9CndyaXRlX2NzdihvYmpla3RpX2RmNCwgIm9iamVrdGkuY3N2IikKYGBgCgpOZWthaiBrb3Jpc3RuaWggYXJndW1lbnRvdiBmdW5rY2lqZSBgd3JpdGVfY3N2YDoKCiogYG5hYCBzIHByaXZ6ZXRvIHZyZWRub3N0am8gYCJOQSJgIHBvZGEgbml6IHpuYWtvdiB6YSB6YXBpc292YW5qZSBuZXpuYW5paCB2cmVkbm9zdGkuCiogYGFwcGVuZGAgcyBwcml2emV0byB2cmVkbm9zdGpvIGBGQUxTRWAgamUgbG9nacSNbmEgdnJlZG5vc3QsIGtpIGRvbG/EjWkgYWxpIG5haiBwb2RhdGtpIHBvdm96aWpvIHByZWrFoW5qbyB2c2ViaW5vIG9ic3RvamXEjWUgZGF0b3Rla2UgKHZyZWRub3N0IGBGQUxTRWApIGFsaSBuYWogc2UgcG9kYXRraSB6YXBpxaFlam8gdiBuYWRhbGpldmFuanUgb2JzdG9qZcSNaWggdnJzdGljIHYgZGF0b3Rla2kgKHZyZWRub3N0IGBUUlVFYCkuCiogYGNvbF9uYW1lc2AgcyBwcml2emV0byB2cmVkbm9zdGpvIGAhYXBwZW5kYCBqZSBsb2dpxI1uYSB2cmVkbm9zdCwga2kgZG9sb8SNaSBhbGkgbmFqIHNlIHYgcHJ2aSB2cnN0aWNpIGRhdG90ZWtlIHphcGnFoWVqbyBpbWVuYSBzdG9scGNldi4KCkZ1bmtjaWphIGByZWFkX2NzdmAgcHJlYmVyZSBwb2RhdGtvdm5vIHRhYmVsbyBpeiBkYXRvdGVrZSBwb2RhbmVnYSBpbWVuYSwga2plciBzbyBwb2RhdGtpIHphcGlzYW5pIHYgZm9ybWF0dSBDU1YuIEtvcmlzdGVuIGFyZ3VtZW50IGZ1bmtjaWplIGplIGBjb2xfbmFtZXNgIHMgcHJpdnpldG8gdnJlZG5vc3RqbyBgVFJVRWAsIGtpIGRvbG/EjWkgYWxpIHBydmkgdnJzdGkgZGF0b3Rla2UgdnNlYnVqZSBpbWVuYSBzdG9scGNldi4KCkRydWdpIHBvZ29zdG8gdXBvcmFibGplbmkgYXJndW1lbnRpIGZ1bmtjaWplIGByZWFkX2NzdmAgdnBsaXZham8gbmEgdGlwZSBzdG9scGNldiB2IHBvZGF0a292bmkgdGFiZWxpIHogdXZvxb5lbmltaSBzdG9scGNpLiBGdW5rY2lqYSBuYW1yZcSNIHNhbW9kZWpubyB1Z290b3ZpIGthdGVyZWdhIHRpcGEgc28gcG9kYXRraSB2IHN0b2xwY3UgdGFrbywgZGEgcHJlZ2xlZGEgcHJ2aWggMTAwMCAodGlzb8SNKSB2cnN0aWMgcHJlZ2xlZG5pY2UuIFRvIGxhaGtvIHNwcmVtZW5pbW8gbmEgZHZhIG5hxI1pbmEsIG9waXNhbmEgdiBuYWRhbGpldmFuanUuCgoqIFogbmFzdGF2aXR2aWpvIGFyZ3VtZW50YSBgZ3Vlc3NfbWF4YCAocHJpdnpldGEgdnJlZG5vc3QgMTAwMCksIGtpIGRvbG/EjWkgxaF0ZXZpbG8gdnJzdGljLCBraSBqaWggcHJlZ2xlZGEgZnVua2NpamEgcHJlZGVuIHNlIG9kbG/EjWkgbyB0aXB1IHZyZWRub3N0aSB2IHN0b2xwY3UuIMSMZSDFvmVsaW1vLCBkYSBmdW5rY2lqYSBwcmVnbGVkYSB2c2UgcmF6cG9sb8W+bGppdmUgdnJlZG5vc3RpLCB1cG9yYWJpbW8gYGd1ZXNzX21heD1JbmZgLgoKKiBaIG5hc3Rhdml0dmlqbyBhcmd1bWVudGEgYGNvbF90eXBlc2AsIGtpIGxhaGtvIHBvdm96aSBzYW1vZGVqbm8gdWdvdGF2bGphbmplIHRpcG92IHN0b2xwY2V2LiBWcmVkbm9zdCBqZSBsYWhrbyB2ZWt0b3Igbml6b3Ygem5ha292IHogbmFzbGVkbmppbWkgdnJlZG5vc3RtaToKCiAgICAqIGAic2tpcCJgOiB2cmVkbm9zdGkgc3RvbHBjYSBuZSB1dmHFvmFtbzsKICAgICogYCJndWVzcyJgOiB0aXAgdnJlZG5vc3RpIGRvbG/EjWkgZnVua2NpamEgc2Ftb2Rlam5vOwogICAgKiBgImxpc3QiYDogdGlwIHZyZWRub3N0aSB2IHN0b3BsY3Ugc2Ugc3ByZW1pbmphOwogICAgKiBgImxvZ2ljYWwiYCwgYCJudW1lcmljImAsIGAidGV4dCJgIGFsaSBgImRhdGUiYDogdnJlZG5vc3RpIHYgc3RvbHBjdSBzbyBsb2dpxI1uZSwgbnVtZXJpxI1uZSwgbml6aSB6bmFrb3YgYWxpIGRhdHVtaS4KCiAgICBWIHByaW1lcnUsIGRhIHphIHZyZWRub3N0IGFyZ3VtZW50YSBgY29sX3R5cGVzYCBwb2RhbW8gbml6IHpuYWtvdiwgcG90ZW0gdGlwIHBvc2FtZXpuZSBrb2xvbmUgdXN0cmV6YSBlbmVtdSB6bmFrdSB2IG5penUuIFBvbWVuIHpuYWtvdiBqZSBgLWAgYWxpIGBfYCB6YSBgc2tpcGAsIGA/YCB6YSBgZ3Vlc3NgLCBgbmAgemEgYG51bWVyaWNgLCBgY2AgemEgYHRleHRgIGluIGBEYCB6YSBgZGF0dW1gLiBDZWxvdGVuIHNlem5hbSBtb8W+bmloIHRpcG92IGluIG9rcmFqxaFhdiBkb2JpbW8geiBgP3JlYWRfY3N2YCBpbiBgP3JlYWRyOjpjb2xzYC4KICAgIAojIyBFeGNlbC1vdmUgcHJlZ2xlZG5pY2UKClYgUi1qdSBsYWhrbyBiZXJlbW8gcG9kYXRrZSBpeiBFeGNlbC1vdmloIHByZWdsZWRuaWMgeiB1cG9yYWJvIHVrYXphIGByZWFkX2V4Y2VsYCB2IGZvcm1hdHUgWExTIGFsaSBYTFNYLCBraSBzZSBkb2xvxI1pIG5hIG9zbm92aSBwb2RhbGrFoWthIHBvZGFuZWdhIGltZW5hIGRhdG90ZWtlLiDEjGUgaG/EjWVtbyB1Z290b3ZpdGkga2F0ZXJpIGxpc3RpIChhbmdsLiBfc2hlZXRzXykgc28gbmEgdm9sam8gdiBkYXRvdGVraSwgdXBvcmFiaW1vIGZ1bmtjaWpvIGBleGNlbF9zaGVldHNgLiBPYmUgZnVua2Npamkga290IHBydmkgYXJndW1lbnQgc3ByZWptZXRhIGltZSBkYXRvdGVrZSB6IEV4Y2VsIHByZWdsZWRuaWNvLgoKTmVrYWoga29yaXN0bmloIHBhcmFtZXRyb3YgZnVua2NpamUgYHJlYWRfZXhjZWxgOgoKKiBgc2hlZXRgIHMgcHJpdnpldG8gdnJlZG5vc3RqbyBgTlVMTGAgZG9sb8SNaSBsaXN0IHByZWdsZWRuaWNlLCBraSBnYSBob8SNZW1vIHByZWJyYXRpLiBWcmVkbm9zdCBqZSBsYWhrbyBjZWxvxaF0ZXZpbMSNbmEgKHBvZGFtbyBwb3ppY2lqbyBsaXN0YSB2IHByZWdsZWRuaWNpKSBhbGkgbml6IHpuYWtvdiAocG9kYW1vIGltZSBsaXN0YSB2IHByZWdsZWRuaWNpKS4gxIxlIHBvZGFuYSB2cmVkbm9zdCBuZSBkb2xvxI1pIGxpc3RhLCBmdW5rY2lqYSBwcmViZXJlIHBvZGF0a2UgaXogcHJ2ZWdhIGxpc3RhIHByZWdsZWRuaWNlLgoqIGByYW5nZWAgcyBwcml2emV0byB2cmVkbm9zdGpvIGBOVUxMYCBkb2xvxI1pIG9ic2VnIGNlbGljIHByZWdsZWRuaWNlLCBrYXRlcmUgdnJlZG5vc3RpIGhvxI1lbW8gdXZveml0aSB2IFIuIFVwb3JhYmxqYW1vIEV4Y2VsLW92IG5hxI1pbiBkb2xvxI1hbmphIG9ic2VnYSwgbnByLiBgIkExOkMxMSJgIGFsaSBwYSBgIlJlenVsdGF0aSFBMTpRMTAyNCJgLiBWIHByaW1lcnUgcG9kYW5lZ2Egb2JzZWdhLCBmdW5rY2lqYSB1dm96aSB0dWRpIHByYXpuZSB2cnN0aWNlIGluIHN0b2xwY2UgaXogdGVnYSBvYnNlZ2EuCiogYGNvbF9uYW1lc2AsIGBndWVzc19tYXhgIGluIGBjb2xfdHlwZXNgIGltYWpvIGlzdGkgcG9tZW4ga290IGlzdG9pbWVuc2tpIGFyZ3VtZW50aSBmdW5rY2lqZSBgcmVhZF9jc3ZgIG9waXNhbmUgdiBwcmVqxaFuamVtIHJhemRlbGt1LgoKClByZWdsZWQgZnVua2NpaiB6YSB1dm96IGluIGl6dm96IHBvZGF0a292bmloIHRhYmVsLCBraSBqaWggcG9udWphIGB0aWR5dmVyc2VgLCBqZSBuYSB2b2xqbyB2IFtwbG9uayBsaXN0dSBfRGF0YSBpbXBvcnQgd2l0aCB0aGUgdGlkeXZlcnNlX10oaHR0cHM6Ly9naXRodWIuY29tL3JzdHVkaW8vY2hlYXRzaGVldHMvYmxvYi9tYWluL2RhdGEtaW1wb3J0LnBkZikuCgotLS0KCiMgTmFsb2dlCgoxLiBEZW5pbW8sIGRhIHNtbyBBbGXFoXUsIEJhcmJhcmksIENpcmlsdSBpbiBEYXJqaSBpem1lcmlsaSB2acWhaW5lIHYgY2VudGltZXRyaWggKDE4MCwgMTY1LCAxNjAsIDE5MykgaW4gdGXFvmUgdiBraWxvZ3JhbWloICg4NywgNTgsIDY1LCAxMDApLiBOYXRvIHNtbyB6YSB2c2FrbyBvc2VibyBpenJhxI11bmFsaSBpbmRla3MgdGVsZXNuZSBtYXNlIChJVE0pIHBvIGZvcm11bGkKJCQgXHRleHR7SVRNfSA9IFxmcmFje1x0ZXh0e3Rlxb5hIHYga2lsb2dyYW1paH19eyhcdGV4dHt2acWhaW5hIHYgbWV0cmlofSleMn0uICQkCkl6IGl6bWVyamVuaWggaXogaXpyYcSNdW5hbmloIHBvZGF0a2Ugc2VzdGF2aSBwb2RhdGtvdm5vIHRhYmVsbyB6IHVyZWplbmltaSBwb2RhdGtpIHRpcGEgX3RpZHkgZGF0YV8uIFByaSB0ZW0gZG9icm8gcHJlbWlzbGkga2FrxaFuZSBtb8W+bm9zdGkgaW1hxaEgbmEgdm9sam86IGthaiBzbyB0aXBpIGVub3QsIHNwcmVtZW5saml2a2UgaW4gdnJzdGljZSB2IHRhYmVsYWggeiB1cmVqZW5pbWkgcG9kYXRraS4KCjEuIFYgZGF0b3Rla2kgW2BpemlkaS54bHN4YF0oLi9pemlkaS54bHN4KSBqZSBwcmVnbGVkbmljYSBzIHBvZGF0a2kgbyBudW1lcmnEjW5paCBpemlkaWggdXBvcmFiZSBkdmVoIHJhemxpxI1uaWggdGVyYXBpaiBuYSB0cmVoIHBhY2llbnRpaC4gUHJhem5lIGNlbGljZSB1c3RyZXpham8gbmV6bmFuaW0gdnJlZG5vc3RpbSBpemlkb3YuIE5hcGnFoWkgcHJvZ3JhbSB2IFItanUsIGtpIHByZWJlcmUgcG9kYXRrZSBpeiBwcmVnbGVkbmljZSBpbiBpeiBuamloIHNlc3RhdmkgcG9kYXRrb3ZubyB0YWJlbG8geiB1cmVqZW5pbWkgcG9kYXRraS4gTmFtaWc6IHJhYmnFoSBzZXN0YXZpdGkgZW5vIHRhYmVsbyB6IGR2ZW1hIGRpbWVuemlqc2tpbWEgaW4gZW5vIG1lcmplbm8gc3ByZW1lbmxqaXZrby4KCjEuIFByb2Zlc29yaWNhLCBraSBuYSBGTUYgaXp2YWphIGR2ZS1zZW1lc3Ryc2tpIHByZWRtZXQsIHNpIGplIHYgZGF0b3Rla2kgW2ByZXp1bHRhdGkta29sb2t2aWpldi54bHN4YF0oLi9yZXp1bHRhdGkta29sb2t2aWpldi54bHN4KSB6YXBpc292YWxhIHJlenVsdGF0ZSBrb2xva3ZpamV2IHYgcHJ2ZW0gaW4gZHJ1Z2VtIHNlbWVzdHJ1LiBOYXBpxaFpIHByb2dyYW0gdiBSLWp1LCBraSBwcmViZXJlIHBvZGF0a2UgaXogdnNlaCBsaXN0b3YgcHJlZ2xlZG5pY2U6IG5haiBwcm9ncmFtIHNhbSB1Z290b3ZpIGtvbGlrbyBsaXN0b3YgaW1hIHByZWdsZWRuaWNhLgoKICAgIE5hdG8gcHJlbWlzbGkga2F0ZXJlIHZzZSBlbm90ZSBvcGF6b3ZhbmphIHJhYmltbyB6YXRvLCBkYSBwcm9mZXNvcmljaSBwb21hZ2FtbyBrb3Jla3RubyBwcmV0dm9yaXRpIHBvZGF0a2UgbyByZXp1bHRhdGloIGtvbG9rdmlqZXYgdiBwb2RhdGtvdm5lIHRhYmVsZSB6IHVyZWplbmltaSBwb2RhdGtpIHRpcGEgX3RpZHkgZGF0YV8sIGtpIGJpIGppIG9tb2dvxI1hbGUgYm9sasWhbyBhbmFsaXpvIHBvZGF0a292LiBSb8SNbm8gYWxpIHMgcHJvZ3JhbW9tIHYgUi1qdSBzZXN0YXZpIHRlIHBvZGF0a292bmUgdGFiZWxlLgoKLS0tCgpadmV6ZWsgc2VtIG9rdG9icmEgMjAyMiBwcmlwcmF2aWwgW0xqdXDEjW8gVG9kb3JvdnNraV0oaHR0cDovL2t0Lmlqcy5zaS9+bGp1cGNvLykgdiBqZXppa3UgW1IgTWFya2Rvd25dKGh0dHA6Ly9ybWFya2Rvd24ucnN0dWRpby5jb20pLiBJZGVqZSB6YSBwcmltZXJlIHNlbSBzaSB2IHZla2lraSBtZXJpIGl6cG9zb2phbCBpeiB1xI1iZW5pa2EgWyhXaWNraGFtIDIwMTkpXShodHRwczovL2Fkdi1yLmhhZGxleS5uei8pLCB2c2UgbW9yZWJpdG5lIG5hcGFrZSB2IHByaW1lcmloIHNvIGl6a2xqdcSNbm8gbW9qZS4KClZlxI0gbyBwcmF2aWxpaCB1cmVqYW5qYSBwb2RhdGtvdiB2IHBvZGF0a292bmUgdGFiZWxlIGxhaGtvIHByZWJlcmV0ZSB2IMSNbGFua3UgV2lnaGFtLCBILiAoMjAxNCkgVGlkeSBEYXRhLiBfSm9ydW5hbCBvZiBTdGF0aXN0aWNhbCBTb2Z0d2FyZSA1OSgxMClfOiAxLTIzLiBET0k6IFsxMC4xODYzNy9qc3MudjA1OS5pMTBdKGh0dHBzOi8vZG9pLm9yZy8xMC4xODYzNy9qc3MudjA1OS5pMTApLg==