1. 商店的類別 (508 Business Categories)
這份資料裡面有11,537家商店(Business)和508種商業類別(Categories), 一家商店可以同時隸屬於很多(0 ~ 10)個類別, 我們首先考慮:
- 每一個商業類別裡面有多少家商店? 這些商店總共被評論過多少?
- 這些商業類別之間有相關性嗎? 哪一些商業類別經常會同時出現?
(1a) 商業類別資料整理
# number of biz per category
CA = biz$cat %>% strsplit('|',T) %>% unlist %>% table %>%
data.frame %>% 'names<-'(c('name','nbiz'))
# number of review per category
CA$nrev = CA$name %>% sapply(function(z){sum(
review$bid %in% biz$bid[grep(z,biz$cat,fixed=T)] )})
# average number of reviews per business
CA$avg.rev = CA$nrev / CA$nbiz
CA = CA[order(-CA$nrev),] # order CA by no. review
rownames(CA)= CA$name
CA$name = NULL
從biz$categoey
裡面整理出 508 個 Categories, 並算出每一個 Category 的:
nbiz
: number of business (in the category)
nrev
: number of revierw (in the category)
avg.rev
: average number of reviews per business
放在 CA
這個 Data Frame
裡面:
CA
# category-business matrix
mxBC = rownames(CA) %>% sapply(function(z)
grepl(z,biz$cat,fixed=T))
rownames(mxBC) = biz$bid
dim(mxBC)
將 Business(11,537) 和 Category(508) 的對應關係放在mxBC
裡面。
(1b) 尺度縮減 (Dimension Reduction)
使用tSNE
,將mxBC
的尺度 [11,537 x 508] 縮減為 [2 x 508] …
t0 = Sys.time()
set.seed(123)
tsneCat = ifelse(mxBC,1,0) %>% t %>%
Rtsne(check_duplicates=F,theta=0.0,max_iter=3000)
Sys.time() - t0
(1c) 階層式集群分析 (Hierarchical Clustering)
在縮減尺度之中做階層式集群分析。
Y = tsneCat$Y # tSNE coordinates
d = dist(Y) # distance matrix
hc = hclust(d) # hi-clustering
K = 60 # number of clusters
CA$grp = cutree(hc,K)
table(CA$grp)
經重覆嘗試之後,將這508個商業類別(Categories) 分成60個商業類別群組(Category Groups)。
(1d) 字雲 (Word Cloud)
pals = distinctColorPalette(K) # palette of K colors
png("category.png", width=3200, height=1800)
textplot(Y[,1],Y[,2],rownames(CA),font=2,
col = pals[CA$grp], # color by group
cex = (0.5+log(CA$nrev)/5)) # size by no. reviews
dev.off()
將字雲畫在category.png
裡面:
- 每個字代表一個商業類別(Categories)
- 字的顏色代表商業類別群組(Category Groups)
- 字的大小代表這個商業類別被評論的次數 (number of reviews)
- 靠在一起的、同一種顏色的字,代表經常一起出現的商業類別
2. 評論的內容 (194 Content Classes)
接下來考慮評論的內容,上週我們已經使用 Empath Text Classifier ,依其預設的194種內容(Class), 對這215,879篇評論分別做過評分。 也就是說,文集之中的每一篇評論都有194個內容評分, 存放在scores
這個Data Frame裡面。
dim(scores)
[1] 215879 194
使用這一些資料,我們可以對這194種內容(Classes)進行分群, 也可以用字雲來呈現不同內容之間的相關性。
(2a) 內容權重 (Class Weights)
計算每一種內容的權重(Weight: the sum of class scores within the corpus), 放在wClass
裡面,並將scores
(內容評分資料)依權重排序。
# define class weight as the sum of class scores within the corpus
# order the score matrix by class weights
scores = scores[,order(-colSums(scores))]
wClass = colSums(scores) # class weights
head(wClass,20)
eating cooking restaurant shopping positive_emotion
7289.6 6286.9 6121.8 3248.1 2733.0
friends business giving party vacation
2693.6 1996.2 1780.7 1726.1 1642.5
optimism achievement shape_and_size negative_emotion occupation
1548.4 1457.1 1382.8 1370.4 1350.9
celebration traveling home children family
1248.4 1235.9 1127.3 1106.8 1098.7
(2b) 尺度縮減 (Dimension Reduction)
使用tSNE
,將scores
的尺度 [216879 x 194] 縮減為 [2 x 194] …
t0 = Sys.time()
set.seed(123)
tsneClass = scores %>% scale %>% as.matrix %>% t %>%
Rtsne(theta=0.0,max_iter=3000)
Sys.time() - t0
Time difference of 46.542 secs
(2c) 階層式集群分析 (Hierarchical Clustering)
在縮減尺度之中做階層式集群分析。
Y = tsneClass$Y # tSNE coordinates
d = dist(Y) # distance matrix
hc = hclust(d) # hi-clustering
K = 40
gpClass = cutree(hc,K) # K groups
table(gpClass) # no. classes per group
gpClass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
3 9 6 5 4 7 4 3 8 6 4 3 5 6 10 4 4 3 4 6 8 9 2 3 7 5 4 6 4
30 31 32 33 34 35 36 37 38 39 40
4 3 2 4 7 2 5 4 3 5 3
返覆嘗試之後,將這194種內容(Classes) 分成40個內容群組(Class Groups)。
(2d) 字雲 (Word Cloud)
pals = distinctColorPalette(K) # palette of K colors
png("classes.png", width=3200, height=1800)
textplot(Y[,1],Y[,2],colnames(scores),font=2,
col = pals[gpClass], # color by group
cex = 0.5+log(wClass)/3) # size by class weight
dev.off()
將字雲畫在classes.png
裡面:
- 每個字代表一種內容(Class)
- 字的顏色代表內容群組(Class Groups)
- 字的大小代表這種內容在文集之中的權重
- 靠在一起的、同一種顏色的字,代表經常一起出現的內容
(2e) 內容之間的相關性
繼續分析之前,先用熱圖檢查一下內容評分之間的相關性 …
# correlation between the classes
cr = cor(scores)
cr %>% d3heatmap(scale='none',col=cm.colors(256))
找出相關係數較高的內容種類
corgrp = function(x, threshold=0.6) {
x = x*lower.tri(x)
check = which(x>threshold,arr.ind=T)
gcr = graph.data.frame(check, directed=F)
gcr = split(unique(as.vector(check)),clusters(gcr)$membership)
lapply(gcr, function(g){rownames(x)[g]}) }
corgrp(cr, 0.6)
$`1`
[1] "cooking" "restaurant" "eating"
$`2`
[1] "achievement" "positive_emotion"
$`3`
[1] "giving" "occupation" "phone" "business"
$`4`
[1] "celebration" "party"
$`5`
[1] "affection" "optimism" "love"
$`6`
[1] "payment" "valuable" "economics" "money"
$`7`
[1] "hygiene" "cleaning"
$`8`
[1] "vehicle" "car" "driving"
$`9`
[1] "pain" "shame" "suffering" "swearing_terms" "violence"
[6] "emotional" "hate"
$`10`
[1] "hearing" "noise" "sound" "listen"
$`11`
[1] "toy" "play"
$`12`
[1] "leader" "order"
$`13`
[1] "dance" "musical" "music"
$`14`
[1] "fire" "warmth"
$`15`
[1] "water" "swimming" "sailing" "exotic" "ocean"
$`16`
[1] "technology" "programming" "computer" "internet"
$`17`
[1] "school" "college"
$`18`
[1] "nervousness" "contentment"
$`19`
[1] "disgust" "anger"
3. 商業類別 與 評論內容
接下來我們可以做商業類別(508 Categories)和評論內容(194 Classes) 之間的交叉分析。
(3a) 交叉查詢範例
先用以下這一個範例來示範交叉查詢的做法
- 在評論數超過500的商業類別之中,哪些類別的評論之中的正面情緒和負面情緒的相關係數是最高的呢?
# correlation between positive & negative emotion
mxBC[,CA$nrev>500] %>% apply(2,function(v) {
i = review$bid %in% rownames(mxBC)[v]
cor(scores$positive_emotion[i], scores$negative_emotion[i])
}) %>% sort %>% tail(20)
Pakistani Middle Eastern Soul Food
0.035701 0.041726 0.042036
Pet Stores Day Spas Car Wash
0.043151 0.051408 0.053279
Drugstores Massage Automotive
0.055246 0.078279 0.089250
Cafes Auto Repair Stadiums & Arenas
0.091302 0.097786 0.102325
Cosmetics & Beauty Supply Skin Care Dentists
0.105822 0.108231 0.138033
Tires Health & Medical Pet Services
0.145028 0.182872 0.218834
Pets Doctors
0.244424 0.275944
我們可以看到Doctors
和Pets
這兩種商業類別的評論最常會同時出現正面和負面情緒。
(3b) 各商業類別的內容權重
不同商業類別的評論裡面會有不一樣的內容, 我們可以用熱圖來呈現各商業類別的內容分布狀況。 先把內容評分依商業類別平均起來, 放在wxClass
這個矩陣裡面。
wxClass = apply(mxBC,2,function(v){ # for every category
i = review$bid %in% rownames(mxBC)[v] # find its reviews
colMeans(scores[i,]) # average their class scores
})
dim(wxClass)
[1] 194 508
由於wxClass
這個矩陣太大, 我們只畫出評論數最多的50個商業類別和權重最大的50種評論內容
wxClass[1:50,1:50] %>% log %>%
d3heatmap(T,T,scale='none',col='PiYG')
(3c) 群組熱圖
因為商業類別(508 categories)和評論內容(194 classes)的數量都很多 (這就是大數據的特徵), 我們要在群組這個層面,才比較容易觀察整個文集的內容分布狀況。 其實,這就是大數據分析之中,我們常常需要先做集群分析的理由。 我們用以下的熱圖呈現商業類別群組(60)和內容群組(40)之間的關係。
x = matrix(0, nrow=max(gpClass), ncol=max(CA$grp),
dimnames=list(sprintf('CLS%02d',1:max(gpClass)),
sprintf('CAT%02d',1:max(CA$grp))))
for(i in 1:nrow(x)) for(j in 1:ncol(x))
x[i,j] = sum( wxClass[gpClass==i, CA$grp==j] )
t(x) %>% {log(0.005+.)} %>% d3heatmap(scale='none',col='PiYG')
從以上的群組熱圖裡面我們可以清楚的看到整個文集(在各商業類別之中)的內容分布狀況。
如果需要看某一群組之中有哪一些商業類別或評論內容,可以這樣做:
rownames(CA)[CA$grp==8]
[1] "Sandwiches" "Delis" "Bagels"
[4] "Sporting Goods" "Bikes" "Food Delivery Services"
names(scores)[gpClass==1]
[1] "eating" "cooking" "restaurant"
4. 趨勢分析
(4a) Trend of Content Classes
txClass = tx = split(scores, cut(review$date,'quarter')) %>%
sapply(colSums) %>% apply(2, function(v) v/sum(v))
# Trend of Classes
df = data.frame(class=rownames(tx), tx[,9:32]) %>% melt('class')
df$date = as.Date(
substr(as.character(df$variable),2,11),format="%Y.%m.%d")
df$value = round(100*df$value,3)
subset(df, class %in% rownames(tx)[1:194]) %>%
hchart("spline",hcaes(x=date,y=value,group=class)) %>%
hc_legend(align="left", layout="vertical",verticalAlign="top") %>%
hc_add_theme(hc_theme_flat()) %>%
hc_title(text='Weights of Classes (% of Total Weight)')
(4b) Trend of Class Groups
# Trend of Class Group
x = split(data.frame(tx),gpClass) %>% sapply(colSums) %>% t
df = data.frame(
class=sprintf('G%02d',1:max(gpClass)), x[,9:32]) %>%
melt('class')
df$date = as.Date(
substr(as.character(df$variable),2,11),format="%Y.%m.%d")
df$value = round(100*df$value,3)
df %>% hchart("spline",hcaes(x=date,y=value,group=class)) %>%
hc_legend(align="left", layout="vertical",verticalAlign="top") %>%
hc_add_theme(hc_theme_flat()) %>%
hc_title(text='Weights of Class Groups (% of Total Weight)')
(4c) Trend of Business Categories
txCat = tx = split(review, cut(review$date,'quarter')) %>%
sapply(function(x) apply(mxBC,2,function(v)
sum(x$bid %in% rownames(mxBC)[v]) )) %>%
apply(2, function(v) v/sum(v))
# Trend of Categories
df = data.frame(category=rownames(tx), tx[,9:32]) %>% melt('category')
df$date = as.Date(
substr(as.character(df$variable),2,11),format="%Y.%m.%d")
df$value = round(100*df$value,3)
subset(df, category %in% rownames(CA)[1:30]) %>%
hchart("spline",hcaes(x=date,y=value,group=category)) %>%
hc_legend(align="left", layout="vertical",verticalAlign="top") %>%
hc_add_theme(hc_theme_flat()) %>%
hc_title(text='Weights of Categories (% of Total Reviews)')
(4d) Trend of Business Category Groups
# Trend of Category Groups
x = split(data.frame(tx),CA$grp) %>% sapply(colSums) %>% t
df = data.frame(
category=sprintf('G%02d',1:max(CA$grp)), x[,9:32]) %>%
melt('category')
df$date = as.Date(
substr(as.character(df$variable),2,11),format="%Y.%m.%d")
df$value = round(100*df$value,3)
df %>% hchart("spline",hcaes(x=date,y=value,group=category)) %>%
hc_legend(align="left", layout="vertical",verticalAlign="top") %>%
hc_add_theme(hc_theme_flat()) %>%
hc_title(text='Weights of Category Groups (% of Total Reviews)')
LS0tDQp0aXRsZTogIkV4cGxvcmluZyBZZWxwIEthZ2dsZSBEYXRhc2V0ICgyKSINCnN1YnRpdGxlOiAi5bC65bqm57iu5rib44CB6ZuG576k5YiG5p6Q44CB6LOH5paZ6KaW6Ka65YyWIg0KYXV0aG9yOiAiVG9ueSBDaHVvIg0KZGF0ZTogIjIwMTflubQ35pyIMjPml6UiDQpvdXRwdXQ6IA0KICBodG1sX25vdGVib29rOg0KICAgIGhpZ2hsaWdodDogdGV4dG1hdGUNCiAgICB0aGVtZTogbHVtZW4NCi0tLQ0KDQo8YnI+DQoNCuS4iumAseaIkeWAkeWwjSBZZWxwIEthZ2dsZSDoo6HpnaLnmoTmloflrZflgZrpgY7vvJoNCg0KKyBCYWcgb2YgV29yZHMNCisgU2VudGltZW50IEFuYWx5c2lzIA0KKyBMaW5ndWlzdGljIElucXVpcnkgJiBXb3JkIENvdW50IChieSBbRW1wYXRoIFRleHQgQ2xhc3NpZmllcl0oaHR0cDovL2hjaS5zdGFuZm9yZC5lZHUvcHVibGljYXRpb25zLzIwMTYvZXRoYW4vZW1wYXRoLWNoaS0yMDE2LnBkZiksIFtMSVdDXShodHRwOi8vbGl3Yy53cGVuZ2luZS5jb20vKSAgYWxpa2UpDQoNCumAmeS4gOS6myBfX+aWh+Wtl+WIhuaekF9fIOS5i+W+jO+8jOacrOWRqOaIkeWAkee5vOe6jOeUqOmAmeS4gOe1hOizh+aWme+8jOS+huekuuevhO+8mg0KDQorIF9f5bC65bqm57iu5ribX18NCisgX1/pm4bnvqTliIbmnpBfXw0KKyBfX+izh+aWmeimluimuuWMll9fDQoNCumAmeW5vueoruWIhuaekOaKgOihk+eahOe2nOWQiOmBi+eUqOOAgjxicj4NCg0KLSAtIC0NCg0KYGBge3Igc2V0LW9wdGlvbnMsIGVjaG89RkFMU0UsIGNhY2hlPUZBTFNFfQ0KbGlicmFyeShrbml0cikNCm9wdGlvbnMod2lkdGg9MTIwKQ0Kb3B0c19jaHVuayRzZXQoY29tbWVudCA9IE5BKQ0KYGBgDQoNCmBgYHtyIHdhcm5pbmc9RiwgbWVzc2FnZT1GLCBjYWNoZT1GfQ0KbGlicmFyeShtYWdyaXR0cikNCmxpYnJhcnkoUnRzbmUpDQpsaWJyYXJ5KFJDb2xvckJyZXdlcikNCmxpYnJhcnkocmFuZG9tY29sb1IpDQpsaWJyYXJ5KHdvcmRjbG91ZCkNCmxpYnJhcnkoZDNoZWF0bWFwKQ0KbGlicmFyeShpZ3JhcGgpICANCmxpYnJhcnkocmVzaGFwZTIpDQpsaWJyYXJ5KGhpZ2hjaGFydGVyKQ0KYGBgDQoNCmBgYHtyfQ0KbG9hZCgnZGF0YS95ZWxwMS5yZGF0YScpICAjIGxvYWRpbmcgeWVscCBkYXRhICYgc2VudGltZW50IHNjb3Jlcw0KbG9hZCgnZGF0YS9lbXBhdGgucmRhdGEnKSAjIGxvYWRpbmcgZW1wYXRoIHNjb3Jlcw0KYGBgDQrovInlhaUgcGFja2FnZXMg5ZKMIGRhdGEg5LmL5b6M77yM5LiA6ZaL5aeL5oiR5YCR5pyJIDYg5YCLIERhdGEgRnJhbWU6DQoNCkRhdGEgRnJhbWUgIHwgTm8uIFJvd3MgeCBDb2xzICB8IE9iamVjdHMNCi0tLS0tLS0tLS0tIHwgLS0tLS0tLS0tLS0tLS0tLSB8IC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tICANCmBiaXpgICAgICAgIHwgMTEsNTM3ICB4IDExICAgICB8IGJ1c2luZXNzZXMNCmB1c2VyYCAgICAgIHwgNDMsODczICB4IDcgICAgICB8IHVzZXJzDQpgY2hlY2tgICAgICB8IDI2Miw3NjQgeCA0ICAgICAgfCBjaGVjay1pbidzDQpgcmV2aWV3YCAgICB8IDIxNSw4NzkgeCA4ICAgICAgfCByZXZpZXdzDQpgc2VudGlgICAgICB8IDIxNSw4NzkgeCAxMCAgICAgfCByZXZpZXdzJyBzZW50aW1lbnQgc2NvcmVzDQpgc2NvcmVzYCAgICB8IDIxNSw4NzkgeCAxOTQgICAgfCByZXZpZXdzJyBFbXBhdGgvTElXQyBzY29yZXMNCg0KPGJyPjxicj4NCg0KIyMgMS4g5ZWG5bqX55qE6aGe5YilICg1MDggQnVzaW5lc3MgQ2F0ZWdvcmllcykNCumAmeS7veizh+aWmeijoemdouaciTExLDUzN+Wutioq5ZWG5bqXKEJ1c2luZXNzKSoq5ZKMNTA456iuKirllYbmpa3poZ7liKUoQ2F0ZWdvcmllcykqKu+8jA0K5LiA5a625ZWG5bqX5Y+v5Lul5ZCM5pmC6Zq45bGs5pa85b6I5aSaKDAgfiAxMCnlgIvpoZ7liKXvvIwNCuaIkeWAkemmluWFiOiAg+aFru+8mg0KDQorIOavj+S4gOWAi+WVhualremhnuWIpeijoemdouacieWkmuWwkeWutuWVhuW6l++8nyDpgJnkupvllYblupfnuL3lhbHooqvoqZXoq5bpgY7lpJrlsJHvvJ8NCisg6YCZ5Lqb5ZWG5qWt6aGe5Yil5LmL6ZaT5pyJ55u46Zec5oCn5ZeO77yfIOWTquS4gOS6m+WVhualremhnuWIpee2k+W4uOacg+WQjOaZguWHuuePvu+8nw0KDQo8YnI+DQoNCiMjIyMgKDFhKSDllYbmpa3poZ7liKXos4fmlpnmlbTnkIYgDQpgYGB7cn0NCiMgbnVtYmVyIG9mIGJpeiBwZXIgY2F0ZWdvcnkNCkNBID0gYml6JGNhdCAlPiUgc3Ryc3BsaXQoJ3wnLFQpICU+JSB1bmxpc3QgJT4lIHRhYmxlICU+JSANCiAgZGF0YS5mcmFtZSAlPiUgJ25hbWVzPC0nKGMoJ25hbWUnLCduYml6JykpDQojIG51bWJlciBvZiByZXZpZXcgcGVyIGNhdGVnb3J5DQpDQSRucmV2ID0gQ0EkbmFtZSAlPiUgc2FwcGx5KGZ1bmN0aW9uKHope3N1bSgNCiAgcmV2aWV3JGJpZCAlaW4lIGJpeiRiaWRbZ3JlcCh6LGJpeiRjYXQsZml4ZWQ9VCldICl9KQ0KIyBhdmVyYWdlIG51bWJlciBvZiByZXZpZXdzIHBlciBidXNpbmVzcw0KQ0EkYXZnLnJldiA9IENBJG5yZXYgLyBDQSRuYml6ICAgIA0KQ0EgPSBDQVtvcmRlcigtQ0EkbnJldiksXSAgIyBvcmRlciBDQSBieSBuby4gcmV2aWV3DQpyb3duYW1lcyhDQSk9IENBJG5hbWUgDQpDQSRuYW1lID0gTlVMTA0KYGBgDQrlvp5gYml6JGNhdGVnb2V5YOijoemdouaVtOeQhuWHuiAqKjUwOCoqIOWAiyAqKkNhdGVnb3JpZXMqKu+8jA0K5Lim566X5Ye65q+P5LiA5YCLIENhdGVnb3J5IOeahO+8mg0KDQorIGBuYml6YCA6IG51bWJlciBvZiBidXNpbmVzcyAoaW4gdGhlIGNhdGVnb3J5KQ0KKyBgbnJldmAgOiBudW1iZXIgb2YgcmV2aWVydyAoaW4gdGhlIGNhdGVnb3J5KQ0KKyBgYXZnLnJldmAgOiBhdmVyYWdlIG51bWJlciBvZiByZXZpZXdzIHBlciBidXNpbmVzcw0KDQrmlL7lnKggYENBYCDpgJnlgIsgYERhdGEgRnJhbWVgIOijoemdou+8mg0KDQpgYGB7cn0NCkNBDQpgYGANCg0KYGBge3J9DQojIGNhdGVnb3J5LWJ1c2luZXNzIG1hdHJpeA0KbXhCQyA9IHJvd25hbWVzKENBKSAlPiUgc2FwcGx5KGZ1bmN0aW9uKHopDQogIGdyZXBsKHosYml6JGNhdCxmaXhlZD1UKSkNCnJvd25hbWVzKG14QkMpID0gYml6JGJpZA0KZGltKG14QkMpIA0KYGBgDQrlsIcgQnVzaW5lc3MoMTEsNTM3KSDlkowgQ2F0ZWdvcnkoNTA4KSDnmoTlsI3mh4npl5zkv4LmlL7lnKhgbXhCQ2Doo6HpnaLjgII8YnI+PGJyPg0KDQojIyMjICgxYikg5bC65bqm57iu5ribIChEaW1lbnNpb24gUmVkdWN0aW9uKSANCuS9v+eUqGB0U05FYO+8jOWwh2BteEJDYOeahOWwuuW6piBbMTEsNTM3IHggNTA4XSDnuK7muJvngrogWzIgeCA1MDhdIC4uLg0KYGBge3J9DQp0MCA9IFN5cy50aW1lKCkNCnNldC5zZWVkKDEyMykNCnRzbmVDYXQgPSBpZmVsc2UobXhCQywxLDApICU+JSB0ICU+JSANCiAgUnRzbmUoY2hlY2tfZHVwbGljYXRlcz1GLHRoZXRhPTAuMCxtYXhfaXRlcj0zMDAwKQ0KU3lzLnRpbWUoKSAtIHQwDQpgYGANCjxicj4NCg0KIyMjIyAoMWMpIOmajuWxpOW8j+mbhue+pOWIhuaekCAoSGllcmFyY2hpY2FsIENsdXN0ZXJpbmcpIA0K5Zyo57iu5rib5bC65bqm5LmL5Lit5YGa6ZqO5bGk5byP6ZuG576k5YiG5p6Q44CCDQpgYGB7cn0NClkgPSB0c25lQ2F0JFkgICAgICAgICAgICMgdFNORSBjb29yZGluYXRlcw0KZCA9IGRpc3QoWSkgICAgICAgICAgICAgIyBkaXN0YW5jZSBtYXRyaXgNCmhjID0gaGNsdXN0KGQpICAgICAgICAgICMgaGktY2x1c3RlcmluZw0KYGBgDQoNCmBgYHtyfQ0KSyA9IDYwICAgICAgICAgICAgICAgICAgIyBudW1iZXIgb2YgY2x1c3RlcnMgDQpDQSRncnAgPSBjdXRyZWUoaGMsSykgICANCnRhYmxlKENBJGdycCkNCmBgYA0K57aT6YeN6KaG5ZiX6Kmm5LmL5b6M77yM5bCH6YCZKio1MDgqKuWAiyoq5ZWG5qWt6aGe5YilKENhdGVnb3JpZXMpKioNCuWIhuaIkCoqNjAqKuWAiyoq5ZWG5qWt6aGe5Yil576k57WEKENhdGVnb3J5IEdyb3VwcykqKuOAgiA8YnI+PGJyPg0KDQojIyMjICgxZCkg5a2X6ZuyIChXb3JkIENsb3VkKQ0KYGBge3J9DQpwYWxzID0gZGlzdGluY3RDb2xvclBhbGV0dGUoSykgICMgcGFsZXR0ZSBvZiBLIGNvbG9ycyANCnBuZygiY2F0ZWdvcnkucG5nIiwgd2lkdGg9MzIwMCwgaGVpZ2h0PTE4MDApDQp0ZXh0cGxvdChZWywxXSxZWywyXSxyb3duYW1lcyhDQSksZm9udD0yLCANCiAgICAgICAgIGNvbCA9IHBhbHNbQ0EkZ3JwXSwgICAgICAgICAjIGNvbG9yIGJ5IGdyb3VwICAgIA0KICAgICAgICAgY2V4ID0gKDAuNStsb2coQ0EkbnJldikvNSkpICMgc2l6ZSBieSBuby4gcmV2aWV3cw0KZGV2Lm9mZigpDQpgYGANCuWwh+Wtl+mbsueVq+WcqGBjYXRlZ29yeS5wbmdg6KOh6Z2i77yaDQoNCisg5q+P5YCL5a2X5Luj6KGo5LiA5YCL5ZWG5qWt6aGe5YilKENhdGVnb3JpZXMpDQorIOWtl+eahOmhj+iJsuS7o+ihqOWVhualremhnuWIpee+pOe1hChDYXRlZ29yeSBHcm91cHMpDQorIOWtl+eahOWkp+Wwj+S7o+ihqOmAmeWAi+WVhualremhnuWIpeiiq+ipleirlueahOasoeaVuCAobnVtYmVyIG9mIHJldmlld3MpDQorIOmdoOWcqOS4gOi1t+eahOOAgeWQjOS4gOeorumhj+iJsueahOWtl++8jOS7o+ihqOe2k+W4uOS4gOi1t+WHuuePvueahOWVhualremhnuWIpQ0KDQohWy5dKGNhdGVnb3J5LnBuZykNCg0KDQoNCiMjIDIuIOipleirlueahOWFp+WuuSAoMTk0IENvbnRlbnQgQ2xhc3NlcykNCuaOpeS4i+S+huiAg+aFruipleirlueahOWFp+Wuue+8jOS4iumAseaIkeWAkeW3sue2k+S9v+eUqA0KW0VtcGF0aCBUZXh0IENsYXNzaWZpZXJdKGh0dHA6Ly9oY2kuc3RhbmZvcmQuZWR1L3B1YmxpY2F0aW9ucy8yMDE2L2V0aGFuL2VtcGF0aC1jaGktMjAxNi5wZGYpDQrvvIzkvp3lhbbpoJDoqK3nmoQxOTTnqK4qKuWFp+WuuShDbGFzcykqKu+8jA0K5bCN6YCZMjE1LDg3Oeevh+ipleirluWIhuWIpeWBmumBjuipleWIhuOAgg0K5Lmf5bCx5piv6Kqq77yM5paH6ZuG5LmL5Lit55qE5q+P5LiA56+H6KmV6KuW6YO95pyJMTk05YCLKirlhaflrrnoqZXliIYqKu+8jA0K5a2Y5pS+5ZyoYHNjb3Jlc2DpgJnlgItEYXRhIEZyYW1l6KOh6Z2i44CCPGJyPg0KYGBge3J9DQpkaW0oc2NvcmVzKQ0KYGBgDQrkvb/nlKjpgJnkuIDkupvos4fmlpnvvIzmiJHlgJHlj6/ku6XlsI3pgJkxOTTnqK7lhaflrrkoQ2xhc3NlcynpgLLooYzliIbnvqTvvIwNCuS5n+WPr+S7peeUqOWtl+mbsuS+huWRiOePvuS4jeWQjOWFp+WuueS5i+mWk+eahOebuOmXnOaAp+OAgjxicj4NCjxicj4NCg0KIyMjIyAoMmEpIOWFp+WuueasiumHjSAoQ2xhc3MgV2VpZ2h0cykNCuioiOeul+avj+S4gOeoruWFp+WuueeahCoq5qyK6YeNKiooKipXZWlnaHQqKjogdGhlIHN1bSBvZiBjbGFzcyBzY29yZXMgd2l0aGluIHRoZSBjb3JwdXMp77yMDQrmlL7lnKhgd0NsYXNzYOijoemdou+8jOS4puWwh2BzY29yZXNgKOWFp+WuueipleWIhuizh+aWmSnkvp3mrIrph43mjpLluo/jgIIgDQpgYGB7cn0NCiMgDQojIG9yZGVyIHRoZSBzY29yZSBtYXRyaXggYnkgY2xhc3Mgd2VpZ2h0cw0Kc2NvcmVzID0gc2NvcmVzWyxvcmRlcigtY29sU3VtcyhzY29yZXMpKV0NCndDbGFzcyA9IGNvbFN1bXMoc2NvcmVzKSAgIyBjbGFzcyB3ZWlnaHRzDQpoZWFkKHdDbGFzcywyMCkNCmBgYA0KPGJyPg0KDQojIyMjICgyYikg5bC65bqm57iu5ribIChEaW1lbnNpb24gUmVkdWN0aW9uKSANCuS9v+eUqGB0U05FYO+8jOWwh2BzY29yZXNg55qE5bC65bqmIFsyMTY4NzkgeCAxOTRdIOe4rua4m+eCuiBbMiB4IDE5NF0gLi4uDQpgYGB7cn0NCnQwID0gU3lzLnRpbWUoKQ0Kc2V0LnNlZWQoMTIzKQ0KdHNuZUNsYXNzID0gc2NvcmVzICU+JSBzY2FsZSAlPiUgYXMubWF0cml4ICU+JSB0ICU+JSANCiAgUnRzbmUodGhldGE9MC4wLG1heF9pdGVyPTMwMDApDQpTeXMudGltZSgpIC0gdDANCmBgYA0KPGJyPg0KDQojIyMjICgyYykg6ZqO5bGk5byP6ZuG576k5YiG5p6QIChIaWVyYXJjaGljYWwgQ2x1c3RlcmluZykgDQrlnKjnuK7muJvlsLrluqbkuYvkuK3lgZrpmo7lsaTlvI/pm4bnvqTliIbmnpDjgIINCmBgYHtyfQ0KWSA9IHRzbmVDbGFzcyRZICAgICAgIyB0U05FIGNvb3JkaW5hdGVzDQpkID0gZGlzdChZKSAgICAgICAgICAjIGRpc3RhbmNlIG1hdHJpeA0KaGMgPSBoY2x1c3QoZCkgICAgICAgIyBoaS1jbHVzdGVyaW5nDQpgYGANCg0KYGBge3J9DQpLID0gNDANCmdwQ2xhc3MgPSBjdXRyZWUoaGMsSykgICMgSyBncm91cHMNCnRhYmxlKGdwQ2xhc3MpICAgICAgICAgICMgbm8uIGNsYXNzZXMgcGVyIGdyb3VwDQpgYGANCui/lOimhuWYl+ippuS5i+W+jO+8jOWwh+mAmSoqMTk0KirnqK4qKuWFp+WuuShDbGFzc2VzKSoqDQrliIbmiJAqKjQwKirlgIsqKuWFp+Wuuee+pOe1hChDbGFzcyBHcm91cHMpKirjgIIgPGJyPjxicj4NCg0KIyMjIyAoMmQpIOWtl+mbsiAoV29yZCBDbG91ZCkNCmBgYHtyfQ0KcGFscyA9IGRpc3RpbmN0Q29sb3JQYWxldHRlKEspICAjIHBhbGV0dGUgb2YgSyBjb2xvcnMgDQpwbmcoImNsYXNzZXMucG5nIiwgd2lkdGg9MzIwMCwgaGVpZ2h0PTE4MDApDQp0ZXh0cGxvdChZWywxXSxZWywyXSxjb2xuYW1lcyhzY29yZXMpLGZvbnQ9MiwgDQogIGNvbCA9IHBhbHNbZ3BDbGFzc10sICAgICAgIyBjb2xvciBieSBncm91cCAgICANCiAgY2V4ID0gMC41K2xvZyh3Q2xhc3MpLzMpICAjIHNpemUgYnkgY2xhc3Mgd2VpZ2h0DQpkZXYub2ZmKCkNCmBgYA0K5bCH5a2X6Zuy55Wr5ZyoYGNsYXNzZXMucG5nYOijoemdou+8mg0KDQorIOavj+WAi+Wtl+S7o+ihqOS4gOeoruWFp+WuuShDbGFzcykNCisg5a2X55qE6aGP6Imy5Luj6KGo5YWn5a65576k57WEKENsYXNzIEdyb3VwcykNCisg5a2X55qE5aSn5bCP5Luj6KGo6YCZ56iu5YWn5a655Zyo5paH6ZuG5LmL5Lit55qE5qyK6YeNDQorIOmdoOWcqOS4gOi1t+eahOOAgeWQjOS4gOeorumhj+iJsueahOWtl++8jOS7o+ihqOe2k+W4uOS4gOi1t+WHuuePvueahOWFp+WuuQ0KDQohWy5dKGNsYXNzZXMucG5nKQ0KDQojIyMjICgyZSkg5YWn5a655LmL6ZaT55qE55u46Zec5oCnDQrnubznuozliIbmnpDkuYvliY3vvIzlhYjnlKjnhrHlnJbmqqLmn6XkuIDkuIvlhaflrrnoqZXliIbkuYvplpPnmoTnm7jpl5zmgKcgLi4uDQpgYGB7ciBmaWcud2lkdGg9MTAsIGZpZy5oZWlnaHQ9MTB9DQojIGNvcnJlbGF0aW9uIGJldHdlZW4gdGhlIGNsYXNzZXMNCmNyID0gY29yKHNjb3JlcykgIA0KY3IgJT4lIGQzaGVhdG1hcChzY2FsZT0nbm9uZScsY29sPWNtLmNvbG9ycygyNTYpKQ0KYGBgDQoNCg0K5om+5Ye655u46Zec5L+C5pW46LyD6auY55qE5YWn5a6556iu6aGeDQpgYGB7cn0NCmNvcmdycCA9IGZ1bmN0aW9uKHgsIHRocmVzaG9sZD0wLjYpIHsNCiAgeCA9IHgqbG93ZXIudHJpKHgpDQogIGNoZWNrID0gd2hpY2goeD50aHJlc2hvbGQsYXJyLmluZD1UKQ0KICBnY3IgPSBncmFwaC5kYXRhLmZyYW1lKGNoZWNrLCBkaXJlY3RlZD1GKQ0KICBnY3IgPSBzcGxpdCh1bmlxdWUoYXMudmVjdG9yKGNoZWNrKSksY2x1c3RlcnMoZ2NyKSRtZW1iZXJzaGlwKQ0KICBsYXBwbHkoZ2NyLCBmdW5jdGlvbihnKXtyb3duYW1lcyh4KVtnXX0pICB9DQpjb3JncnAoY3IsIDAuNikgDQpgYGANCjxicj48YnI+DQoNCg0KIyMgMy4g5ZWG5qWt6aGe5YilIOiIhyDoqZXoq5blhaflrrkNCuaOpeS4i+S+huaIkeWAkeWPr+S7peWBmioq5ZWG5qWt6aGe5YilKDUwOCBDYXRlZ29yaWVzKSoq5ZKMKiroqZXoq5blhaflrrkoMTk0IENsYXNzZXMpKioNCuS5i+mWk+eahOS6pOWPieWIhuaekOOAgjxicj4NCjxicj4NCg0KIyMjIyAoM2EpIOS6pOWPieafpeipouevhOS+iw0K5YWI55So5Lul5LiL6YCZ5LiA5YCL56+E5L6L5L6G56S656+E5Lqk5Y+J5p+l6Kmi55qE5YGa5rOVDQoNCisg5Zyo6KmV6KuW5pW46LaF6YGONTAw55qE5ZWG5qWt6aGe5Yil5LmL5Lit77yM5ZOq5Lqb6aGe5Yil55qE6KmV6KuW5LmL5Lit55qE5q2j6Z2i5oOF57eS5ZKM6LKg6Z2i5oOF57eS55qE55u46Zec5L+C5pW45piv5pyA6auY55qE5ZGi77yfIA0KYGBge3J9DQojIGNvcnJlbGF0aW9uIGJldHdlZW4gcG9zaXRpdmUgJiBuZWdhdGl2ZSBlbW90aW9uIA0KbXhCQ1ssQ0EkbnJldj41MDBdICU+JSBhcHBseSgyLGZ1bmN0aW9uKHYpIHsNCiAgaSA9IHJldmlldyRiaWQgJWluJSByb3duYW1lcyhteEJDKVt2XQ0KICBjb3Ioc2NvcmVzJHBvc2l0aXZlX2Vtb3Rpb25baV0sIHNjb3JlcyRuZWdhdGl2ZV9lbW90aW9uW2ldKQ0KfSkgJT4lIHNvcnQgJT4lIHRhaWwoMjApDQpgYGANCuaIkeWAkeWPr+S7peeci+WIsGBEb2N0b3JzYOWSjGBQZXRzYOmAmeWFqeeoruWVhualremhnuWIpeeahOipleirluacgOW4uOacg+WQjOaZguWHuuePvuato+mdouWSjOiyoOmdouaDhee3kuOAgg0KPGJyPjxicj4NCg0KIyMjIyAoM2IpIOWQhOWVhualremhnuWIpeeahOWFp+WuueasiumHjQ0K5LiN5ZCM5ZWG5qWt6aGe5Yil55qE6KmV6KuW6KOh6Z2i5pyD5pyJ5LiN5LiA5qij55qE5YWn5a6577yMDQrmiJHlgJHlj6/ku6XnlKjnhrHlnJbkvoblkYjnj77lkITllYbmpa3poZ7liKXnmoTlhaflrrnliIbluIPni4Dms4HjgIINCuWFiOaKiuWFp+WuueipleWIhuS+neWVhualremhnuWIpeW5s+Wdh+i1t+S+hu+8jA0K5pS+5ZyoYHd4Q2xhc3Ng6YCZ5YCL55+p6Zmj6KOh6Z2i44CCDQpgYGB7cn0NCnd4Q2xhc3MgPSBhcHBseShteEJDLDIsZnVuY3Rpb24odil7ICAgICAjIGZvciBldmVyeSBjYXRlZ29yeQ0KICBpID0gcmV2aWV3JGJpZCAlaW4lIHJvd25hbWVzKG14QkMpW3ZdICMgZmluZCBpdHMgcmV2aWV3cyANCiAgY29sTWVhbnMoc2NvcmVzW2ksXSkgICAgICAgICAgICAgICAgICAjIGF2ZXJhZ2UgdGhlaXIgY2xhc3Mgc2NvcmVzDQp9KQ0KZGltKHd4Q2xhc3MpDQpgYGANCueUseaWvGB3eENsYXNzYOmAmeWAi+efqemZo+WkquWkp++8jA0K5oiR5YCR5Y+q55Wr5Ye66KmV6KuW5pW45pyA5aSa55qENTDlgIvllYbmpa3poZ7liKXlkozmrIrph43mnIDlpKfnmoQ1MOeoruipleirluWFp+WuuQ0KYGBge3IgZmlnLndpZHRoPTksIGZpZy5oZWlnaHQ9OX0NCnd4Q2xhc3NbMTo1MCwxOjUwXSAlPiUgbG9nICU+JSANCiAgZDNoZWF0bWFwKFQsVCxzY2FsZT0nbm9uZScsY29sPSdQaVlHJykNCmBgYA0KPGJyPjxicj4NCg0KIyMjIyAoM2MpIOe+pOe1hOeGseWcliANCuWboOeCuuWVhualremhnuWIpSg1MDggY2F0ZWdvcmllcynlkozoqZXoq5blhaflrrkoMTk0IGNsYXNzZXMp55qE5pW46YeP6YO95b6I5aSaDQoo6YCZ5bCx5piv5aSn5pW45pOa55qE54m55b61Ke+8jA0K5oiR5YCR6KaB5Zyo576k57WE6YCZ5YCL5bGk6Z2i77yM5omN5q+U6LyD5a655piT6KeA5a+f5pW05YCL5paH6ZuG55qE5YWn5a655YiG5biD54uA5rOB44CCDQrlhbblr6bvvIzpgJnlsLHmmK/lpKfmlbjmk5rliIbmnpDkuYvkuK3vvIzmiJHlgJHluLjluLjpnIDopoHlhYjlgZrpm4bnvqTliIbmnpDnmoTnkIbnlLHjgIINCuaIkeWAkeeUqOS7peS4i+eahOeGseWcluWRiOePvioq5ZWG5qWt6aGe5Yil576k57WEKDYwKSoq5ZKMKirlhaflrrnnvqTntYQoNDApKirkuYvplpPnmoTpl5zkv4LjgIINCmBgYHtyIGZpZy53aWR0aD04LCBmaWcuaGVpZ2h0PTEyfQ0KeCA9IG1hdHJpeCgwLCBucm93PW1heChncENsYXNzKSwgbmNvbD1tYXgoQ0EkZ3JwKSwNCiAgZGltbmFtZXM9bGlzdChzcHJpbnRmKCdDTFMlMDJkJywxOm1heChncENsYXNzKSksDQogICAgICAgICAgICAgICAgc3ByaW50ZignQ0FUJTAyZCcsMTptYXgoQ0EkZ3JwKSkpKQ0KZm9yKGkgaW4gMTpucm93KHgpKSBmb3IoaiBpbiAxOm5jb2woeCkpICANCiAgeFtpLGpdID0gc3VtKCB3eENsYXNzW2dwQ2xhc3M9PWksIENBJGdycD09al0gKQ0KdCh4KSAlPiUge2xvZygwLjAwNSsuKX0gJT4lIGQzaGVhdG1hcChzY2FsZT0nbm9uZScsY29sPSdQaVlHJykNCmBgYA0K5b6e5Lul5LiK55qE576k57WE54ax5ZyW6KOh6Z2i5oiR5YCR5Y+v5Lul5riF5qWa55qE55yL5Yiw5pW05YCL5paH6ZuGKOWcqOWQhOWVhualremhnuWIpeS5i+S4rSnnmoTlhaflrrnliIbluIPni4Dms4HjgIINCjxicj4NCuWmguaenOmcgOimgeeci+afkOS4gOe+pOe1hOS5i+S4reacieWTquS4gOS6m+WVhualremhnuWIpeaIluipleirluWFp+Wuue+8jOWPr+S7pemAmeaoo+WBmu+8mg0KYGBge3J9DQpyb3duYW1lcyhDQSlbQ0EkZ3JwPT04XQ0KYGBgDQoNCmBgYHtyfQ0KbmFtZXMoc2NvcmVzKVtncENsYXNzPT0xXQ0KYGBgDQoNCjxicj48YnI+DQoNCiMjIDQuIOi2qOWLouWIhuaekA0KDQo8YnI+DQoNCiMjIyMgKDRhKSBUcmVuZCBvZiBDb250ZW50IENsYXNzZXMNCmBgYHtyfQ0KdHhDbGFzcyA9IHR4ID0gc3BsaXQoc2NvcmVzLCBjdXQocmV2aWV3JGRhdGUsJ3F1YXJ0ZXInKSkgJT4lIA0KICBzYXBwbHkoY29sU3VtcykgJT4lIGFwcGx5KDIsIGZ1bmN0aW9uKHYpIHYvc3VtKHYpKQ0KDQojIFRyZW5kIG9mIENsYXNzZXMNCmRmID0gZGF0YS5mcmFtZShjbGFzcz1yb3duYW1lcyh0eCksIHR4Wyw5OjMyXSkgJT4lIG1lbHQoJ2NsYXNzJykNCmRmJGRhdGUgPSBhcy5EYXRlKA0KICBzdWJzdHIoYXMuY2hhcmFjdGVyKGRmJHZhcmlhYmxlKSwyLDExKSxmb3JtYXQ9IiVZLiVtLiVkIikNCmRmJHZhbHVlID0gcm91bmQoMTAwKmRmJHZhbHVlLDMpDQpzdWJzZXQoZGYsIGNsYXNzICVpbiUgcm93bmFtZXModHgpWzE6MTk0XSkgJT4lIA0KICBoY2hhcnQoInNwbGluZSIsaGNhZXMoeD1kYXRlLHk9dmFsdWUsZ3JvdXA9Y2xhc3MpKSAlPiUgDQogIGhjX2xlZ2VuZChhbGlnbj0ibGVmdCIsIGxheW91dD0idmVydGljYWwiLHZlcnRpY2FsQWxpZ249InRvcCIpICU+JSANCiAgaGNfYWRkX3RoZW1lKGhjX3RoZW1lX2ZsYXQoKSkgJT4lIA0KICBoY190aXRsZSh0ZXh0PSdXZWlnaHRzIG9mIENsYXNzZXMgKCUgb2YgVG90YWwgV2VpZ2h0KScpDQpgYGANCjxicj48YnI+DQoNCg0KIyMjIyAoNGIpIFRyZW5kIG9mIENsYXNzIEdyb3Vwcw0KYGBge3J9DQojIFRyZW5kIG9mIENsYXNzIEdyb3VwDQp4ID0gc3BsaXQoZGF0YS5mcmFtZSh0eCksZ3BDbGFzcykgJT4lIHNhcHBseShjb2xTdW1zKSAlPiUgdA0KZGYgPSBkYXRhLmZyYW1lKA0KICBjbGFzcz1zcHJpbnRmKCdHJTAyZCcsMTptYXgoZ3BDbGFzcykpLCB4Wyw5OjMyXSkgJT4lIA0KICBtZWx0KCdjbGFzcycpDQpkZiRkYXRlID0gYXMuRGF0ZSgNCiAgc3Vic3RyKGFzLmNoYXJhY3RlcihkZiR2YXJpYWJsZSksMiwxMSksZm9ybWF0PSIlWS4lbS4lZCIpDQpkZiR2YWx1ZSA9IHJvdW5kKDEwMCpkZiR2YWx1ZSwzKQ0KZGYgJT4lIGhjaGFydCgic3BsaW5lIixoY2Flcyh4PWRhdGUseT12YWx1ZSxncm91cD1jbGFzcykpICU+JSANCiAgaGNfbGVnZW5kKGFsaWduPSJsZWZ0IiwgbGF5b3V0PSJ2ZXJ0aWNhbCIsdmVydGljYWxBbGlnbj0idG9wIikgJT4lIA0KICBoY19hZGRfdGhlbWUoaGNfdGhlbWVfZmxhdCgpKSAlPiUgDQogIGhjX3RpdGxlKHRleHQ9J1dlaWdodHMgb2YgQ2xhc3MgR3JvdXBzICglIG9mIFRvdGFsIFdlaWdodCknKQ0KYGBgDQo8YnI+PGJyPg0KDQojIyMjICg0YykgVHJlbmQgb2YgQnVzaW5lc3MgQ2F0ZWdvcmllcw0KYGBge3J9DQp0eENhdCA9IHR4ID0gc3BsaXQocmV2aWV3LCBjdXQocmV2aWV3JGRhdGUsJ3F1YXJ0ZXInKSkgJT4lIA0KICBzYXBwbHkoZnVuY3Rpb24oeCkgYXBwbHkobXhCQywyLGZ1bmN0aW9uKHYpDQogICAgICBzdW0oeCRiaWQgJWluJSByb3duYW1lcyhteEJDKVt2XSkgKSkgJT4lIA0KICBhcHBseSgyLCBmdW5jdGlvbih2KSB2L3N1bSh2KSkNCg0KIyBUcmVuZCBvZiBDYXRlZ29yaWVzDQpkZiA9IGRhdGEuZnJhbWUoY2F0ZWdvcnk9cm93bmFtZXModHgpLCB0eFssOTozMl0pICU+JSBtZWx0KCdjYXRlZ29yeScpDQpkZiRkYXRlID0gYXMuRGF0ZSgNCiAgc3Vic3RyKGFzLmNoYXJhY3RlcihkZiR2YXJpYWJsZSksMiwxMSksZm9ybWF0PSIlWS4lbS4lZCIpDQpkZiR2YWx1ZSA9IHJvdW5kKDEwMCpkZiR2YWx1ZSwzKQ0Kc3Vic2V0KGRmLCBjYXRlZ29yeSAlaW4lIHJvd25hbWVzKENBKVsxOjMwXSkgJT4lIA0KICBoY2hhcnQoInNwbGluZSIsaGNhZXMoeD1kYXRlLHk9dmFsdWUsZ3JvdXA9Y2F0ZWdvcnkpKSAlPiUgDQogIGhjX2xlZ2VuZChhbGlnbj0ibGVmdCIsIGxheW91dD0idmVydGljYWwiLHZlcnRpY2FsQWxpZ249InRvcCIpICU+JSANCiAgaGNfYWRkX3RoZW1lKGhjX3RoZW1lX2ZsYXQoKSkgJT4lIA0KICBoY190aXRsZSh0ZXh0PSdXZWlnaHRzIG9mIENhdGVnb3JpZXMgKCUgb2YgVG90YWwgUmV2aWV3cyknKQ0KYGBgDQo8YnI+PGJyPg0KDQoNCiMjIyMgKDRkKSBUcmVuZCBvZiBCdXNpbmVzcyBDYXRlZ29yeSBHcm91cHMNCmBgYHtyfQ0KIyBUcmVuZCBvZiBDYXRlZ29yeSBHcm91cHMNCnggPSBzcGxpdChkYXRhLmZyYW1lKHR4KSxDQSRncnApICU+JSBzYXBwbHkoY29sU3VtcykgJT4lIHQNCmRmID0gZGF0YS5mcmFtZSgNCiAgY2F0ZWdvcnk9c3ByaW50ZignRyUwMmQnLDE6bWF4KENBJGdycCkpLCB4Wyw5OjMyXSkgJT4lIA0KICBtZWx0KCdjYXRlZ29yeScpDQpkZiRkYXRlID0gYXMuRGF0ZSgNCiAgc3Vic3RyKGFzLmNoYXJhY3RlcihkZiR2YXJpYWJsZSksMiwxMSksZm9ybWF0PSIlWS4lbS4lZCIpDQpkZiR2YWx1ZSA9IHJvdW5kKDEwMCpkZiR2YWx1ZSwzKQ0KZGYgJT4lIGhjaGFydCgic3BsaW5lIixoY2Flcyh4PWRhdGUseT12YWx1ZSxncm91cD1jYXRlZ29yeSkpICU+JSANCiAgaGNfbGVnZW5kKGFsaWduPSJsZWZ0IiwgbGF5b3V0PSJ2ZXJ0aWNhbCIsdmVydGljYWxBbGlnbj0idG9wIikgJT4lIA0KICBoY19hZGRfdGhlbWUoaGNfdGhlbWVfZmxhdCgpKSAlPiUgDQogIGhjX3RpdGxlKHRleHQ9J1dlaWdodHMgb2YgQ2F0ZWdvcnkgR3JvdXBzICglIG9mIFRvdGFsIFJldmlld3MpJykNCmBgYA0KPGJyPjxicj4NCg0KDQo=