library(tidyverse)

Exercice 5.1 : Les prénoms aux USA

  1. Commençons par une solution en R classique
library(babynames)
babynames2000 <- babynames[babynames$year==2000,]
babynames2000_split <- split(babynames2000, babynames2000$sex)
babynames2000_split_top10 <- lapply(babynames2000_split, function(df) {df[order(df$n, decreasing = TRUE)[1:10],]})
babynames2000_top10 <- do.call(rbind, babynames2000_split_top10)
babynames2000_top10

Avec data.table, on peut faire

library(data.table)
babynames_dt <- data.table(babynames)
babynames2000_top10_dt <- babynames_dt[year == 2000,.SD[order(n, decreasing = TRUE)[1:10],], by = sex]
babynames2000_top10_dt

Enfin avec dplyr,

babynames_tbl <- babynames
babynames2000_top10_tbl <- babynames_tbl %>%
  filter(year == 2000) %>% 
  group_by(sex) %>%
  top_n(10, n) %>%
  arrange(desc(n), .by_group = TRUE)
babynames2000_top10_tbl      
  1. Il faut tout d’abord calculé, pour chaque année et chaque sexe, les 2 prénoms les plus utilisées. On somme ensuite les proportions associées à chaque prénom.
  • En R classique ça donne
babynames_split <- split(babynames,list(babynames$sex,babynames$year))
babynames_split_prop <- lapply(babynames_split,
                               function(df) {
                                 df <- df[order(df$prop, decreasing = TRUE)[1:10],]
                                 data.frame(year = df$year[[1]], sex = df$sex[[1]], prop_tot = sum(df$prop))
                               })
babynames_prop <- do.call(rbind, babynames_split_prop)
babynames_prop
  • en data.table
babynames_dt[, .SD[order(prop, decreasing = TRUE)[1:10],],by = c("sex", "year")][, .(prop_tot = sum(prop)), by = c("sex", "year")]
  • enfin en dplyr
babynames %>% group_by(sex,year) %>% top_n(10,n) %>% summarise(sum_prop=sum(prop))
  1. On doit tout d’abord faire une jointure entre la table initiale et la table de la question 1. On somme ensuite les proportions associées à chaue prénom.
  • En R classique ça donne
babynames_merge <- merge(babynames2000_top10[,c("sex", "name")],babynames)
babynames_split <- split(babynames_merge,list(babynames_merge$sex,babynames_merge$year))
babynames_split_prop <- lapply(babynames_split,
                               function(df) {
                                 df <- df[order(df$prop, decreasing = TRUE),]
                                 data.frame(year = df$year[[1]], sex = df$sex[[1]], prop_tot = sum(df$prop))
                               })
babynames_prop <- do.call(rbind, babynames_split_prop)
babynames_prop
  • en data.table
babynames_dt[babynames2000_top10_dt[,c("sex", "name")],, on = c("sex", "name")][, .(prop_tot = sum(prop)), by = c("sex", "year")]
  • enfin en dplyr
left_join(babynames2000_top10_tbl %>% select(sex, name),babynames_tbl) %>% group_by(sex,year) %>% summarize(prop2000=sum(prop))
Joining, by = c("sex", "name")
  1. Pour simuler la base à distance, on commence par copier notre table à l’aide de SQLite
library(DBI)
con <- dbConnect(RSQLite::SQLite())
dbWriteTable(con, name = "babynames",babynames, overwrite = TRUE)

On peut alors lancer des requêtes SQL avec dbGetQuery.

  • Pour la question 1 on a
dbGetQuery(con,
           'SELECT * FROM
           (SELECT *, RANK() OVER (PARTITION BY "sex" ORDER BY "n" DESC) as rank  FROM  babynames WHERE ("year" = 2000)) as TMP
           WHERE rank <= 10')
  • pour la question 2
dbGetQuery(con,
           'WITH babynames_top AS
           (SELECT * 
           FROM
           (SELECT *, RANK() OVER (PARTITION BY sex,  "year" ORDER BY n DESC) as rank  FROM  babynames) as TMP
           WHERE rank <= 10)
           
           SELECT "year", sex, SUM(prop) as prop_tot FROM
           babynames_top 
           GROUP BY "year", sex')
  • et enfin la question 3
dbGetQuery(con,
           'WITH babynames_top2000 AS (
           SELECT * 
           FROM
           (SELECT *, RANK() OVER (PARTITION BY "sex" ORDER BY "n" DESC) as rank  FROM  babynames WHERE ("year" = 2000)) as TMP
           WHERE rank <= 10
           ), 
           babynames_merge AS (
            SELECT  babynames."year" as "year",
                    babynames.sex as sex,
                    babynames.name as name,
                    babynames.n as n,
                    babynames.prop as prop
            FROM 
            babynames_top2000
            LEFT JOIN 
            babynames ON (babynames_top2000.sex = babynames.sex AND babynames_top2000.name = babynames.name))
            SELECT "year", sex, SUM(prop) as prop_tot FROM
            babynames_merge 
            GROUP BY "year", sex')

Une alternative aux requêtes SQL est l’utilisation du package dbplyr :

babynames_sql <- tbl(con, "babynames")
  • On obtient pour la question 1
babynames2000_sql <- babynames_sql %>%
  filter(year == 2000) %>% 
  group_by(sex) 

babynames2000_top10_sql <- babynames2000_sql %>% collect() %>% top_n(10,n)
babynames2000_top10_sql
  • Pour la question 2
babynames_sql %>% group_by(sex,year) %>% arrange(desc(n),.by_group=TRUE) %>% collect() %>% top_n(10,n) %>% summarise(sum_prop=sum(prop))
  • et enfin pour la question 3
dbWriteTable(con, name = "babynames2000_top10_sql",babynames2000_top10_sql, overwrite = TRUE)
babynames2000_top10_sql <- tbl(con, "babynames2000_top10_sql")

left_join(babynames2000_top10_sql %>% select(sex, name),babynames_sql) %>% group_by(sex,year) %>% summarize(prop2000=sum(prop))

Et on n’oublie pas de se déconnecter !

dbDisconnect(con)

Exercice 5.2 : Les tournois majeurs au tennis en 2013

  1. On utilise read_csv pour travailler avec un tibble.
df <- read_csv("FrenchOpen-men-2013.csv")
df %>% distinct(Player1)
df %>% distinct(Player2)
df %>% filter(Player1=="Roger Federer" | Player2=="Roger Federer") %>% select(contains("Play"))
df %>% filter(Round==6) %>% select(contains("Play"))
df %>% mutate(nb_points=TPW.1+TPW.2) %>% summarise(nb_points_moy=mean(nb_points))
df %>% mutate(nb_ace=ACE.1+ACE.2) %>% group_by(Round) %>% 
  summarise(min=min(nb_ace),max=max(nb_ace),moy=mean(nb_ace))
df %>% mutate(dbf=DBF.1+DBF.2) %>% summarize(tot.df=sum(dbf,na.rm=TRUE))
df %>% mutate(dbf=DBF.1+DBF.2) %>% ggplot() + aes(x=dbf) + geom_histogram(bins=10) + theme_bw()

df %>% mutate(dbf=DBF.1+DBF.2) %>% group_by(Round) %>% summarize(dbf=mean(dbf,na.rm=TRUE)) %>%
  ggplot() + aes(x=Round,y=dbf) + geom_bar(stat="identity",fill="red") + theme_classic()

df %>% select(Result,FSP.1,FSP.2) %>% gather(key="Player",value="FSP",-Result) %>% 
  mutate(Result=as.character((Result==1 & Player=="FSP.1") | (Result==0 & Player=="FSP.2"))) %>% 
  mutate(Result=fct_recode(Result,vic="TRUE",def="FALSE")) %>%
  ggplot() + aes(x=Result,y=FSP)+geom_boxplot()+theme_classic()

Il semble qu’il y ait une légère influence du pourcentage de premier service sur le résultat. On effectue un test de comparaison de moyennes pour vérifier :

df1 <- df %>% select(Result,FSP.1,FSP.2) %>% gather(key="Player",value="FSP",-Result) %>% 
  mutate(Result=as.character((Result==1 & Player=="FSP.1") | (Result==0 & Player=="FSP.2"))) %>% 
  mutate(Result=fct_recode(Result,vic="TRUE",def="FALSE"))
t.test(FSP~Result,data=df1)

    Welch Two Sample t-test

data:  FSP by Result
t = -1.8395, df = 247.3, p-value = 0.06704
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
 -3.6279302  0.1239302
sample estimates:
mean in group def mean in group vic 
           61.536            63.288 

Au niveau 10%, on concluera qu’il y a une influence.

Exercice 5.3 : Le vélo STAR, encore !

  1. On importe les tables
url <- paste0(
"https://data.rennesmetropole.fr/api/records/1.0/search/",
"?dataset=etat-des-stations-le-velo-star-en-temps-reel",
"&rows=100",
"&facet=nom",
"&facet=etat",
"&facet=nombreemplacementsactuels",
"&facet=nombreemplacementsdisponibles",
"&facet=nombrevelosdisponibles",
"&facet=coordonnees"
)
ll <- jsonlite::fromJSON(url)
etat <- ll$records$fields
url1 <- paste0(
"https://data.rennesmetropole.fr/api/records/1.0/search/",
"?dataset=topologie-des-stations-le-velo-star",
"&rows=100")
ll1 <- jsonlite::fromJSON(url1)
topo <- ll1$records$fields
  1. On créé la table demandée
glimpse(etat)
Observations: 55
Variables: 8
$ etat                          <chr> "En fonctionnement", "En fonctionnement", "En fonctionnement", "En fonctionnement", "En fonctionnement", "En fonctionne…
$ lastupdate                    <chr> "2020-03-15T16:59:10+00:00", "2020-03-15T16:59:10+00:00", "2020-03-15T16:59:10+00:00", "2020-03-15T16:59:10+00:00", "20…
$ nombrevelosdisponibles        <int> 6, 3, 21, 10, 14, 4, 8, 1, 9, 16, 15, 5, 12, 13, 4, 0, 15, 7, 27, 8, 1, 8, 9, 9, 10, 11, 3, 6, 6, 11, 20, 15, 11, 7, 4,…
$ nombreemplacementsactuels     <int> 16, 24, 23, 24, 20, 24, 18, 18, 16, 28, 20, 24, 24, 26, 28, 18, 20, 20, 30, 28, 16, 28, 20, 20, 24, 16, 30, 18, 14, 25,…
$ nom                           <chr> "Musée Beaux-Arts", "Champs Libres", "La Criée", "Brest - Verdun", "Marbeuf", "Plaine de Baud", "Gros-Chêne", "Painlevé…
$ nombreemplacementsdisponibles <int> 10, 21, 2, 14, 6, 20, 10, 17, 7, 12, 5, 19, 12, 13, 24, 18, 5, 13, 3, 20, 15, 20, 11, 11, 14, 5, 27, 12, 8, 14, 4, 7, 5…
$ idstation                     <chr> "5510", "5516", "5521", "5534", "5538", "5546", "5578", "5580", "5581", "5582", "5587", "5502", "5505", "5506", "5512",…
$ coordonnees                   <list> [<48.10960, -1.67408>, <48.105537, -1.674328>, <48.107626, -1.680085>, <48.113009, -1.693264>, <48.111749, -1.702077>,…
latlong <- etat$coordonnees %>% unlist() %>% matrix(ncol=2,byrow=T) %>% as.data.frame()
names(latlong) <- c("latitude","longitude")
etat1 <- bind_cols(etat,latlong) %>% select(-coordonnees)
df <- inner_join(etat1,topo,by="nom") %>% select(id,nom,idstationproche1,latitude,longitude) %>% mutate(id=as.numeric(id))
df1 <- left_join(df,df,by=c("idstationproche1"="id")) %>% mutate(dist=(latitude.x-latitude.y)^2+(longitude.x-longitude.y)^2) 
  1. On calcule les stations les plus proches du point demandé
etat1 %>% mutate(distance=(latitude-48.1179151)^2+(longitude+1.7028661)^2) %>% arrange(distance) %>% head(3) %>%
  arrange(distance,nombreemplacementsdisponibles) %>% select(nom,distance,nombreemplacementsdisponibles)

Exercice 5.4 : Se cultiver par hasard

library(rvest)
  1. On se connecte à la page wikipedia
wikipedia <- "https://fr.wikipedia.org/"
accueil <- "wiki/Wikipédia:Accueil_principal"
url0 <- paste0(wikipedia,accueil)
data_html <- read_html(url0)
  1. On obtient les liens de la page avec
internal_links <- 'a[href*="/wiki/"]:not([href*="//"])'
lien0 <- data_html %>% html_nodes(internal_links) %>% html_attr("href")
head(lien0)
[1] "/wiki/Wikip%C3%A9dia:Accueil_de_la_communaut%C3%A9" "/wiki/Aide:D%C3%A9buter"                           
[3] "/wiki/Portail:Accueil"                              "/wiki/Wikip%C3%A9dia:Principes_fondateurs"         
[5] "/wiki/Aide:Accueil"                                 "/wiki/Aide:Poser_une_question"                     
  1. On choisit une page au hasard
set.seed(123)
lien_sel <- lien0 %>% sample(1)
lien_sel
[1] "/wiki/Guerre_civile_syrienne"

que l’on lit puis, sur laquelle on rechoisit une page au hasard

set.seed(1234)
url1 <- paste0(wikipedia,lien_sel)
read_html(url1) %>% html_nodes(internal_links) %>% html_attr("href") %>% sample(1)
[1] "/wiki/Front_islamique_de_lib%C3%A9ration_syrien"
  1. On peut écrire une fonction qui sélectionne un lien aléatoirement sur une page donnée :
select_link <- function(url){
  read_html(url) %>%
    html_nodes(internal_links) %>%
    html_attr("href") %>%
    sample(1)
}

On peut ainsi en déduire les pages visitées pendand un nombre de jour donnée :

library(magrittr)
pages_visitees <- function(nb_jours){
  liens_visites <- c()
  for(i in 1:nb_jours){
    wikipedia %>%
      select_link() %T>%
      {liens_visites <<- append(liens_visites, .)} %>%
      str_c(wikipedia, .) %>%
      select_link() %>%
      {liens_visites <<- append(liens_visites, .)}
  }
  return(unique(liens_visites))
}
  1. Par exemple en 10 ans on aura visité :
pages_10ans <- pages_visitees(3652)
head(pages_10ans)
[1] "/wiki/%C3%89lection_pr%C3%A9sidentielle_fran%C3%A7aise_de_1981" "/wiki/Syst%C3%A8me_mon%C3%A9taire_europ%C3%A9en"               
[3] "/wiki/Wikip%C3%A9dia:V%C3%A9rifiabilit%C3%A9"                   "/wiki/Wikip%C3%A9dia:R%C3%A8gles_et_recommandations"           
[5] "/wiki/44_av._J.-C."                                             "/wiki/Marcus_Junius_Brutus"                                    
length(pages_10ans)
[1] 2583
  1. On peut renouveler le protocole 20 fois (il faudrait plus mais c’est très long !):
set.seed(1234)
nb_visite <- rep(0,20)
for (i in 1:20){
  nb_visite[i] <- pages_visitees(3652) %>% length()
}

On peut regarder le résumé

summary(nb_visite)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   2520    2545    2563    2566    2585    2629 

et visualiser l’histogramme

Exercice 5.5 : Un peu de musique

  1. On se connecte à la base de données et on inspecte les différentes bases.
library(DBI)
conn <- dbConnect(RSQLite::SQLite(), dbname = "Chinook_Sqlite.sqlite")
dbListTables(conn)
 [1] "Album"         "Artist"        "Customer"      "Employee"      "Genre"         "Invoice"       "InvoiceLine"   "MediaType"     "Playlist"     
[10] "PlaylistTrack" "Track"        
dbListFields(conn,"Playlist")
[1] "PlaylistId" "Name"      
dbListFields(conn,"PlaylistTrack")
[1] "PlaylistId" "TrackId"   
dbListFields(conn,"Track")
[1] "TrackId"      "Name"         "AlbumId"      "MediaTypeId"  "GenreId"      "Composer"     "Milliseconds" "Bytes"        "UnitPrice"   
dbListFields(conn,"Album")
[1] "AlbumId"  "Title"    "ArtistId"
  1. On affecte les tables de la base de données à une liste de tibbles. On pourrait croire que c’est très gourmand en mémoire pour de gros tableaux mais en fait pas du tout. C’est juste une liste d’objets qui permettent de faire une connexion avec la base de données. Donc on peut il aller gaiment !
musique <- list()
for(name in dbListTables(conn)){
  musique[[name]] <- tbl(conn, name)
}
names(musique)
 [1] "Album"         "Artist"        "Customer"      "Employee"      "Genre"         "Invoice"       "InvoiceLine"   "MediaType"     "Playlist"     
[10] "PlaylistTrack" "Track"        

La requête (après avoir regardé un peu la structure des tables) est la suivante

result <- musique$Playlist %>%
  filter(Name == "Classical") %>%
  left_join(musique$PlaylistTrack, by = "PlaylistId") %>%
  left_join(musique$Track, by = "TrackId") %>%
  select(Song = Name.y, AlbumId) %>%
  left_join(musique$Album, by = "AlbumId") %>%
  select(Song, Album = Title)

On regarde les résultats

result %>% collect()
  1. On peut regarder la requête SQL
result %>% show_query()
<SQL>
SELECT `Song`, `Title` AS `Album`
FROM (SELECT `LHS`.`Song` AS `Song`, `LHS`.`AlbumId` AS `AlbumId`, `RHS`.`Title` AS `Title`, `RHS`.`ArtistId` AS `ArtistId`
FROM (SELECT `Name.y` AS `Song`, `AlbumId`
FROM (SELECT `LHS`.`PlaylistId` AS `PlaylistId`, `LHS`.`Name` AS `Name.x`, `LHS`.`TrackId` AS `TrackId`, `RHS`.`Name` AS `Name.y`, `RHS`.`AlbumId` AS `AlbumId`, `RHS`.`MediaTypeId` AS `MediaTypeId`, `RHS`.`GenreId` AS `GenreId`, `RHS`.`Composer` AS `Composer`, `RHS`.`Milliseconds` AS `Milliseconds`, `RHS`.`Bytes` AS `Bytes`, `RHS`.`UnitPrice` AS `UnitPrice`
FROM (SELECT `LHS`.`PlaylistId` AS `PlaylistId`, `LHS`.`Name` AS `Name`, `RHS`.`TrackId` AS `TrackId`
FROM (SELECT *
FROM `Playlist`
WHERE (`Name` = 'Classical')) AS `LHS`
LEFT JOIN `PlaylistTrack` AS `RHS`
ON (`LHS`.`PlaylistId` = `RHS`.`PlaylistId`)
) AS `LHS`
LEFT JOIN `Track` AS `RHS`
ON (`LHS`.`TrackId` = `RHS`.`TrackId`)
)) AS `LHS`
LEFT JOIN `Album` AS `RHS`
ON (`LHS`.`AlbumId` = `RHS`.`AlbumId`)
)

Et on n’oublie pas de se déconnecter !

dbDisconnect(conn)

Exercice 5.6 : Du trampoline sur Wikipedia

  1. On récupère tout d’abord le tableau
url <- "https://fr.wikipedia.org/wiki/Liste_des_médaillés_olympiques_au_trampoline"
tramp <- read_html(url)
tab <- tramp %>% html_table()
tramp.f <- tab[[2]]
  1. On créé le dataframe
tramp.f1 <- tramp.f %>% gather(key="med",value="ath_p",Or,Argent,Bronze) %>%
  separate(ath_p,into=c("nom","pays"),sep="\\(") %>% mutate(pays=str_remove(pays,"\\)"))
tramp.f1

3.On le classe en fonction des médailles

tramp.f1 %>% group_by(pays,med) %>% summarize(nomb=n()) %>% spread(med,nomb) %>% select(c(1,4,2,3)) %>%
  arrange(desc(Or,Argent,Bronze))
  1. On fait la même chose pour les hommes
tramp.h <- tab[[1]]
tramp.h1 <- tramp.h %>% gather(key="med",value="ath_p",Or,Argent,Bronze) %>%
  separate(ath_p,into=c("nom","pays"),sep="\\(") %>% mutate(pays=str_remove(pays,"\\)"))
tramp.h1 %>% group_by(pays,med) %>% summarize(nomb=n()) %>% spread(med,nomb) %>% select(c(1,4,2,3)) %>%
  arrange(desc(Or,Argent,Bronze))

et enfin pour le tableau mixte

tramp.fh <- bind_rows(tab[[1]],tab[[2]])
tramp.fh1 <- tramp.fh %>% gather(key="med",value="ath_p",Or,Argent,Bronze) %>%
  separate(ath_p,into=c("nom","pays"),sep="\\(") %>% mutate(pays=str_remove(pays,"\\)"))
tramp.fh1 %>% group_by(pays,med) %>% summarize(nomb=n()) %>% spread(med,nomb) %>% select(c(1,4,2,3)) %>%
  arrange(desc(Or,Argent,Bronze))

Exercice 5.7 : Débouchés dans l’enseignement supérieur français

df <- jsonlite::fromJSON("fr-esr-insertion_professionnelle-master.json")$fields
df1 <- df %>% select(etablissement,salaire_net_median_des_emplois_a_temps_plein,salaire_net_mensuel_median_regional) %>%
  mutate(salaire_net_median_des_emplois_a_temps_plein=as.numeric(salaire_net_median_des_emplois_a_temps_plein),
         salaire_net_mensuel_median_regional=as.numeric(salaire_net_mensuel_median_regional))
NAs introduced by coercionNAs introduced by coercion
df2 <- df1 %>% group_by(etablissement) %>% summarize(sal.temps.plein=mean(salaire_net_median_des_emplois_a_temps_plein,na.rm=TRUE),
                                              sal.reg=mean(salaire_net_mensuel_median_regional,na.rm=TRUE))
df2
  1. On classe selon le ratio demandé.
df2 %>% mutate(ratio=sal.temps.plein/sal.reg) %>% arrange(desc(ratio))
LS0tCnRpdGxlOiAiQ2hhcGl0cmUgNSIKb3V0cHV0OiAKICBodG1sX25vdGVib29rOgogICAgY3NzOiB+L0Ryb3Bib3gvRklDSElFUlNfU1RZTEUvc3R5bGVzLmNzcwogICAgdG9jOiB5ZXMKICAgIHRvY19mbG9hdDogeWVzCiAgaHRtbF9kb2N1bWVudDoKICAgIGNzczogfi9Ecm9wYm94L0ZJQ0hJRVJTX1NUWUxFL3N0eWxlcy5jc3MKICAgIGRmX3ByaW50OiBwYWdlZAogICAgdG9jOiB5ZXMKLS0tCgoKCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CmxpYnJhcnkodGlkeXZlcnNlKQpgYGAKCiMjIEV4ZXJjaWNlIDUuMSA6IExlcyBwcsOpbm9tcyBhdXggVVNBCgoxLiBDb21tZW7Dp29ucyBwYXIgdW5lIHNvbHV0aW9uIGVuIFIgY2xhc3NpcXVlCgpgYGB7cixtZXNzYWdlPUZBTFNFLHdhcm5pbmc9RkFMU0V9CmxpYnJhcnkoYmFieW5hbWVzKQpiYWJ5bmFtZXMyMDAwIDwtIGJhYnluYW1lc1tiYWJ5bmFtZXMkeWVhcj09MjAwMCxdCmJhYnluYW1lczIwMDBfc3BsaXQgPC0gc3BsaXQoYmFieW5hbWVzMjAwMCwgYmFieW5hbWVzMjAwMCRzZXgpCmJhYnluYW1lczIwMDBfc3BsaXRfdG9wMTAgPC0gbGFwcGx5KGJhYnluYW1lczIwMDBfc3BsaXQsIGZ1bmN0aW9uKGRmKSB7ZGZbb3JkZXIoZGYkbiwgZGVjcmVhc2luZyA9IFRSVUUpWzE6MTBdLF19KQpiYWJ5bmFtZXMyMDAwX3RvcDEwIDwtIGRvLmNhbGwocmJpbmQsIGJhYnluYW1lczIwMDBfc3BsaXRfdG9wMTApCmJhYnluYW1lczIwMDBfdG9wMTAKYGBgCgpBdmVjICoqZGF0YS50YWJsZSoqLCBvbiBwZXV0IGZhaXJlCgpgYGB7cixtZXNzYWdlPUZBTFNFLHdhcm5pbmc9RkFMU0V9CmxpYnJhcnkoZGF0YS50YWJsZSkKYmFieW5hbWVzX2R0IDwtIGRhdGEudGFibGUoYmFieW5hbWVzKQpiYWJ5bmFtZXMyMDAwX3RvcDEwX2R0IDwtIGJhYnluYW1lc19kdFt5ZWFyID09IDIwMDAsLlNEW29yZGVyKG4sIGRlY3JlYXNpbmcgPSBUUlVFKVsxOjEwXSxdLCBieSA9IHNleF0KYmFieW5hbWVzMjAwMF90b3AxMF9kdApgYGAKCkVuZmluIGF2ZWMgKipkcGx5cioqLAoKYGBge3IsbWVzc2FnZT1GQUxTRSx3YXJuaW5nPUZBTFNFfQpiYWJ5bmFtZXNfdGJsIDwtIGJhYnluYW1lcwpiYWJ5bmFtZXMyMDAwX3RvcDEwX3RibCA8LSBiYWJ5bmFtZXNfdGJsICU+JQogIGZpbHRlcih5ZWFyID09IDIwMDApICU+JSAKICBncm91cF9ieShzZXgpICU+JQogIHRvcF9uKDEwLCBuKSAlPiUKICBhcnJhbmdlKGRlc2MobiksIC5ieV9ncm91cCA9IFRSVUUpCmJhYnluYW1lczIwMDBfdG9wMTBfdGJsICAgICAgCmBgYAoKMi4gSWwgZmF1dCB0b3V0IGQnYWJvcmQgY2FsY3Vsw6ksIHBvdXIgY2hhcXVlIGFubsOpZSBldCBjaGFxdWUgc2V4ZSwgbGVzIDIgcHLDqW5vbXMgbGVzIHBsdXMgdXRpbGlzw6llcy4gT24gc29tbWUgZW5zdWl0ZSBsZXMgcHJvcG9ydGlvbnMgYXNzb2Npw6llcyDDoCBjaGFxdWUgcHLDqW5vbS4KCiAgKiBFbiBSIGNsYXNzaXF1ZSDDp2EgZG9ubmUgCiAgCmBgYHtyfQpiYWJ5bmFtZXNfc3BsaXQgPC0gc3BsaXQoYmFieW5hbWVzLGxpc3QoYmFieW5hbWVzJHNleCxiYWJ5bmFtZXMkeWVhcikpCmJhYnluYW1lc19zcGxpdF9wcm9wIDwtIGxhcHBseShiYWJ5bmFtZXNfc3BsaXQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBmdW5jdGlvbihkZikgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkZiA8LSBkZltvcmRlcihkZiRwcm9wLCBkZWNyZWFzaW5nID0gVFJVRSlbMToxMF0sXQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkYXRhLmZyYW1lKHllYXIgPSBkZiR5ZWFyW1sxXV0sIHNleCA9IGRmJHNleFtbMV1dLCBwcm9wX3RvdCA9IHN1bShkZiRwcm9wKSkKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0pCmJhYnluYW1lc19wcm9wIDwtIGRvLmNhbGwocmJpbmQsIGJhYnluYW1lc19zcGxpdF9wcm9wKQpiYWJ5bmFtZXNfcHJvcApgYGAKICAKICAqIGVuICoqZGF0YS50YWJsZSoqCiAgCmBgYHtyfQpiYWJ5bmFtZXNfZHRbLCAuU0Rbb3JkZXIocHJvcCwgZGVjcmVhc2luZyA9IFRSVUUpWzE6MTBdLF0sYnkgPSBjKCJzZXgiLCAieWVhciIpXVssIC4ocHJvcF90b3QgPSBzdW0ocHJvcCkpLCBieSA9IGMoInNleCIsICJ5ZWFyIildCmBgYAogIAogICogZW5maW4gZW4gKipkcGx5cioqCiAgCmBgYHtyfQpiYWJ5bmFtZXMgJT4lIGdyb3VwX2J5KHNleCx5ZWFyKSAlPiUgdG9wX24oMTAsbikgJT4lIHN1bW1hcmlzZShzdW1fcHJvcD1zdW0ocHJvcCkpCmBgYAogIAoKMy4gT24gZG9pdCB0b3V0IGQnYWJvcmQgZmFpcmUgdW5lIGpvaW50dXJlIGVudHJlIGxhIHRhYmxlIGluaXRpYWxlIGV0IGxhIHRhYmxlIGRlIGxhIHF1ZXN0aW9uIDEuIE9uIHNvbW1lIGVuc3VpdGUgbGVzIHByb3BvcnRpb25zIGFzc29jacOpZXMgw6AgY2hhdWUgcHLDqW5vbS4KCiAgKiBFbiBSIGNsYXNzaXF1ZSDDp2EgZG9ubmUKICAKYGBge3J9CmJhYnluYW1lc19tZXJnZSA8LSBtZXJnZShiYWJ5bmFtZXMyMDAwX3RvcDEwWyxjKCJzZXgiLCAibmFtZSIpXSxiYWJ5bmFtZXMpCmJhYnluYW1lc19zcGxpdCA8LSBzcGxpdChiYWJ5bmFtZXNfbWVyZ2UsbGlzdChiYWJ5bmFtZXNfbWVyZ2Ukc2V4LGJhYnluYW1lc19tZXJnZSR5ZWFyKSkKYmFieW5hbWVzX3NwbGl0X3Byb3AgPC0gbGFwcGx5KGJhYnluYW1lc19zcGxpdCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGZ1bmN0aW9uKGRmKSB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRmIDwtIGRmW29yZGVyKGRmJHByb3AsIGRlY3JlYXNpbmcgPSBUUlVFKSxdCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRhdGEuZnJhbWUoeWVhciA9IGRmJHllYXJbWzFdXSwgc2V4ID0gZGYkc2V4W1sxXV0sIHByb3BfdG90ID0gc3VtKGRmJHByb3ApKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSkKYmFieW5hbWVzX3Byb3AgPC0gZG8uY2FsbChyYmluZCwgYmFieW5hbWVzX3NwbGl0X3Byb3ApCmJhYnluYW1lc19wcm9wCmBgYAogIAogICogZW4gKipkYXRhLnRhYmxlKioKICAKYGBge3J9CmJhYnluYW1lc19kdFtiYWJ5bmFtZXMyMDAwX3RvcDEwX2R0WyxjKCJzZXgiLCAibmFtZSIpXSwsIG9uID0gYygic2V4IiwgIm5hbWUiKV1bLCAuKHByb3BfdG90ID0gc3VtKHByb3ApKSwgYnkgPSBjKCJzZXgiLCAieWVhciIpXQpgYGAKICAKICAqIGVuZmluIGVuICoqZHBseXIqKgoKYGBge3J9CmxlZnRfam9pbihiYWJ5bmFtZXMyMDAwX3RvcDEwX3RibCAlPiUgc2VsZWN0KHNleCwgbmFtZSksYmFieW5hbWVzX3RibCkgJT4lIGdyb3VwX2J5KHNleCx5ZWFyKSAlPiUgc3VtbWFyaXplKHByb3AyMDAwPXN1bShwcm9wKSkKYGBgCgo0LiBQb3VyIHNpbXVsZXIgbGEgYmFzZSDDoCBkaXN0YW5jZSwgb24gY29tbWVuY2UgcGFyIGNvcGllciBub3RyZSB0YWJsZSDDoCBsJ2FpZGUgZGUgKipTUUxpdGUqKgoKYGBge3J9CmxpYnJhcnkoREJJKQpjb24gPC0gZGJDb25uZWN0KFJTUUxpdGU6OlNRTGl0ZSgpKQpkYldyaXRlVGFibGUoY29uLCBuYW1lID0gImJhYnluYW1lcyIsYmFieW5hbWVzLCBvdmVyd3JpdGUgPSBUUlVFKQpgYGAKCgoKT24gcGV1dCBhbG9ycyBsYW5jZXIgZGVzIHJlcXXDqnRlcyBTUUwgYXZlYyAqZGJHZXRRdWVyeSouIAoKICAqIFBvdXIgbGEgcXVlc3Rpb24gMSBvbiBhCiAgCmBgYHtyfQpkYkdldFF1ZXJ5KGNvbiwKICAgICAgICAgICAnU0VMRUNUICogRlJPTQogICAgICAgICAgIChTRUxFQ1QgKiwgUkFOSygpIE9WRVIgKFBBUlRJVElPTiBCWSAic2V4IiBPUkRFUiBCWSAibiIgREVTQykgYXMgcmFuayAgRlJPTSAgYmFieW5hbWVzIFdIRVJFICgieWVhciIgPSAyMDAwKSkgYXMgVE1QCiAgICAgICAgICAgV0hFUkUgcmFuayA8PSAxMCcpCmBgYAogIAogICogcG91ciBsYSBxdWVzdGlvbiAyCiAgCmBgYHtyfQpkYkdldFF1ZXJ5KGNvbiwKICAgICAgICAgICAnV0lUSCBiYWJ5bmFtZXNfdG9wIEFTCiAgICAgICAgICAgKFNFTEVDVCAqIAogICAgICAgICAgIEZST00KICAgICAgICAgICAoU0VMRUNUICosIFJBTksoKSBPVkVSIChQQVJUSVRJT04gQlkgc2V4LCAgInllYXIiIE9SREVSIEJZIG4gREVTQykgYXMgcmFuayAgRlJPTSAgYmFieW5hbWVzKSBhcyBUTVAKICAgICAgICAgICBXSEVSRSByYW5rIDw9IDEwKQogICAgICAgICAgIAogICAgICAgICAgIFNFTEVDVCAieWVhciIsIHNleCwgU1VNKHByb3ApIGFzIHByb3BfdG90IEZST00KICAgICAgICAgICBiYWJ5bmFtZXNfdG9wIAogICAgICAgICAgIEdST1VQIEJZICJ5ZWFyIiwgc2V4JykKYGBgCiAgCiAgKiBldCBlbmZpbiBsYSBxdWVzdGlvbiAzCgpgYGB7cn0KZGJHZXRRdWVyeShjb24sCiAgICAgICAgICAgJ1dJVEggYmFieW5hbWVzX3RvcDIwMDAgQVMgKAogICAgICAgICAgIFNFTEVDVCAqIAogICAgICAgICAgIEZST00KICAgICAgICAgICAoU0VMRUNUICosIFJBTksoKSBPVkVSIChQQVJUSVRJT04gQlkgInNleCIgT1JERVIgQlkgIm4iIERFU0MpIGFzIHJhbmsgIEZST00gIGJhYnluYW1lcyBXSEVSRSAoInllYXIiID0gMjAwMCkpIGFzIFRNUAogICAgICAgICAgIFdIRVJFIHJhbmsgPD0gMTAKICAgICAgICAgICApLCAKICAgICAgICAgICBiYWJ5bmFtZXNfbWVyZ2UgQVMgKAogICAgICAgICAgICBTRUxFQ1QgIGJhYnluYW1lcy4ieWVhciIgYXMgInllYXIiLAogICAgICAgICAgICAgICAgICAgIGJhYnluYW1lcy5zZXggYXMgc2V4LAogICAgICAgICAgICAgICAgICAgIGJhYnluYW1lcy5uYW1lIGFzIG5hbWUsCiAgICAgICAgICAgICAgICAgICAgYmFieW5hbWVzLm4gYXMgbiwKICAgICAgICAgICAgICAgICAgICBiYWJ5bmFtZXMucHJvcCBhcyBwcm9wCiAgICAgICAgICAgIEZST00gCiAgICAgICAgICAgIGJhYnluYW1lc190b3AyMDAwCiAgICAgICAgICAgIExFRlQgSk9JTiAKICAgICAgICAgICAgYmFieW5hbWVzIE9OIChiYWJ5bmFtZXNfdG9wMjAwMC5zZXggPSBiYWJ5bmFtZXMuc2V4IEFORCBiYWJ5bmFtZXNfdG9wMjAwMC5uYW1lID0gYmFieW5hbWVzLm5hbWUpKQogICAgICAgICAgICBTRUxFQ1QgInllYXIiLCBzZXgsIFNVTShwcm9wKSBhcyBwcm9wX3RvdCBGUk9NCiAgICAgICAgICAgIGJhYnluYW1lc19tZXJnZSAKICAgICAgICAgICAgR1JPVVAgQlkgInllYXIiLCBzZXgnKQpgYGAKClVuZSBhbHRlcm5hdGl2ZSBhdXggcmVxdcOqdGVzIFNRTCBlc3QgbCd1dGlsaXNhdGlvbiBkdSBwYWNrYWdlICoqZGJwbHlyKiogOgoKYGBge3IsbWVzc2FnZT1GQUxTRSx3YXJuaW5nPUZBTFNFfQpiYWJ5bmFtZXNfc3FsIDwtIHRibChjb24sICJiYWJ5bmFtZXMiKQpgYGAKCiAgKiBPbiBvYnRpZW50IHBvdXIgbGEgcXVlc3Rpb24gMQogIApgYGB7cn0KYmFieW5hbWVzMjAwMF9zcWwgPC0gYmFieW5hbWVzX3NxbCAlPiUKICBmaWx0ZXIoeWVhciA9PSAyMDAwKSAlPiUgCiAgZ3JvdXBfYnkoc2V4KSAKCmJhYnluYW1lczIwMDBfdG9wMTBfc3FsIDwtIGJhYnluYW1lczIwMDBfc3FsICU+JSBjb2xsZWN0KCkgJT4lIHRvcF9uKDEwLG4pCmJhYnluYW1lczIwMDBfdG9wMTBfc3FsCmBgYAogIAoKICAgKiBQb3VyIGxhIHF1ZXN0aW9uIDIKICAgCmBgYHtyfQpiYWJ5bmFtZXNfc3FsICU+JSBncm91cF9ieShzZXgseWVhcikgJT4lIGFycmFuZ2UoZGVzYyhuKSwuYnlfZ3JvdXA9VFJVRSkgJT4lIGNvbGxlY3QoKSAlPiUgdG9wX24oMTAsbikgJT4lIHN1bW1hcmlzZShzdW1fcHJvcD1zdW0ocHJvcCkpCmBgYAogICAKICAgKiBldCBlbmZpbiBwb3VyIGxhIHF1ZXN0aW9uIDMKICAgCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CmRiV3JpdGVUYWJsZShjb24sIG5hbWUgPSAiYmFieW5hbWVzMjAwMF90b3AxMF9zcWwiLGJhYnluYW1lczIwMDBfdG9wMTBfc3FsLCBvdmVyd3JpdGUgPSBUUlVFKQpiYWJ5bmFtZXMyMDAwX3RvcDEwX3NxbCA8LSB0YmwoY29uLCAiYmFieW5hbWVzMjAwMF90b3AxMF9zcWwiKQoKbGVmdF9qb2luKGJhYnluYW1lczIwMDBfdG9wMTBfc3FsICU+JSBzZWxlY3Qoc2V4LCBuYW1lKSxiYWJ5bmFtZXNfc3FsKSAlPiUgZ3JvdXBfYnkoc2V4LHllYXIpICU+JSBzdW1tYXJpemUocHJvcDIwMDA9c3VtKHByb3ApKQpgYGAKICAgCkV0IG9uIG4nb3VibGllIHBhcyBkZSBzZSBkw6ljb25uZWN0ZXIgIQpgYGB7cn0KZGJEaXNjb25uZWN0KGNvbikKYGBgCiAgIAoKCgojIyBFeGVyY2ljZSA1LjIgOiBMZXMgdG91cm5vaXMgbWFqZXVycyBhdSB0ZW5uaXMgZW4gMjAxMwoKMS4gT24gdXRpbGlzZSAqKnJlYWRfY3N2KiogcG91ciB0cmF2YWlsbGVyIGF2ZWMgdW4gKip0aWJibGUqKi4KCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CmRmIDwtIHJlYWRfY3N2KCJGcmVuY2hPcGVuLW1lbi0yMDEzLmNzdiIpCmBgYAoKMi4gCgpgYGB7cn0KZGYgJT4lIGRpc3RpbmN0KFBsYXllcjEpCmBgYAoKMy4gCgpgYGB7cn0KZGYgJT4lIGRpc3RpbmN0KFBsYXllcjIpCmBgYAoKNC4gCgpgYGB7cn0KZGYgJT4lIGZpbHRlcihQbGF5ZXIxPT0iUm9nZXIgRmVkZXJlciIgfCBQbGF5ZXIyPT0iUm9nZXIgRmVkZXJlciIpICU+JSBzZWxlY3QoY29udGFpbnMoIlBsYXkiKSkKYGBgCgo1LgoKYGBge3J9CmRmICU+JSBmaWx0ZXIoUm91bmQ9PTYpICU+JSBzZWxlY3QoY29udGFpbnMoIlBsYXkiKSkKYGBgCgoKNi4gCgpgYGB7cn0KZGYgJT4lIG11dGF0ZShuYl9wb2ludHM9VFBXLjErVFBXLjIpICU+JSBzdW1tYXJpc2UobmJfcG9pbnRzX21veT1tZWFuKG5iX3BvaW50cykpCmBgYAoKNy4KCmBgYHtyfQpkZiAlPiUgbXV0YXRlKG5iX2FjZT1BQ0UuMStBQ0UuMikgJT4lIGdyb3VwX2J5KFJvdW5kKSAlPiUgCiAgc3VtbWFyaXNlKG1pbj1taW4obmJfYWNlKSxtYXg9bWF4KG5iX2FjZSksbW95PW1lYW4obmJfYWNlKSkKYGBgCgo4LgoKYGBge3J9CmRmICU+JSBtdXRhdGUoZGJmPURCRi4xK0RCRi4yKSAlPiUgc3VtbWFyaXplKHRvdC5kZj1zdW0oZGJmLG5hLnJtPVRSVUUpKQpgYGAKCjkuCgpgYGB7cn0KZGYgJT4lIG11dGF0ZShkYmY9REJGLjErREJGLjIpICU+JSBnZ3Bsb3QoKSArIGFlcyh4PWRiZikgKyBnZW9tX2hpc3RvZ3JhbShiaW5zPTEwKSArIHRoZW1lX2J3KCkKYGBgCgoxMC4KCmBgYHtyfQpkZiAlPiUgbXV0YXRlKGRiZj1EQkYuMStEQkYuMikgJT4lIGdyb3VwX2J5KFJvdW5kKSAlPiUgc3VtbWFyaXplKGRiZj1tZWFuKGRiZixuYS5ybT1UUlVFKSkgJT4lCiAgZ2dwbG90KCkgKyBhZXMoeD1Sb3VuZCx5PWRiZikgKyBnZW9tX2JhcihzdGF0PSJpZGVudGl0eSIsZmlsbD0icmVkIikgKyB0aGVtZV9jbGFzc2ljKCkKYGBgCgoKMTEuCgpgYGB7cn0KZGYgJT4lIHNlbGVjdChSZXN1bHQsRlNQLjEsRlNQLjIpICU+JSBnYXRoZXIoa2V5PSJQbGF5ZXIiLHZhbHVlPSJGU1AiLC1SZXN1bHQpICU+JSAKICBtdXRhdGUoUmVzdWx0PWFzLmNoYXJhY3RlcigoUmVzdWx0PT0xICYgUGxheWVyPT0iRlNQLjEiKSB8IChSZXN1bHQ9PTAgJiBQbGF5ZXI9PSJGU1AuMiIpKSkgJT4lIAogIG11dGF0ZShSZXN1bHQ9ZmN0X3JlY29kZShSZXN1bHQsdmljPSJUUlVFIixkZWY9IkZBTFNFIikpICU+JQogIGdncGxvdCgpICsgYWVzKHg9UmVzdWx0LHk9RlNQKStnZW9tX2JveHBsb3QoKSt0aGVtZV9jbGFzc2ljKCkKYGBgCgpJbCBzZW1ibGUgcXUnaWwgeSBhaXQgdW5lIGzDqWfDqHJlIGluZmx1ZW5jZSBkdSBwb3VyY2VudGFnZSBkZSBwcmVtaWVyIHNlcnZpY2Ugc3VyIGxlIHLDqXN1bHRhdC4gT24gZWZmZWN0dWUgdW4gdGVzdCBkZSBjb21wYXJhaXNvbiBkZSBtb3llbm5lcyBwb3VyIHbDqXJpZmllciA6CgpgYGB7cn0KZGYxIDwtIGRmICU+JSBzZWxlY3QoUmVzdWx0LEZTUC4xLEZTUC4yKSAlPiUgZ2F0aGVyKGtleT0iUGxheWVyIix2YWx1ZT0iRlNQIiwtUmVzdWx0KSAlPiUgCiAgbXV0YXRlKFJlc3VsdD1hcy5jaGFyYWN0ZXIoKFJlc3VsdD09MSAmIFBsYXllcj09IkZTUC4xIikgfCAoUmVzdWx0PT0wICYgUGxheWVyPT0iRlNQLjIiKSkpICU+JSAKICBtdXRhdGUoUmVzdWx0PWZjdF9yZWNvZGUoUmVzdWx0LHZpYz0iVFJVRSIsZGVmPSJGQUxTRSIpKQp0LnRlc3QoRlNQflJlc3VsdCxkYXRhPWRmMSkKYGBgCgpBdSBuaXZlYXUgMTAlLCBvbiBjb25jbHVlcmEgcXUnaWwgeSBhIHVuZSBpbmZsdWVuY2UuIAoKIyMgRXhlcmNpY2UgNS4zIDogTGUgdsOpbG8gU1RBUiwgZW5jb3JlICEKCjEuIE9uIGltcG9ydGUgbGVzIHRhYmxlcwoKCmBgYHtyfQp1cmwgPC0gcGFzdGUwKAoiaHR0cHM6Ly9kYXRhLnJlbm5lc21ldHJvcG9sZS5mci9hcGkvcmVjb3Jkcy8xLjAvc2VhcmNoLyIsCiI/ZGF0YXNldD1ldGF0LWRlcy1zdGF0aW9ucy1sZS12ZWxvLXN0YXItZW4tdGVtcHMtcmVlbCIsCiImcm93cz0xMDAiLAoiJmZhY2V0PW5vbSIsCiImZmFjZXQ9ZXRhdCIsCiImZmFjZXQ9bm9tYnJlZW1wbGFjZW1lbnRzYWN0dWVscyIsCiImZmFjZXQ9bm9tYnJlZW1wbGFjZW1lbnRzZGlzcG9uaWJsZXMiLAoiJmZhY2V0PW5vbWJyZXZlbG9zZGlzcG9uaWJsZXMiLAoiJmZhY2V0PWNvb3Jkb25uZWVzIgopCmxsIDwtIGpzb25saXRlOjpmcm9tSlNPTih1cmwpCmV0YXQgPC0gbGwkcmVjb3JkcyRmaWVsZHMKYGBgCgpgYGB7cn0KdXJsMSA8LSBwYXN0ZTAoCiJodHRwczovL2RhdGEucmVubmVzbWV0cm9wb2xlLmZyL2FwaS9yZWNvcmRzLzEuMC9zZWFyY2gvIiwKIj9kYXRhc2V0PXRvcG9sb2dpZS1kZXMtc3RhdGlvbnMtbGUtdmVsby1zdGFyIiwKIiZyb3dzPTEwMCIpCmxsMSA8LSBqc29ubGl0ZTo6ZnJvbUpTT04odXJsMSkKdG9wbyA8LSBsbDEkcmVjb3JkcyRmaWVsZHMKYGBgCgoyLiBPbiBjcsOpw6kgbGEgdGFibGUgZGVtYW5kw6llCgpgYGB7cn0KZ2xpbXBzZShldGF0KQpsYXRsb25nIDwtIGV0YXQkY29vcmRvbm5lZXMgJT4lIHVubGlzdCgpICU+JSBtYXRyaXgobmNvbD0yLGJ5cm93PVQpICU+JSBhcy5kYXRhLmZyYW1lKCkKbmFtZXMobGF0bG9uZykgPC0gYygibGF0aXR1ZGUiLCJsb25naXR1ZGUiKQpldGF0MSA8LSBiaW5kX2NvbHMoZXRhdCxsYXRsb25nKSAlPiUgc2VsZWN0KC1jb29yZG9ubmVlcykKZGYgPC0gaW5uZXJfam9pbihldGF0MSx0b3BvLGJ5PSJub20iKSAlPiUgc2VsZWN0KGlkLG5vbSxpZHN0YXRpb25wcm9jaGUxLGxhdGl0dWRlLGxvbmdpdHVkZSkgJT4lIG11dGF0ZShpZD1hcy5udW1lcmljKGlkKSkKZGYxIDwtIGxlZnRfam9pbihkZixkZixieT1jKCJpZHN0YXRpb25wcm9jaGUxIj0iaWQiKSkgJT4lIG11dGF0ZShkaXN0PShsYXRpdHVkZS54LWxhdGl0dWRlLnkpXjIrKGxvbmdpdHVkZS54LWxvbmdpdHVkZS55KV4yKSAKCmBgYAoKMy4gT24gY2FsY3VsZSBsZXMgc3RhdGlvbnMgbGVzIHBsdXMgcHJvY2hlcyBkdSBwb2ludCBkZW1hbmTDqQoKYGBge3J9CmV0YXQxICU+JSBtdXRhdGUoZGlzdGFuY2U9KGxhdGl0dWRlLTQ4LjExNzkxNTEpXjIrKGxvbmdpdHVkZSsxLjcwMjg2NjEpXjIpICU+JSBhcnJhbmdlKGRpc3RhbmNlKSAlPiUgaGVhZCgzKSAlPiUKICBhcnJhbmdlKGRpc3RhbmNlLG5vbWJyZWVtcGxhY2VtZW50c2Rpc3BvbmlibGVzKSAlPiUgc2VsZWN0KG5vbSxkaXN0YW5jZSxub21icmVlbXBsYWNlbWVudHNkaXNwb25pYmxlcykKYGBgCgojIyBFeGVyY2ljZSA1LjQgOiBTZSBjdWx0aXZlciBwYXIgaGFzYXJkCgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpsaWJyYXJ5KHJ2ZXN0KQpgYGAKCjEuIE9uIHNlIGNvbm5lY3RlIMOgIGxhIHBhZ2UgKip3aWtpcGVkaWEqKgoKYGBge3J9Cndpa2lwZWRpYSA8LSAiaHR0cHM6Ly9mci53aWtpcGVkaWEub3JnLyIKYWNjdWVpbCA8LSAid2lraS9XaWtpcMOpZGlhOkFjY3VlaWxfcHJpbmNpcGFsIgp1cmwwIDwtIHBhc3RlMCh3aWtpcGVkaWEsYWNjdWVpbCkKZGF0YV9odG1sIDwtIHJlYWRfaHRtbCh1cmwwKQpgYGAKCjIuIE9uIG9idGllbnQgbGVzIGxpZW5zIGRlIGxhIHBhZ2UgYXZlYyAKCgpgYGB7cn0KaW50ZXJuYWxfbGlua3MgPC0gJ2FbaHJlZio9Ii93aWtpLyJdOm5vdChbaHJlZio9Ii8vIl0pJwpsaWVuMCA8LSBkYXRhX2h0bWwgJT4lIGh0bWxfbm9kZXMoaW50ZXJuYWxfbGlua3MpICU+JSBodG1sX2F0dHIoImhyZWYiKQpoZWFkKGxpZW4wKQpgYGAKCjQuIE9uIGNob2lzaXQgdW5lIHBhZ2UgYXUgaGFzYXJkCgpgYGB7cn0Kc2V0LnNlZWQoMTIzKQpsaWVuX3NlbCA8LSBsaWVuMCAlPiUgc2FtcGxlKDEpCmxpZW5fc2VsCmBgYAoKcXVlIGwnb24gbGl0IHB1aXMsIHN1ciBsYXF1ZWxsZSBvbiByZWNob2lzaXQgdW5lIHBhZ2UgYXUgaGFzYXJkCgpgYGB7cn0Kc2V0LnNlZWQoMTIzNCkKdXJsMSA8LSBwYXN0ZTAod2lraXBlZGlhLGxpZW5fc2VsKQpyZWFkX2h0bWwodXJsMSkgJT4lIGh0bWxfbm9kZXMoaW50ZXJuYWxfbGlua3MpICU+JSBodG1sX2F0dHIoImhyZWYiKSAlPiUgc2FtcGxlKDEpCmBgYAoKCjUuIE9uIHBldXQgw6ljcmlyZSB1bmUgZm9uY3Rpb24gcXVpIHPDqWxlY3Rpb25uZSB1biBsaWVuIGFsw6lhdG9pcmVtZW50IHN1ciB1bmUgcGFnZSBkb25uw6llIDoKCmBgYHtyfQpzZWxlY3RfbGluayA8LSBmdW5jdGlvbih1cmwpewogIHJlYWRfaHRtbCh1cmwpICU+JQogICAgaHRtbF9ub2RlcyhpbnRlcm5hbF9saW5rcykgJT4lCiAgICBodG1sX2F0dHIoImhyZWYiKSAlPiUKICAgIHNhbXBsZSgxKQp9CmBgYAoKT24gcGV1dCBhaW5zaSBlbiBkw6lkdWlyZSBsZXMgcGFnZXMgdmlzaXTDqWVzIHBlbmRhbmQgdW4gbm9tYnJlIGRlIGpvdXIgZG9ubsOpZSA6CgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpsaWJyYXJ5KG1hZ3JpdHRyKQpwYWdlc192aXNpdGVlcyA8LSBmdW5jdGlvbihuYl9qb3Vycyl7CiAgbGllbnNfdmlzaXRlcyA8LSBjKCkKICBmb3IoaSBpbiAxOm5iX2pvdXJzKXsKICAgIHdpa2lwZWRpYSAlPiUKICAgICAgc2VsZWN0X2xpbmsoKSAlVD4lCiAgICAgIHtsaWVuc192aXNpdGVzIDw8LSBhcHBlbmQobGllbnNfdmlzaXRlcywgLil9ICU+JQogICAgICBzdHJfYyh3aWtpcGVkaWEsIC4pICU+JQogICAgICBzZWxlY3RfbGluaygpICU+JQogICAgICB7bGllbnNfdmlzaXRlcyA8PC0gYXBwZW5kKGxpZW5zX3Zpc2l0ZXMsIC4pfQogIH0KICByZXR1cm4odW5pcXVlKGxpZW5zX3Zpc2l0ZXMpKQp9CmBgYAoKNi4gUGFyIGV4ZW1wbGUgZW4gMTAgYW5zIG9uIGF1cmEgdmlzaXTDqSA6CgpgYGB7cn0KcGFnZXNfMTBhbnMgPC0gcGFnZXNfdmlzaXRlZXMoMzY1MikKaGVhZChwYWdlc18xMGFucykKbGVuZ3RoKHBhZ2VzXzEwYW5zKQpgYGAKCjcuIE9uIHBldXQgcmVub3V2ZWxlciBsZSBwcm90b2NvbGUgMjAgZm9pcyAoaWwgZmF1ZHJhaXQgcGx1cyBtYWlzIGMnZXN0IHRyw6hzIGxvbmcgISk6CgpgYGB7cn0Kc2V0LnNlZWQoMTIzNCkKbmJfdmlzaXRlIDwtIHJlcCgwLDIwKQpmb3IgKGkgaW4gMToyMCl7CiAgbmJfdmlzaXRlW2ldIDwtIHBhZ2VzX3Zpc2l0ZWVzKDM2NTIpICU+JSBsZW5ndGgoKQp9CmBgYAoKT24gcGV1dCByZWdhcmRlciBsZSByw6lzdW3DqSAKCmBgYHtyfQpzdW1tYXJ5KG5iX3Zpc2l0ZSkKYGBgCgpldCB2aXN1YWxpc2VyIGwnaGlzdG9ncmFtbWUKCmBgYHtyfQp0aWJibGUobmJfdmlzaXRlKSAlPiUgZ2dwbG90KCkgK2Flcyh4PW5iX3Zpc2l0ZSkrZ2VvbV9oaXN0b2dyYW0oYmlucyA9IDUsZmlsbD0icmVkIikrdGhlbWVfY2xhc3NpYygpCmBgYAoKCiMjIEV4ZXJjaWNlIDUuNSA6IFVuIHBldSBkZSBtdXNpcXVlCgoxLiBPbiBzZSBjb25uZWN0ZSDDoCBsYSBiYXNlIGRlIGRvbm7DqWVzIGV0IG9uIGluc3BlY3RlIGxlcyBkaWZmw6lyZW50ZXMgYmFzZXMuCgpgYGB7cn0KbGlicmFyeShEQkkpCmNvbm4gPC0gZGJDb25uZWN0KFJTUUxpdGU6OlNRTGl0ZSgpLCBkYm5hbWUgPSAiQ2hpbm9va19TcWxpdGUuc3FsaXRlIikKZGJMaXN0VGFibGVzKGNvbm4pCmBgYAoKYGBge3J9CmRiTGlzdEZpZWxkcyhjb25uLCJQbGF5bGlzdCIpCmRiTGlzdEZpZWxkcyhjb25uLCJQbGF5bGlzdFRyYWNrIikKZGJMaXN0RmllbGRzKGNvbm4sIlRyYWNrIikKZGJMaXN0RmllbGRzKGNvbm4sIkFsYnVtIikKYGBgCgoKMi4gT24gYWZmZWN0ZSBsZXMgdGFibGVzIGRlIGxhIGJhc2UgZGUgZG9ubsOpZXMgw6AgdW5lIGxpc3RlIGRlIGB0aWJibGVzYC4gT24gcG91cnJhaXQgY3JvaXJlIHF1ZSBjJ2VzdCB0csOocyBnb3VybWFuZCBlbiBtw6ltb2lyZSBwb3VyIGRlIGdyb3MgdGFibGVhdXggbWFpcyBlbiBmYWl0IHBhcyBkdSB0b3V0LiBDJ2VzdCBqdXN0ZSB1bmUgbGlzdGUgZCdvYmpldHMgcXVpIHBlcm1ldHRlbnQgZGUgZmFpcmUgdW5lIGNvbm5leGlvbiBhdmVjIGxhIGJhc2UgZGUgZG9ubsOpZXMuIERvbmMgb24gcGV1dCBpbCBhbGxlciBnYWltZW50ICEKCgpgYGB7cn0KbXVzaXF1ZSA8LSBsaXN0KCkKZm9yKG5hbWUgaW4gZGJMaXN0VGFibGVzKGNvbm4pKXsKICBtdXNpcXVlW1tuYW1lXV0gPC0gdGJsKGNvbm4sIG5hbWUpCn0KbmFtZXMobXVzaXF1ZSkKYGBgCgpMYSByZXF1w6p0ZSAoYXByw6hzIGF2b2lyIHJlZ2FyZMOpIHVuIHBldSBsYSBzdHJ1Y3R1cmUgZGVzIHRhYmxlcykgZXN0IGxhIHN1aXZhbnRlCgoKYGBge3J9CnJlc3VsdCA8LSBtdXNpcXVlJFBsYXlsaXN0ICU+JQogIGZpbHRlcihOYW1lID09ICJDbGFzc2ljYWwiKSAlPiUKICBsZWZ0X2pvaW4obXVzaXF1ZSRQbGF5bGlzdFRyYWNrLCBieSA9ICJQbGF5bGlzdElkIikgJT4lCiAgbGVmdF9qb2luKG11c2lxdWUkVHJhY2ssIGJ5ID0gIlRyYWNrSWQiKSAlPiUKICBzZWxlY3QoU29uZyA9IE5hbWUueSwgQWxidW1JZCkgJT4lCiAgbGVmdF9qb2luKG11c2lxdWUkQWxidW0sIGJ5ID0gIkFsYnVtSWQiKSAlPiUKICBzZWxlY3QoU29uZywgQWxidW0gPSBUaXRsZSkKYGBgCgpPbiByZWdhcmRlIGxlcyByw6lzdWx0YXRzCgpgYGB7cn0KcmVzdWx0ICU+JSBjb2xsZWN0KCkKYGBgCgozLiBPbiBwZXV0IHJlZ2FyZGVyIGxhIHJlcXXDqnRlIFNRTAoKYGBge3J9CnJlc3VsdCAlPiUgc2hvd19xdWVyeSgpCmBgYAoKRXQgb24gbidvdWJsaWUgcGFzIGRlIHNlIGTDqWNvbm5lY3RlciAhCmBgYHtyfQpkYkRpc2Nvbm5lY3QoY29ubikKYGBgCgojIyBFeGVyY2ljZSA1LjYgOiBEdSB0cmFtcG9saW5lIHN1ciBXaWtpcGVkaWEKCjEuIE9uIHLDqWN1cMOocmUgdG91dCBkJ2Fib3JkIGxlIHRhYmxlYXUKCgpgYGB7cn0KdXJsIDwtICJodHRwczovL2ZyLndpa2lwZWRpYS5vcmcvd2lraS9MaXN0ZV9kZXNfbcOpZGFpbGzDqXNfb2x5bXBpcXVlc19hdV90cmFtcG9saW5lIgp0cmFtcCA8LSByZWFkX2h0bWwodXJsKQp0YWIgPC0gdHJhbXAgJT4lIGh0bWxfdGFibGUoKQp0cmFtcC5mIDwtIHRhYltbMl1dCmBgYAoKMi4gT24gY3LDqcOpIGxlIGRhdGFmcmFtZQoKYGBge3J9CnRyYW1wLmYxIDwtIHRyYW1wLmYgJT4lIGdhdGhlcihrZXk9Im1lZCIsdmFsdWU9ImF0aF9wIixPcixBcmdlbnQsQnJvbnplKSAlPiUKICBzZXBhcmF0ZShhdGhfcCxpbnRvPWMoIm5vbSIsInBheXMiKSxzZXA9IlxcKCIpICU+JSBtdXRhdGUocGF5cz1zdHJfcmVtb3ZlKHBheXMsIlxcKSIpKQp0cmFtcC5mMQpgYGAKCjMuT24gbGUgY2xhc3NlIGVuIGZvbmN0aW9uIGRlcyBtw6lkYWlsbGVzCgpgYGB7cn0KdHJhbXAuZjEgJT4lIGdyb3VwX2J5KHBheXMsbWVkKSAlPiUgc3VtbWFyaXplKG5vbWI9bigpKSAlPiUgc3ByZWFkKG1lZCxub21iKSAlPiUgc2VsZWN0KGMoMSw0LDIsMykpICU+JQogIGFycmFuZ2UoZGVzYyhPcixBcmdlbnQsQnJvbnplKSkKYGBgCgo0LiBPbiBmYWl0IGxhIG3Dqm1lIGNob3NlIHBvdXIgbGVzIGhvbW1lcwoKYGBge3J9CnRyYW1wLmggPC0gdGFiW1sxXV0KdHJhbXAuaDEgPC0gdHJhbXAuaCAlPiUgZ2F0aGVyKGtleT0ibWVkIix2YWx1ZT0iYXRoX3AiLE9yLEFyZ2VudCxCcm9uemUpICU+JQogIHNlcGFyYXRlKGF0aF9wLGludG89Yygibm9tIiwicGF5cyIpLHNlcD0iXFwoIikgJT4lIG11dGF0ZShwYXlzPXN0cl9yZW1vdmUocGF5cywiXFwpIikpCnRyYW1wLmgxICU+JSBncm91cF9ieShwYXlzLG1lZCkgJT4lIHN1bW1hcml6ZShub21iPW4oKSkgJT4lIHNwcmVhZChtZWQsbm9tYikgJT4lIHNlbGVjdChjKDEsNCwyLDMpKSAlPiUKICBhcnJhbmdlKGRlc2MoT3IsQXJnZW50LEJyb256ZSkpCmBgYAoKZXQgZW5maW4gcG91ciBsZSB0YWJsZWF1IG1peHRlCgpgYGB7cn0KdHJhbXAuZmggPC0gYmluZF9yb3dzKHRhYltbMV1dLHRhYltbMl1dKQp0cmFtcC5maDEgPC0gdHJhbXAuZmggJT4lIGdhdGhlcihrZXk9Im1lZCIsdmFsdWU9ImF0aF9wIixPcixBcmdlbnQsQnJvbnplKSAlPiUKICBzZXBhcmF0ZShhdGhfcCxpbnRvPWMoIm5vbSIsInBheXMiKSxzZXA9IlxcKCIpICU+JSBtdXRhdGUocGF5cz1zdHJfcmVtb3ZlKHBheXMsIlxcKSIpKQp0cmFtcC5maDEgJT4lIGdyb3VwX2J5KHBheXMsbWVkKSAlPiUgc3VtbWFyaXplKG5vbWI9bigpKSAlPiUgc3ByZWFkKG1lZCxub21iKSAlPiUgc2VsZWN0KGMoMSw0LDIsMykpICU+JQogIGFycmFuZ2UoZGVzYyhPcixBcmdlbnQsQnJvbnplKSkKYGBgCgoKIyMgRXhlcmNpY2UgNS43IDogRMOpYm91Y2jDqXMgZGFucyBsJ2Vuc2VpZ25lbWVudCBzdXDDqXJpZXVyIGZyYW7Dp2FpcwoKMS4gCgpgYGB7cn0KZGYgPC0ganNvbmxpdGU6OmZyb21KU09OKCJmci1lc3ItaW5zZXJ0aW9uX3Byb2Zlc3Npb25uZWxsZS1tYXN0ZXIuanNvbiIpJGZpZWxkcwpgYGAKCmBgYHtyfQpkZjEgPC0gZGYgJT4lIHNlbGVjdChldGFibGlzc2VtZW50LHNhbGFpcmVfbmV0X21lZGlhbl9kZXNfZW1wbG9pc19hX3RlbXBzX3BsZWluLHNhbGFpcmVfbmV0X21lbnN1ZWxfbWVkaWFuX3JlZ2lvbmFsKSAlPiUKICBtdXRhdGUoc2FsYWlyZV9uZXRfbWVkaWFuX2Rlc19lbXBsb2lzX2FfdGVtcHNfcGxlaW49YXMubnVtZXJpYyhzYWxhaXJlX25ldF9tZWRpYW5fZGVzX2VtcGxvaXNfYV90ZW1wc19wbGVpbiksCiAgICAgICAgIHNhbGFpcmVfbmV0X21lbnN1ZWxfbWVkaWFuX3JlZ2lvbmFsPWFzLm51bWVyaWMoc2FsYWlyZV9uZXRfbWVuc3VlbF9tZWRpYW5fcmVnaW9uYWwpKQpkZjIgPC0gZGYxICU+JSBncm91cF9ieShldGFibGlzc2VtZW50KSAlPiUgc3VtbWFyaXplKHNhbC50ZW1wcy5wbGVpbj1tZWFuKHNhbGFpcmVfbmV0X21lZGlhbl9kZXNfZW1wbG9pc19hX3RlbXBzX3BsZWluLG5hLnJtPVRSVUUpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2FsLnJlZz1tZWFuKHNhbGFpcmVfbmV0X21lbnN1ZWxfbWVkaWFuX3JlZ2lvbmFsLG5hLnJtPVRSVUUpKQpkZjIKYGBgCgoyLiBPbiBjbGFzc2Ugc2Vsb24gbGUgcmF0aW8gZGVtYW5kw6kuCgpgYGB7cn0KZGYyICU+JSBtdXRhdGUocmF0aW89c2FsLnRlbXBzLnBsZWluL3NhbC5yZWcpICU+JSBhcnJhbmdlKGRlc2MocmF0aW8pKQpgYGAKCg==