上週我們對 Yelp Kaggle 裡面的文字做過:

這一些 文字分析 之後,本周我們繼續用這一組資料,來示範:

這幾種分析技術的綜合運用。


library(magrittr)
library(Rtsne)
library(RColorBrewer)
library(randomcoloR)
library(wordcloud)
library(d3heatmap)
library(igraph)  
library(reshape2)
library(highcharter)
load('data/yelp1.rdata')  # loading yelp data & sentiment scores
load('data/empath.rdata') # loading empath scores

載入 packages 和 data 之後,一開始我們有 6 個 Data Frame:

Data Frame No. Rows x Cols Objects
biz 11,537 x 11 businesses
user 43,873 x 7 users
check 262,764 x 4 check-in’s
review 215,879 x 8 reviews
senti 215,879 x 10 reviews’ sentiment scores
scores 215,879 x 194 reviews’ Empath/LIWC scores



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裡面整理出 508Categories, 並算出每一個 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 

我們可以看到DoctorsPets這兩種商業類別的評論最常會同時出現正面和負面情緒。

(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=