上週我們對 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) 
[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 

我們可以看到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)')



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