Introduction

This notebook uses Starbucks Customer Data and corresponding Task from Kaggle.

Data

  1. portfolio.csv : Information about the promotional offers that are possible to receive, and basic information about each one including the promotional type, duration of the promotion, reward, and how the promotion was distributed to customers

  2. profile.csv : Dimensional data about each person, including their age, salary, and gender. There is one unique customer for each record.

  3. transcript.csv : Records show the different steps of promotional offers that a customer received. The different values of receiving a promotion are receiving, viewing, and completing. You also see the different transactions that a person made in the time since he became a customer. With all records, you see the day that they interacted with Starbucks and the amount that it is worth.

Data dictionary

portfolio.csv

variable description
# id
reward Reward given for completing an offer
channels (list of strings)
difficulty Minimum required spend to complete an offer
duration Unknown
offer_type Type of offer ie BOGO, discount, informational
id Offer id

profile.csv

variable description
# id
gender Gender of the customer (note some entries contain ‘O’ for other rather than M or F)
age Age of the customer
id Customer id
became_member_on Date when customer created an app account
income Customer’s income

transcript.csv

variable description
# id
person customer id
event record description (ie transaction, offer received, offer viewed, etc.)
value (dict of strings) - either an offer id or transaction amount depending on the record
time time in hours. The data begins at time t=0

Questions

Overview of Starbucks population
* What is gender distribution?
* What is the income distribution?
* When do people typically become a member?
* What is the average purchase distribution?

Reactions to different promotions
* Do people react to different promotions differently?
* Does the reward of the promotion make people react differently?
* Does it make sense to offer certain rewards?
* Would Starbucks save money overall if they offer a certain reward?

Clustering
* How many clusters should Starbucks use?

Load libaries

library(tidyverse)
library(lubridate)
library(patchwork)
library(colorspace)
library(scales)
library(ggstatsplot)
library(plotly)
library(factoextra)
library(NbClust)
library(dendextend)
library(ggdendro)

theme_set(theme_bw(base_size=10))

Import data

portfolio = read.csv("Starbucks Customer Data/portfolio.csv", stringsAsFactors = T)
profile = read.csv("Starbucks Customer Data/profile.csv",stringsAsFactors = T)
transcript = read.csv("Starbucks Customer Data/transcript.csv",stringsAsFactors = T)
str(portfolio)
'data.frame':   10 obs. of  7 variables:
 $ X         : int  0 1 2 3 4 5 6 7 8 9
 $ reward    : int  10 10 0 5 5 3 2 0 5 2
 $ channels  : Factor w/ 4 levels "['email', 'mobile', 'social']",..: 1 2 3 3 4 2 2 1 2 3
 $ difficulty: int  10 10 0 5 20 7 10 0 5 10
 $ duration  : int  7 5 4 7 10 7 10 3 5 7
 $ offer_type: Factor w/ 3 levels "bogo","discount",..: 1 1 3 1 2 2 2 3 1 2
 $ id        : Factor w/ 10 levels "0b1e1539f2cc45b7b9fa7c272da2e1d7",..: 8 5 4 7 1 2 10 6 9 3
profile$became_member_on = ymd(profile$became_member_on)
str(profile)
'data.frame':   17000 obs. of  6 variables:
 $ X               : int  0 1 2 3 4 5 6 7 8 9 ...
 $ gender          : Factor w/ 4 levels "","F","M","O": 1 2 1 2 1 3 1 1 3 1 ...
 $ age             : int  118 55 118 75 118 68 118 118 65 118 ...
 $ id              : Factor w/ 17000 levels "0009655768c64bdeb2e877511632db8f",..: 6962 399 3747 7997 10736 15044 9525 6940 3729 9147 ...
 $ became_member_on: Date, format: "2017-02-12" "2017-07-15" "2018-07-12" ...
 $ income          : num  NA 112000 NA 100000 NA 70000 NA NA 53000 NA ...
str(transcript)
'data.frame':   306534 obs. of  5 variables:
 $ X     : int  0 1 2 3 4 5 6 7 8 9 ...
 $ person: Factor w/ 17000 levels "0009655768c64bdeb2e877511632db8f",..: 7997 10736 15044 9525 6940 3729 13109 3060 11411 3275 ...
 $ event : Factor w/ 4 levels "offer completed",..: 2 2 2 2 2 2 2 2 2 2 ...
 $ value : Factor w/ 5121 levels "{'amount': 0.05}",..: 5110 5104 5106 5113 5108 5112 5105 5107 5104 5104 ...
 $ time  : int  0 0 0 0 0 0 0 0 0 0 ...

Summary

summary(portfolio)
       X            reward                                     channels   difficulty      duration   
 Min.   :0.00   Min.   : 0.0   ['email', 'mobile', 'social']       :2   Min.   : 0.0   Min.   : 3.0  
 1st Qu.:2.25   1st Qu.: 2.0   ['web', 'email', 'mobile', 'social']:4   1st Qu.: 5.0   1st Qu.: 5.0  
 Median :4.50   Median : 4.0   ['web', 'email', 'mobile']          :3   Median : 8.5   Median : 7.0  
 Mean   :4.50   Mean   : 4.2   ['web', 'email']                    :1   Mean   : 7.7   Mean   : 6.5  
 3rd Qu.:6.75   3rd Qu.: 5.0                                            3rd Qu.:10.0   3rd Qu.: 7.0  
 Max.   :9.00   Max.   :10.0                                            Max.   :20.0   Max.   :10.0  
                                                                                                     
         offer_type                                id   
 bogo         :4    0b1e1539f2cc45b7b9fa7c272da2e1d7:1  
 discount     :4    2298d6c36e964ae4a3e7e9706d1fb8c2:1  
 informational:2    2906b810c7d4411798c6938adc9daaa5:1  
                    3f207df678b143eea3cee63160fa8bed:1  
                    4d5c57ea9a6940dd891ad53e9dbe8da0:1  
                    5a8bc65990b245e5a138643cd4eb9837:1  
                    (Other)                         :4  
summary(profile)
       X         gender        age                                        id        became_member_on    
 Min.   :    0    :2175   Min.   : 18.00   0009655768c64bdeb2e877511632db8f:    1   Min.   :2013-07-29  
 1st Qu.: 4250   F:6129   1st Qu.: 45.00   00116118485d4dfda04fdbaba9a87b5c:    1   1st Qu.:2016-05-26  
 Median : 8500   M:8484   Median : 58.00   0011e0d4e6b944f998e987f904e8c1e5:    1   Median :2017-08-02  
 Mean   : 8500   O: 212   Mean   : 62.53   0020c2b971eb4e9188eac86d93036a77:    1   Mean   :2017-02-23  
 3rd Qu.:12749            3rd Qu.: 73.00   0020ccbbb6d84e358d3414a3ff76cffd:    1   3rd Qu.:2017-12-30  
 Max.   :16999            Max.   :118.00   003d66b6608740288d6cc97a6903f4f0:    1   Max.   :2018-07-26  
                                           (Other)                         :16994                       
     income      
 Min.   : 30000  
 1st Qu.: 49000  
 Median : 64000  
 Mean   : 65405  
 3rd Qu.: 80000  
 Max.   :120000  
 NA's   :2175    
summary(transcript)
       X                                       person                   event       
 Min.   :     0   94de646f7b6041228ca7dec82adb97d2:    51   offer completed: 33579  
 1st Qu.: 76633   8dbfa485249f409aa223a2130f40634a:    49   offer received : 76277  
 Median :153266   5e60c6aa3b834e44b822ea43a3efea26:    48   offer viewed   : 57725  
 Mean   :153266   79d9d4f86aca4bed9290350fb43817c2:    48   transaction    :138953  
 3rd Qu.:229900   d0a80415b84c4df4908b8403b19765e3:    48                           
 Max.   :306533   28681c16026943e68f26feaccab0907f:    46                           
                  (Other)                         :306244                           
                                              value             time      
 {'offer id': '2298d6c36e964ae4a3e7e9706d1fb8c2'}: 14983   Min.   :  0.0  
 {'offer id': 'fafdcd668e3743c1bb461111dcafc2a4'}: 14924   1st Qu.:186.0  
 {'offer id': '4d5c57ea9a6940dd891ad53e9dbe8da0'}: 14891   Median :408.0  
 {'offer id': 'f19421c1d4aa40978ebb69ca19b0e20d'}: 14835   Mean   :366.4  
 {'offer id': 'ae264e3637204a6fb9bb56bc8210ddfd'}: 14374   3rd Qu.:528.0  
 {'offer id': '5a8bc65990b245e5a138643cd4eb9837'}: 14305   Max.   :714.0  
 (Other)                                         :218222                  

Data completeness

# profile data
profile %>% summarise(across(everything(), ~mean(!is.na(.)))) %>% 
  gather() %>%
  mutate(key= fct_reorder(key, value))

# portfolio data
portfolio %>% summarise(across(everything(), ~mean(!is.na(.)))) %>% 
  gather() %>%
  mutate(key= fct_reorder(key, value))

# transcript data
transcript %>% summarise(across(everything(), ~mean(!is.na(.)))) %>% 
  gather() %>%
  mutate(key= fct_reorder(key, value))

Profile data

dim(profile)
[1] 17000     6
n_distinct(profile$id)
[1] 17000
p1 = profile %>% ggplot(aes(x=age)) + geom_boxplot(color="#2B3A67") + theme(axis.ticks.y=element_blank(), axis.text.y=element_blank())
p2 = profile %>% ggplot(aes(x=income))  + geom_boxplot(color="#66999B") + theme(axis.ticks.y=element_blank(), axis.text.y=element_blank())
p3 = profile %>% group_by(became_member_on) %>% tally() %>%
  ggplot(aes(x=became_member_on, y=n)) + 
  geom_line(alpha=0.9, color="#496A81")
p4 = profile %>% ggplot(aes(x=gender)) + geom_bar(alpha=0.9, fill="#B3AF8F") 

(p1 | p3) / (p2 | p4) + plot_annotation(subtitle="Profile")

# clean profile data
profile %>%
  filter(age<=100) %>% #drop obs with age >100
  summary()  #no missing values in gender and income 
       X         gender        age                                        id       
 Min.   :    1    :   0   Min.   : 18.00   0009655768c64bdeb2e877511632db8f:    1  
 1st Qu.: 4274   F:6124   1st Qu.: 42.00   0011e0d4e6b944f998e987f904e8c1e5:    1  
 Median : 8490   M:8484   Median : 55.00   0020c2b971eb4e9188eac86d93036a77:    1  
 Mean   : 8496   O: 212   Mean   : 54.38   0020ccbbb6d84e358d3414a3ff76cffd:    1  
 3rd Qu.:12729            3rd Qu.: 66.00   003d66b6608740288d6cc97a6903f4f0:    1  
 Max.   :16999            Max.   :100.00   00426fe3ffde4c6b9cb9ad6d077a13ea:    1  
                                           (Other)                         :14814  
 became_member_on         income      
 Min.   :2013-07-29   Min.   : 30000  
 1st Qu.:2016-05-20   1st Qu.: 49000  
 Median :2017-08-02   Median : 64000  
 Mean   :2017-02-18   Mean   : 65404  
 3rd Qu.:2017-12-30   3rd Qu.: 80000  
 Max.   :2018-07-26   Max.   :120000  
                                      
# get components of become_member_on date
profile_cln = profile %>% 
  filter(age<=100) %>%
  mutate(year= year(became_member_on),
         month= month(became_member_on),
         day= day(became_member_on),
         wday = wday(became_member_on, label=TRUE)) 
p4 = profile_cln %>% 
  group_by(gender) %>% tally() %>% 
  mutate(prop=round(n/sum(n)*100,1),
          gender_long=recode(gender,"F"="Female", "M"="Male","O"="Other")) %>% 
  mutate(perc = paste0("(",prop,"%",")")) %>%
  ggplot(aes(x=gender_long, y=n, fill=gender_long)) + 
  geom_col(show.legend = F) +
  geom_text(aes(label=paste(n, perc)),vjust=-0.5, size=2.7) + 
  scale_fill_manual(values=c("#496A81","#FFC482","#B3AF8F")) +
  labs(y="count", subtitle="Gender distribution", x="gender")

p5 = profile_cln %>% 
  ggplot(aes(x=income)) +
  geom_histogram(binwidth = 1000, fill="#89b0ae", alpha=0.9) + 
  geom_vline(aes(xintercept=mean(income)),
             linetype="dashed", size=1) +
  labs(subtitle="Income distribution")

p6 = profile_cln %>% 
  ggplot(aes(x=income)) + 
  geom_boxplot(color="#15616d") + 
  theme(axis.text.y=element_blank(),
        axis.ticks.y=element_blank(),
        panel.grid.minor=element_blank(),
        panel.grid.major.y=element_blank())

p4 | (p5/p6) + plot_layout(nrow = 2, heights = c(4, 1))

# became_member_on
p7 = profile_cln %>%
  group_by(year, month) %>%
  summarise(n=n_distinct(id)) %>%
  mutate(year_month=paste0(year,'-',month,'-',"1")) %>% 
  mutate(date2 = ymd(year_month)) %>%
  ggplot(aes(x=date2, y=n, fill=n)) + 
  geom_col(alpha=0.9, show.legend=F) + 
  scale_fill_continuous_sequential(palette="batlow") + 
  labs(y="count", fill="", x="date")

p8 = profile_cln %>% ggplot(aes(y=forcats::fct_rev(fct_infreq(factor(year))))) + geom_bar(fill="#2B3A67") + labs(y="year")
p9 = profile_cln %>% ggplot(aes(y=forcats::fct_rev(fct_infreq(factor(month))))) + geom_bar(fill="#496A81") + labs(y="month")
p10 = profile_cln %>% ggplot(aes(y=forcats::fct_rev(fct_infreq(factor(wday))))) + geom_bar(fill="#66999B") + labs(y="day of the week")

p7 / (p8 + p9 + p10) + plot_annotation(subtitle="became_member_on")

# age
p11= profile_cln %>% 
  ggplot(aes(x=age)) + 
  geom_histogram(binwidth = 1, fill="#89b0ae", alpha=0.9) + 
  geom_vline(aes(xintercept=mean(age)),
             linetype="dashed", size=1) +
  labs(subtitle="Age distribution")

p12= profile_cln %>% 
  ggplot(aes(x=age)) + 
  geom_boxplot(color="#15616d") + 
  theme(axis.text.y=element_blank(),
        axis.ticks.y=element_blank(),
        panel.grid.minor=element_blank(),
        panel.grid.major.y=element_blank())

# age group 
profile_cln$age_group = cut(profile_cln$age, 
                            breaks = c(0, 20, 40, 60, 80, 100),
                            labels = c("0-20", "21-40", "41-60","61-80","81-100")) #recode to categorical
p13= profile_cln %>% 
  group_by(age_group) %>% tally() %>% mutate(prop=round(n/sum(n)*100,1)) %>% 
  mutate(perc = paste0("(",prop,"%",")")) %>%
  ggplot(aes(x=age_group, y=n, fill=age_group)) + 
  geom_col(show.legend = F) +
  geom_text(aes(label=paste(n, perc)),vjust=-0.5, size=2.7) + 
  scale_fill_manual(values=c("#2B3A67","#496A81","#66999B", "#B3AF8F","#FFC482")) +
  labs(y="count", subtitle="Age group")


 (p11/p12)+ plot_layout(nrow = 2, heights = c(4, 1)) | p13  

NA

Portfolio data

p16a =portfolio %>% ggplot(aes(y=channels)) + geom_bar(fill="#FFC482") 
p16b =portfolio %>% ggplot(aes(x=duration)) + geom_bar(fill="#496A81")
p16c =portfolio %>% ggplot(aes(x=offer_type)) + geom_bar(fill="#2B3A67")
p16d =portfolio %>% ggplot(aes(x=difficulty)) + geom_boxplot(fill="#66999B", alpha=0.9, outlier.colour = "red")
p16e =portfolio %>% ggplot(aes(x=reward)) + geom_bar(fill="#B3AF8F")

(p16c | p16a) /
  (p16d | p16e | p16b) + plot_annotation(subtitle="Portfolio")

# channel
p17 = portfolio %>% 
  mutate(email= str_detect(channels, "email"),
         mobile= str_detect(channels, "mobile"),
         social= str_detect(channels, "social"),
         web= str_detect(channels, "web")) %>%
  pivot_longer(email:web) %>% 
  group_by(name, value) %>% tally() %>%
  ggplot(aes(y=fct_rev(name), x=n, fill=(factor(value)))) + 
  geom_col() +
  scale_fill_manual(values=c("grey","#496A81")) + 
  scale_x_continuous(breaks=seq(0,10,2)) + 
  labs(x= "count of promotion", y="channel component", fill="value")

# channel and duration 
p18 = portfolio %>% ggplot(aes(x=factor(duration), label=channels)) +
  geom_bar(aes(fill=channels), width=.8, position=position_dodge2(width=0.3,preserve = "single")) +
  scale_fill_manual(values=c("#2B3A67","#66999B", "#B3AF8F","#FFC482")) + 
  labs(x="duration", y= "count of promotion", subtitle="Duration and channel combination")

(p17/ p18)

Transcript data

n_distinct(transcript$person)
[1] 17000
n_distinct(transcript$value)
[1] 5121
n_distinct(transcript$time)
[1] 120
summary(transcript)
       X                                       person                   event       
 Min.   :     0   94de646f7b6041228ca7dec82adb97d2:    51   offer completed: 33579  
 1st Qu.: 76633   8dbfa485249f409aa223a2130f40634a:    49   offer received : 76277  
 Median :153266   5e60c6aa3b834e44b822ea43a3efea26:    48   offer viewed   : 57725  
 Mean   :153266   79d9d4f86aca4bed9290350fb43817c2:    48   transaction    :138953  
 3rd Qu.:229900   d0a80415b84c4df4908b8403b19765e3:    48                           
 Max.   :306533   28681c16026943e68f26feaccab0907f:    46                           
                  (Other)                         :306244                           
                                              value             time      
 {'offer id': '2298d6c36e964ae4a3e7e9706d1fb8c2'}: 14983   Min.   :  0.0  
 {'offer id': 'fafdcd668e3743c1bb461111dcafc2a4'}: 14924   1st Qu.:186.0  
 {'offer id': '4d5c57ea9a6940dd891ad53e9dbe8da0'}: 14891   Median :408.0  
 {'offer id': 'f19421c1d4aa40978ebb69ca19b0e20d'}: 14835   Mean   :366.4  
 {'offer id': 'ae264e3637204a6fb9bb56bc8210ddfd'}: 14374   3rd Qu.:528.0  
 {'offer id': '5a8bc65990b245e5a138643cd4eb9837'}: 14305   Max.   :714.0  
 (Other)                                         :218222                  
transcript %>% group_by(person) %>% tally(sort=T) %>% top_n(5)
transcript %>% group_by(person) %>% tally() %>% arrange(n) %>% slice(1:5)
# event type
p14 = transcript %>% 
  group_by(event) %>% tally() %>% mutate(prop=round(n/sum(n)*100,1)) %>% 
  mutate(perc = paste0("(",prop,"%",")")) %>%
  ggplot(aes(x=event, y=n/1000, fill=event)) + 
  geom_col(show.legend = F) +
  geom_text(aes(label=paste(n, perc)),vjust=-0.5, size=2.7) + 
  scale_fill_manual(values=c("#2B3A67","#496A81","#66999B", "#B3AF8F","#FFC482")) +
  labs(y="count", subtitle="Event distribution") + 
  scale_y_continuous(labels=unit_format(unit = "K", sep = ""))

# time intervals 
transcript$time_group = cut(transcript$time, 
                            breaks = c(0,100,200,300,400,500,600,700,800),
                            labels = c("0-100", "101-200", "201-300","301-400",
                                       "401-500","501-600","601-700","701-800"))

p15 = transcript %>% 
  group_by(time_group) %>% tally() %>% mutate(prop=round(n/sum(n)*100,1)) %>% 
  mutate(perc = paste0("(",prop,"%",")")) %>%
  ggplot(aes(x=time_group, y=n/1000, fill=I(ifelse(n==max(n),"#FFC482","slategrey")))) + 
  geom_col(show.legend = F) +
  geom_text(aes(label=paste(n, perc)),vjust=-0.5, size=2.7) + 
  labs(y="count", x="time_group (in days)",subtitle="Time group") +
  scale_y_continuous(labels=unit_format(unit = "K", sep = ""))

# record count per person
p16 = transcript %>% group_by(person) %>% tally(sort=T) %>%
  ggplot(aes(x=n)) + 
  geom_histogram(binwidth=1,fill="#89b0ae", alpha=0.9) +
  geom_vline(aes(xintercept=mean(n)),
             linetype="dashed", size=1) +
  labs(subtitle="Records count/person distribution", x="record_count")

p17 = transcript %>% group_by(person) %>% tally(sort=T) %>% 
  ggplot(aes(x=n)) + 
  geom_boxplot(color="#15616d") + 
  theme(axis.text.y=element_blank(),
        axis.ticks.y=element_blank(),
        panel.grid.minor=element_blank(),
        panel.grid.major.y=element_blank()) + 
  labs(x="record_count")

(p16/p17)+ plot_layout(nrow = 2, heights = c(4, 1)) | p14

p15

summary(transcript$event)
offer completed  offer received    offer viewed     transaction 
          33579           76277           57725          138953 
# subsets 
viewed = transcript %>% filter(event=="offer viewed")
completed = transcript %>% filter(event=="offer completed")
received = transcript %>% filter(event=="offer received")
# received and viewed offer
received %>% mutate(received_viewed=ifelse(received$person %in% viewed$person, "yes","no")) %>%
  group_by(received_viewed) %>% tally() %>% mutate("%"=round(n/sum(n)*100,1)) 

# viewed and completed offer
viewed %>% mutate(viewed_completed=ifelse(viewed$person %in% completed$person, "yes","no")) %>%
  group_by(viewed_completed) %>% tally() %>% mutate("%"=round(n/sum(n)*100,1))

# received and completed offer 
received %>% mutate(recieved_completed=ifelse(received$person %in% completed$person, "yes","no")) %>%
  group_by(recieved_completed) %>% tally() %>% mutate("%"=round(n/sum(n)*100,1)) 

Join dataframes

# prepare df for joining 
df1 = transcript %>% mutate(promo_id= substr(value,15,46)) %>% select(-X) #extract promo string
df2 = portfolio %>% mutate(promo_id=id, promo_ref=paste("Promo",X)) %>% select(-X, -id) #get promo ref
df3 = profile_cln %>% rename(person=id) %>% select(-X)

dim(df1)
[1] 306534      5
dim(df2)
[1] 10  7
dim(df3)
[1] 14820     9
df4 = df1 %>% left_join(df2, by="promo_id") 
dim(df4) # check for obs number
[1] 306534     11

Count plot of promotion ID in transcript data

df4 %>% filter(event!="transaction") %>% group_by(promo_id, promo_ref) %>% tally(sort=T) %>% ungroup() %>%
  mutate(promo_ref2=paste0("(",promo_ref,")"),
         promo_long=paste(promo_id, promo_ref2),
         prop=round(n/sum(n)*100,1),
         perc = paste0("(",prop,"%",")")) %>%
  ggplot(aes(y=reorder(promo_long,n), x=n, fill=I(ifelse(n==max(n),"#496A81","grey")))) + 
  geom_col() +
  geom_text(aes(label=paste(n, perc), color=I(ifelse(n==max(n),"white","black"))),hjust=1.2, size=3) + 
  scale_y_discrete(labels = function(x) str_wrap(x, width = 10)) + 
  theme(plot.title.position = "plot") +
  scale_x_continuous(expand=c(0,500)) +
  labs(y="promotion_id", x="count", subtitle="Count plot of promotion ID in transcript data",fill="count")

Offer response by promo_id

# offer response by promo_id
df4 %>% filter(event!="transaction") %>%
  group_by(promo_ref, event) %>% tally() %>%
  ggplot(aes(y=fct_rev(promo_ref), x=n, color=event)) + 
  geom_line(aes(group=promo_ref), color="grey") + 
  geom_point(size=2.5, alpha=0.9) + 
  theme(panel.grid.minor=element_blank()) + 
  labs(y="promotion reference", x="count", subtitle= "Offer response") + 
  scale_color_manual(values=c("#023e8a","#2a9d8f","#ffb703"))

Offer response percentage

# percentage 
df4 %>% filter(event!="transaction") %>%
  group_by(promo_ref, event) %>% tally() %>% ungroup() %>%
  pivot_wider(names_from=event, values_from=n) %>% #pivot wider to calculate perc
  mutate(viewed_recieved = round(`offer viewed`/`offer received`,3),
         completed_recieved = round(`offer completed`/`offer received`,3),
         completed_viewed = round(`offer completed`/`offer viewed`,3)) %>%
  select(-`offer completed`,-`offer received`,-`offer viewed`) %>%
  pivot_longer(cols=!promo_ref) -> df4_table

data_line = df4_table %>% group_by(name) %>% summarise(mean_x=mean(value,na.rm=T))
data_line

df4_table %>%
  ggplot(aes(y=fct_rev(promo_ref), x=value, fill=name)) + 
  geom_vline(data=data_line, aes(xintercept=mean_x), color="#ffb703", linetype="dashed") +
  geom_col(show.legend = F, width=0.8) +
  geom_text(aes(label=value*100), color="white", size=2.7, hjust=1.5) + 
  facet_wrap(~factor(name, levels=c("viewed_recieved","completed_recieved","completed_viewed", ordered=T)), ncol = 3) + 
  scale_x_continuous(labels=scales::percent_format()) + 
  scale_fill_manual(values=c("#2B3A67","#496A81","#66999B")) +
  theme(strip.background =element_rect(fill=NA)) + 
  labs(x="percentage", y="promotion reference", subtitle= "Offer response percentage")

Event count per person

df4 %>% select(person, event) %>%
  group_by(person) %>% count(event) %>% ungroup() ->tdf

ggstatsplot::ggbetweenstats(data=tdf, 
                            x=event,
                            y=n,
                            messages=FALSE,
                            results.subtitle=FALSE,
                            pairwise.comparisons = FALSE) 

Clustering

# clustering
# data preparation
cdf= df4 %>% select(person, event) %>% 
  group_by(person) %>% count(event) %>% ungroup() %>%
  pivot_wider(names_from=event, values_from=n) %>%
  replace(is.na(.), 0) %>% #replace all NAs with 0 
  select(-person) %>%
  clean_names()
dim(cdf)
[1] 17000     4
# drop outliers
cdf2 = cdf %>% 
  # get zscore
  mutate(zscore_oc =(offer_completed- mean(offer_completed))/ sd(offer_completed),
         zscore_or =(offer_received- mean(offer_received))/ sd(offer_received),
         zscore_ov =(offer_viewed- mean(offer_viewed))/ sd(offer_viewed),
         zscore_t =(transaction- mean(transaction))/ sd(transaction)) %>%
  # drop outliers
  filter(between(zscore_oc,-3,3)) %>%
  filter(between(zscore_or,-3,3)) %>%
  filter(between(zscore_ov,-3,3)) %>%
  filter(between(zscore_t,-3,3)) %>%
  # select variables
  select(offer_completed, offer_received, offer_viewed, transaction)
dim(cdf2)
[1] 16792     4

Correlation

# correlation
set.seed(123)
c1 = ggcorrmat(data = cdf,
          car.vars=c("offer completed":"transaction"),
          title="Correlation")

# correlation after dropping outliers
set.seed(123)
c2 = ggcorrmat(data = cdf2,
          car.vars=c("offer completed":"transaction"),
          title="Correlation after dropping outliers")

# combine plot
c1 + c2

# scale df without outliers
cdf2_scaled = scale(cdf2)
head(cdf2_scaled)
     offer_completed offer_received offer_viewed transaction
[1,]       0.6407481      0.4778884    0.4662897 -0.01035126
[2,]      -1.2302422     -2.3778776   -1.0868068 -1.03281189
[3,]       0.6407481      0.4778884    1.2428379 -0.62382764
[4,]       0.6407481      0.4778884   -0.3102586 -0.01035126
[5,]       0.6407481     -0.4740336    0.4662897  0.80761725
[6,]       0.6407481      0.4778884    0.4662897  2.03457001

Find optimal clusters

Elbow method
# elbow method
set.seed(123)
fviz_nbclust(cdf2_scaled,kmeans,method="wss") + ggtitle("Elbow method")

Silhouette method

fviz_nbclust(cdf2_scaled, kmeans, method = "silhouette", k.max = 20) + ggtitle("Silhouette method")

NBClust method

res.nbclust <- NbClust(cdf_scaled, distance = "euclidean",
                  min.nc = 2, max.nc = 9, 
                  method = "complete", index ="all")
factoextra::fviz_nbclust(res.nbclust) + ggtitle("NbClust method")

Hierarchical clustering

# hclust 
set.seed(1234)
h1= hclust(dist(cdf2_scaled))
plot(h1)

#color 3 clusters
dend_obj=as.dendrogram(h1)
dend3= color_branches(dend_obj,k=3)
plot(dend3)

#color 4 clusters
dend4= color_branches(dend_obj,k=4)
plot(dend4)

K-means clustering

# 4 clusters
set.seed(123)
km4 = kmeans(cdf2_scaled, centers=4, nstart =5)
km4
K-means clustering with 4 clusters of sizes 4395, 5206, 3687, 3504

Cluster means:
  offer_completed offer_received offer_viewed transaction
1      -0.6465954      0.5268382    0.5320180  -0.6553739
2      -0.5755514     -0.9357322   -0.9897010  -0.5382363
3       0.3602940     -0.1469153   -0.1173326   1.0980770
4       1.2870157      0.8840310    0.9265895   0.4662718

Clustering vector:
   [1] 4 2 4 3 3 3 3 2 4 1 2 1 2 4 1 3 1 1 4 1 3 1 1 1 4 2 4 3 2 2 1 3 4 2 4 1 2 1 3 3 1 2 1 4 4
  [46] 4 1 3 1 2 3 3 1 1 3 2 2 2 4 3 4 2 1 1 1 1 3 2 3 1 4 4 1 3 4 2 1 2 2 4 1 3 4 3 2 3 4 4 3 1
  [91] 4 2 2 1 4 2 3 3 2 2 2 2 4 4 4 2 1 2 2 3 1 1 1 2 2 2 2 4 4 1 1 3 3 2 4 2 4 4 3 3 2 4 4 2 1
 [136] 1 3 4 1 3 2 3 3 1 1 3 1 1 1 1 2 2 3 2 2 2 4 3 2 2 4 4 3 3 3 3 3 2 2 4 1 2 2 2 4 3 3 1 2 2
 [181] 3 3 2 2 1 1 3 1 3 3 1 4 4 4 2 2 1 2 2 1 1 3 3 2 4 1 1 1 2 2 2 3 3 4 1 2 2 1 2 1 1 2 2 1 4
 [226] 4 3 1 2 2 2 1 2 2 3 2 2 4 2 3 3 2 1 3 2 3 4 4 1 4 3 2 2 1 1 4 2 1 2 1 3 1 3 1 2 3 1 1 1 4
 [271] 2 1 1 2 1 2 1 3 1 2 1 2 2 2 4 4 1 1 3 2 2 2 2 1 3 1 4 2 2 4 3 4 4 2 2 4 4 2 1 4 3 2 2 2 4
 [316] 4 1 3 2 3 3 4 2 1 2 4 3 4 1 3 3 1 1 2 2 2 2 2 1 3 2 1 3 3 2 3 2 2 2 2 2 1 1 1 2 4 1 3 3 2
 [361] 4 2 4 3 2 4 3 2 2 3 1 3 2 1 4 3 3 4 4 3 3 3 1 4 1 2 2 4 3 1 4 4 2 1 3 2 4 3 1 4 2 2 3 3 4
 [406] 4 2 4 3 2 1 4 4 3 4 3 3 1 3 4 1 2 3 2 2 2 2 1 4 3 4 3 3 2 1 4 2 4 2 4 1 1 2 3 3 1 1 3 3 1
 [451] 2 3 1 4 1 3 4 4 1 2 2 3 1 4 1 3 1 1 3 2 3 2 4 1 1 2 4 3 4 2 3 3 4 1 2 2 2 2 2 1 2 3 3 3 4
 [496] 3 4 3 4 3 1 4 2 3 4 4 1 1 3 3 1 1 3 2 1 1 4 2 4 1 1 2 1 4 2 4 2 1 4 2 4 2 2 3 4 3 4 1 4 3
 [541] 1 1 3 3 2 2 1 4 4 3 3 2 2 1 4 2 3 4 3 1 3 4 3 2 1 4 3 1 2 1 3 4 1 1 3 2 1 2 1 2 3 4 1 1 4
 [586] 1 2 4 2 1 3 2 2 4 3 2 2 3 4 1 2 3 1 2 1 2 3 1 4 4 4 4 3 2 4 1 3 2 2 4 4 3 3 1 1 1 3 2 1 2
 [631] 2 2 2 2 3 1 2 3 2 1 4 4 4 1 1 3 2 4 4 2 3 4 1 4 4 3 2 1 4 2 4 2 3 4 2 2 3 4 2 2 1 1 2 2 1
 [676] 3 3 3 4 3 2 1 4 1 4 2 3 2 1 2 1 3 3 2 1 4 1 4 4 4 3 4 2 2 2 4 1 2 4 2 1 3 2 4 3 2 3 1 4 2
 [721] 2 1 1 1 3 3 4 1 2 1 2 2 2 4 1 3 3 2 4 4 1 3 2 4 2 2 2 4 1 2 3 3 2 3 3 1 1 4 1 4 4 1 2 4 1
 [766] 3 2 2 3 3 1 2 3 3 2 4 2 3 1 3 2 1 1 2 2 1 3 4 1 2 3 2 3 3 4 1 1 2 1 2 2 4 2 3 3 1 2 2 1 2
 [811] 2 2 1 4 2 1 4 1 2 2 1 3 4 2 1 1 4 2 3 4 2 2 1 4 2 4 4 2 2 3 2 3 2 3 1 3 4 3 1 2 4 1 1 4 3
 [856] 2 4 2 4 2 4 2 4 2 4 1 4 1 2 2 2 1 4 1 2 1 3 1 3 4 2 1 1 2 2 1 3 1 3 2 2 2 2 3 4 2 3 2 1 4
 [901] 4 1 2 4 3 2 2 1 1 2 1 1 2 2 3 1 1 4 2 1 2 3 1 1 2 3 4 1 4 2 4 2 1 3 3 2 4 4 2 3 2 2 4 1 2
 [946] 2 4 1 2 1 4 1 4 2 3 2 2 2 2 4 1 4 4 2 2 4 4 1 2 4 3 1 4 2 2 1 1 2 2 1 2 1 3 4 3 2 2 4 3 4
 [991] 1 4 1 2 3 2 2 2 3 3
 [ reached getOption("max.print") -- omitted 15792 entries ]

Within cluster sum of squares by cluster:
[1] 7349.483 9346.092 7504.223 6517.442
 (between_SS / total_SS =  54.3 %)

Available components:

[1] "cluster"      "centers"      "totss"        "withinss"     "tot.withinss" "betweenss"   
[7] "size"         "iter"         "ifault"      
fviz_cluster(km4, data=cdf2_scaled, labelsize=0) #cluster plot

with(cdf2,pairs(cdf2_scaled,col=(1:4)[km4$cluster])) #pair plot

# 3 clusters
set.seed(123)
km3 = kmeans(cdf2_scaled, centers=3, nstart =5)
km3
K-means clustering with 3 clusters of sizes 6111, 5849, 4832

Cluster means:
  offer_completed offer_received offer_viewed transaction
1      -0.4707424     -0.9087929   -0.9382575  -0.3410322
2       0.9680941      0.5101128    0.5285569   0.8600251
3      -0.5765056      0.5318675    0.5468051  -0.6097349

Clustering vector:
   [1] 2 1 3 2 2 2 2 1 2 3 1 3 1 2 3 2 3 3 2 3 3 3 3 3 2 1 3 2 1 1 3 2 2 1 2 3 1 3 2 2 3 1 3 2 2
  [46] 2 3 2 3 1 2 1 3 3 2 1 1 1 2 1 2 1 3 3 3 3 2 1 2 3 2 2 3 1 2 1 3 1 1 2 3 1 2 1 1 2 2 2 2 3
  [91] 2 1 1 3 2 1 2 2 1 1 1 1 2 2 2 1 3 1 1 1 3 3 3 1 1 1 1 2 2 3 3 2 1 1 2 1 2 2 1 2 1 2 2 1 3
 [136] 3 2 2 3 2 1 2 2 3 3 2 3 3 3 3 1 1 2 1 1 1 2 2 1 1 2 3 1 2 1 1 3 1 1 2 3 1 1 1 2 2 2 3 1 1
 [181] 2 2 1 1 3 3 2 3 2 2 3 2 2 2 1 1 3 1 1 3 3 2 2 1 2 3 3 3 1 1 1 2 2 2 3 1 1 3 1 3 3 1 1 3 2
 [226] 2 2 3 1 1 1 3 1 1 3 1 1 2 1 3 2 1 3 2 1 1 2 2 3 2 2 1 1 3 3 2 1 3 1 3 1 3 3 3 1 2 3 3 3 2
 [271] 1 3 3 1 3 1 3 1 3 1 3 3 1 1 2 2 3 3 2 1 1 1 1 3 2 3 2 1 1 2 2 2 3 1 1 2 2 1 3 3 1 1 1 1 3
 [316] 3 3 1 1 2 2 2 1 3 1 2 2 2 3 2 1 3 3 1 1 1 1 1 3 2 1 3 2 3 1 2 1 1 1 1 1 3 3 3 1 2 3 2 2 1
 [361] 2 1 2 2 1 2 2 1 1 1 3 2 1 3 2 2 1 2 2 2 2 2 3 2 3 1 1 2 2 3 2 2 1 3 2 1 2 2 3 2 1 1 1 1 2
 [406] 2 1 2 1 1 3 2 2 2 2 1 1 3 1 2 3 1 2 1 1 1 1 3 2 2 2 2 2 1 3 2 1 2 1 2 3 3 1 2 1 3 3 2 2 2
 [451] 1 2 3 2 3 3 2 2 3 1 1 1 3 2 3 1 3 3 1 1 1 1 2 3 3 1 2 3 2 1 2 2 2 3 1 1 1 1 1 3 1 2 2 2 2
 [496] 2 2 1 2 2 3 2 1 1 2 2 3 3 1 2 3 3 2 1 3 3 2 1 2 3 3 1 3 2 1 2 1 3 2 1 2 1 1 1 3 1 2 3 2 2
 [541] 3 3 1 1 1 1 3 2 2 2 2 1 1 3 2 1 1 2 2 3 1 2 1 1 3 2 1 3 1 3 2 2 3 3 2 1 3 1 3 1 1 2 3 3 2
 [586] 3 1 3 1 3 2 1 1 2 1 1 1 2 2 3 1 1 3 1 3 1 2 3 2 2 2 2 2 1 2 3 2 1 1 2 3 2 2 3 3 3 1 1 3 1
 [631] 1 1 1 1 2 3 1 2 1 3 2 2 2 3 3 1 1 2 2 1 3 2 3 2 2 1 1 3 3 1 3 1 2 2 1 1 2 2 1 1 3 3 1 1 3
 [676] 2 2 2 3 2 1 3 2 3 2 1 2 1 3 1 3 2 2 1 3 2 3 2 2 2 1 2 1 1 1 2 3 1 2 1 3 1 1 2 2 1 2 3 2 1
 [721] 1 3 3 3 2 2 2 3 1 3 1 1 1 2 3 2 2 1 2 2 3 2 1 2 1 1 1 2 3 1 3 2 1 2 1 3 3 2 3 2 2 3 1 2 3
 [766] 2 1 1 1 2 3 1 2 2 1 2 1 2 3 2 1 3 3 1 1 3 2 2 3 1 2 1 1 1 2 3 3 1 3 1 1 2 1 2 3 3 1 1 3 1
 [811] 1 1 3 2 1 3 2 3 1 1 3 2 2 1 3 3 3 1 2 2 1 1 3 2 1 2 2 1 1 2 1 2 1 2 3 2 2 1 3 1 2 3 3 2 1
 [856] 1 2 1 2 1 2 1 2 1 2 3 2 3 1 1 1 3 2 3 1 3 2 3 2 2 1 3 3 1 1 3 2 3 2 1 1 1 1 2 2 1 2 1 3 2
 [901] 3 3 1 2 2 1 1 3 3 1 3 3 1 1 2 3 3 3 1 3 1 2 3 3 1 1 2 3 2 1 2 1 3 2 2 1 2 2 1 2 1 1 2 3 1
 [946] 1 2 3 1 3 2 3 2 1 2 1 1 1 1 2 3 2 2 1 1 2 2 3 1 2 1 3 3 1 1 3 3 1 1 3 1 3 1 2 2 1 1 2 1 3
 [991] 3 2 3 1 2 1 1 1 2 1
 [ reached getOption("max.print") -- omitted 15792 entries ]

Within cluster sum of squares by cluster:
[1] 12330.933 14467.088  8696.321
 (between_SS / total_SS =  47.2 %)

Available components:

[1] "cluster"      "centers"      "totss"        "withinss"     "tot.withinss" "betweenss"   
[7] "size"         "iter"         "ifault"      
fviz_cluster(km3, data=cdf2_scaled, labelsize=0) #cluster plot

with(cdf2,pairs(cdf2_scaled,col=(1:3)[km3$cluster])) #pair plot

k-means 3 clusters
* Cluster means:
+ offer completed: c2 > c1 > c3
+ offer received: c3 > c2 > c1
+ offer viewed: c3 > c2 > c1
+ transaction: c2 > c1 > c3

  • Summary of cluster means:
    • c2: highest offer_completed and transaction (ideal customer group)
    • c3: highest offer_received/offer_viewed, lowest transaction/offer_completed
    • c1: lowest offer_received/offer_viewed
Summary by cluster ID (4 clusters)
cdf2$cluster_id = as.factor(km4$cluster)

# summary
by(cdf2,cdf2$cluster_id,summary)
cdf2$cluster_id: 1
 offer_completed  offer_received   offer_viewed    transaction     cluster_id
 Min.   :0.0000   Min.   :4.000   Min.   :1.000   Min.   : 0.000   1:4395    
 1st Qu.:0.0000   1st Qu.:5.000   1st Qu.:4.000   1st Qu.: 3.000   2:   0    
 Median :1.0000   Median :5.000   Median :4.000   Median : 5.000   3:   0    
 Mean   :0.9358   Mean   :5.051   Mean   :4.085   Mean   : 4.846   4:   0    
 3rd Qu.:2.0000   3rd Qu.:6.000   3rd Qu.:5.000   3rd Qu.: 7.000             
 Max.   :3.0000   Max.   :6.000   Max.   :6.000   Max.   :17.000             
------------------------------------------------------------------------ 
cdf2$cluster_id: 2
 offer_completed offer_received   offer_viewed    transaction     cluster_id
 Min.   :0.00    Min.   :2.000   Min.   :0.000   Min.   : 0.000   1:   0    
 1st Qu.:0.00    1st Qu.:3.000   1st Qu.:2.000   1st Qu.: 3.000   2:5206    
 Median :1.00    Median :4.000   Median :2.000   Median : 5.000   3:   0    
 Mean   :1.05    Mean   :3.515   Mean   :2.125   Mean   : 5.419   4:   0    
 3rd Qu.:2.00    3rd Qu.:4.000   3rd Qu.:3.000   3rd Qu.: 7.000             
 Max.   :4.00    Max.   :6.000   Max.   :3.000   Max.   :16.000             
------------------------------------------------------------------------ 
cdf2$cluster_id: 3
 offer_completed offer_received   offer_viewed    transaction    cluster_id
 Min.   :0.00    Min.   :2.000   Min.   :0.000   Min.   : 5.00   1:   0    
 1st Qu.:2.00    1st Qu.:4.000   1st Qu.:3.000   1st Qu.:10.00   2:   0    
 Median :3.00    Median :4.000   Median :3.000   Median :13.00   3:3687    
 Mean   :2.55    Mean   :4.344   Mean   :3.248   Mean   :13.42   4:   0    
 3rd Qu.:3.00    3rd Qu.:5.000   3rd Qu.:4.000   3rd Qu.:16.00             
 Max.   :5.00    Max.   :6.000   Max.   :6.000   Max.   :23.00             
------------------------------------------------------------------------ 
cdf2$cluster_id: 4
 offer_completed offer_received   offer_viewed    transaction    cluster_id
 Min.   :0.000   Min.   :4.000   Min.   :1.000   Min.   : 1.00   1:   0    
 1st Qu.:3.000   1st Qu.:5.000   1st Qu.:4.000   1st Qu.: 7.00   2:   0    
 Median :4.000   Median :5.000   Median :5.000   Median :10.00   3:   0    
 Mean   :4.036   Mean   :5.427   Mean   :4.593   Mean   :10.33   4:3504    
 3rd Qu.:5.000   3rd Qu.:6.000   3rd Qu.:5.000   3rd Qu.:13.00             
 Max.   :6.000   Max.   :6.000   Max.   :6.000   Max.   :23.00             
# pivot longer
cdf2_long = cdf2 %>% pivot_longer(!cluster_id)

# distribution by cluster id 
cdf2_long %>% ggplot(aes(x= cluster_id, y=value, color=cluster_id, fill=cluster_id)) +
  geom_half_violin(side="r") +
  geom_half_boxplot(fill=NA, outlier.colour = "red", outlier.shape=21, outlier.size = 0.5) + 
  facet_wrap(~name, ncol=2, scales="free") + 
  scale_fill_manual(values=c("#2B3A67","#66999B", "#B3AF8F","#FFC482")) + 
  scale_color_manual(values=c("#2B3A67","#66999B", "#B3AF8F","#FFC482")) + 
  theme(legend.position = "none",
        panel.grid.minor = element_blank(),
        strip.background=element_rect(fill="slategrey"),
        strip.text=element_text(color="white"))

Summary by cluster ID (3 clusters)
cdf2$cluster_id = as.factor(km3$cluster)

# summary
by(cdf2,cdf2$cluster_id,summary)
cdf2$cluster_id: 1
 offer_completed offer_received   offer_viewed    transaction     cluster_id
 Min.   :0.000   Min.   :2.000   Min.   :0.000   Min.   : 0.000   1:6111    
 1st Qu.:0.000   1st Qu.:3.000   1st Qu.:2.000   1st Qu.: 4.000   2:   0    
 Median :1.000   Median :4.000   Median :2.000   Median : 6.000   3:   0    
 Mean   :1.218   Mean   :3.543   Mean   :2.191   Mean   : 6.383             
 3rd Qu.:2.000   3rd Qu.:4.000   3rd Qu.:3.000   3rd Qu.: 8.000             
 Max.   :4.000   Max.   :6.000   Max.   :3.000   Max.   :22.000             
------------------------------------------------------------------------ 
cdf2$cluster_id: 2
 offer_completed offer_received   offer_viewed   transaction    cluster_id
 Min.   :0.000   Min.   :3.000   Min.   :1.00   Min.   : 3.00   1:   0    
 1st Qu.:3.000   1st Qu.:5.000   1st Qu.:3.00   1st Qu.: 9.00   2:5849    
 Median :4.000   Median :5.000   Median :4.00   Median :12.00   3:   0    
 Mean   :3.525   Mean   :5.034   Mean   :4.08   Mean   :12.26             
 3rd Qu.:4.000   3rd Qu.:6.000   3rd Qu.:5.00   3rd Qu.:15.00             
 Max.   :6.000   Max.   :6.000   Max.   :6.00   Max.   :23.00             
------------------------------------------------------------------------ 
cdf2$cluster_id: 3
 offer_completed offer_received   offer_viewed    transaction     cluster_id
 Min.   :0.000   Min.   :4.000   Min.   :1.000   Min.   : 0.000   1:   0    
 1st Qu.:0.000   1st Qu.:5.000   1st Qu.:4.000   1st Qu.: 3.000   2:   0    
 Median :1.000   Median :5.000   Median :4.000   Median : 5.000   3:4832    
 Mean   :1.048   Mean   :5.057   Mean   :4.104   Mean   : 5.069             
 3rd Qu.:2.000   3rd Qu.:6.000   3rd Qu.:5.000   3rd Qu.: 7.000             
 Max.   :4.000   Max.   :6.000   Max.   :6.000   Max.   :16.000             
# pivot longer
cdf2_long = cdf2 %>% pivot_longer(!cluster_id)

# distribution by cluster id 
cdf2_long %>% ggplot(aes(x= cluster_id, y=value, color=cluster_id, fill=cluster_id)) +
  geom_half_violin(side="r") +
  geom_half_boxplot(fill=NA, outlier.colour = "red", outlier.shape=21, outlier.size = 0.5) + 
  facet_wrap(~name, ncol=2, scales="free") + 
  scale_fill_manual(values=c("#2B3A67","#66999B", "#B3AF8F")) + 
  scale_color_manual(values=c("#2B3A67","#66999B", "#B3AF8F")) + 
  theme(legend.position = "none",
        panel.grid.minor = element_blank(),
        strip.background=element_rect(fill="slategrey"),
        strip.text=element_text(color="white"))

The clusters are more unique in 3-groups than 4-groups, especially in offer_received and transaction variables. Hence, it is more suitable to cluster customers into 3 groups than 4 groups.

LS0tCnRpdGxlOiAiU3RhcmJ1Y2tzIEN1c3RvbWVycyIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKIyMjIEludHJvZHVjdGlvbiAKVGhpcyBub3RlYm9vayB1c2VzIFtTdGFyYnVja3MgQ3VzdG9tZXIgRGF0YV0oaHR0cHM6Ly93d3cua2FnZ2xlLmNvbS9paG9ybXVsaWFyL3N0YXJidWNrcy1jdXN0b21lci1kYXRhKSBhbmQgY29ycmVzcG9uZGluZwpbVGFza10oaHR0cHM6Ly93d3cua2FnZ2xlLmNvbS9paG9ybXVsaWFyL3N0YXJidWNrcy1jdXN0b21lci1kYXRhL3Rhc2tzP3Rhc2tJZD0zOTIwKSBmcm9tIFtLYWdnbGVdKGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vKS4gCgoKIyMjIyBEYXRhCgoxLiBfcG9ydGZvbGlvLmNzdl8gOiBJbmZvcm1hdGlvbiBhYm91dCB0aGUgcHJvbW90aW9uYWwgb2ZmZXJzIHRoYXQgYXJlIHBvc3NpYmxlIHRvIHJlY2VpdmUsIGFuZCBiYXNpYyBpbmZvcm1hdGlvbiBhYm91dCBlYWNoIG9uZSBpbmNsdWRpbmcgdGhlIHByb21vdGlvbmFsIHR5cGUsIGR1cmF0aW9uIG9mIHRoZSBwcm9tb3Rpb24sIHJld2FyZCwgYW5kIGhvdyB0aGUgcHJvbW90aW9uIHdhcyBkaXN0cmlidXRlZCB0byBjdXN0b21lcnMKCjIuIF9wcm9maWxlLmNzdl8gOiBEaW1lbnNpb25hbCBkYXRhIGFib3V0IGVhY2ggcGVyc29uLCBpbmNsdWRpbmcgdGhlaXIgYWdlLCBzYWxhcnksIGFuZCBnZW5kZXIuIFRoZXJlIGlzIG9uZSB1bmlxdWUgY3VzdG9tZXIgZm9yIGVhY2ggcmVjb3JkLgoKMy4gX3RyYW5zY3JpcHQuY3N2XyA6IFJlY29yZHMgc2hvdyB0aGUgZGlmZmVyZW50IHN0ZXBzIG9mIHByb21vdGlvbmFsIG9mZmVycyB0aGF0IGEgY3VzdG9tZXIgcmVjZWl2ZWQuIFRoZSBkaWZmZXJlbnQgdmFsdWVzIG9mIHJlY2VpdmluZyBhIHByb21vdGlvbiBhcmUgcmVjZWl2aW5nLCB2aWV3aW5nLCBhbmQgY29tcGxldGluZy4gWW91IGFsc28gc2VlIHRoZSBkaWZmZXJlbnQgdHJhbnNhY3Rpb25zIHRoYXQgYSBwZXJzb24gbWFkZSBpbiB0aGUgdGltZSBzaW5jZSBoZSBiZWNhbWUgYSBjdXN0b21lci4gV2l0aCBhbGwgcmVjb3JkcywgeW91IHNlZSB0aGUgZGF5IHRoYXQgdGhleSBpbnRlcmFjdGVkIHdpdGggU3RhcmJ1Y2tzIGFuZCB0aGUgYW1vdW50IHRoYXQgaXQgaXMgd29ydGguCgoKIyMjIyBEYXRhIGRpY3Rpb25hcnkKCl9wb3J0Zm9saW8uY3N2XwoKfCB2YXJpYWJsZSAgIHwgZGVzY3JpcHRpb24gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8CnwtLS0tLS0tLS0tLS18LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tfAp8ICMgICAgICAgICAgfCBpZCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKfCByZXdhcmQgICAgIHwgUmV3YXJkIGdpdmVuIGZvciBjb21wbGV0aW5nIGFuIG9mZmVyICAgICAgICAgICB8CnwgY2hhbm5lbHMgICB8IChsaXN0IG9mIHN0cmluZ3MpICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAp8IGRpZmZpY3VsdHkgfCBNaW5pbXVtIHJlcXVpcmVkIHNwZW5kIHRvIGNvbXBsZXRlIGFuIG9mZmVyICAgIHwKfCBkdXJhdGlvbiAgIHwgVW5rbm93biAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8Cnwgb2ZmZXJfdHlwZSB8IFR5cGUgb2Ygb2ZmZXIgaWUgQk9HTywgZGlzY291bnQsIGluZm9ybWF0aW9uYWwgfAp8IGlkICAgICAgICAgfCBPZmZlciBpZCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKCgpfcHJvZmlsZS5jc3ZfCgp8IHZhcmlhYmxlICAgICAgICAgfCBkZXNjcmlwdGlvbiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8CnwtLS0tLS0tLS0tLS0tLS0tLS18LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLXwKfCAjICAgICAgICAgICAgICAgIHwgaWQgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAp8IGdlbmRlciAgICAgICAgICAgfCBHZW5kZXIgb2YgdGhlIGN1c3RvbWVyIChub3RlIHNvbWUgZW50cmllcyBjb250YWluICdPJyBmb3Igb3RoZXIgcmF0aGVyIHRoYW4gTSBvciBGKSB8CnwgYWdlICAgICAgICAgICAgICB8IEFnZSBvZiB0aGUgY3VzdG9tZXIgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKfCBpZCAgICAgICAgICAgICAgIHwgQ3VzdG9tZXIgaWQgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAp8IGJlY2FtZV9tZW1iZXJfb24gfCBEYXRlIHdoZW4gY3VzdG9tZXIgY3JlYXRlZCBhbiBhcHAgYWNjb3VudCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8CnwgaW5jb21lICAgICAgICAgICB8IEN1c3RvbWVyJ3MgaW5jb21lICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKCl90cmFuc2NyaXB0LmNzdl8KCnwgdmFyaWFibGUgfCBkZXNjcmlwdGlvbiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAp8LS0tLS0tLS0tLXwtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLXwKfCAjICAgICAgICB8IGlkICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8CnwgcGVyc29uICAgfCBjdXN0b21lciBpZCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAp8IGV2ZW50ICAgIHwgcmVjb3JkIGRlc2NyaXB0aW9uIChpZSB0cmFuc2FjdGlvbiwgb2ZmZXIgcmVjZWl2ZWQsIG9mZmVyIHZpZXdlZCwgZXRjLikgICAgICAgICAgICAgIHwKfCB2YWx1ZSAgICB8IChkaWN0IG9mIHN0cmluZ3MpIC0gZWl0aGVyIGFuIG9mZmVyIGlkIG9yIHRyYW5zYWN0aW9uIGFtb3VudCBkZXBlbmRpbmcgb24gdGhlIHJlY29yZCB8CnwgdGltZSAgICAgfCB0aW1lIGluIGhvdXJzLiBUaGUgZGF0YSBiZWdpbnMgYXQgdGltZSB0PTAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAoKCiMjIyMgUXVlc3Rpb25zIAoKT3ZlcnZpZXcgb2YgU3RhcmJ1Y2tzIHBvcHVsYXRpb24gICAKKiBXaGF0IGlzIGdlbmRlciBkaXN0cmlidXRpb24/ICAgCiogV2hhdCBpcyB0aGUgaW5jb21lIGRpc3RyaWJ1dGlvbj8gICAKKiBXaGVuIGRvIHBlb3BsZSB0eXBpY2FsbHkgYmVjb21lIGEgbWVtYmVyPyAgIAoqIFdoYXQgaXMgdGhlIGF2ZXJhZ2UgcHVyY2hhc2UgZGlzdHJpYnV0aW9uPyAgIAoKUmVhY3Rpb25zIHRvIGRpZmZlcmVudCBwcm9tb3Rpb25zICAgCiogRG8gcGVvcGxlIHJlYWN0IHRvIGRpZmZlcmVudCBwcm9tb3Rpb25zIGRpZmZlcmVudGx5PyAgIAoqIERvZXMgdGhlIHJld2FyZCBvZiB0aGUgcHJvbW90aW9uIG1ha2UgcGVvcGxlIHJlYWN0IGRpZmZlcmVudGx5PyAgIAoqIERvZXMgaXQgbWFrZSBzZW5zZSB0byBvZmZlciBjZXJ0YWluIHJld2FyZHM/ICAgCiogV291bGQgU3RhcmJ1Y2tzIHNhdmUgbW9uZXkgb3ZlcmFsbCBpZiB0aGV5IG9mZmVyIGEgY2VydGFpbiByZXdhcmQ/ICAgCgpDbHVzdGVyaW5nICAgCiogSG93IG1hbnkgY2x1c3RlcnMgc2hvdWxkIFN0YXJidWNrcyB1c2U/ICAgIAoKIyMjIExvYWQgbGliYXJpZXMgCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQpsaWJyYXJ5KHRpZHl2ZXJzZSkKbGlicmFyeShsdWJyaWRhdGUpCmxpYnJhcnkocGF0Y2h3b3JrKQpsaWJyYXJ5KGNvbG9yc3BhY2UpCmxpYnJhcnkoc2NhbGVzKQpsaWJyYXJ5KGdnc3RhdHNwbG90KQpsaWJyYXJ5KHBsb3RseSkKbGlicmFyeShmYWN0b2V4dHJhKQpsaWJyYXJ5KE5iQ2x1c3QpCmxpYnJhcnkoZGVuZGV4dGVuZCkKbGlicmFyeShnZ2RlbmRybykKCnRoZW1lX3NldCh0aGVtZV9idyhiYXNlX3NpemU9MTApKQpgYGAKCgojIyMgSW1wb3J0IGRhdGEKYGBge3J9CnBvcnRmb2xpbyA9IHJlYWQuY3N2KCJTdGFyYnVja3MgQ3VzdG9tZXIgRGF0YS9wb3J0Zm9saW8uY3N2Iiwgc3RyaW5nc0FzRmFjdG9ycyA9IFQpCnByb2ZpbGUgPSByZWFkLmNzdigiU3RhcmJ1Y2tzIEN1c3RvbWVyIERhdGEvcHJvZmlsZS5jc3YiLHN0cmluZ3NBc0ZhY3RvcnMgPSBUKQp0cmFuc2NyaXB0ID0gcmVhZC5jc3YoIlN0YXJidWNrcyBDdXN0b21lciBEYXRhL3RyYW5zY3JpcHQuY3N2IixzdHJpbmdzQXNGYWN0b3JzID0gVCkKYGBgCgpgYGB7cn0Kc3RyKHBvcnRmb2xpbykKCnByb2ZpbGUkYmVjYW1lX21lbWJlcl9vbiA9IHltZChwcm9maWxlJGJlY2FtZV9tZW1iZXJfb24pCnN0cihwcm9maWxlKQoKc3RyKHRyYW5zY3JpcHQpCmBgYAoKCiMjIyBTdW1tYXJ5CmBgYHtyfQpzdW1tYXJ5KHBvcnRmb2xpbykKc3VtbWFyeShwcm9maWxlKQpzdW1tYXJ5KHRyYW5zY3JpcHQpCmBgYAoKIyMjIERhdGEgY29tcGxldGVuZXNzIApgYGB7cn0KIyBwcm9maWxlIGRhdGEKcHJvZmlsZSAlPiUgc3VtbWFyaXNlKGFjcm9zcyhldmVyeXRoaW5nKCksIH5tZWFuKCFpcy5uYSguKSkpKSAlPiUgCiAgZ2F0aGVyKCkgJT4lCiAgbXV0YXRlKGtleT0gZmN0X3Jlb3JkZXIoa2V5LCB2YWx1ZSkpCgojIHBvcnRmb2xpbyBkYXRhCnBvcnRmb2xpbyAlPiUgc3VtbWFyaXNlKGFjcm9zcyhldmVyeXRoaW5nKCksIH5tZWFuKCFpcy5uYSguKSkpKSAlPiUgCiAgZ2F0aGVyKCkgJT4lCiAgbXV0YXRlKGtleT0gZmN0X3Jlb3JkZXIoa2V5LCB2YWx1ZSkpCgojIHRyYW5zY3JpcHQgZGF0YQp0cmFuc2NyaXB0ICU+JSBzdW1tYXJpc2UoYWNyb3NzKGV2ZXJ5dGhpbmcoKSwgfm1lYW4oIWlzLm5hKC4pKSkpICU+JSAKICBnYXRoZXIoKSAlPiUKICBtdXRhdGUoa2V5PSBmY3RfcmVvcmRlcihrZXksIHZhbHVlKSkKYGBgCgoKIyMjIFByb2ZpbGUgZGF0YQoKYGBge3J9CmRpbShwcm9maWxlKQpuX2Rpc3RpbmN0KHByb2ZpbGUkaWQpCmBgYAoKCmBgYHtyLCB3YXJuaW5nPUZBTFNFfQpwMSA9IHByb2ZpbGUgJT4lIGdncGxvdChhZXMoeD1hZ2UpKSArIGdlb21fYm94cGxvdChjb2xvcj0iIzJCM0E2NyIpICsgdGhlbWUoYXhpcy50aWNrcy55PWVsZW1lbnRfYmxhbmsoKSwgYXhpcy50ZXh0Lnk9ZWxlbWVudF9ibGFuaygpKQpwMiA9IHByb2ZpbGUgJT4lIGdncGxvdChhZXMoeD1pbmNvbWUpKSAgKyBnZW9tX2JveHBsb3QoY29sb3I9IiM2Njk5OUIiKSArIHRoZW1lKGF4aXMudGlja3MueT1lbGVtZW50X2JsYW5rKCksIGF4aXMudGV4dC55PWVsZW1lbnRfYmxhbmsoKSkKcDMgPSBwcm9maWxlICU+JSBncm91cF9ieShiZWNhbWVfbWVtYmVyX29uKSAlPiUgdGFsbHkoKSAlPiUKICBnZ3Bsb3QoYWVzKHg9YmVjYW1lX21lbWJlcl9vbiwgeT1uKSkgKyAKICBnZW9tX2xpbmUoYWxwaGE9MC45LCBjb2xvcj0iIzQ5NkE4MSIpCnA0ID0gcHJvZmlsZSAlPiUgZ2dwbG90KGFlcyh4PWdlbmRlcikpICsgZ2VvbV9iYXIoYWxwaGE9MC45LCBmaWxsPSIjQjNBRjhGIikgCgoocDEgfCBwMykgLyAocDIgfCBwNCkgKyBwbG90X2Fubm90YXRpb24oc3VidGl0bGU9IlByb2ZpbGUiKQpgYGAKCmBgYHtyfQojIGNsZWFuIHByb2ZpbGUgZGF0YQpwcm9maWxlICU+JQogIGZpbHRlcihhZ2U8PTEwMCkgJT4lICNkcm9wIG9icyB3aXRoIGFnZSA+MTAwCiAgc3VtbWFyeSgpICAjbm8gbWlzc2luZyB2YWx1ZXMgaW4gZ2VuZGVyIGFuZCBpbmNvbWUgCgojIGdldCBjb21wb25lbnRzIG9mIGJlY29tZV9tZW1iZXJfb24gZGF0ZQpwcm9maWxlX2NsbiA9IHByb2ZpbGUgJT4lIAogIGZpbHRlcihhZ2U8PTEwMCkgJT4lCiAgbXV0YXRlKHllYXI9IHllYXIoYmVjYW1lX21lbWJlcl9vbiksCiAgICAgICAgIG1vbnRoPSBtb250aChiZWNhbWVfbWVtYmVyX29uKSwKICAgICAgICAgZGF5PSBkYXkoYmVjYW1lX21lbWJlcl9vbiksCiAgICAgICAgIHdkYXkgPSB3ZGF5KGJlY2FtZV9tZW1iZXJfb24sIGxhYmVsPVRSVUUpKSAKYGBgCgoKYGBge3J9CnA0ID0gcHJvZmlsZV9jbG4gJT4lIAogIGdyb3VwX2J5KGdlbmRlcikgJT4lIHRhbGx5KCkgJT4lIAogIG11dGF0ZShwcm9wPXJvdW5kKG4vc3VtKG4pKjEwMCwxKSwKICAgICAgICAgIGdlbmRlcl9sb25nPXJlY29kZShnZW5kZXIsIkYiPSJGZW1hbGUiLCAiTSI9Ik1hbGUiLCJPIj0iT3RoZXIiKSkgJT4lIAogIG11dGF0ZShwZXJjID0gcGFzdGUwKCIoIixwcm9wLCIlIiwiKSIpKSAlPiUKICBnZ3Bsb3QoYWVzKHg9Z2VuZGVyX2xvbmcsIHk9biwgZmlsbD1nZW5kZXJfbG9uZykpICsgCiAgZ2VvbV9jb2woc2hvdy5sZWdlbmQgPSBGKSArCiAgZ2VvbV90ZXh0KGFlcyhsYWJlbD1wYXN0ZShuLCBwZXJjKSksdmp1c3Q9LTAuNSwgc2l6ZT0yLjcpICsgCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPWMoIiM0OTZBODEiLCIjRkZDNDgyIiwiI0IzQUY4RiIpKSArCiAgbGFicyh5PSJjb3VudCIsIHN1YnRpdGxlPSJHZW5kZXIgZGlzdHJpYnV0aW9uIiwgeD0iZ2VuZGVyIikKCnA1ID0gcHJvZmlsZV9jbG4gJT4lIAogIGdncGxvdChhZXMoeD1pbmNvbWUpKSArCiAgZ2VvbV9oaXN0b2dyYW0oYmlud2lkdGggPSAxMDAwLCBmaWxsPSIjODliMGFlIiwgYWxwaGE9MC45KSArIAogIGdlb21fdmxpbmUoYWVzKHhpbnRlcmNlcHQ9bWVhbihpbmNvbWUpKSwKICAgICAgICAgICAgIGxpbmV0eXBlPSJkYXNoZWQiLCBzaXplPTEpICsKICBsYWJzKHN1YnRpdGxlPSJJbmNvbWUgZGlzdHJpYnV0aW9uIikKCnA2ID0gcHJvZmlsZV9jbG4gJT4lIAogIGdncGxvdChhZXMoeD1pbmNvbWUpKSArIAogIGdlb21fYm94cGxvdChjb2xvcj0iIzE1NjE2ZCIpICsgCiAgdGhlbWUoYXhpcy50ZXh0Lnk9ZWxlbWVudF9ibGFuaygpLAogICAgICAgIGF4aXMudGlja3MueT1lbGVtZW50X2JsYW5rKCksCiAgICAgICAgcGFuZWwuZ3JpZC5taW5vcj1lbGVtZW50X2JsYW5rKCksCiAgICAgICAgcGFuZWwuZ3JpZC5tYWpvci55PWVsZW1lbnRfYmxhbmsoKSkKCnA0IHwgKHA1L3A2KSArIHBsb3RfbGF5b3V0KG5yb3cgPSAyLCBoZWlnaHRzID0gYyg0LCAxKSkKYGBgCgoKCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQojIGJlY2FtZV9tZW1iZXJfb24KcDcgPSBwcm9maWxlX2NsbiAlPiUKICBncm91cF9ieSh5ZWFyLCBtb250aCkgJT4lCiAgc3VtbWFyaXNlKG49bl9kaXN0aW5jdChpZCkpICU+JQogIG11dGF0ZSh5ZWFyX21vbnRoPXBhc3RlMCh5ZWFyLCctJyxtb250aCwnLScsIjEiKSkgJT4lIAogIG11dGF0ZShkYXRlMiA9IHltZCh5ZWFyX21vbnRoKSkgJT4lCiAgZ2dwbG90KGFlcyh4PWRhdGUyLCB5PW4sIGZpbGw9bikpICsgCiAgZ2VvbV9jb2woYWxwaGE9MC45LCBzaG93LmxlZ2VuZD1GKSArIAogIHNjYWxlX2ZpbGxfY29udGludW91c19zZXF1ZW50aWFsKHBhbGV0dGU9ImJhdGxvdyIpICsgCiAgbGFicyh5PSJjb3VudCIsIGZpbGw9IiIsIHg9ImRhdGUiKQoKcDggPSBwcm9maWxlX2NsbiAlPiUgZ2dwbG90KGFlcyh5PWZvcmNhdHM6OmZjdF9yZXYoZmN0X2luZnJlcShmYWN0b3IoeWVhcikpKSkpICsgZ2VvbV9iYXIoZmlsbD0iIzJCM0E2NyIpICsgbGFicyh5PSJ5ZWFyIikKcDkgPSBwcm9maWxlX2NsbiAlPiUgZ2dwbG90KGFlcyh5PWZvcmNhdHM6OmZjdF9yZXYoZmN0X2luZnJlcShmYWN0b3IobW9udGgpKSkpKSArIGdlb21fYmFyKGZpbGw9IiM0OTZBODEiKSArIGxhYnMoeT0ibW9udGgiKQpwMTAgPSBwcm9maWxlX2NsbiAlPiUgZ2dwbG90KGFlcyh5PWZvcmNhdHM6OmZjdF9yZXYoZmN0X2luZnJlcShmYWN0b3Iod2RheSkpKSkpICsgZ2VvbV9iYXIoZmlsbD0iIzY2OTk5QiIpICsgbGFicyh5PSJkYXkgb2YgdGhlIHdlZWsiKQoKcDcgLyAocDggKyBwOSArIHAxMCkgKyBwbG90X2Fubm90YXRpb24oc3VidGl0bGU9ImJlY2FtZV9tZW1iZXJfb24iKQpgYGAKCmBgYHtyfQojIGFnZQpwMTE9IHByb2ZpbGVfY2xuICU+JSAKICBnZ3Bsb3QoYWVzKHg9YWdlKSkgKyAKICBnZW9tX2hpc3RvZ3JhbShiaW53aWR0aCA9IDEsIGZpbGw9IiM4OWIwYWUiLCBhbHBoYT0wLjkpICsgCiAgZ2VvbV92bGluZShhZXMoeGludGVyY2VwdD1tZWFuKGFnZSkpLAogICAgICAgICAgICAgbGluZXR5cGU9ImRhc2hlZCIsIHNpemU9MSkgKwogIGxhYnMoc3VidGl0bGU9IkFnZSBkaXN0cmlidXRpb24iKQoKcDEyPSBwcm9maWxlX2NsbiAlPiUgCiAgZ2dwbG90KGFlcyh4PWFnZSkpICsgCiAgZ2VvbV9ib3hwbG90KGNvbG9yPSIjMTU2MTZkIikgKyAKICB0aGVtZShheGlzLnRleHQueT1lbGVtZW50X2JsYW5rKCksCiAgICAgICAgYXhpcy50aWNrcy55PWVsZW1lbnRfYmxhbmsoKSwKICAgICAgICBwYW5lbC5ncmlkLm1pbm9yPWVsZW1lbnRfYmxhbmsoKSwKICAgICAgICBwYW5lbC5ncmlkLm1ham9yLnk9ZWxlbWVudF9ibGFuaygpKQoKIyBhZ2UgZ3JvdXAgCnByb2ZpbGVfY2xuJGFnZV9ncm91cCA9IGN1dChwcm9maWxlX2NsbiRhZ2UsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgYnJlYWtzID0gYygwLCAyMCwgNDAsIDYwLCA4MCwgMTAwKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxhYmVscyA9IGMoIjAtMjAiLCAiMjEtNDAiLCAiNDEtNjAiLCI2MS04MCIsIjgxLTEwMCIpKSAjcmVjb2RlIHRvIGNhdGVnb3JpY2FsCnAxMz0gcHJvZmlsZV9jbG4gJT4lIAogIGdyb3VwX2J5KGFnZV9ncm91cCkgJT4lIHRhbGx5KCkgJT4lIG11dGF0ZShwcm9wPXJvdW5kKG4vc3VtKG4pKjEwMCwxKSkgJT4lIAogIG11dGF0ZShwZXJjID0gcGFzdGUwKCIoIixwcm9wLCIlIiwiKSIpKSAlPiUKICBnZ3Bsb3QoYWVzKHg9YWdlX2dyb3VwLCB5PW4sIGZpbGw9YWdlX2dyb3VwKSkgKyAKICBnZW9tX2NvbChzaG93LmxlZ2VuZCA9IEYpICsKICBnZW9tX3RleHQoYWVzKGxhYmVsPXBhc3RlKG4sIHBlcmMpKSx2anVzdD0tMC41LCBzaXplPTIuNykgKyAKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9YygiIzJCM0E2NyIsIiM0OTZBODEiLCIjNjY5OTlCIiwgIiNCM0FGOEYiLCIjRkZDNDgyIikpICsKICBsYWJzKHk9ImNvdW50Iiwgc3VidGl0bGU9IkFnZSBncm91cCIpCgoKIChwMTEvcDEyKSsgcGxvdF9sYXlvdXQobnJvdyA9IDIsIGhlaWdodHMgPSBjKDQsIDEpKSB8IHAxMyAgCiAgCmBgYAoKIyMjIFBvcnRmb2xpbyBkYXRhCgoKYGBge3J9CnAxNmEgPXBvcnRmb2xpbyAlPiUgZ2dwbG90KGFlcyh5PWNoYW5uZWxzKSkgKyBnZW9tX2JhcihmaWxsPSIjRkZDNDgyIikgCnAxNmIgPXBvcnRmb2xpbyAlPiUgZ2dwbG90KGFlcyh4PWR1cmF0aW9uKSkgKyBnZW9tX2JhcihmaWxsPSIjNDk2QTgxIikKcDE2YyA9cG9ydGZvbGlvICU+JSBnZ3Bsb3QoYWVzKHg9b2ZmZXJfdHlwZSkpICsgZ2VvbV9iYXIoZmlsbD0iIzJCM0E2NyIpCnAxNmQgPXBvcnRmb2xpbyAlPiUgZ2dwbG90KGFlcyh4PWRpZmZpY3VsdHkpKSArIGdlb21fYm94cGxvdChmaWxsPSIjNjY5OTlCIiwgYWxwaGE9MC45LCBvdXRsaWVyLmNvbG91ciA9ICJyZWQiKQpwMTZlID1wb3J0Zm9saW8gJT4lIGdncGxvdChhZXMoeD1yZXdhcmQpKSArIGdlb21fYmFyKGZpbGw9IiNCM0FGOEYiKQoKKHAxNmMgfCBwMTZhKSAvCiAgKHAxNmQgfCBwMTZlIHwgcDE2YikgKyBwbG90X2Fubm90YXRpb24oc3VidGl0bGU9IlBvcnRmb2xpbyIpCmBgYAoKCgpgYGB7cn0KIyBjaGFubmVsCnAxNyA9IHBvcnRmb2xpbyAlPiUgCiAgbXV0YXRlKGVtYWlsPSBzdHJfZGV0ZWN0KGNoYW5uZWxzLCAiZW1haWwiKSwKICAgICAgICAgbW9iaWxlPSBzdHJfZGV0ZWN0KGNoYW5uZWxzLCAibW9iaWxlIiksCiAgICAgICAgIHNvY2lhbD0gc3RyX2RldGVjdChjaGFubmVscywgInNvY2lhbCIpLAogICAgICAgICB3ZWI9IHN0cl9kZXRlY3QoY2hhbm5lbHMsICJ3ZWIiKSkgJT4lCiAgcGl2b3RfbG9uZ2VyKGVtYWlsOndlYikgJT4lIAogIGdyb3VwX2J5KG5hbWUsIHZhbHVlKSAlPiUgdGFsbHkoKSAlPiUKICBnZ3Bsb3QoYWVzKHk9ZmN0X3JldihuYW1lKSwgeD1uLCBmaWxsPShmYWN0b3IodmFsdWUpKSkpICsgCiAgZ2VvbV9jb2woKSArCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPWMoImdyZXkiLCIjNDk2QTgxIikpICsgCiAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcz1zZXEoMCwxMCwyKSkgKyAKICBsYWJzKHg9ICJjb3VudCBvZiBwcm9tb3Rpb24iLCB5PSJjaGFubmVsIGNvbXBvbmVudCIsIGZpbGw9InZhbHVlIikKCiMgY2hhbm5lbCBhbmQgZHVyYXRpb24gCnAxOCA9IHBvcnRmb2xpbyAlPiUgZ2dwbG90KGFlcyh4PWZhY3RvcihkdXJhdGlvbiksIGxhYmVsPWNoYW5uZWxzKSkgKwogIGdlb21fYmFyKGFlcyhmaWxsPWNoYW5uZWxzKSwgd2lkdGg9LjgsIHBvc2l0aW9uPXBvc2l0aW9uX2RvZGdlMih3aWR0aD0wLjMscHJlc2VydmUgPSAic2luZ2xlIikpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9YygiIzJCM0E2NyIsIiM2Njk5OUIiLCAiI0IzQUY4RiIsIiNGRkM0ODIiKSkgKyAKICBsYWJzKHg9ImR1cmF0aW9uIiwgeT0gImNvdW50IG9mIHByb21vdGlvbiIsIHN1YnRpdGxlPSJEdXJhdGlvbiBhbmQgY2hhbm5lbCBjb21iaW5hdGlvbiIpCgoocDE3LyBwMTgpCmBgYAoKCiMjIyBUcmFuc2NyaXB0IGRhdGEKCmBgYHtyfQpuX2Rpc3RpbmN0KHRyYW5zY3JpcHQkcGVyc29uKQpuX2Rpc3RpbmN0KHRyYW5zY3JpcHQkdmFsdWUpCm5fZGlzdGluY3QodHJhbnNjcmlwdCR0aW1lKQpzdW1tYXJ5KHRyYW5zY3JpcHQpCmBgYAoKYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9CnRyYW5zY3JpcHQgJT4lIGdyb3VwX2J5KHBlcnNvbikgJT4lIHRhbGx5KHNvcnQ9VCkgJT4lIHRvcF9uKDUpCnRyYW5zY3JpcHQgJT4lIGdyb3VwX2J5KHBlcnNvbikgJT4lIHRhbGx5KCkgJT4lIGFycmFuZ2UobikgJT4lIHNsaWNlKDE6NSkKYGBgCgoKYGBge3J9CiMgZXZlbnQgdHlwZQpwMTQgPSB0cmFuc2NyaXB0ICU+JSAKICBncm91cF9ieShldmVudCkgJT4lIHRhbGx5KCkgJT4lIG11dGF0ZShwcm9wPXJvdW5kKG4vc3VtKG4pKjEwMCwxKSkgJT4lIAogIG11dGF0ZShwZXJjID0gcGFzdGUwKCIoIixwcm9wLCIlIiwiKSIpKSAlPiUKICBnZ3Bsb3QoYWVzKHg9ZXZlbnQsIHk9bi8xMDAwLCBmaWxsPWV2ZW50KSkgKyAKICBnZW9tX2NvbChzaG93LmxlZ2VuZCA9IEYpICsKICBnZW9tX3RleHQoYWVzKGxhYmVsPXBhc3RlKG4sIHBlcmMpKSx2anVzdD0tMC41LCBzaXplPTIuNykgKyAKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9YygiIzJCM0E2NyIsIiM0OTZBODEiLCIjNjY5OTlCIiwgIiNCM0FGOEYiLCIjRkZDNDgyIikpICsKICBsYWJzKHk9ImNvdW50Iiwgc3VidGl0bGU9IkV2ZW50IGRpc3RyaWJ1dGlvbiIpICsgCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscz11bml0X2Zvcm1hdCh1bml0ID0gIksiLCBzZXAgPSAiIikpCgojIHRpbWUgaW50ZXJ2YWxzIAp0cmFuc2NyaXB0JHRpbWVfZ3JvdXAgPSBjdXQodHJhbnNjcmlwdCR0aW1lLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJyZWFrcyA9IGMoMCwxMDAsMjAwLDMwMCw0MDAsNTAwLDYwMCw3MDAsODAwKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxhYmVscyA9IGMoIjAtMTAwIiwgIjEwMS0yMDAiLCAiMjAxLTMwMCIsIjMwMS00MDAiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiNDAxLTUwMCIsIjUwMS02MDAiLCI2MDEtNzAwIiwiNzAxLTgwMCIpKQoKcDE1ID0gdHJhbnNjcmlwdCAlPiUgCiAgZ3JvdXBfYnkodGltZV9ncm91cCkgJT4lIHRhbGx5KCkgJT4lIG11dGF0ZShwcm9wPXJvdW5kKG4vc3VtKG4pKjEwMCwxKSkgJT4lIAogIG11dGF0ZShwZXJjID0gcGFzdGUwKCIoIixwcm9wLCIlIiwiKSIpKSAlPiUKICBnZ3Bsb3QoYWVzKHg9dGltZV9ncm91cCwgeT1uLzEwMDAsIGZpbGw9SShpZmVsc2Uobj09bWF4KG4pLCIjRkZDNDgyIiwic2xhdGVncmV5IikpKSkgKyAKICBnZW9tX2NvbChzaG93LmxlZ2VuZCA9IEYpICsKICBnZW9tX3RleHQoYWVzKGxhYmVsPXBhc3RlKG4sIHBlcmMpKSx2anVzdD0tMC41LCBzaXplPTIuNykgKyAKICBsYWJzKHk9ImNvdW50IiwgeD0idGltZV9ncm91cCAoaW4gZGF5cykiLHN1YnRpdGxlPSJUaW1lIGdyb3VwIikgKwogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHM9dW5pdF9mb3JtYXQodW5pdCA9ICJLIiwgc2VwID0gIiIpKQoKIyByZWNvcmQgY291bnQgcGVyIHBlcnNvbgpwMTYgPSB0cmFuc2NyaXB0ICU+JSBncm91cF9ieShwZXJzb24pICU+JSB0YWxseShzb3J0PVQpICU+JQogIGdncGxvdChhZXMoeD1uKSkgKyAKICBnZW9tX2hpc3RvZ3JhbShiaW53aWR0aD0xLGZpbGw9IiM4OWIwYWUiLCBhbHBoYT0wLjkpICsKICBnZW9tX3ZsaW5lKGFlcyh4aW50ZXJjZXB0PW1lYW4obikpLAogICAgICAgICAgICAgbGluZXR5cGU9ImRhc2hlZCIsIHNpemU9MSkgKwogIGxhYnMoc3VidGl0bGU9IlJlY29yZHMgY291bnQvcGVyc29uIGRpc3RyaWJ1dGlvbiIsIHg9InJlY29yZF9jb3VudCIpCgpwMTcgPSB0cmFuc2NyaXB0ICU+JSBncm91cF9ieShwZXJzb24pICU+JSB0YWxseShzb3J0PVQpICU+JSAKICBnZ3Bsb3QoYWVzKHg9bikpICsgCiAgZ2VvbV9ib3hwbG90KGNvbG9yPSIjMTU2MTZkIikgKyAKICB0aGVtZShheGlzLnRleHQueT1lbGVtZW50X2JsYW5rKCksCiAgICAgICAgYXhpcy50aWNrcy55PWVsZW1lbnRfYmxhbmsoKSwKICAgICAgICBwYW5lbC5ncmlkLm1pbm9yPWVsZW1lbnRfYmxhbmsoKSwKICAgICAgICBwYW5lbC5ncmlkLm1ham9yLnk9ZWxlbWVudF9ibGFuaygpKSArIAogIGxhYnMoeD0icmVjb3JkX2NvdW50IikKCihwMTYvcDE3KSsgcGxvdF9sYXlvdXQobnJvdyA9IDIsIGhlaWdodHMgPSBjKDQsIDEpKSB8IHAxNApwMTUKYGBgCgpgYGB7cn0Kc3VtbWFyeSh0cmFuc2NyaXB0JGV2ZW50KQpgYGAKCgpgYGB7cn0KIyBzdWJzZXRzIAp2aWV3ZWQgPSB0cmFuc2NyaXB0ICU+JSBmaWx0ZXIoZXZlbnQ9PSJvZmZlciB2aWV3ZWQiKQpjb21wbGV0ZWQgPSB0cmFuc2NyaXB0ICU+JSBmaWx0ZXIoZXZlbnQ9PSJvZmZlciBjb21wbGV0ZWQiKQpyZWNlaXZlZCA9IHRyYW5zY3JpcHQgJT4lIGZpbHRlcihldmVudD09Im9mZmVyIHJlY2VpdmVkIikKYGBgCgpgYGB7cn0KIyByZWNlaXZlZCBhbmQgdmlld2VkIG9mZmVyCnJlY2VpdmVkICU+JSBtdXRhdGUocmVjZWl2ZWRfdmlld2VkPWlmZWxzZShyZWNlaXZlZCRwZXJzb24gJWluJSB2aWV3ZWQkcGVyc29uLCAieWVzIiwibm8iKSkgJT4lCiAgZ3JvdXBfYnkocmVjZWl2ZWRfdmlld2VkKSAlPiUgdGFsbHkoKSAlPiUgbXV0YXRlKCIlIj1yb3VuZChuL3N1bShuKSoxMDAsMSkpIAoKIyB2aWV3ZWQgYW5kIGNvbXBsZXRlZCBvZmZlcgp2aWV3ZWQgJT4lIG11dGF0ZSh2aWV3ZWRfY29tcGxldGVkPWlmZWxzZSh2aWV3ZWQkcGVyc29uICVpbiUgY29tcGxldGVkJHBlcnNvbiwgInllcyIsIm5vIikpICU+JQogIGdyb3VwX2J5KHZpZXdlZF9jb21wbGV0ZWQpICU+JSB0YWxseSgpICU+JSBtdXRhdGUoIiUiPXJvdW5kKG4vc3VtKG4pKjEwMCwxKSkKCiMgcmVjZWl2ZWQgYW5kIGNvbXBsZXRlZCBvZmZlciAKcmVjZWl2ZWQgJT4lIG11dGF0ZShyZWNpZXZlZF9jb21wbGV0ZWQ9aWZlbHNlKHJlY2VpdmVkJHBlcnNvbiAlaW4lIGNvbXBsZXRlZCRwZXJzb24sICJ5ZXMiLCJubyIpKSAlPiUKICBncm91cF9ieShyZWNpZXZlZF9jb21wbGV0ZWQpICU+JSB0YWxseSgpICU+JSBtdXRhdGUoIiUiPXJvdW5kKG4vc3VtKG4pKjEwMCwxKSkgCmBgYAoKIyMjIEpvaW4gZGF0YWZyYW1lcwpgYGB7cn0KIyBwcmVwYXJlIGRmIGZvciBqb2luaW5nIApkZjEgPSB0cmFuc2NyaXB0ICU+JSBtdXRhdGUocHJvbW9faWQ9IHN1YnN0cih2YWx1ZSwxNSw0NikpICU+JSBzZWxlY3QoLVgpICNleHRyYWN0IHByb21vIHN0cmluZwpkZjIgPSBwb3J0Zm9saW8gJT4lIG11dGF0ZShwcm9tb19pZD1pZCwgcHJvbW9fcmVmPXBhc3RlKCJQcm9tbyIsWCkpICU+JSBzZWxlY3QoLVgsIC1pZCkgI2dldCBwcm9tbyByZWYKZGYzID0gcHJvZmlsZV9jbG4gJT4lIHJlbmFtZShwZXJzb249aWQpICU+JSBzZWxlY3QoLVgpCgpkaW0oZGYxKQpkaW0oZGYyKQpkaW0oZGYzKQpgYGAKCgpgYGB7cn0KZGY0ID0gZGYxICU+JSBsZWZ0X2pvaW4oZGYyLCBieT0icHJvbW9faWQiKSAKZGltKGRmNCkgIyBjaGVjayBmb3Igb2JzIG51bWJlcgpgYGAKCiMjIyMgQ291bnQgcGxvdCBvZiBwcm9tb3Rpb24gSUQgaW4gdHJhbnNjcmlwdCBkYXRhCmBgYHtyfQpkZjQgJT4lIGZpbHRlcihldmVudCE9InRyYW5zYWN0aW9uIikgJT4lIGdyb3VwX2J5KHByb21vX2lkLCBwcm9tb19yZWYpICU+JSB0YWxseShzb3J0PVQpICU+JSB1bmdyb3VwKCkgJT4lCiAgbXV0YXRlKHByb21vX3JlZjI9cGFzdGUwKCIoIixwcm9tb19yZWYsIikiKSwKICAgICAgICAgcHJvbW9fbG9uZz1wYXN0ZShwcm9tb19pZCwgcHJvbW9fcmVmMiksCiAgICAgICAgIHByb3A9cm91bmQobi9zdW0obikqMTAwLDEpLAogICAgICAgICBwZXJjID0gcGFzdGUwKCIoIixwcm9wLCIlIiwiKSIpKSAlPiUKICBnZ3Bsb3QoYWVzKHk9cmVvcmRlcihwcm9tb19sb25nLG4pLCB4PW4sIGZpbGw9SShpZmVsc2Uobj09bWF4KG4pLCIjNDk2QTgxIiwiZ3JleSIpKSkpICsgCiAgZ2VvbV9jb2woKSArCiAgZ2VvbV90ZXh0KGFlcyhsYWJlbD1wYXN0ZShuLCBwZXJjKSwgY29sb3I9SShpZmVsc2Uobj09bWF4KG4pLCJ3aGl0ZSIsImJsYWNrIikpKSxoanVzdD0xLjIsIHNpemU9MykgKyAKICBzY2FsZV95X2Rpc2NyZXRlKGxhYmVscyA9IGZ1bmN0aW9uKHgpIHN0cl93cmFwKHgsIHdpZHRoID0gMTApKSArIAogIHRoZW1lKHBsb3QudGl0bGUucG9zaXRpb24gPSAicGxvdCIpICsKICBzY2FsZV94X2NvbnRpbnVvdXMoZXhwYW5kPWMoMCw1MDApKSArCiAgbGFicyh5PSJwcm9tb3Rpb25faWQiLCB4PSJjb3VudCIsIHN1YnRpdGxlPSJDb3VudCBwbG90IG9mIHByb21vdGlvbiBJRCBpbiB0cmFuc2NyaXB0IGRhdGEiLGZpbGw9ImNvdW50IikKYGBgCgojIyMjIE9mZmVyIHJlc3BvbnNlIGJ5IHByb21vX2lkCmBgYHtyfQojIG9mZmVyIHJlc3BvbnNlIGJ5IHByb21vX2lkCmRmNCAlPiUgZmlsdGVyKGV2ZW50IT0idHJhbnNhY3Rpb24iKSAlPiUKICBncm91cF9ieShwcm9tb19yZWYsIGV2ZW50KSAlPiUgdGFsbHkoKSAlPiUKICBnZ3Bsb3QoYWVzKHk9ZmN0X3Jldihwcm9tb19yZWYpLCB4PW4sIGNvbG9yPWV2ZW50KSkgKyAKICBnZW9tX2xpbmUoYWVzKGdyb3VwPXByb21vX3JlZiksIGNvbG9yPSJncmV5IikgKyAKICBnZW9tX3BvaW50KHNpemU9Mi41LCBhbHBoYT0wLjkpICsgCiAgdGhlbWUocGFuZWwuZ3JpZC5taW5vcj1lbGVtZW50X2JsYW5rKCkpICsgCiAgbGFicyh5PSJwcm9tb3Rpb24gcmVmZXJlbmNlIiwgeD0iY291bnQiLCBzdWJ0aXRsZT0gIk9mZmVyIHJlc3BvbnNlIikgKyAKICBzY2FsZV9jb2xvcl9tYW51YWwodmFsdWVzPWMoIiMwMjNlOGEiLCIjMmE5ZDhmIiwiI2ZmYjcwMyIpKQpgYGAKCiMjIyMgT2ZmZXIgcmVzcG9uc2UgcGVyY2VudGFnZQoKYGBge3IsIHdhcm5pbmc9Rn0KIyBwZXJjZW50YWdlIApkZjQgJT4lIGZpbHRlcihldmVudCE9InRyYW5zYWN0aW9uIikgJT4lCiAgZ3JvdXBfYnkocHJvbW9fcmVmLCBldmVudCkgJT4lIHRhbGx5KCkgJT4lIHVuZ3JvdXAoKSAlPiUKICBwaXZvdF93aWRlcihuYW1lc19mcm9tPWV2ZW50LCB2YWx1ZXNfZnJvbT1uKSAlPiUgI3Bpdm90IHdpZGVyIHRvIGNhbGN1bGF0ZSBwZXJjCiAgbXV0YXRlKHZpZXdlZF9yZWNpZXZlZCA9IHJvdW5kKGBvZmZlciB2aWV3ZWRgL2BvZmZlciByZWNlaXZlZGAsMyksCiAgICAgICAgIGNvbXBsZXRlZF9yZWNpZXZlZCA9IHJvdW5kKGBvZmZlciBjb21wbGV0ZWRgL2BvZmZlciByZWNlaXZlZGAsMyksCiAgICAgICAgIGNvbXBsZXRlZF92aWV3ZWQgPSByb3VuZChgb2ZmZXIgY29tcGxldGVkYC9gb2ZmZXIgdmlld2VkYCwzKSkgJT4lCiAgc2VsZWN0KC1gb2ZmZXIgY29tcGxldGVkYCwtYG9mZmVyIHJlY2VpdmVkYCwtYG9mZmVyIHZpZXdlZGApICU+JQogIHBpdm90X2xvbmdlcihjb2xzPSFwcm9tb19yZWYpIC0+IGRmNF90YWJsZQoKZGF0YV9saW5lID0gZGY0X3RhYmxlICU+JSBncm91cF9ieShuYW1lKSAlPiUgc3VtbWFyaXNlKG1lYW5feD1tZWFuKHZhbHVlLG5hLnJtPVQpKQpkYXRhX2xpbmUKCmRmNF90YWJsZSAlPiUKICBnZ3Bsb3QoYWVzKHk9ZmN0X3Jldihwcm9tb19yZWYpLCB4PXZhbHVlLCBmaWxsPW5hbWUpKSArIAogIGdlb21fdmxpbmUoZGF0YT1kYXRhX2xpbmUsIGFlcyh4aW50ZXJjZXB0PW1lYW5feCksIGNvbG9yPSIjZmZiNzAzIiwgbGluZXR5cGU9ImRhc2hlZCIpICsKICBnZW9tX2NvbChzaG93LmxlZ2VuZCA9IEYsIHdpZHRoPTAuOCkgKwogIGdlb21fdGV4dChhZXMobGFiZWw9dmFsdWUqMTAwKSwgY29sb3I9IndoaXRlIiwgc2l6ZT0yLjcsIGhqdXN0PTEuNSkgKyAKICBmYWNldF93cmFwKH5mYWN0b3IobmFtZSwgbGV2ZWxzPWMoInZpZXdlZF9yZWNpZXZlZCIsImNvbXBsZXRlZF9yZWNpZXZlZCIsImNvbXBsZXRlZF92aWV3ZWQiLCBvcmRlcmVkPVQpKSwgbmNvbCA9IDMpICsgCiAgc2NhbGVfeF9jb250aW51b3VzKGxhYmVscz1zY2FsZXM6OnBlcmNlbnRfZm9ybWF0KCkpICsgCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPWMoIiMyQjNBNjciLCIjNDk2QTgxIiwiIzY2OTk5QiIpKSArCiAgdGhlbWUoc3RyaXAuYmFja2dyb3VuZCA9ZWxlbWVudF9yZWN0KGZpbGw9TkEpKSArIAogIGxhYnMoeD0icGVyY2VudGFnZSIsIHk9InByb21vdGlvbiByZWZlcmVuY2UiLCBzdWJ0aXRsZT0gIk9mZmVyIHJlc3BvbnNlIHBlcmNlbnRhZ2UiKQpgYGAKCgojIyMjIEV2ZW50IGNvdW50IHBlciBwZXJzb24KCmBgYHtyfQpkZjQgJT4lIHNlbGVjdChwZXJzb24sIGV2ZW50KSAlPiUKICBncm91cF9ieShwZXJzb24pICU+JSBjb3VudChldmVudCkgJT4lIHVuZ3JvdXAoKSAtPnRkZgoKZ2dzdGF0c3Bsb3Q6OmdnYmV0d2VlbnN0YXRzKGRhdGE9dGRmLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHg9ZXZlbnQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB5PW4sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZXNzYWdlcz1GQUxTRSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlc3VsdHMuc3VidGl0bGU9RkFMU0UsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBwYWlyd2lzZS5jb21wYXJpc29ucyA9IEZBTFNFKSAKYGBgCgojIyMgQ2x1c3RlcmluZyAKCmBgYHtyfQojIGNsdXN0ZXJpbmcKIyBkYXRhIHByZXBhcmF0aW9uCmNkZj0gZGY0ICU+JSBzZWxlY3QocGVyc29uLCBldmVudCkgJT4lIAogIGdyb3VwX2J5KHBlcnNvbikgJT4lIGNvdW50KGV2ZW50KSAlPiUgdW5ncm91cCgpICU+JQogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb209ZXZlbnQsIHZhbHVlc19mcm9tPW4pICU+JQogIHJlcGxhY2UoaXMubmEoLiksIDApICU+JSAjcmVwbGFjZSBhbGwgTkFzIHdpdGggMCAKICBzZWxlY3QoLXBlcnNvbikgJT4lCiAgY2xlYW5fbmFtZXMoKQpkaW0oY2RmKQpgYGAKCgoKYGBge3J9CiMgZHJvcCBvdXRsaWVycwpjZGYyID0gY2RmICU+JSAKICAjIGdldCB6c2NvcmUKICBtdXRhdGUoenNjb3JlX29jID0ob2ZmZXJfY29tcGxldGVkLSBtZWFuKG9mZmVyX2NvbXBsZXRlZCkpLyBzZChvZmZlcl9jb21wbGV0ZWQpLAogICAgICAgICB6c2NvcmVfb3IgPShvZmZlcl9yZWNlaXZlZC0gbWVhbihvZmZlcl9yZWNlaXZlZCkpLyBzZChvZmZlcl9yZWNlaXZlZCksCiAgICAgICAgIHpzY29yZV9vdiA9KG9mZmVyX3ZpZXdlZC0gbWVhbihvZmZlcl92aWV3ZWQpKS8gc2Qob2ZmZXJfdmlld2VkKSwKICAgICAgICAgenNjb3JlX3QgPSh0cmFuc2FjdGlvbi0gbWVhbih0cmFuc2FjdGlvbikpLyBzZCh0cmFuc2FjdGlvbikpICU+JQogICMgZHJvcCBvdXRsaWVycwogIGZpbHRlcihiZXR3ZWVuKHpzY29yZV9vYywtMywzKSkgJT4lCiAgZmlsdGVyKGJldHdlZW4oenNjb3JlX29yLC0zLDMpKSAlPiUKICBmaWx0ZXIoYmV0d2Vlbih6c2NvcmVfb3YsLTMsMykpICU+JQogIGZpbHRlcihiZXR3ZWVuKHpzY29yZV90LC0zLDMpKSAlPiUKICAjIHNlbGVjdCB2YXJpYWJsZXMKICBzZWxlY3Qob2ZmZXJfY29tcGxldGVkLCBvZmZlcl9yZWNlaXZlZCwgb2ZmZXJfdmlld2VkLCB0cmFuc2FjdGlvbikKZGltKGNkZjIpCmBgYAoKIyMjIyBDb3JyZWxhdGlvbgpgYGB7ciwgZmlnLndpZHRoPTQuNSwgZmlnLmhlaWdodD0yfQojIGNvcnJlbGF0aW9uCnNldC5zZWVkKDEyMykKYzEgPSBnZ2NvcnJtYXQoZGF0YSA9IGNkZiwKICAgICAgICAgIGNhci52YXJzPWMoIm9mZmVyIGNvbXBsZXRlZCI6InRyYW5zYWN0aW9uIiksCiAgICAgICAgICB0aXRsZT0iQ29ycmVsYXRpb24iKQoKIyBjb3JyZWxhdGlvbiBhZnRlciBkcm9wcGluZyBvdXRsaWVycwpzZXQuc2VlZCgxMjMpCmMyID0gZ2djb3JybWF0KGRhdGEgPSBjZGYyLAogICAgICAgICAgY2FyLnZhcnM9Yygib2ZmZXIgY29tcGxldGVkIjoidHJhbnNhY3Rpb24iKSwKICAgICAgICAgIHRpdGxlPSJDb3JyZWxhdGlvbiBhZnRlciBkcm9wcGluZyBvdXRsaWVycyIpCgojIGNvbWJpbmUgcGxvdApjMSArIGMyCmBgYAoKYGBge3J9CiMgc2NhbGUgZGYgd2l0aG91dCBvdXRsaWVycwpjZGYyX3NjYWxlZCA9IHNjYWxlKGNkZjIpCmhlYWQoY2RmMl9zY2FsZWQpCmBgYAoKIyMjIyBGaW5kIG9wdGltYWwgY2x1c3RlcnMKCiMjIyMjIEVsYm93IG1ldGhvZApgYGB7cn0KIyBlbGJvdyBtZXRob2QKc2V0LnNlZWQoMTIzKQpmdml6X25iY2x1c3QoY2RmMl9zY2FsZWQsa21lYW5zLG1ldGhvZD0id3NzIikgKyBnZ3RpdGxlKCJFbGJvdyBtZXRob2QiKQpgYGAKCiMjIyMgU2lsaG91ZXR0ZSBtZXRob2QKYGBge3J9CmZ2aXpfbmJjbHVzdChjZGYyX3NjYWxlZCwga21lYW5zLCBtZXRob2QgPSAic2lsaG91ZXR0ZSIsIGsubWF4ID0gMjApICsgZ2d0aXRsZSgiU2lsaG91ZXR0ZSBtZXRob2QiKQpgYGAKCgojIyMjIE5CQ2x1c3QgbWV0aG9kCmBgYHtyfQpyZXMubmJjbHVzdCA8LSBOYkNsdXN0KGNkZl9zY2FsZWQsIGRpc3RhbmNlID0gImV1Y2xpZGVhbiIsCiAgICAgICAgICAgICAgICAgIG1pbi5uYyA9IDIsIG1heC5uYyA9IDksIAogICAgICAgICAgICAgICAgICBtZXRob2QgPSAiY29tcGxldGUiLCBpbmRleCA9ImFsbCIpCmZhY3RvZXh0cmE6OmZ2aXpfbmJjbHVzdChyZXMubmJjbHVzdCkgKyBnZ3RpdGxlKCJOYkNsdXN0IG1ldGhvZCIpCmBgYAoKCgojIyMjIEhpZXJhcmNoaWNhbCBjbHVzdGVyaW5nIApgYGB7cn0KIyBoY2x1c3QgCnNldC5zZWVkKDEyMzQpCmgxPSBoY2x1c3QoZGlzdChjZGYyX3NjYWxlZCkpCnBsb3QoaDEpCmBgYAoKYGBge3J9CiNjb2xvciAzIGNsdXN0ZXJzCmRlbmRfb2JqPWFzLmRlbmRyb2dyYW0oaDEpCmRlbmQzPSBjb2xvcl9icmFuY2hlcyhkZW5kX29iaixrPTMpCnBsb3QoZGVuZDMpCmBgYAoKYGBge3J9CiNjb2xvciA0IGNsdXN0ZXJzCmRlbmQ0PSBjb2xvcl9icmFuY2hlcyhkZW5kX29iaixrPTQpCnBsb3QoZGVuZDQpCmBgYAoKCiMjIyMgSy1tZWFucyBjbHVzdGVyaW5nIAoKYGBge3J9CiMgNCBjbHVzdGVycwpzZXQuc2VlZCgxMjMpCmttNCA9IGttZWFucyhjZGYyX3NjYWxlZCwgY2VudGVycz00LCBuc3RhcnQgPTUpCmttNApmdml6X2NsdXN0ZXIoa200LCBkYXRhPWNkZjJfc2NhbGVkLCBsYWJlbHNpemU9MCkgI2NsdXN0ZXIgcGxvdAp3aXRoKGNkZjIscGFpcnMoY2RmMl9zY2FsZWQsY29sPSgxOjQpW2ttNCRjbHVzdGVyXSkpICNwYWlyIHBsb3QKYGBgCgpgYGB7cn0KIyAzIGNsdXN0ZXJzCnNldC5zZWVkKDEyMykKa20zID0ga21lYW5zKGNkZjJfc2NhbGVkLCBjZW50ZXJzPTMsIG5zdGFydCA9NSkKa20zCmZ2aXpfY2x1c3RlcihrbTMsIGRhdGE9Y2RmMl9zY2FsZWQsIGxhYmVsc2l6ZT0wKSAjY2x1c3RlciBwbG90CndpdGgoY2RmMixwYWlycyhjZGYyX3NjYWxlZCxjb2w9KDE6Mylba20zJGNsdXN0ZXJdKSkgI3BhaXIgcGxvdApgYGAKCmstbWVhbnMgMyBjbHVzdGVycyAgIAoqIENsdXN0ZXIgbWVhbnM6ICAgCiAgKyBvZmZlciBjb21wbGV0ZWQ6IGMyID4gYzEgPiBjMyAgIAogICsgb2ZmZXIgcmVjZWl2ZWQ6IGMzID4gYzIgPiBjMSAgIAogICsgb2ZmZXIgdmlld2VkOiBjMyA+IGMyID4gYzEgICAKICArIHRyYW5zYWN0aW9uOiBjMiA+IGMxID4gYzMgICAgCgoqIFN1bW1hcnkgb2YgY2x1c3RlciBtZWFuczogICAKICArIGMyOiBoaWdoZXN0IG9mZmVyX2NvbXBsZXRlZCBhbmQgdHJhbnNhY3Rpb24gKGlkZWFsIGN1c3RvbWVyIGdyb3VwKSAgIAogICsgYzM6IGhpZ2hlc3Qgb2ZmZXJfcmVjZWl2ZWQvb2ZmZXJfdmlld2VkLCBsb3dlc3QgdHJhbnNhY3Rpb24vb2ZmZXJfY29tcGxldGVkICAgCiAgKyBjMTogbG93ZXN0IG9mZmVyX3JlY2VpdmVkL29mZmVyX3ZpZXdlZCAgIAoKCiMjIyMjIFN1bW1hcnkgYnkgY2x1c3RlciBJRCAgKDQgY2x1c3RlcnMpCmBgYHtyfQpjZGYyJGNsdXN0ZXJfaWQgPSBhcy5mYWN0b3Ioa200JGNsdXN0ZXIpCgojIHN1bW1hcnkKYnkoY2RmMixjZGYyJGNsdXN0ZXJfaWQsc3VtbWFyeSkKYGBgCgpgYGB7cn0KIyBwaXZvdCBsb25nZXIKY2RmMl9sb25nID0gY2RmMiAlPiUgcGl2b3RfbG9uZ2VyKCFjbHVzdGVyX2lkKQoKIyBkaXN0cmlidXRpb24gYnkgY2x1c3RlciBpZCAKY2RmMl9sb25nICU+JSBnZ3Bsb3QoYWVzKHg9IGNsdXN0ZXJfaWQsIHk9dmFsdWUsIGNvbG9yPWNsdXN0ZXJfaWQsIGZpbGw9Y2x1c3Rlcl9pZCkpICsKICBnZW9tX2hhbGZfdmlvbGluKHNpZGU9InIiKSArCiAgZ2VvbV9oYWxmX2JveHBsb3QoZmlsbD1OQSwgb3V0bGllci5jb2xvdXIgPSAicmVkIiwgb3V0bGllci5zaGFwZT0yMSwgb3V0bGllci5zaXplID0gMC41KSArIAogIGZhY2V0X3dyYXAofm5hbWUsIG5jb2w9Miwgc2NhbGVzPSJmcmVlIikgKyAKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9YygiIzJCM0E2NyIsIiM2Njk5OUIiLCAiI0IzQUY4RiIsIiNGRkM0ODIiKSkgKyAKICBzY2FsZV9jb2xvcl9tYW51YWwodmFsdWVzPWMoIiMyQjNBNjciLCIjNjY5OTlCIiwgIiNCM0FGOEYiLCIjRkZDNDgyIikpICsgCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiLAogICAgICAgIHBhbmVsLmdyaWQubWlub3IgPSBlbGVtZW50X2JsYW5rKCksCiAgICAgICAgc3RyaXAuYmFja2dyb3VuZD1lbGVtZW50X3JlY3QoZmlsbD0ic2xhdGVncmV5IiksCiAgICAgICAgc3RyaXAudGV4dD1lbGVtZW50X3RleHQoY29sb3I9IndoaXRlIikpCmBgYAoKIyMjIyMgU3VtbWFyeSBieSBjbHVzdGVyIElEICAoMyBjbHVzdGVycykKCmBgYHtyfQpjZGYyJGNsdXN0ZXJfaWQgPSBhcy5mYWN0b3Ioa20zJGNsdXN0ZXIpCgojIHN1bW1hcnkKYnkoY2RmMixjZGYyJGNsdXN0ZXJfaWQsc3VtbWFyeSkKYGBgCgpgYGB7cn0KIyBwaXZvdCBsb25nZXIKY2RmMl9sb25nID0gY2RmMiAlPiUgcGl2b3RfbG9uZ2VyKCFjbHVzdGVyX2lkKQoKIyBkaXN0cmlidXRpb24gYnkgY2x1c3RlciBpZCAKY2RmMl9sb25nICU+JSBnZ3Bsb3QoYWVzKHg9IGNsdXN0ZXJfaWQsIHk9dmFsdWUsIGNvbG9yPWNsdXN0ZXJfaWQsIGZpbGw9Y2x1c3Rlcl9pZCkpICsKICBnZW9tX2hhbGZfdmlvbGluKHNpZGU9InIiKSArCiAgZ2VvbV9oYWxmX2JveHBsb3QoZmlsbD1OQSwgb3V0bGllci5jb2xvdXIgPSAicmVkIiwgb3V0bGllci5zaGFwZT0yMSwgb3V0bGllci5zaXplID0gMC41KSArIAogIGZhY2V0X3dyYXAofm5hbWUsIG5jb2w9Miwgc2NhbGVzPSJmcmVlIikgKyAKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9YygiIzJCM0E2NyIsIiM2Njk5OUIiLCAiI0IzQUY4RiIpKSArIAogIHNjYWxlX2NvbG9yX21hbnVhbCh2YWx1ZXM9YygiIzJCM0E2NyIsIiM2Njk5OUIiLCAiI0IzQUY4RiIpKSArIAogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIiwKICAgICAgICBwYW5lbC5ncmlkLm1pbm9yID0gZWxlbWVudF9ibGFuaygpLAogICAgICAgIHN0cmlwLmJhY2tncm91bmQ9ZWxlbWVudF9yZWN0KGZpbGw9InNsYXRlZ3JleSIpLAogICAgICAgIHN0cmlwLnRleHQ9ZWxlbWVudF90ZXh0KGNvbG9yPSJ3aGl0ZSIpKQpgYGAKVGhlIGNsdXN0ZXJzIGFyZSBtb3JlIHVuaXF1ZSBpbiAzLWdyb3VwcyB0aGFuIDQtZ3JvdXBzLCBlc3BlY2lhbGx5IGluIG9mZmVyX3JlY2VpdmVkIGFuZCB0cmFuc2FjdGlvbiB2YXJpYWJsZXMuIEhlbmNlLCBpdCBpcyBtb3JlIHN1aXRhYmxlIHRvIGNsdXN0ZXIgY3VzdG9tZXJzIGludG8gMyBncm91cHMgdGhhbiA0IGdyb3Vwcy4gCgoKCgoKCgoKCgoK