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)
[1] 11537 508
將 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
Time difference of 23.86 secs
(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)
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
5 2 10 7 9 6 4 6 14 8 3 2 5 8 9 3 2 7 11 12 10 3 15 5 4 6 7 8 7
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
9 7 12 6 5 11 9 11 21 18 12 8 10 8 14 15 2 5 11 6 12 15 6 4 8 4 13 15 13
59 60
10 10
經重覆嘗試之後,將這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()
null device
1
將字雲畫在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(內容評分資料)依權重排序。
#
# 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.549 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()
null device
1
將字雲畫在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"
[5] "violence" "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)')
LS0tDQp0aXRsZTogIuWwuuW6pue4rua4m+OAgembhue+pOWIhuaekOOAgeizh+aWmeimluimuuWMliINCnN1YnRpdGxlOiAiRXhwbG9yaW5nIFllbHAgS2FnZ2xlIERhdGFzZXQiDQphdXRob3I6ICJUb255IENodW8iDQpkYXRlOiAiMjAxN+W5tDfmnIgyM+aXpSINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6DQogICAgaGlnaGxpZ2h0OiB0ZXh0bWF0ZQ0KICAgIHRoZW1lOiBsdW1lbg0KLS0tDQoNCjxicj4NCg0K5LiK6YCx5oiR5YCR5bCNIFllbHAgS2FnZ2xlIOijoemdoueahOaWh+Wtl+WBmumBju+8mg0KDQorIEJhZyBvZiBXb3Jkcw0KKyBTZW50aW1lbnQgQW5hbHlzaXMgDQorIExpbmd1aXN0aWMgSW5xdWlyeSAmIFdvcmQgQ291bnQgKGJ5IFtFbXBhdGggVGV4dCBDbGFzc2lmaWVyXShodHRwOi8vaGNpLnN0YW5mb3JkLmVkdS9wdWJsaWNhdGlvbnMvMjAxNi9ldGhhbi9lbXBhdGgtY2hpLTIwMTYucGRmKSwgW0xJV0NdKGh0dHA6Ly9saXdjLndwZW5naW5lLmNvbS8pICBhbGlrZSkNCg0K6YCZ5LiA5LqbIF9f5paH5a2X5YiG5p6QX18g5LmL5b6M77yM5pys5ZGo5oiR5YCR57m857qM55So6YCZ5LiA57WE6LOH5paZ77yM5L6G56S656+E77yaDQoNCisgX1/lsLrluqbnuK7muJtfXw0KKyBfX+mbhue+pOWIhuaekF9fDQorIF9f6LOH5paZ6KaW6Ka65YyWX18NCg0K6YCZ5bm+56iu5YiG5p6Q5oqA6KGT55qE57ac5ZCI6YGL55So44CCPGJyPg0KDQotIC0gLQ0KDQpgYGB7ciBzZXQtb3B0aW9ucywgZWNobz1GQUxTRSwgY2FjaGU9RkFMU0V9DQpsaWJyYXJ5KGtuaXRyKQ0Kb3B0aW9ucyh3aWR0aD0xMjApDQpvcHRzX2NodW5rJHNldChjb21tZW50ID0gTkEpDQpgYGANCg0KYGBge3Igd2FybmluZz1GLCBtZXNzYWdlPUYsIGNhY2hlPUZ9DQpsaWJyYXJ5KG1hZ3JpdHRyKQ0KbGlicmFyeShSdHNuZSkNCmxpYnJhcnkoUkNvbG9yQnJld2VyKQ0KbGlicmFyeShyYW5kb21jb2xvUikNCmxpYnJhcnkod29yZGNsb3VkKQ0KbGlicmFyeShkM2hlYXRtYXApDQpsaWJyYXJ5KGlncmFwaCkgIA0KbGlicmFyeShyZXNoYXBlMikNCmxpYnJhcnkoaGlnaGNoYXJ0ZXIpDQpgYGANCg0KYGBge3J9DQpsb2FkKCdkYXRhL3llbHAxLnJkYXRhJykgICMgbG9hZGluZyB5ZWxwIGRhdGEgJiBzZW50aW1lbnQgc2NvcmVzDQpsb2FkKCdkYXRhL2VtcGF0aC5yZGF0YScpICMgbG9hZGluZyBlbXBhdGggc2NvcmVzDQpgYGANCui8ieWFpSBwYWNrYWdlcyDlkowgZGF0YSDkuYvlvozvvIzkuIDplovlp4vmiJHlgJHmnIkgNiDlgIsgRGF0YSBGcmFtZToNCg0KRGF0YSBGcmFtZSAgfCBOby4gUm93cyB4IENvbHMgIHwgT2JqZWN0cw0KLS0tLS0tLS0tLS0gfCAtLS0tLS0tLS0tLS0tLS0tIHwgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gIA0KYGJpemAgICAgICAgfCAxMSw1MzcgIHggMTEgICAgIHwgYnVzaW5lc3Nlcw0KYHVzZXJgICAgICAgfCA0Myw4NzMgIHggNyAgICAgIHwgdXNlcnMNCmBjaGVja2AgICAgIHwgMjYyLDc2NCB4IDQgICAgICB8IGNoZWNrLWluJ3MNCmByZXZpZXdgICAgIHwgMjE1LDg3OSB4IDggICAgICB8IHJldmlld3MNCmBzZW50aWAgICAgIHwgMjE1LDg3OSB4IDEwICAgICB8IHJldmlld3MnIHNlbnRpbWVudCBzY29yZXMNCmBzY29yZXNgICAgIHwgMjE1LDg3OSB4IDE5NCAgICB8IHJldmlld3MnIEVtcGF0aC9MSVdDIHNjb3Jlcw0KDQo8YnI+PGJyPg0KDQojIyAxLiDllYblupfnmoTpoZ7liKUgKDUwOCBCdXNpbmVzcyBDYXRlZ29yaWVzKQ0K6YCZ5Lu96LOH5paZ6KOh6Z2i5pyJMTEsNTM35a62KirllYblupcoQnVzaW5lc3MpKirlkow1MDjnqK4qKuWVhualremhnuWIpShDYXRlZ29yaWVzKSoq77yMDQrkuIDlrrbllYblupflj6/ku6XlkIzmmYLpmrjlsazmlrzlvojlpJooMCB+IDEwKeWAi+mhnuWIpe+8jA0K5oiR5YCR6aaW5YWI6ICD5oWu77yaDQoNCisg5q+P5LiA5YCL5ZWG5qWt6aGe5Yil6KOh6Z2i5pyJ5aSa5bCR5a625ZWG5bqX77yfIOmAmeS6m+WVhuW6l+e4veWFseiiq+ipleirlumBjuWkmuWwke+8nw0KKyDpgJnkupvllYbmpa3poZ7liKXkuYvplpPmnInnm7jpl5zmgKfll47vvJ8g5ZOq5LiA5Lqb5ZWG5qWt6aGe5Yil57aT5bi45pyD5ZCM5pmC5Ye654++77yfDQoNCjxicj4NCg0KIyMjIyAoMWEpIOWVhualremhnuWIpeizh+aWmeaVtOeQhiANCmBgYHtyfQ0KIyBudW1iZXIgb2YgYml6IHBlciBjYXRlZ29yeQ0KQ0EgPSBiaXokY2F0ICU+JSBzdHJzcGxpdCgnfCcsVCkgJT4lIHVubGlzdCAlPiUgdGFibGUgJT4lIA0KICBkYXRhLmZyYW1lICU+JSAnbmFtZXM8LScoYygnbmFtZScsJ25iaXonKSkNCiMgbnVtYmVyIG9mIHJldmlldyBwZXIgY2F0ZWdvcnkNCkNBJG5yZXYgPSBDQSRuYW1lICU+JSBzYXBwbHkoZnVuY3Rpb24oeil7c3VtKA0KICByZXZpZXckYmlkICVpbiUgYml6JGJpZFtncmVwKHosYml6JGNhdCxmaXhlZD1UKV0gKX0pDQojIGF2ZXJhZ2UgbnVtYmVyIG9mIHJldmlld3MgcGVyIGJ1c2luZXNzDQpDQSRhdmcucmV2ID0gQ0EkbnJldiAvIENBJG5iaXogICAgDQpDQSA9IENBW29yZGVyKC1DQSRucmV2KSxdICAjIG9yZGVyIENBIGJ5IG5vLiByZXZpZXcNCnJvd25hbWVzKENBKT0gQ0EkbmFtZSANCkNBJG5hbWUgPSBOVUxMDQpgYGANCuW+nmBiaXokY2F0ZWdvZXlg6KOh6Z2i5pW055CG5Ye6ICoqNTA4Kiog5YCLICoqQ2F0ZWdvcmllcyoq77yMDQrkuKbnrpflh7rmr4/kuIDlgIsgQ2F0ZWdvcnkg55qE77yaDQoNCisgYG5iaXpgIDogbnVtYmVyIG9mIGJ1c2luZXNzIChpbiB0aGUgY2F0ZWdvcnkpDQorIGBucmV2YCA6IG51bWJlciBvZiByZXZpZXJ3IChpbiB0aGUgY2F0ZWdvcnkpDQorIGBhdmcucmV2YCA6IGF2ZXJhZ2UgbnVtYmVyIG9mIHJldmlld3MgcGVyIGJ1c2luZXNzDQoNCuaUvuWcqCBgQ0FgIOmAmeWAiyBgRGF0YSBGcmFtZWAg6KOh6Z2i77yaDQoNCmBgYHtyfQ0KQ0ENCmBgYA0KDQpgYGB7cn0NCiMgY2F0ZWdvcnktYnVzaW5lc3MgbWF0cml4DQpteEJDID0gcm93bmFtZXMoQ0EpICU+JSBzYXBwbHkoZnVuY3Rpb24oeikNCiAgZ3JlcGwoeixiaXokY2F0LGZpeGVkPVQpKQ0Kcm93bmFtZXMobXhCQykgPSBiaXokYmlkDQpkaW0obXhCQykgDQpgYGANCuWwhyBCdXNpbmVzcygxMSw1MzcpIOWSjCBDYXRlZ29yeSg1MDgpIOeahOWwjeaHiemXnOS/guaUvuWcqGBteEJDYOijoemdouOAgjxicj48YnI+DQoNCiMjIyMgKDFiKSDlsLrluqbnuK7muJsgKERpbWVuc2lvbiBSZWR1Y3Rpb24pIA0K5L2/55SoYHRTTkVg77yM5bCHYG14QkNg55qE5bC65bqmIFsxMSw1MzcgeCA1MDhdIOe4rua4m+eCuiBbMiB4IDUwOF0gLi4uDQpgYGB7cn0NCnQwID0gU3lzLnRpbWUoKQ0Kc2V0LnNlZWQoMTIzKQ0KdHNuZUNhdCA9IGlmZWxzZShteEJDLDEsMCkgJT4lIHQgJT4lIA0KICBSdHNuZShjaGVja19kdXBsaWNhdGVzPUYsdGhldGE9MC4wLG1heF9pdGVyPTMwMDApDQpTeXMudGltZSgpIC0gdDANCmBgYA0KPGJyPg0KDQojIyMjICgxYykg6ZqO5bGk5byP6ZuG576k5YiG5p6QIChIaWVyYXJjaGljYWwgQ2x1c3RlcmluZykgDQrlnKjnuK7muJvlsLrluqbkuYvkuK3lgZrpmo7lsaTlvI/pm4bnvqTliIbmnpDjgIINCmBgYHtyfQ0KWSA9IHRzbmVDYXQkWSAgICAgICAgICAgIyB0U05FIGNvb3JkaW5hdGVzDQpkID0gZGlzdChZKSAgICAgICAgICAgICAjIGRpc3RhbmNlIG1hdHJpeA0KaGMgPSBoY2x1c3QoZCkgICAgICAgICAgIyBoaS1jbHVzdGVyaW5nDQpgYGANCg0KYGBge3J9DQpLID0gNjAgICAgICAgICAgICAgICAgICAjIG51bWJlciBvZiBjbHVzdGVycyANCkNBJGdycCA9IGN1dHJlZShoYyxLKSAgIA0KdGFibGUoQ0EkZ3JwKQ0KYGBgDQrntpPph43opoblmJfoqabkuYvlvozvvIzlsIfpgJkqKjUwOCoq5YCLKirllYbmpa3poZ7liKUoQ2F0ZWdvcmllcykqKg0K5YiG5oiQKio2MCoq5YCLKirllYbmpa3poZ7liKXnvqTntYQoQ2F0ZWdvcnkgR3JvdXBzKSoq44CCIDxicj48YnI+DQoNCiMjIyMgKDFkKSDlrZfpm7IgKFdvcmQgQ2xvdWQpDQpgYGB7cn0NCnBhbHMgPSBkaXN0aW5jdENvbG9yUGFsZXR0ZShLKSAgIyBwYWxldHRlIG9mIEsgY29sb3JzIA0KcG5nKCJjYXRlZ29yeS5wbmciLCB3aWR0aD0zMjAwLCBoZWlnaHQ9MTgwMCkNCnRleHRwbG90KFlbLDFdLFlbLDJdLHJvd25hbWVzKENBKSxmb250PTIsIA0KICAgICAgICAgY29sID0gcGFsc1tDQSRncnBdLCAgICAgICAgICMgY29sb3IgYnkgZ3JvdXAgICAgDQogICAgICAgICBjZXggPSAoMC41K2xvZyhDQSRucmV2KS81KSkgIyBzaXplIGJ5IG5vLiByZXZpZXdzDQpkZXYub2ZmKCkNCmBgYA0K5bCH5a2X6Zuy55Wr5ZyoYGNhdGVnb3J5LnBuZ2Doo6HpnaLvvJoNCg0KKyDmr4/lgIvlrZfku6PooajkuIDlgIvllYbmpa3poZ7liKUoQ2F0ZWdvcmllcykNCisg5a2X55qE6aGP6Imy5Luj6KGo5ZWG5qWt6aGe5Yil576k57WEKENhdGVnb3J5IEdyb3VwcykNCisg5a2X55qE5aSn5bCP5Luj6KGo6YCZ5YCL5ZWG5qWt6aGe5Yil6KKr6KmV6KuW55qE5qyh5pW4IChudW1iZXIgb2YgcmV2aWV3cykNCisg6Z2g5Zyo5LiA6LW355qE44CB5ZCM5LiA56iu6aGP6Imy55qE5a2X77yM5Luj6KGo57aT5bi45LiA6LW35Ye654++55qE5ZWG5qWt6aGe5YilDQoNCiFbLl0oY2F0ZWdvcnkucG5nKQ0KDQoNCg0KIyMgMi4g6KmV6KuW55qE5YWn5a65ICgxOTQgQ29udGVudCBDbGFzc2VzKQ0K5o6l5LiL5L6G6ICD5oWu6KmV6KuW55qE5YWn5a6577yM5LiK6YCx5oiR5YCR5bey57aT5L2/55SoDQpbRW1wYXRoIFRleHQgQ2xhc3NpZmllcl0oaHR0cDovL2hjaS5zdGFuZm9yZC5lZHUvcHVibGljYXRpb25zLzIwMTYvZXRoYW4vZW1wYXRoLWNoaS0yMDE2LnBkZikNCu+8jOS+neWFtumgkOioreeahDE5NOeorioq5YWn5a65KENsYXNzKSoq77yMDQrlsI3pgJkyMTUsODc556+H6KmV6KuW5YiG5Yil5YGa6YGO6KmV5YiG44CCDQrkuZ/lsLHmmK/oqqrvvIzmlofpm4bkuYvkuK3nmoTmr4/kuIDnr4foqZXoq5bpg73mnIkxOTTlgIsqKuWFp+WuueipleWIhioq77yMDQrlrZjmlL7lnKhgc2NvcmVzYOmAmeWAi0RhdGEgRnJhbWXoo6HpnaLjgII8YnI+DQpgYGB7cn0NCmRpbShzY29yZXMpDQpgYGANCuS9v+eUqOmAmeS4gOS6m+izh+aWme+8jOaIkeWAkeWPr+S7peWwjemAmTE5NOeoruWFp+WuuShDbGFzc2VzKemAsuihjOWIhue+pO+8jA0K5Lmf5Y+v5Lul55So5a2X6Zuy5L6G5ZGI54++5LiN5ZCM5YWn5a655LmL6ZaT55qE55u46Zec5oCn44CCPGJyPg0KPGJyPg0KDQojIyMjICgyYSkg5YWn5a655qyK6YeNIChDbGFzcyBXZWlnaHRzKQ0K6KiI566X5q+P5LiA56iu5YWn5a6555qEKirmrIrph40qKigqKldlaWdodCoqOiB0aGUgc3VtIG9mIGNsYXNzIHNjb3JlcyB3aXRoaW4gdGhlIGNvcnB1cynvvIwNCuaUvuWcqGB3Q2xhc3Ng6KOh6Z2i77yM5Lim5bCHYHNjb3Jlc2Ao5YWn5a656KmV5YiG6LOH5paZKeS+neasiumHjeaOkuW6j+OAgiANCmBgYHtyfQ0KIyANCiMgb3JkZXIgdGhlIHNjb3JlIG1hdHJpeCBieSBjbGFzcyB3ZWlnaHRzDQpzY29yZXMgPSBzY29yZXNbLG9yZGVyKC1jb2xTdW1zKHNjb3JlcykpXQ0Kd0NsYXNzID0gY29sU3VtcyhzY29yZXMpICAjIGNsYXNzIHdlaWdodHMNCmhlYWQod0NsYXNzLDIwKQ0KYGBgDQo8YnI+DQoNCiMjIyMgKDJiKSDlsLrluqbnuK7muJsgKERpbWVuc2lvbiBSZWR1Y3Rpb24pIA0K5L2/55SoYHRTTkVg77yM5bCHYHNjb3Jlc2DnmoTlsLrluqYgWzIxNjg3OSB4IDE5NF0g57iu5rib54K6IFsyIHggMTk0XSAuLi4NCmBgYHtyfQ0KdDAgPSBTeXMudGltZSgpDQpzZXQuc2VlZCgxMjMpDQp0c25lQ2xhc3MgPSBzY29yZXMgJT4lIHNjYWxlICU+JSBhcy5tYXRyaXggJT4lIHQgJT4lIA0KICBSdHNuZSh0aGV0YT0wLjAsbWF4X2l0ZXI9MzAwMCkNClN5cy50aW1lKCkgLSB0MA0KYGBgDQo8YnI+DQoNCiMjIyMgKDJjKSDpmo7lsaTlvI/pm4bnvqTliIbmnpAgKEhpZXJhcmNoaWNhbCBDbHVzdGVyaW5nKSANCuWcqOe4rua4m+WwuuW6puS5i+S4reWBmumajuWxpOW8j+mbhue+pOWIhuaekOOAgg0KYGBge3J9DQpZID0gdHNuZUNsYXNzJFkgICAgICAjIHRTTkUgY29vcmRpbmF0ZXMNCmQgPSBkaXN0KFkpICAgICAgICAgICMgZGlzdGFuY2UgbWF0cml4DQpoYyA9IGhjbHVzdChkKSAgICAgICAjIGhpLWNsdXN0ZXJpbmcNCmBgYA0KDQpgYGB7cn0NCksgPSA0MA0KZ3BDbGFzcyA9IGN1dHJlZShoYyxLKSAgIyBLIGdyb3Vwcw0KdGFibGUoZ3BDbGFzcykgICAgICAgICAgIyBuby4gY2xhc3NlcyBwZXIgZ3JvdXANCmBgYA0K6L+U6KaG5ZiX6Kmm5LmL5b6M77yM5bCH6YCZKioxOTQqKueorioq5YWn5a65KENsYXNzZXMpKioNCuWIhuaIkCoqNDAqKuWAiyoq5YWn5a65576k57WEKENsYXNzIEdyb3VwcykqKuOAgiA8YnI+PGJyPg0KDQojIyMjICgyZCkg5a2X6ZuyIChXb3JkIENsb3VkKQ0KYGBge3J9DQpwYWxzID0gZGlzdGluY3RDb2xvclBhbGV0dGUoSykgICMgcGFsZXR0ZSBvZiBLIGNvbG9ycyANCnBuZygiY2xhc3Nlcy5wbmciLCB3aWR0aD0zMjAwLCBoZWlnaHQ9MTgwMCkNCnRleHRwbG90KFlbLDFdLFlbLDJdLGNvbG5hbWVzKHNjb3JlcyksZm9udD0yLCANCiAgY29sID0gcGFsc1tncENsYXNzXSwgICAgICAjIGNvbG9yIGJ5IGdyb3VwICAgIA0KICBjZXggPSAwLjUrbG9nKHdDbGFzcykvMykgICMgc2l6ZSBieSBjbGFzcyB3ZWlnaHQNCmRldi5vZmYoKQ0KYGBgDQrlsIflrZfpm7LnlavlnKhgY2xhc3Nlcy5wbmdg6KOh6Z2i77yaDQoNCisg5q+P5YCL5a2X5Luj6KGo5LiA56iu5YWn5a65KENsYXNzKQ0KKyDlrZfnmoTpoY/oibLku6PooajlhaflrrnnvqTntYQoQ2xhc3MgR3JvdXBzKQ0KKyDlrZfnmoTlpKflsI/ku6PooajpgJnnqK7lhaflrrnlnKjmlofpm4bkuYvkuK3nmoTmrIrph40NCisg6Z2g5Zyo5LiA6LW355qE44CB5ZCM5LiA56iu6aGP6Imy55qE5a2X77yM5Luj6KGo57aT5bi45LiA6LW35Ye654++55qE5YWn5a65DQoNCiFbLl0oY2xhc3Nlcy5wbmcpDQoNCiMjIyMgKDJlKSDlhaflrrnkuYvplpPnmoTnm7jpl5zmgKcNCue5vOe6jOWIhuaekOS5i+WJje+8jOWFiOeUqOeGseWcluaqouafpeS4gOS4i+WFp+WuueipleWIhuS5i+mWk+eahOebuOmXnOaApyAuLi4NCmBgYHtyIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD0xMH0NCiMgY29ycmVsYXRpb24gYmV0d2VlbiB0aGUgY2xhc3Nlcw0KY3IgPSBjb3Ioc2NvcmVzKSAgDQpjciAlPiUgZDNoZWF0bWFwKHNjYWxlPSdub25lJyxjb2w9Y20uY29sb3JzKDI1NikpDQpgYGANCg0KDQrmib7lh7rnm7jpl5zkv4LmlbjovIPpq5jnmoTlhaflrrnnqK7poZ4NCmBgYHtyfQ0KY29yZ3JwID0gZnVuY3Rpb24oeCwgdGhyZXNob2xkPTAuNikgew0KICB4ID0geCpsb3dlci50cmkoeCkNCiAgY2hlY2sgPSB3aGljaCh4PnRocmVzaG9sZCxhcnIuaW5kPVQpDQogIGdjciA9IGdyYXBoLmRhdGEuZnJhbWUoY2hlY2ssIGRpcmVjdGVkPUYpDQogIGdjciA9IHNwbGl0KHVuaXF1ZShhcy52ZWN0b3IoY2hlY2spKSxjbHVzdGVycyhnY3IpJG1lbWJlcnNoaXApDQogIGxhcHBseShnY3IsIGZ1bmN0aW9uKGcpe3Jvd25hbWVzKHgpW2ddfSkgIH0NCmNvcmdycChjciwgMC42KSANCmBgYA0KPGJyPjxicj4NCg0KDQojIyAzLiDllYbmpa3poZ7liKUg6IiHIOipleirluWFp+WuuQ0K5o6l5LiL5L6G5oiR5YCR5Y+v5Lul5YGaKirllYbmpa3poZ7liKUoNTA4IENhdGVnb3JpZXMpKirlkowqKuipleirluWFp+WuuSgxOTQgQ2xhc3NlcykqKg0K5LmL6ZaT55qE5Lqk5Y+J5YiG5p6Q44CCPGJyPg0KPGJyPg0KDQojIyMjICgzYSkg5Lqk5Y+J5p+l6Kmi56+E5L6LDQrlhYjnlKjku6XkuIvpgJnkuIDlgIvnr4TkvovkvobnpLrnr4TkuqTlj4nmn6XoqaLnmoTlgZrms5UNCg0KKyDlnKjoqZXoq5bmlbjotoXpgY41MDDnmoTllYbmpa3poZ7liKXkuYvkuK3vvIzlk6rkupvpoZ7liKXnmoToqZXoq5bkuYvkuK3nmoTmraPpnaLmg4Xnt5LlkozosqDpnaLmg4Xnt5LnmoTnm7jpl5zkv4LmlbjmmK/mnIDpq5jnmoTlkaLvvJ8gDQpgYGB7cn0NCiMgY29ycmVsYXRpb24gYmV0d2VlbiBwb3NpdGl2ZSAmIG5lZ2F0aXZlIGVtb3Rpb24gDQpteEJDWyxDQSRucmV2PjUwMF0gJT4lIGFwcGx5KDIsZnVuY3Rpb24odikgew0KICBpID0gcmV2aWV3JGJpZCAlaW4lIHJvd25hbWVzKG14QkMpW3ZdDQogIGNvcihzY29yZXMkcG9zaXRpdmVfZW1vdGlvbltpXSwgc2NvcmVzJG5lZ2F0aXZlX2Vtb3Rpb25baV0pDQp9KSAlPiUgc29ydCAlPiUgdGFpbCgyMCkNCmBgYA0K5oiR5YCR5Y+v5Lul55yL5YiwYERvY3RvcnNg5ZKMYFBldHNg6YCZ5YWp56iu5ZWG5qWt6aGe5Yil55qE6KmV6KuW5pyA5bi45pyD5ZCM5pmC5Ye654++5q2j6Z2i5ZKM6LKg6Z2i5oOF57eS44CCDQo8YnI+PGJyPg0KDQojIyMjICgzYikg5ZCE5ZWG5qWt6aGe5Yil55qE5YWn5a655qyK6YeNDQrkuI3lkIzllYbmpa3poZ7liKXnmoToqZXoq5boo6HpnaLmnIPmnInkuI3kuIDmqKPnmoTlhaflrrnvvIwNCuaIkeWAkeWPr+S7peeUqOeGseWcluS+huWRiOePvuWQhOWVhualremhnuWIpeeahOWFp+WuueWIhuW4g+eLgOazgeOAgg0K5YWI5oqK5YWn5a656KmV5YiG5L6d5ZWG5qWt6aGe5Yil5bmz5Z2H6LW35L6G77yMDQrmlL7lnKhgd3hDbGFzc2DpgJnlgIvnn6npmaPoo6HpnaLjgIINCmBgYHtyfQ0Kd3hDbGFzcyA9IGFwcGx5KG14QkMsMixmdW5jdGlvbih2KXsgICAgICMgZm9yIGV2ZXJ5IGNhdGVnb3J5DQogIGkgPSByZXZpZXckYmlkICVpbiUgcm93bmFtZXMobXhCQylbdl0gIyBmaW5kIGl0cyByZXZpZXdzIA0KICBjb2xNZWFucyhzY29yZXNbaSxdKSAgICAgICAgICAgICAgICAgICMgYXZlcmFnZSB0aGVpciBjbGFzcyBzY29yZXMNCn0pDQpkaW0od3hDbGFzcykNCmBgYA0K55Sx5pa8YHd4Q2xhc3Ng6YCZ5YCL55+p6Zmj5aSq5aSn77yMDQrmiJHlgJHlj6rnlavlh7roqZXoq5bmlbjmnIDlpJrnmoQ1MOWAi+WVhualremhnuWIpeWSjOasiumHjeacgOWkp+eahDUw56iu6KmV6KuW5YWn5a65DQpgYGB7ciBmaWcud2lkdGg9OSwgZmlnLmhlaWdodD05fQ0Kd3hDbGFzc1sxOjUwLDE6NTBdICU+JSBsb2cgJT4lIA0KICBkM2hlYXRtYXAoVCxULHNjYWxlPSdub25lJyxjb2w9J1BpWUcnKQ0KYGBgDQo8YnI+PGJyPg0KDQojIyMjICgzYykg576k57WE54ax5ZyWIA0K5Zug54K65ZWG5qWt6aGe5YilKDUwOCBjYXRlZ29yaWVzKeWSjOipleirluWFp+WuuSgxOTQgY2xhc3NlcynnmoTmlbjph4/pg73lvojlpJoNCijpgJnlsLHmmK/lpKfmlbjmk5rnmoTnibnlvrUp77yMDQrmiJHlgJHopoHlnKjnvqTntYTpgJnlgIvlsaTpnaLvvIzmiY3mr5TovIPlrrnmmJPop4Dlr5/mlbTlgIvmlofpm4bnmoTlhaflrrnliIbluIPni4Dms4HjgIINCuWFtuWvpu+8jOmAmeWwseaYr+Wkp+aVuOaTmuWIhuaekOS5i+S4re+8jOaIkeWAkeW4uOW4uOmcgOimgeWFiOWBmumbhue+pOWIhuaekOeahOeQhueUseOAgg0K5oiR5YCR55So5Lul5LiL55qE54ax5ZyW5ZGI54++KirllYbmpa3poZ7liKXnvqTntYQoNjApKirlkowqKuWFp+Wuuee+pOe1hCg0MCkqKuS5i+mWk+eahOmXnOS/guOAgg0KYGBge3IgZmlnLndpZHRoPTgsIGZpZy5oZWlnaHQ9MTJ9DQp4ID0gbWF0cml4KDAsIG5yb3c9bWF4KGdwQ2xhc3MpLCBuY29sPW1heChDQSRncnApLA0KICBkaW1uYW1lcz1saXN0KHNwcmludGYoJ0NMUyUwMmQnLDE6bWF4KGdwQ2xhc3MpKSwNCiAgICAgICAgICAgICAgICBzcHJpbnRmKCdDQVQlMDJkJywxOm1heChDQSRncnApKSkpDQpmb3IoaSBpbiAxOm5yb3coeCkpIGZvcihqIGluIDE6bmNvbCh4KSkgIA0KICB4W2ksal0gPSBzdW0oIHd4Q2xhc3NbZ3BDbGFzcz09aSwgQ0EkZ3JwPT1qXSApDQp0KHgpICU+JSB7bG9nKDAuMDA1Ky4pfSAlPiUgZDNoZWF0bWFwKHNjYWxlPSdub25lJyxjb2w9J1BpWUcnKQ0KYGBgDQrlvp7ku6XkuIrnmoTnvqTntYTnhrHlnJboo6HpnaLmiJHlgJHlj6/ku6XmuIXmpZrnmoTnnIvliLDmlbTlgIvmlofpm4Yo5Zyo5ZCE5ZWG5qWt6aGe5Yil5LmL5LitKeeahOWFp+WuueWIhuW4g+eLgOazgeOAgg0KPGJyPg0K5aaC5p6c6ZyA6KaB55yL5p+Q5LiA576k57WE5LmL5Lit5pyJ5ZOq5LiA5Lqb5ZWG5qWt6aGe5Yil5oiW6KmV6KuW5YWn5a6577yM5Y+v5Lul6YCZ5qij5YGa77yaDQpgYGB7cn0NCnJvd25hbWVzKENBKVtDQSRncnA9PThdDQpgYGANCg0KYGBge3J9DQpuYW1lcyhzY29yZXMpW2dwQ2xhc3M9PTFdDQpgYGANCg0KPGJyPjxicj4NCg0KIyMgNC4g6Lao5Yui5YiG5p6QDQoNCjxicj4NCg0KIyMjIyAoNGEpIFRyZW5kIG9mIENvbnRlbnQgQ2xhc3Nlcw0KYGBge3J9DQp0eENsYXNzID0gdHggPSBzcGxpdChzY29yZXMsIGN1dChyZXZpZXckZGF0ZSwncXVhcnRlcicpKSAlPiUgDQogIHNhcHBseShjb2xTdW1zKSAlPiUgYXBwbHkoMiwgZnVuY3Rpb24odikgdi9zdW0odikpDQoNCiMgVHJlbmQgb2YgQ2xhc3Nlcw0KZGYgPSBkYXRhLmZyYW1lKGNsYXNzPXJvd25hbWVzKHR4KSwgdHhbLDk6MzJdKSAlPiUgbWVsdCgnY2xhc3MnKQ0KZGYkZGF0ZSA9IGFzLkRhdGUoDQogIHN1YnN0cihhcy5jaGFyYWN0ZXIoZGYkdmFyaWFibGUpLDIsMTEpLGZvcm1hdD0iJVkuJW0uJWQiKQ0KZGYkdmFsdWUgPSByb3VuZCgxMDAqZGYkdmFsdWUsMykNCnN1YnNldChkZiwgY2xhc3MgJWluJSByb3duYW1lcyh0eClbMToxOTRdKSAlPiUgDQogIGhjaGFydCgic3BsaW5lIixoY2Flcyh4PWRhdGUseT12YWx1ZSxncm91cD1jbGFzcykpICU+JSANCiAgaGNfbGVnZW5kKGFsaWduPSJsZWZ0IiwgbGF5b3V0PSJ2ZXJ0aWNhbCIsdmVydGljYWxBbGlnbj0idG9wIikgJT4lIA0KICBoY19hZGRfdGhlbWUoaGNfdGhlbWVfZmxhdCgpKSAlPiUgDQogIGhjX3RpdGxlKHRleHQ9J1dlaWdodHMgb2YgQ2xhc3NlcyAoJSBvZiBUb3RhbCBXZWlnaHQpJykNCmBgYA0KPGJyPjxicj4NCg0KDQojIyMjICg0YikgVHJlbmQgb2YgQ2xhc3MgR3JvdXBzDQpgYGB7cn0NCiMgVHJlbmQgb2YgQ2xhc3MgR3JvdXANCnggPSBzcGxpdChkYXRhLmZyYW1lKHR4KSxncENsYXNzKSAlPiUgc2FwcGx5KGNvbFN1bXMpICU+JSB0DQpkZiA9IGRhdGEuZnJhbWUoDQogIGNsYXNzPXNwcmludGYoJ0clMDJkJywxOm1heChncENsYXNzKSksIHhbLDk6MzJdKSAlPiUgDQogIG1lbHQoJ2NsYXNzJykNCmRmJGRhdGUgPSBhcy5EYXRlKA0KICBzdWJzdHIoYXMuY2hhcmFjdGVyKGRmJHZhcmlhYmxlKSwyLDExKSxmb3JtYXQ9IiVZLiVtLiVkIikNCmRmJHZhbHVlID0gcm91bmQoMTAwKmRmJHZhbHVlLDMpDQpkZiAlPiUgaGNoYXJ0KCJzcGxpbmUiLGhjYWVzKHg9ZGF0ZSx5PXZhbHVlLGdyb3VwPWNsYXNzKSkgJT4lIA0KICBoY19sZWdlbmQoYWxpZ249ImxlZnQiLCBsYXlvdXQ9InZlcnRpY2FsIix2ZXJ0aWNhbEFsaWduPSJ0b3AiKSAlPiUgDQogIGhjX2FkZF90aGVtZShoY190aGVtZV9mbGF0KCkpICU+JSANCiAgaGNfdGl0bGUodGV4dD0nV2VpZ2h0cyBvZiBDbGFzcyBHcm91cHMgKCUgb2YgVG90YWwgV2VpZ2h0KScpDQpgYGANCjxicj48YnI+DQoNCiMjIyMgKDRjKSBUcmVuZCBvZiBCdXNpbmVzcyBDYXRlZ29yaWVzDQpgYGB7cn0NCnR4Q2F0ID0gdHggPSBzcGxpdChyZXZpZXcsIGN1dChyZXZpZXckZGF0ZSwncXVhcnRlcicpKSAlPiUgDQogIHNhcHBseShmdW5jdGlvbih4KSBhcHBseShteEJDLDIsZnVuY3Rpb24odikNCiAgICAgIHN1bSh4JGJpZCAlaW4lIHJvd25hbWVzKG14QkMpW3ZdKSApKSAlPiUgDQogIGFwcGx5KDIsIGZ1bmN0aW9uKHYpIHYvc3VtKHYpKQ0KDQojIFRyZW5kIG9mIENhdGVnb3JpZXMNCmRmID0gZGF0YS5mcmFtZShjYXRlZ29yeT1yb3duYW1lcyh0eCksIHR4Wyw5OjMyXSkgJT4lIG1lbHQoJ2NhdGVnb3J5JykNCmRmJGRhdGUgPSBhcy5EYXRlKA0KICBzdWJzdHIoYXMuY2hhcmFjdGVyKGRmJHZhcmlhYmxlKSwyLDExKSxmb3JtYXQ9IiVZLiVtLiVkIikNCmRmJHZhbHVlID0gcm91bmQoMTAwKmRmJHZhbHVlLDMpDQpzdWJzZXQoZGYsIGNhdGVnb3J5ICVpbiUgcm93bmFtZXMoQ0EpWzE6MzBdKSAlPiUgDQogIGhjaGFydCgic3BsaW5lIixoY2Flcyh4PWRhdGUseT12YWx1ZSxncm91cD1jYXRlZ29yeSkpICU+JSANCiAgaGNfbGVnZW5kKGFsaWduPSJsZWZ0IiwgbGF5b3V0PSJ2ZXJ0aWNhbCIsdmVydGljYWxBbGlnbj0idG9wIikgJT4lIA0KICBoY19hZGRfdGhlbWUoaGNfdGhlbWVfZmxhdCgpKSAlPiUgDQogIGhjX3RpdGxlKHRleHQ9J1dlaWdodHMgb2YgQ2F0ZWdvcmllcyAoJSBvZiBUb3RhbCBSZXZpZXdzKScpDQpgYGANCjxicj48YnI+DQoNCg0KIyMjIyAoNGQpIFRyZW5kIG9mIEJ1c2luZXNzIENhdGVnb3J5IEdyb3Vwcw0KYGBge3J9DQojIFRyZW5kIG9mIENhdGVnb3J5IEdyb3Vwcw0KeCA9IHNwbGl0KGRhdGEuZnJhbWUodHgpLENBJGdycCkgJT4lIHNhcHBseShjb2xTdW1zKSAlPiUgdA0KZGYgPSBkYXRhLmZyYW1lKA0KICBjYXRlZ29yeT1zcHJpbnRmKCdHJTAyZCcsMTptYXgoQ0EkZ3JwKSksIHhbLDk6MzJdKSAlPiUgDQogIG1lbHQoJ2NhdGVnb3J5JykNCmRmJGRhdGUgPSBhcy5EYXRlKA0KICBzdWJzdHIoYXMuY2hhcmFjdGVyKGRmJHZhcmlhYmxlKSwyLDExKSxmb3JtYXQ9IiVZLiVtLiVkIikNCmRmJHZhbHVlID0gcm91bmQoMTAwKmRmJHZhbHVlLDMpDQpkZiAlPiUgaGNoYXJ0KCJzcGxpbmUiLGhjYWVzKHg9ZGF0ZSx5PXZhbHVlLGdyb3VwPWNhdGVnb3J5KSkgJT4lIA0KICBoY19sZWdlbmQoYWxpZ249ImxlZnQiLCBsYXlvdXQ9InZlcnRpY2FsIix2ZXJ0aWNhbEFsaWduPSJ0b3AiKSAlPiUgDQogIGhjX2FkZF90aGVtZShoY190aGVtZV9mbGF0KCkpICU+JSANCiAgaGNfdGl0bGUodGV4dD0nV2VpZ2h0cyBvZiBDYXRlZ29yeSBHcm91cHMgKCUgb2YgVG90YWwgUmV2aWV3cyknKQ0KYGBgDQo8YnI+PGJyPg0KDQoNCg==