Motivation

Synthetic data refers to artificially generated information that mimics real-world data in structure, distribution, and relationships but does not directly derive from actual observations. In this lesson, we present a worked example of engineering a synthetic dataset on restaurant visits and sales transactions. Specifically, we use a mix of statistical methods, rule-based data generation, made more realistic by incorporating noise and missing data.

The number of transactions generated as well as many parameters for the rules and statistical methods are configurable.

Data Engineering

To create synthetic data for restaurant visits, a CSV file should include attributes that capture details about the visit. The structure can reflect real-world scenarios, ensuring it is versatile and relevant for analysis. In this example, we will generate a synthetic tabular dataset (CSV) for use for database design, data warehouse design, data analytics, and regression modeling.

Suggested CSV Structure for Restaurant Visits

  1. VisitID: A unique identifier for each restaurant visit (e.g., numeric or UUID).
  2. CustomerID: A unique identifier for the customer (useful for tracking repeat visitors).
  3. VisitDate: The date of the visit in YYYY-MM-DD format.
  4. VisitTime: The time of the visit in HH:MM format.
  5. PartySize: Number of people in the visiting party (integer).
  6. MealType: Type of meal (e.g., “Breakfast,” “Lunch,” “Dinner”).
  7. FoodBill: Total bill amount in USD (decimal).
  8. TipAmount: Amount tipped in USD (decimal).
  9. PaymentMethod: Payment method used (e.g., “Credit Card,” “Cash,” “Mobile Payment”).
  10. WaitTime: Time spent waiting for a table in minutes (integer).
  11. ServerID: Identifier for the server handling the table.
  12. DiscountApplied: Any discounts applied as a percent.
  13. LoyaltyMember: Boolean indicating if the customer is a loyalty program member.

Here is what an example row will look like:

goes here

This structure ensures the dataset is rich enough for various types of analyses, including customer behavior, meal preferences, peak hours, and revenue forecasting.

Data Generation

We will use a rules-based approach to generating the data, augmented with statistical methods and random noise.

Common Parameters

The code below configures common parameters used later during the generation of the synthetic data.

The code below defines vectors of meal types, accepted payment methods, and genders for customers. Additional values can be added to the lists and they will be considered in the code that generates the data as values are drawn from those lists at random. Note that other values depend on those values drawn, e.g., the base amount for the amount spent by each person in a party is dependent on the type of meal and is set in perPersonCheck, so if a meal type is added to the vector mealTypes, then a corresponding value for the average price of that meal and its standard deviation must be added as well to perPersonCheck.

mealTypes <- c("Breakfast","Lunch","Dinner","Take-Out")
payTypes <- c("Credit Card", "Cash", "Mobile Payment")
genders <- c("f","m","n","u")
genderProbs <- c(0.4,0.4,0.05,0.15)

perPersonCheck <- data.frame(
  mean = c(8, 12, 22, 19),
  sd = c(2, 4, 2.5, 2.1)
)

partySizeProbs <- c(0.38,0.3,0.09,0.13,0.07,0.03)

tippingScale <- data.frame (
  amount = c(0.15,0.18,0.2,0.22,0.25,0.3),
  prob = c(0.4,0.2,0.2,0.05,0.1,0.05)              
)
numTxns <- 10                              ## number of transactions/restaurant visits
#numTxns <- 139874                          ## number of transactions/restaurant visits

probCustomerKnown <- 0.3                   ## probability that customer is known and is in
                                           ## loyalty program

percentVisitTimesToRemove <- 0.06          ## percentage of visits that should not 
                                           ## have a known visit time, i.e., missing value

avgWaitTime <- 8                           ## average wait time (in minutes) for a table
sdWaitTime <- 4.5                          ## standard deviation for wait time assuming
                                           ## wait times are normally distributed

minHourlyWage <- 10.25                     ## minimum hourly wage for servers
hourlyWageIncrease <- 1.25                 ## hourly wage increases

hireBirthYearMin <- 1960                   ## birth years for servers
hireBirthYearMax <- 2007                        

firstHireYear <- 2018                      ## first year for hire date
lastHireYear <- 2024                       ## also range of dates for visits

curYear <- substr(Sys.Date(), 0, 4)        ## current year
avgCostPerDrink <- 8.25

Dataframe for Result

The dataframe df below is the shell for the synthetically generated data. Additional columns are added as they are generated.

df <- data.frame(VisitID = 1:numTxns)

Server Names and Personal Information

This section generates personal information about servers. The server names are read from a CSV containing artificial names generated using generatedata.com. The code below generates starting and end hire dates; some percentage of servers has no hiring end date which means that they still work as servers. Birth dates are generated as well, along with tax id or social security numbers. Based on the time a server is employed, an hourly wage is generated based on a starting minimum hourly wage from the parameters set above. The hourly wage is increased every six months by the hourlyWageIncrease parameter.

## load server names from generated CSV
server.names <- read.csv("synth-server-names.csv")

num.Servers <- nrow(server.names)

## dataframe for server information
servers.df <- data.frame(
  EmpID = sample(1008:3389, num.Servers, replace = F),
  ServerName = server.names$x
)

## generate a hire date after the first year parameter
## add a leading 0 to months < 10 and days < 10, so date is 2023-05-09 instead of 2023-5-9
days <- sample(1:12, num.Servers, replace = T)
days <- paste0(ifelse(days < 10, "0", ""), days)
months <- sample(1:28, num.Servers, replace = T) 
months <- paste0(ifelse(months < 10, "0", ""), months)
servers.df$StartDateHired <- paste0("",
                               sample(firstHireYear:lastHireYear, num.Servers, replace = T), "-",
                               days, "-", months)

## generate a termination date assuming that 40% of servers still work with us
for (y in 1:num.Servers) {
  startDate <- as.Date(servers.df$StartDateHired[y])
  
  if (runif(1) < 0.4) {
    # still working with restaurant; no termination date
    servers.df$EndDateHired[y] <- ""
  } else {
    servers.df$EndDateHired[y] <- (function(s) {
      daysElapsed <- Sys.Date() - s
      daysInFuture <- sample(2:(daysElapsed-2), 1)
      return(paste0("", s + daysInFuture))
    }) (startDate)
  }
}

## if there's no end date of employment, use today's date to calculate hourly rate
hireEndDates <- ifelse(servers.df$EndDateHired == "", 
                                  paste0("",Sys.Date()), 
                                  servers.df$EndDateHired)

## assign hourly rate based on number of months worked
servers.df$HourlyRate <- round((minHourlyWage + 
                                  as.integer(((as.Date(hireEndDates) - 
                                                 as.Date(servers.df$StartDateHired))) / 
                                               (6*30))* hourlyWageIncrease), 2)

## generate birthdate
servers.df$BirthDate <- paste0("",
                               sample(1:12, num.Servers, replace = T), "/",
                               sample(1:28, num.Servers, replace = T), "/",
                               sample(hireBirthYearMin:hireBirthYearMax, num.Servers, replace = T))

## generate artificial social security number
servers.df$SSN <- paste0("",
                         sample(100:999, num.Servers, replace = T), "-",
                         sample(10:99, num.Servers, replace = T), "-",
                         sample(1111:9899, num.Servers, replace = T))

Data Doping

To make the data more realistic, we will remove several values for the columns hiring start and end dates, birth dates, and social security numbers. In addition, for two of the servers, we will add a middle initial to their name.

## remove some random values to create missing values
servers.df$BirthDate[sample(1:num.Servers, 4)] <- ""
servers.df$SSN[sample(1:num.Servers, 6)] <- ""

## for two of the names, add a middle initial at the end
z <- sample(1:num.Servers, 2)
servers.df$ServerName[z] <- paste0(servers.df$ServerName[z], " K.")
head(servers.df)
##   EmpID         ServerName StartDateHired EndDateHired HourlyRate  BirthDate
## 1  3279       Smith, Emily     2021-02-27   2022-05-29      12.75  6/24/1990
## 2  3200   Johnson, Michael     2022-11-09   2023-07-28      11.50  2/28/1987
## 3  1102      Brown, Olivia     2018-02-01                   26.50 10/14/1965
## 4  2385     Garcia, Daniel     2018-01-02   2018-07-26      11.50   7/6/1990
## 5  1837   Martinez, Sophia     2019-04-22                   24.00  6/27/1961
## 6  1544 Davis, Christopher     2024-10-26                   10.25  2/20/1981
##           SSN
## 1 266-84-9166
## 2 278-89-9665
## 3 800-39-4198
## 4            
## 5 491-25-4420
## 6 127-58-7150

Restaurant Information

Names of restaurants are in a CSV with names generated by ChatGPT 4o. The idea is that the restaurants are all part of a holding company that operates several restaurants, so servers can move between restaurants.

restaurants.df <- read.csv("restaurants.csv")

head(restaurants.df)
##      RestaurantName hasServers
## 1      Burger Haven        yes
## 2      Patty Palace        yes
## 3    Grill & Thrill        yes
## 4  The Burger Joint        yes
## 5 Stacked & Sizzled        yes
## 6        Bite & Bun        yes

Assign Servers to Restaurants

Assign the servers to a restaurant at random if the restaurant has service.

r <- sample(1:nrow(restaurants.df), nrow(servers.df), replace = T)

servers.df$Restaurant <- ifelse(restaurants.df$hasServers[r] == "yes", 
                                restaurants.df$RestaurantName[r], "")

To make it more realistic, we will now assign a few servers to more than one restaurant. Some will work at both restaurants while some will work at two restaurants. We are using servers from the first restaurant to work also at the second restaurant but at a higher hourly rate.

s <- sample(which(servers.df$Restaurant == restaurants.df$RestaurantName[1]), 3, replace = F)

servers.df <- rbind(servers.df,servers.df[s,])
servers.df$Restaurant[nrow(servers.df)-2] <- restaurants.df$RestaurantName[2]
servers.df$Restaurant[nrow(servers.df)-1] <- restaurants.df$RestaurantName[2]
servers.df$Restaurant[nrow(servers.df)] <- restaurants.df$RestaurantName[3]

servers.df$HourlyRate[nrow(servers.df)-1] <- servers.df$HourlyRate[nrow(servers.df)-1] + 1.45
servers.df$HourlyRate[nrow(servers.df)] <- servers.df$HourlyRate[nrow(servers.df)] + 2.10

servers.df$EndDateHired[nrow(servers.df)-1] <- ""

Visit Transactions

Assign Restaurant and Server

Assign a restaurant for the visit using a probability vector from the parameters and choose a server for that restaurant if the restaurant is a “sit-down” restaurant and has table service. Add all of the available server information. Choose different default values for missing information when a restaurant doesn’t have service.

for (t in 1:numTxns) {
  ## assign a random restaurant assuming a visit to any restaurant is equally likely
  r <- sample(1:nrow(restaurants.df), 1)
  df$Restaurant[t] <- restaurants.df$RestaurantName[r]

  ## assign a random server from that restaurant
  ## add all server information to the row
  if (restaurants.df$hasServers[r] == "no") {
    ## restaurant has no servers
    df$ServerEmpID[t] <- ""
    df$ServerName[t] <- "N/A"
    df$StartDateHired[t] <- "0000-00-00"
    df$EndDateHired[t] <- "9999-99-99"
    df$HourlyRate[t] <- 0
    df$ServerBirthDate[t] <- ""
    df$ServerTIN[t] <- ""
  } else {
    # choose a random server at that restaurant
    e <- sample(which(servers.df$Restaurant == df$Restaurant[t]), 1)
    df$ServerEmpID[t] <- servers.df$EmpID[e]
    df$ServerName[t] <- servers.df$ServerName[e]
    df$StartDateHired[t] <- servers.df$StartDateHired[e]
    df$EndDateHired[t] <- servers.df$EndDateHired[e]
    df$HourlyRate[t] <- servers.df$HourlyRate[e]
    df$ServerBirthDate[t] <- servers.df$BirthDate[e]
    df$ServerTIN[t] <- servers.df$SSN[e]
  }
}
Assign a Date for the Visit

Assign a random date for the visit that falls within the time that the server assigned to that visit works at that restaurant. A server can work at different times at more than one restaurant. Assign a random date for a restaurant without servers (where the end date is “9999-99-99”).

for (t in 1:numTxns) {
  if (df$EndDateHired[t] == "9999-99-99") {
    ## no servers at the restaurant, so pick a date before today
    month <- sample(1:12,1)
    if (month < 10) month <- paste0("0", month)
    df$VisitDate[t] <- paste0("20", sample(19:24, 1), "-",
                              month, "-",
                              sample(10:28, 1))
  } else {
    ## find the server's hire start and end assigned to that visit
    theServer.start <- as.Date(df$StartDateHired[t])
  
    theServer.end <- as.Date(ifelse(df$EndDateHired[t] == "", Sys.Date(), df$EndDateHired[t]))
    
    ## calculate the days between then and now; if there's no end date, use "today"
    numDays <- as.integer(theServer.end - theServer.start)
    
    ## pick random date (hack is necessary to coerce date to a string)
    df$VisitDate[t] <- paste0("", theServer.start + sample(0:numDays, 1))
  }
}
Assign a Visit Time

Assigning a visit time first requires determining a meal type, so that the time is during the common meal time for that type of meal.

genVisitTime <- function (mealType)
{
  minute  <- sample(1:59,1)
  hour <- sample(5:23,1)
  
  if (mealType == "Breakfast") hour <- sample(5:11,1)
  if (mealType == "Lunch") hour <- sample(11:15,1)
  if (mealType == "Dinner") hour <- sample(16:20,1)
  
  if (hour < 10) hour <- paste0("0",hour)
  
  if (minute < 10) minute <- paste0("0",minute)
  
  return (paste0(hour,":", minute))
}

for (t in 1:numTxns) {
  mealType <- mealTypes[sample(1:length(mealTypes),1)]
  
  ## add the visit time
  df$VisitTime[t] <- genVisitTime(mealType)
  
  ## add the meal type
  df$MealType[t] <- mealType
}
Data Doping

We’ll introduce some “noise” by removing some visit times so that it can be used as an example for data imputation.

numVisitToRemove <- nrow(df) * percentVisitTimesToRemove

df$VisitTime[sample(1:nrow(df), numVisitToRemove)] <- ""
Generate Party Size, Genders, and Wait Time

Generate a random size of the party, i.e., how many guests are eating in. For “take-out” this is the number of meals ordered. Add some random wait times for some customers but only at times when it is likely to be busy and it is not “take-out”. The party sizes are based on probabilities as not all sizes of parties are equally likely. The wait time is assumed to be drawn from a normal distribution with the mean being avgWaitTime and the standard deviation being adjusted for the meal type. Since the random numbers drawn could be negative, an adjustment is made to ensure they are positive.

The genders for each member of the party of randomly selected from the genders parameter vector based on a configurable probability distribution in genderProbs.

genWaitTime <- function (mealType, partySize, visitTime)
{
  ## more likely to wait at dinner
  ## more wait time for parties > 4
  ## average wait time configurable
  
  t <- 1
  hour <- ifelse(visitTime == "",
                 0,
                 as.integer(substring(visitTime, 0, 2)))
  
  if (mealType == "Dinner" & partySize > 4) {
    t <- rnorm(1, mean = avgWaitTime, sd = 4) + 10
  }
  
  if (mealType == "Dinner") {
    t <- rnorm(1, mean = avgWaitTime, sd = 4)
  }
  
  if (mealType == "Lunch") {
    t <- rnorm(1, mean = avgWaitTime, sd = 3) - 1
  }
  
  if ((hour < 7) | (hour == 10) | (hour >= 13 & hour < 18) | (hour >= 20)) 
    t <- 0
  
  return (t)
}

Now, let’s assign a random party size (based on an empirical probability distribution making larger parties a lot less likely), random genders, and a random wait time (based on meal type and time of day).

for (t in 1:numTxns) {
  ## random party size
  df$PartySize[t] <- sample(1:length(partySizeProbs), 1, prob = partySizeProbs)
  
  ## random party genders
  memGenders <- ""
  for (m in 1:df$PartySize[t]) {
    ## assign a random gender to each party member
    memGenders <- paste0(memGenders, 
                         genders[sample(1:length(genders), 1, prob = genderProbs)])
  }

  ## add a vector
  df$Genders[t] <- memGenders

  ## add a random wait time based on party size, time of day, and whether or not
  ## take-out or sit-down restaurant
  df$WaitTime[t] <- round(genWaitTime(df$MealType[t], df$PartySize[t], df$VisitTime[t]),0)
}
Data Doping

We’ll introduce some “noise” by making some party sizes an “outlier” or “nonsensical” value so that it can be used as an example for bad data detection and used for data imputation, perhaps by predicting them from the other features and the total bill amount. We’ll use the value of 99 to indicate a missing party size – this, of course, is poor data quality management, but, unfortunately, quite common.

numPartySizesToRemove <- 8

df$PartySize[sample(1:nrow(df), numPartySizesToRemove)] <- 99
Generate Customer Information

In this section, we will generate information about the party visiting the restaurant, i.e., the “customer”. A customer ID, name, phone, and email were synthetically generated using generatedata.com for about 2500 customers.

customers.df <- read.csv("customers.csv")

The function below, select a random customer, although for some configurable percentage of customers no customer info is generated and thus is left blank.

## generate a customer info for some customers randomly
genCustomerInfo <- function ()
{
  if (runif(1) > probCustomerKnown) {
    # no customer info
    return (NULL)
  } else {
    # pick a random customer from the list of customers
    set.seed(80763+(runif(1)*500))
    custInfo <- customers.df[sample(1:nrow(customers.df),1),]
    return (custInfo)
  }
}

Next, we’ll add the customer information to the visit. If a customer was selected, then we assume that this means that they are a member of the restaurant chain’s loyalty program. Of course, the same customer can visit the same restaurant or different restaurants multiple times.

for (t in 1:numTxns) {
  ## choose a random customer or no information  
  cust <- genCustomerInfo()
  
  ## add to dataframe if information was selected
  if (!is.null(cust)) {
    df$CustomerName[t] <- cust$name
    df$CustomerPhone[t] <- cust$phone
    df$CustomerEmail[t] <- cust$email
    df$LoyaltyMember[t] <- TRUE
  } else {
    df$CustomerName[t] <- ""
    df$CustomerPhone[t] <- ""
    df$CustomerEmail[t] <- ""
    df$LoyaltyMember[t] <- FALSE
  }
}
Generate Bill, Tip, Discount, and Payment Type

Next, we’ll generate the total amount for the bill. It is a random value based on the meal type and the size of the party.

The cost of a meal (per person) is based on parameters for the meal type and is then selected randomly from a normal distribution. The code below selects costs repeatedly until the cost is more than $0.90.

genFoodBill <- function (mealType, partySize)
{
  repeat {
    c <- rnorm(1,
               mean = perPersonCheck$mean[which(mealTypes == mealType)],
               sd = perPersonCheck$sd[which(mealTypes == mealType)])
    
    if (c > 0.9)
      break
  }
  
  ## multiply the average meal cost by size of party
  ## if the size of party is 99, then it means it is missing
  ## and we'll generate a random party size
  if (partySize == 99)
    partySize <- sample(1:length(partySizeProbs), 1, prob = partySizeProbs)
  
  return(round(c * partySize, 2))
}

The tip is a random value between 15% and 25% – this is a bit unrealistic as many customers would also round up to a whole dollar. The tip also varies based on meal type and party size.

genTipAmount <- function (mt, ps, gn)
{
  tipAmount <- 0.15        # standard tip is 15%
  
  tipAmount <- ifelse(mt == "Take-Out", 0,
                  sample(tippingScale$amount, 1,
                         prob = tippingScale$prob))
  
  return(tipAmount)
}

If a customer is part of the loyalty program, we’ll generate an discount for them that is either 10% or 15%. The probability that the discount is 10% is 80%, 15% otherwise.

genDiscount <- function (flag)
{
  discount <- 0
  
  ## discount only for non-loyalty program members
  if (flag == TRUE)
    discount <- ifelse(runif(1) < 0.8, 0.1, 0.15) 
  
  return(discount)
}
for (t in 1:numTxns) 
{
  ## generate total amount for bill based on meal type and party size
  df$FoodBill[t] <- genFoodBill(df$MealType[t], df$PartySize[t])
  
  ## add a random tip amount if it is not "take-out"
  df$TipAmount[t] <- genTipAmount(df$MealType[t], df$PartySize[t])
  
  ## apply discount if in loyalty program
  df$DiscountApplied[t] <- genDiscount(df$LoyaltyMember[t])
  
  ## add a random payment type
  df$PaymentMethod[t] <- payTypes[sample(1:length(payTypes), 1)]
}
Add Alcohol

Randomly add flag whether party ordered alcohol (if not take-out) and then add a surcharge to the party and recalculate bill total and tip.

The function encodes the following “drinking rules”:

  • Never alcohol if breakfast or take-out.
  • Single person for dinner has probability of drinking alcohol of 90% or more.
  • Single person for lunch has probability of drinking alcohol of 40% or more.
  • If party of 2 or more for dinner then probability of drinking is 60% or more.
  • A same-sex party of 2 or more for dinner has probability of drinking of 80% or greater.
genAlcoholOrdered <- function (ps, mt, gn)
{
  ## no alcohol of take-out or breakfast
  if (mt == "Take-Out" || mt == "Breakfast") {
    return ("no")
  }
  
  if ((mt == "Dinner") && (ps == 1) && (runif(1) < 0.9)) {
    ## single person for dinner
    return ("yes")
  }
  
  if ((mt == "Lunch") && (ps == 1) && (runif(1) < 0.4)) {
    ## single person for lunch
    return ("yes")
  }
  
  if ((mt == "Dinner") && (ps == 2) && (gn == "mm" || gn == "ff") && (runif(1) < 0.8)) {
    ## party of two of same-sex for dinner
    return ("yes")
  }
  
  if ((mt == "Dinner") && (ps > 1) && (runif(1) < 0.6)) {
    ## large party for dinner
    return ("yes")
  }
  
  if ((mt == "lunch") && (runif(1) < 0.1)) {
    return ("yes")
  }
  
  ## otherwise default is no drinking
  return ("no")
}

The amount spent on alcohol should depend on the meal type, the number of people in the party, the average cost per drink, and the genders. The implementation below is a first pass at this.

genAlcoholBill <- function (mt, ps, gn)
{
  ## alcohol bill is number of people in the party
  ## multiplied by average cost per drink by random number of drinks
  ## for percentage of party who will drink
  if (ps == 99)
    ps <- sample(1:length(partySizeProbs), 1, prob = partySizeProbs)
  
  alcoholBill <- ps * avgCostPerDrink * 1.25
  
  return (alcoholBill)
}
for (t in 1:numTxns) 
{
  ## randomly add alcohol
  df$orderedAlcohol[t] <- genAlcoholOrdered(df$PartySize[t],
                                            df$MealType[t],
                                            df$Genders[t])
  
  ## if alcohol was consumed, then up the bill by a random amount
  ## based on a random number of people drinking
  if (df$orderedAlcohol[t] == "yes") {
    ## add a separate amount for alcohol
    df$AlcoholBill[t] <- genAlcoholBill(df$MealType[t], 
                                        df$PartySize[t],
                                        df$Genders[t])
    
    ## adjust tip amount based on new bill
    df$TipAmount[t] <- genTipAmount(df$MealType[t], 
                                    df$PartySize[t],
                                    df$Genders[t])
  } else {
    df$AlcoholBill[t] <- 0
  }
}
Final Look at Data
head(df,10)
##    VisitID       Restaurant ServerEmpID           ServerName StartDateHired
## 1        1 Big Bite Burgers        1154     Thomas, Benjamin     2019-11-22
## 2        2      Flame Shack                              N/A     0000-00-00
## 3        3      Flame Shack                              N/A     0000-00-00
## 4        4 Big Bite Burgers        1154     Thomas, Benjamin     2019-11-22
## 5        5 Big Bite Burgers        1057 Hernandez, Charlotte     2018-02-19
## 6        6 The Burger Joint        2050        Walker, Grace     2018-06-18
## 7        7     Burger Haven        1151       Turner, Andrew     2022-07-22
## 8        8           Bun Fi                              N/A     0000-00-00
## 9        9 The Burger Joint        2050        Walker, Grace     2018-06-18
## 10      10     Burger Haven        2221            Lee, Liam     2023-05-26
##    EndDateHired HourlyRate ServerBirthDate   ServerTIN  VisitDate VisitTime
## 1                    22.75       3/16/1970             2020-11-03     19:08
## 2    9999-99-99       0.00                             2020-07-14     12:20
## 3    9999-99-99       0.00                             2024-06-26     13:02
## 4                    22.75       3/16/1970             2020-08-02     08:04
## 5                    26.50       1/12/1977 896-81-4376 2023-05-30     18:39
## 6                    26.50       12/7/2006 912-95-4514 2019-02-13     06:43
## 7    2023-03-07      11.50                 635-99-5777 2022-10-15     14:48
## 8    9999-99-99       0.00                             2024-08-27     14:31
## 9                    26.50       12/7/2006 912-95-4514 2022-10-26     14:36
## 10   2023-06-17      10.25        9/1/1966 979-83-2646 2023-06-03     09:08
##     MealType PartySize Genders WaitTime     CustomerName  CustomerPhone
## 1     Dinner        99       f        7                                
## 2   Take-Out        99    fmfn        1                                
## 3      Lunch        99   ummmf        0                                
## 4  Breakfast         1       f        1                                
## 5     Dinner         1       m       12                                
## 6  Breakfast        99      um        0                                
## 7      Lunch        99      uf        0                                
## 8      Lunch        99      um        0 Aphrodite Watson (570) 651-7873
## 9      Lunch        99  mffmff        0                                
## 10 Breakfast        99       u        1                                
##         CustomerEmail LoyaltyMember FoodBill TipAmount DiscountApplied
## 1                             FALSE    20.14      0.18             0.0
## 2                             FALSE    78.52      0.00             0.0
## 3                             FALSE    15.38      0.20             0.0
## 4                             FALSE    10.60      0.15             0.0
## 5                             FALSE    21.78      0.20             0.0
## 6                             FALSE     4.62      0.15             0.0
## 7                             FALSE     9.12      0.20             0.0
## 8  lobortis@yahoo.org          TRUE    70.09      0.15             0.1
## 9                             FALSE    26.77      0.22             0.0
## 10                            FALSE    52.85      0.18             0.0
##     PaymentMethod orderedAlcohol AlcoholBill
## 1  Mobile Payment            yes     10.3125
## 2            Cash             no      0.0000
## 3     Credit Card             no      0.0000
## 4  Mobile Payment             no      0.0000
## 5  Mobile Payment            yes     10.3125
## 6     Credit Card             no      0.0000
## 7     Credit Card             no      0.0000
## 8     Credit Card             no      0.0000
## 9     Credit Card             no      0.0000
## 10 Mobile Payment             no      0.0000

Generate Data Files

Generate CSV
write.csv(df, 
          file = "restaurant-visits.csv",
          row.names = F, 
          quote = T)
Generate XML
Preamble
xml.fn <- "RestaurantTxns.xml"

xml <- '<?xml version="1.0" encoding="UTF-8"?>\n\n'
xml <- paste0(xml, '<dataset>', '\n')
Meta Data
## add meta data: source, author, link to source (or program that generated data), date
## features with definitions, intended use, CC4

xml <- paste0(xml, '<meta-data>', '\n')
xml <- paste0(xml, '  <date-generated>', Sys.Date(), '</date-generated>', '\n')
xml <- paste0(xml, '  <author>', '\n')
xml <- paste0(xml, '    <name>','Martin Schedlbauer, PhD', '</name>', '\n')
xml <- paste0(xml, '    <contact>','m.schedlbauer@northeastern.edu', '</contact>', '\n')
xml <- paste0(xml, '  </author>', '\n')
xml <- paste0(xml, '  <source>','Synthetic', '</source>', '\n')
xml <- paste0(xml, '  <description>', '\n')
xml <- paste0(xml, '      A data set containing visits to a restaurant that is part of a chain\n')
xml <- paste0(xml, '      of restaurants.')
xml <- paste0(xml, '  </description>', '\n')
xml <- paste0(xml, '  <use-cases>', '\n')
xml <- paste0(xml, '     <use-case>machine learning</use-case>\n')
xml <- paste0(xml, '     <use-case>data analytics</use-case>\n')
xml <- paste0(xml, '     <use-case>missing values, outlier, and extreme value handling</use-case>\n')
xml <- paste0(xml, '     <use-case>normalization with functional dependencies</use-case>\n')
xml <- paste0(xml, '     <use-case>database import</use-case>\n')
xml <- paste0(xml, '     <use-case>CSV queries with SQL through sqldf</use-case>\n')
xml <- paste0(xml, '     <use-case>XML queries using XPath</use-case>\n')
xml <- paste0(xml, '  </use-cases>', '\n')
xml <- paste0(xml, '  <fields>', '\n')
xml <- paste0(xml, 
              '    <field name="VisitID" type="numeric">',
              'Unique ID for the visit.',
              '    </field>', '\n')
xml <- paste0(xml, '    <field name="BirthDate" type="date">')
xml <- paste0(xml, 'Birth date of server/employee. Blank if not known.')
xml <- paste0(xml, '    </field>', '\n')
xml <- paste0(xml, '  </fields>', '\n')
xml <- paste0(xml, '</meta-data>', '\n')
Data
xml <- paste0(xml, '<txns>', '\n')

for (r in 1:nrow(df))
{
  xml <- paste0(xml, '  <visit>', '\n')
  xml <- paste0(xml, '  </visit>', '\n')
}

xml <- paste0(xml, '</txns>', '\n')
Write XML File
## add end tag for document
xml <- paste0(xml, '</dataset>')

conn <- file(xml.fn)
writeLines(xml, conn)

Validation

df.validation <- read.csv("restaurant-visits.csv",
                          header = T,
                          stringsAsFactors = F)

print(paste0(nrow(df.validation), " rows read vs ",
             numTxns, " rows written"))
## [1] "10 rows read vs 10 rows written"
print(paste0("Num columns read: ", ncol(df.validation)))
## [1] "Num columns read: 25"
head(df.validation)
##   VisitID       Restaurant ServerEmpID           ServerName StartDateHired
## 1       1 Big Bite Burgers        1154     Thomas, Benjamin     2019-11-22
## 2       2      Flame Shack          NA                  N/A     0000-00-00
## 3       3      Flame Shack          NA                  N/A     0000-00-00
## 4       4 Big Bite Burgers        1154     Thomas, Benjamin     2019-11-22
## 5       5 Big Bite Burgers        1057 Hernandez, Charlotte     2018-02-19
## 6       6 The Burger Joint        2050        Walker, Grace     2018-06-18
##   EndDateHired HourlyRate ServerBirthDate   ServerTIN  VisitDate VisitTime
## 1                   22.75       3/16/1970             2020-11-03     19:08
## 2   9999-99-99       0.00                             2020-07-14     12:20
## 3   9999-99-99       0.00                             2024-06-26     13:02
## 4                   22.75       3/16/1970             2020-08-02     08:04
## 5                   26.50       1/12/1977 896-81-4376 2023-05-30     18:39
## 6                   26.50       12/7/2006 912-95-4514 2019-02-13     06:43
##    MealType PartySize Genders WaitTime CustomerName CustomerPhone CustomerEmail
## 1    Dinner        99       f        7                                         
## 2  Take-Out        99    fmfn        1                                         
## 3     Lunch        99   ummmf        0                                         
## 4 Breakfast         1       f        1                                         
## 5    Dinner         1       m       12                                         
## 6 Breakfast        99      um        0                                         
##   LoyaltyMember FoodBill TipAmount DiscountApplied  PaymentMethod
## 1         FALSE    20.14      0.18               0 Mobile Payment
## 2         FALSE    78.52      0.00               0           Cash
## 3         FALSE    15.38      0.20               0    Credit Card
## 4         FALSE    10.60      0.15               0 Mobile Payment
## 5         FALSE    21.78      0.20               0 Mobile Payment
## 6         FALSE     4.62      0.15               0    Credit Card
##   orderedAlcohol AlcoholBill
## 1            yes     10.3125
## 2             no      0.0000
## 3             no      0.0000
## 4             no      0.0000
## 5            yes     10.3125
## 6             no      0.0000

Summary

Synthetic data is artificially generated information that mimics real-world data in structure and characteristics. In this lesson, we presented a worked example showing some of the technique for synthetic data generation. It is essential to keep in mind that synthetic data has limitations, including reduced realism, risks of overfitting, validation challenges, and potential regulatory issues.


Files & Resources

All Files for Lesson 3.981

References

Granville, V. (2024). Synthetic Data and Generative AI. Elsevier.

Singh, J. (2021). The Rise of Synthetic Data: Enhancing AI and Machine Learning Model Training to Address Data Scarcity and Mitigate Privacy Risks. Journal of Artificial Intelligence Research and Applications, 1(2), 292-332.

Hansen, L., Seedat, N., van der Schaar, M., & Petrovic, A. (2023). Reimagining synthetic tabular data generation through data-centric AI: A comprehensive benchmark. Advances in Neural Information Processing Systems, 36, 33781-33823.

Errata

None collected yet. Let us know.

LS0tCnRpdGxlOiAiU3ludGhldGljIEVuZ2luZWVyaW5nIG9mIGEgRGF0YXNldCBvbiBSZXN0YXVyYW50IFZpc2l0cyIKcGFyYW1zOgogIGNhdGVnb3J5OiAzCiAgc3RhY2tzOiAwCiAgbnVtYmVyOiA5ODEKICB0aW1lOiA2MAogIGxldmVsOiBiZWdpbm5lcgogIHRhZ3M6IG1hY2hpbmUgbGVhcm5pbmcsc3ludGhldGljIGRhdGEsZGF0YSBlbmdpbmVlcmluZwogIGRlc2NyaXB0aW9uOiAiUHJlc2VudHMgYSB3b3JrZWQgZXhhbXBsZSBvZiBlbmdpbmVlcmluZyBhIHN5bnRoZXRpYyBkYXRhCiAgICAgICAgICAgICAgICBzZXQgb2YgcmVzdGF1cmFudCB2aXNpdHMsIHNlcnZlcnMsIGNvbnN1bXB0aW9uLCBhbmQKICAgICAgICAgICAgICAgIGN1c3RvbWVycy4gQXBwcm9wcmlhdGUgZm9yIHJlZ3Jlc3Npb24sIGRhdGEgYW5hbHl0aWNzLCBhbmQgCiAgICAgICAgICAgICAgICBkYXRhIG1pbmluZy4gQ29udGFpbnMgY29uZmlndXJhYmxlIHBhcmFtZXRlcnMuIgpkYXRlOiAiPHNtYWxsPmByIFN5cy5EYXRlKClgPC9zbWFsbD4iCmF1dGhvcjogIjxzbWFsbD5NYXJ0aW4gU2NoZWRsYmF1ZXI8L3NtYWxsPiIKZW1haWw6ICJtLnNjaGVkbGJhdWVyQG5ldS5lZHUiCmFmZmlsaXRhdGlvbjogIk5vcnRoZWFzdGVybiBVbml2ZXJzaXR5IgpvdXRwdXQ6IAogIGJvb2tkb3duOjpodG1sX2RvY3VtZW50MjoKICAgIHRvYzogdHJ1ZQogICAgdG9jX2Zsb2F0OiB0cnVlCiAgICBjb2xsYXBzZWQ6IGZhbHNlCiAgICBudW1iZXJfc2VjdGlvbnM6IGZhbHNlCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlCiAgICB0aGVtZTogc3BhY2VsYWIKICAgIGhpZ2hsaWdodDogdGFuZ28KLS0tCgotLS0KdGl0bGU6ICI8c21hbGw+YHIgcGFyYW1zJGNhdGVnb3J5YC5gciBwYXJhbXMkbnVtYmVyYDwvc21hbGw+PGJyLz48c3BhbiBzdHlsZT0nY29sb3I6ICMyRTQwNTM7IGZvbnQtc2l6ZTogMC45ZW0nPmByIHJtYXJrZG93bjo6bWV0YWRhdGEkdGl0bGVgPC9zcGFuPiIKLS0tCgpgYGB7ciBjb2RlPXhmdW46OnJlYWRfdXRmOChwYXN0ZTAoaGVyZTo6aGVyZSgpLCcvUi9faW5zZXJ0MkRCLlInKSksIGluY2x1ZGUgPSBGQUxTRX0KYGBgCgojIyBNb3RpdmF0aW9uCgpTeW50aGV0aWMgZGF0YSByZWZlcnMgdG8gYXJ0aWZpY2lhbGx5IGdlbmVyYXRlZCBpbmZvcm1hdGlvbiB0aGF0IG1pbWljcyByZWFsLXdvcmxkIGRhdGEgaW4gc3RydWN0dXJlLCBkaXN0cmlidXRpb24sIGFuZCByZWxhdGlvbnNoaXBzIGJ1dCBkb2VzIG5vdCBkaXJlY3RseSBkZXJpdmUgZnJvbSBhY3R1YWwgb2JzZXJ2YXRpb25zLiBJbiB0aGlzIGxlc3Nvbiwgd2UgcHJlc2VudCBhIHdvcmtlZCBleGFtcGxlIG9mIGVuZ2luZWVyaW5nIGEgc3ludGhldGljIGRhdGFzZXQgb24gcmVzdGF1cmFudCB2aXNpdHMgYW5kIHNhbGVzIHRyYW5zYWN0aW9ucy4gU3BlY2lmaWNhbGx5LCB3ZSB1c2UgYSBtaXggb2Ygc3RhdGlzdGljYWwgbWV0aG9kcywgcnVsZS1iYXNlZCBkYXRhIGdlbmVyYXRpb24sIG1hZGUgbW9yZSByZWFsaXN0aWMgYnkgaW5jb3Jwb3JhdGluZyBub2lzZSBhbmQgbWlzc2luZyBkYXRhLgoKVGhlIG51bWJlciBvZiB0cmFuc2FjdGlvbnMgZ2VuZXJhdGVkIGFzIHdlbGwgYXMgbWFueSBwYXJhbWV0ZXJzIGZvciB0aGUgcnVsZXMgYW5kIHN0YXRpc3RpY2FsIG1ldGhvZHMgYXJlIGNvbmZpZ3VyYWJsZS4KCiMjIERhdGEgRW5naW5lZXJpbmcKClRvIGNyZWF0ZSBzeW50aGV0aWMgZGF0YSBmb3IgcmVzdGF1cmFudCB2aXNpdHMsIGEgQ1NWIGZpbGUgc2hvdWxkIGluY2x1ZGUgYXR0cmlidXRlcyB0aGF0IGNhcHR1cmUgZGV0YWlscyBhYm91dCB0aGUgdmlzaXQuIFRoZSBzdHJ1Y3R1cmUgY2FuIHJlZmxlY3QgcmVhbC13b3JsZCBzY2VuYXJpb3MsIGVuc3VyaW5nIGl0IGlzIHZlcnNhdGlsZSBhbmQgcmVsZXZhbnQgZm9yIGFuYWx5c2lzLiBJbiB0aGlzIGV4YW1wbGUsIHdlIHdpbGwgZ2VuZXJhdGUgYSBzeW50aGV0aWMgdGFidWxhciBkYXRhc2V0IChDU1YpIGZvciB1c2UgZm9yIGRhdGFiYXNlIGRlc2lnbiwgZGF0YSB3YXJlaG91c2UgZGVzaWduLCBkYXRhIGFuYWx5dGljcywgYW5kIHJlZ3Jlc3Npb24gbW9kZWxpbmcuCgojIyMgU3VnZ2VzdGVkIENTViBTdHJ1Y3R1cmUgZm9yIFJlc3RhdXJhbnQgVmlzaXRzCgoxLiAgKipWaXNpdElEKio6IEEgdW5pcXVlIGlkZW50aWZpZXIgZm9yIGVhY2ggcmVzdGF1cmFudCB2aXNpdCAoZS5nLiwgbnVtZXJpYyBvciBVVUlEKS4KMi4gICoqQ3VzdG9tZXJJRCoqOiBBIHVuaXF1ZSBpZGVudGlmaWVyIGZvciB0aGUgY3VzdG9tZXIgKHVzZWZ1bCBmb3IgdHJhY2tpbmcgcmVwZWF0IHZpc2l0b3JzKS4KMy4gICoqVmlzaXREYXRlKio6IFRoZSBkYXRlIG9mIHRoZSB2aXNpdCBpbiBgWVlZWS1NTS1ERGAgZm9ybWF0Lgo0LiAgKipWaXNpdFRpbWUqKjogVGhlIHRpbWUgb2YgdGhlIHZpc2l0IGluIGBISDpNTWAgZm9ybWF0Lgo1LiAgKipQYXJ0eVNpemUqKjogTnVtYmVyIG9mIHBlb3BsZSBpbiB0aGUgdmlzaXRpbmcgcGFydHkgKGludGVnZXIpLgo2LiAgKipNZWFsVHlwZSoqOiBUeXBlIG9mIG1lYWwgKCplLmcuKiwgIkJyZWFrZmFzdCwiICJMdW5jaCwiICJEaW5uZXIiKS4KNy4gICoqRm9vZEJpbGwqKjogVG90YWwgYmlsbCBhbW91bnQgaW4gVVNEIChkZWNpbWFsKS4KOC4gICoqVGlwQW1vdW50Kio6IEFtb3VudCB0aXBwZWQgaW4gVVNEIChkZWNpbWFsKS4KOS4gICoqUGF5bWVudE1ldGhvZCoqOiBQYXltZW50IG1ldGhvZCB1c2VkICgqZS5nLiosICJDcmVkaXQgQ2FyZCwiICJDYXNoLCIgIk1vYmlsZSBQYXltZW50IikuCjEwLiAqKldhaXRUaW1lKio6IFRpbWUgc3BlbnQgd2FpdGluZyBmb3IgYSB0YWJsZSBpbiBtaW51dGVzIChpbnRlZ2VyKS4KMTEuICoqU2VydmVySUQqKjogSWRlbnRpZmllciBmb3IgdGhlIHNlcnZlciBoYW5kbGluZyB0aGUgdGFibGUuCjEyLiAqKkRpc2NvdW50QXBwbGllZCoqOiBBbnkgZGlzY291bnRzIGFwcGxpZWQgYXMgYSBwZXJjZW50LgoxMy4gKipMb3lhbHR5TWVtYmVyKio6IEJvb2xlYW4gaW5kaWNhdGluZyBpZiB0aGUgY3VzdG9tZXIgaXMgYSBsb3lhbHR5IHByb2dyYW0gbWVtYmVyLgoKSGVyZSBpcyB3aGF0IGFuIGV4YW1wbGUgcm93IHdpbGwgbG9vayBsaWtlOgoKYGBgICAgICAgICAgCmdvZXMgaGVyZQpgYGAKClRoaXMgc3RydWN0dXJlIGVuc3VyZXMgdGhlIGRhdGFzZXQgaXMgcmljaCBlbm91Z2ggZm9yIHZhcmlvdXMgdHlwZXMgb2YgYW5hbHlzZXMsIGluY2x1ZGluZyBjdXN0b21lciBiZWhhdmlvciwgbWVhbCBwcmVmZXJlbmNlcywgcGVhayBob3VycywgYW5kIHJldmVudWUgZm9yZWNhc3RpbmcuCgojIyMgRGF0YSBHZW5lcmF0aW9uCgpXZSB3aWxsIHVzZSBhIHJ1bGVzLWJhc2VkIGFwcHJvYWNoIHRvIGdlbmVyYXRpbmcgdGhlIGRhdGEsIGF1Z21lbnRlZCB3aXRoIHN0YXRpc3RpY2FsIG1ldGhvZHMgYW5kIHJhbmRvbSBub2lzZS4KCiMjIyMgQ29tbW9uIFBhcmFtZXRlcnMKClRoZSBjb2RlIGJlbG93IGNvbmZpZ3VyZXMgY29tbW9uIHBhcmFtZXRlcnMgdXNlZCBsYXRlciBkdXJpbmcgdGhlIGdlbmVyYXRpb24gb2YgdGhlIHN5bnRoZXRpYyBkYXRhLgoKVGhlIGNvZGUgYmVsb3cgZGVmaW5lcyB2ZWN0b3JzIG9mIG1lYWwgdHlwZXMsIGFjY2VwdGVkIHBheW1lbnQgbWV0aG9kcywgYW5kIGdlbmRlcnMgZm9yIGN1c3RvbWVycy4gQWRkaXRpb25hbCB2YWx1ZXMgY2FuIGJlIGFkZGVkIHRvIHRoZSBsaXN0cyBhbmQgdGhleSB3aWxsIGJlIGNvbnNpZGVyZWQgaW4gdGhlIGNvZGUgdGhhdCBnZW5lcmF0ZXMgdGhlIGRhdGEgYXMgdmFsdWVzIGFyZSBkcmF3biBmcm9tIHRob3NlIGxpc3RzIGF0IHJhbmRvbS4gTm90ZSB0aGF0IG90aGVyIHZhbHVlcyBkZXBlbmQgb24gdGhvc2UgdmFsdWVzIGRyYXduLCAqZS5nLiosIHRoZSBiYXNlIGFtb3VudCBmb3IgdGhlIGFtb3VudCBzcGVudCBieSBlYWNoIHBlcnNvbiBpbiBhIHBhcnR5IGlzIGRlcGVuZGVudCBvbiB0aGUgdHlwZSBvZiBtZWFsIGFuZCBpcyBzZXQgaW4gYHBlclBlcnNvbkNoZWNrYCwgc28gaWYgYSBtZWFsIHR5cGUgaXMgYWRkZWQgdG8gdGhlIHZlY3RvciBgbWVhbFR5cGVzYCwgdGhlbiBhIGNvcnJlc3BvbmRpbmcgdmFsdWUgZm9yIHRoZSBhdmVyYWdlIHByaWNlIG9mIHRoYXQgbWVhbCBhbmQgaXRzIHN0YW5kYXJkIGRldmlhdGlvbiBtdXN0IGJlIGFkZGVkIGFzIHdlbGwgdG8gYHBlclBlcnNvbkNoZWNrYC4KCmBgYHtyfQptZWFsVHlwZXMgPC0gYygiQnJlYWtmYXN0IiwiTHVuY2giLCJEaW5uZXIiLCJUYWtlLU91dCIpCnBheVR5cGVzIDwtIGMoIkNyZWRpdCBDYXJkIiwgIkNhc2giLCAiTW9iaWxlIFBheW1lbnQiKQpnZW5kZXJzIDwtIGMoImYiLCJtIiwibiIsInUiKQpnZW5kZXJQcm9icyA8LSBjKDAuNCwwLjQsMC4wNSwwLjE1KQoKcGVyUGVyc29uQ2hlY2sgPC0gZGF0YS5mcmFtZSgKICBtZWFuID0gYyg4LCAxMiwgMjIsIDE5KSwKICBzZCA9IGMoMiwgNCwgMi41LCAyLjEpCikKCnBhcnR5U2l6ZVByb2JzIDwtIGMoMC4zOCwwLjMsMC4wOSwwLjEzLDAuMDcsMC4wMykKCnRpcHBpbmdTY2FsZSA8LSBkYXRhLmZyYW1lICgKICBhbW91bnQgPSBjKDAuMTUsMC4xOCwwLjIsMC4yMiwwLjI1LDAuMyksCiAgcHJvYiA9IGMoMC40LDAuMiwwLjIsMC4wNSwwLjEsMC4wNSkgICAgICAgICAgICAgIAopCmBgYAoKYGBge3J9Cm51bVR4bnMgPC0gMTAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIyBudW1iZXIgb2YgdHJhbnNhY3Rpb25zL3Jlc3RhdXJhbnQgdmlzaXRzCiNudW1UeG5zIDwtIDEzOTg3NCAgICAgICAgICAgICAgICAgICAgICAgICAgIyMgbnVtYmVyIG9mIHRyYW5zYWN0aW9ucy9yZXN0YXVyYW50IHZpc2l0cwoKcHJvYkN1c3RvbWVyS25vd24gPC0gMC4zICAgICAgICAgICAgICAgICAgICMjIHByb2JhYmlsaXR5IHRoYXQgY3VzdG9tZXIgaXMga25vd24gYW5kIGlzIGluCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIyBsb3lhbHR5IHByb2dyYW0KCnBlcmNlbnRWaXNpdFRpbWVzVG9SZW1vdmUgPC0gMC4wNiAgICAgICAgICAjIyBwZXJjZW50YWdlIG9mIHZpc2l0cyB0aGF0IHNob3VsZCBub3QgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIyBoYXZlIGEga25vd24gdmlzaXQgdGltZSwgaS5lLiwgbWlzc2luZyB2YWx1ZQoKYXZnV2FpdFRpbWUgPC0gOCAgICAgICAgICAgICAgICAgICAgICAgICAgICMjIGF2ZXJhZ2Ugd2FpdCB0aW1lIChpbiBtaW51dGVzKSBmb3IgYSB0YWJsZQpzZFdhaXRUaW1lIDwtIDQuNSAgICAgICAgICAgICAgICAgICAgICAgICAgIyMgc3RhbmRhcmQgZGV2aWF0aW9uIGZvciB3YWl0IHRpbWUgYXNzdW1pbmcKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMjIHdhaXQgdGltZXMgYXJlIG5vcm1hbGx5IGRpc3RyaWJ1dGVkCgptaW5Ib3VybHlXYWdlIDwtIDEwLjI1ICAgICAgICAgICAgICAgICAgICAgIyMgbWluaW11bSBob3VybHkgd2FnZSBmb3Igc2VydmVycwpob3VybHlXYWdlSW5jcmVhc2UgPC0gMS4yNSAgICAgICAgICAgICAgICAgIyMgaG91cmx5IHdhZ2UgaW5jcmVhc2VzCgpoaXJlQmlydGhZZWFyTWluIDwtIDE5NjAgICAgICAgICAgICAgICAgICAgIyMgYmlydGggeWVhcnMgZm9yIHNlcnZlcnMKaGlyZUJpcnRoWWVhck1heCA8LSAyMDA3ICAgICAgICAgICAgICAgICAgICAgICAgCgpmaXJzdEhpcmVZZWFyIDwtIDIwMTggICAgICAgICAgICAgICAgICAgICAgIyMgZmlyc3QgeWVhciBmb3IgaGlyZSBkYXRlCmxhc3RIaXJlWWVhciA8LSAyMDI0ICAgICAgICAgICAgICAgICAgICAgICAjIyBhbHNvIHJhbmdlIG9mIGRhdGVzIGZvciB2aXNpdHMKCmN1clllYXIgPC0gc3Vic3RyKFN5cy5EYXRlKCksIDAsIDQpICAgICAgICAjIyBjdXJyZW50IHllYXIKYGBgCgpgYGB7cn0KYXZnQ29zdFBlckRyaW5rIDwtIDguMjUKYGBgCgojIyMjIERhdGFmcmFtZSBmb3IgUmVzdWx0CgpUaGUgZGF0YWZyYW1lIGBkZmAgYmVsb3cgaXMgdGhlIHNoZWxsIGZvciB0aGUgc3ludGhldGljYWxseSBnZW5lcmF0ZWQgZGF0YS4gQWRkaXRpb25hbCBjb2x1bW5zIGFyZSBhZGRlZCBhcyB0aGV5IGFyZSBnZW5lcmF0ZWQuCgpgYGB7cn0KZGYgPC0gZGF0YS5mcmFtZShWaXNpdElEID0gMTpudW1UeG5zKQpgYGAKCiMjIyMgU2VydmVyIE5hbWVzIGFuZCBQZXJzb25hbCBJbmZvcm1hdGlvbgoKVGhpcyBzZWN0aW9uIGdlbmVyYXRlcyBwZXJzb25hbCBpbmZvcm1hdGlvbiBhYm91dCBzZXJ2ZXJzLiBUaGUgc2VydmVyIG5hbWVzIGFyZSByZWFkIGZyb20gYSBDU1YgY29udGFpbmluZyBhcnRpZmljaWFsIG5hbWVzIGdlbmVyYXRlZCB1c2luZyBbZ2VuZXJhdGVkYXRhLmNvbV0oaHR0cDovL2dlbmVyYXRlZGF0YS5jb20pLiBUaGUgY29kZSBiZWxvdyBnZW5lcmF0ZXMgc3RhcnRpbmcgYW5kIGVuZCBoaXJlIGRhdGVzOyBzb21lIHBlcmNlbnRhZ2Ugb2Ygc2VydmVycyBoYXMgbm8gaGlyaW5nIGVuZCBkYXRlIHdoaWNoIG1lYW5zIHRoYXQgdGhleSBzdGlsbCB3b3JrIGFzIHNlcnZlcnMuIEJpcnRoIGRhdGVzIGFyZSBnZW5lcmF0ZWQgYXMgd2VsbCwgYWxvbmcgd2l0aCB0YXggaWQgb3Igc29jaWFsIHNlY3VyaXR5IG51bWJlcnMuIEJhc2VkIG9uIHRoZSB0aW1lIGEgc2VydmVyIGlzIGVtcGxveWVkLCBhbiBob3VybHkgd2FnZSBpcyBnZW5lcmF0ZWQgYmFzZWQgb24gYSBzdGFydGluZyBtaW5pbXVtIGhvdXJseSB3YWdlIGZyb20gdGhlIHBhcmFtZXRlcnMgc2V0IGFib3ZlLiBUaGUgaG91cmx5IHdhZ2UgaXMgaW5jcmVhc2VkIGV2ZXJ5IHNpeCBtb250aHMgYnkgdGhlIGBob3VybHlXYWdlSW5jcmVhc2VgIHBhcmFtZXRlci4KCmBgYHtyfQojIyBsb2FkIHNlcnZlciBuYW1lcyBmcm9tIGdlbmVyYXRlZCBDU1YKc2VydmVyLm5hbWVzIDwtIHJlYWQuY3N2KCJzeW50aC1zZXJ2ZXItbmFtZXMuY3N2IikKCm51bS5TZXJ2ZXJzIDwtIG5yb3coc2VydmVyLm5hbWVzKQoKIyMgZGF0YWZyYW1lIGZvciBzZXJ2ZXIgaW5mb3JtYXRpb24Kc2VydmVycy5kZiA8LSBkYXRhLmZyYW1lKAogIEVtcElEID0gc2FtcGxlKDEwMDg6MzM4OSwgbnVtLlNlcnZlcnMsIHJlcGxhY2UgPSBGKSwKICBTZXJ2ZXJOYW1lID0gc2VydmVyLm5hbWVzJHgKKQoKIyMgZ2VuZXJhdGUgYSBoaXJlIGRhdGUgYWZ0ZXIgdGhlIGZpcnN0IHllYXIgcGFyYW1ldGVyCiMjIGFkZCBhIGxlYWRpbmcgMCB0byBtb250aHMgPCAxMCBhbmQgZGF5cyA8IDEwLCBzbyBkYXRlIGlzIDIwMjMtMDUtMDkgaW5zdGVhZCBvZiAyMDIzLTUtOQpkYXlzIDwtIHNhbXBsZSgxOjEyLCBudW0uU2VydmVycywgcmVwbGFjZSA9IFQpCmRheXMgPC0gcGFzdGUwKGlmZWxzZShkYXlzIDwgMTAsICIwIiwgIiIpLCBkYXlzKQptb250aHMgPC0gc2FtcGxlKDE6MjgsIG51bS5TZXJ2ZXJzLCByZXBsYWNlID0gVCkgCm1vbnRocyA8LSBwYXN0ZTAoaWZlbHNlKG1vbnRocyA8IDEwLCAiMCIsICIiKSwgbW9udGhzKQpzZXJ2ZXJzLmRmJFN0YXJ0RGF0ZUhpcmVkIDwtIHBhc3RlMCgiIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNhbXBsZShmaXJzdEhpcmVZZWFyOmxhc3RIaXJlWWVhciwgbnVtLlNlcnZlcnMsIHJlcGxhY2UgPSBUKSwgIi0iLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZGF5cywgIi0iLCBtb250aHMpCgojIyBnZW5lcmF0ZSBhIHRlcm1pbmF0aW9uIGRhdGUgYXNzdW1pbmcgdGhhdCA0MCUgb2Ygc2VydmVycyBzdGlsbCB3b3JrIHdpdGggdXMKZm9yICh5IGluIDE6bnVtLlNlcnZlcnMpIHsKICBzdGFydERhdGUgPC0gYXMuRGF0ZShzZXJ2ZXJzLmRmJFN0YXJ0RGF0ZUhpcmVkW3ldKQogIAogIGlmIChydW5pZigxKSA8IDAuNCkgewogICAgIyBzdGlsbCB3b3JraW5nIHdpdGggcmVzdGF1cmFudDsgbm8gdGVybWluYXRpb24gZGF0ZQogICAgc2VydmVycy5kZiRFbmREYXRlSGlyZWRbeV0gPC0gIiIKICB9IGVsc2UgewogICAgc2VydmVycy5kZiRFbmREYXRlSGlyZWRbeV0gPC0gKGZ1bmN0aW9uKHMpIHsKICAgICAgZGF5c0VsYXBzZWQgPC0gU3lzLkRhdGUoKSAtIHMKICAgICAgZGF5c0luRnV0dXJlIDwtIHNhbXBsZSgyOihkYXlzRWxhcHNlZC0yKSwgMSkKICAgICAgcmV0dXJuKHBhc3RlMCgiIiwgcyArIGRheXNJbkZ1dHVyZSkpCiAgICB9KSAoc3RhcnREYXRlKQogIH0KfQoKIyMgaWYgdGhlcmUncyBubyBlbmQgZGF0ZSBvZiBlbXBsb3ltZW50LCB1c2UgdG9kYXkncyBkYXRlIHRvIGNhbGN1bGF0ZSBob3VybHkgcmF0ZQpoaXJlRW5kRGF0ZXMgPC0gaWZlbHNlKHNlcnZlcnMuZGYkRW5kRGF0ZUhpcmVkID09ICIiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhc3RlMCgiIixTeXMuRGF0ZSgpKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZXJ2ZXJzLmRmJEVuZERhdGVIaXJlZCkKCiMjIGFzc2lnbiBob3VybHkgcmF0ZSBiYXNlZCBvbiBudW1iZXIgb2YgbW9udGhzIHdvcmtlZApzZXJ2ZXJzLmRmJEhvdXJseVJhdGUgPC0gcm91bmQoKG1pbkhvdXJseVdhZ2UgKyAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFzLmludGVnZXIoKChhcy5EYXRlKGhpcmVFbmREYXRlcykgLSAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFzLkRhdGUoc2VydmVycy5kZiRTdGFydERhdGVIaXJlZCkpKSAvIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICg2KjMwKSkqIGhvdXJseVdhZ2VJbmNyZWFzZSksIDIpCgojIyBnZW5lcmF0ZSBiaXJ0aGRhdGUKc2VydmVycy5kZiRCaXJ0aERhdGUgPC0gcGFzdGUwKCIiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2FtcGxlKDE6MTIsIG51bS5TZXJ2ZXJzLCByZXBsYWNlID0gVCksICIvIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNhbXBsZSgxOjI4LCBudW0uU2VydmVycywgcmVwbGFjZSA9IFQpLCAiLyIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzYW1wbGUoaGlyZUJpcnRoWWVhck1pbjpoaXJlQmlydGhZZWFyTWF4LCBudW0uU2VydmVycywgcmVwbGFjZSA9IFQpKQoKIyMgZ2VuZXJhdGUgYXJ0aWZpY2lhbCBzb2NpYWwgc2VjdXJpdHkgbnVtYmVyCnNlcnZlcnMuZGYkU1NOIDwtIHBhc3RlMCgiIiwKICAgICAgICAgICAgICAgICAgICAgICAgIHNhbXBsZSgxMDA6OTk5LCBudW0uU2VydmVycywgcmVwbGFjZSA9IFQpLCAiLSIsCiAgICAgICAgICAgICAgICAgICAgICAgICBzYW1wbGUoMTA6OTksIG51bS5TZXJ2ZXJzLCByZXBsYWNlID0gVCksICItIiwKICAgICAgICAgICAgICAgICAgICAgICAgIHNhbXBsZSgxMTExOjk4OTksIG51bS5TZXJ2ZXJzLCByZXBsYWNlID0gVCkpCmBgYAoKIyMjIyBEYXRhIERvcGluZwoKVG8gbWFrZSB0aGUgZGF0YSBtb3JlIHJlYWxpc3RpYywgd2Ugd2lsbCByZW1vdmUgc2V2ZXJhbCB2YWx1ZXMgZm9yIHRoZSBjb2x1bW5zIGhpcmluZyBzdGFydCBhbmQgZW5kIGRhdGVzLCBiaXJ0aCBkYXRlcywgYW5kIHNvY2lhbCBzZWN1cml0eSBudW1iZXJzLiBJbiBhZGRpdGlvbiwgZm9yIHR3byBvZiB0aGUgc2VydmVycywgd2Ugd2lsbCBhZGQgYSBtaWRkbGUgaW5pdGlhbCB0byB0aGVpciBuYW1lLgoKYGBge3J9CiMjIHJlbW92ZSBzb21lIHJhbmRvbSB2YWx1ZXMgdG8gY3JlYXRlIG1pc3NpbmcgdmFsdWVzCnNlcnZlcnMuZGYkQmlydGhEYXRlW3NhbXBsZSgxOm51bS5TZXJ2ZXJzLCA0KV0gPC0gIiIKc2VydmVycy5kZiRTU05bc2FtcGxlKDE6bnVtLlNlcnZlcnMsIDYpXSA8LSAiIgoKIyMgZm9yIHR3byBvZiB0aGUgbmFtZXMsIGFkZCBhIG1pZGRsZSBpbml0aWFsIGF0IHRoZSBlbmQKeiA8LSBzYW1wbGUoMTpudW0uU2VydmVycywgMikKc2VydmVycy5kZiRTZXJ2ZXJOYW1lW3pdIDwtIHBhc3RlMChzZXJ2ZXJzLmRmJFNlcnZlck5hbWVbel0sICIgSy4iKQpgYGAKCmBgYHtyfQpoZWFkKHNlcnZlcnMuZGYpCmBgYAoKIyMjIyBSZXN0YXVyYW50IEluZm9ybWF0aW9uCgpOYW1lcyBvZiByZXN0YXVyYW50cyBhcmUgaW4gYSBDU1Ygd2l0aCBuYW1lcyBnZW5lcmF0ZWQgYnkgW0NoYXRHUFQgNG9dKGh0dHA6Ly9jaGF0Z3B0LmNvbSkuIFRoZSBpZGVhIGlzIHRoYXQgdGhlIHJlc3RhdXJhbnRzIGFyZSBhbGwgcGFydCBvZiBhIGhvbGRpbmcgY29tcGFueSB0aGF0IG9wZXJhdGVzIHNldmVyYWwgcmVzdGF1cmFudHMsIHNvIHNlcnZlcnMgY2FuIG1vdmUgYmV0d2VlbiByZXN0YXVyYW50cy4KCmBgYHtyfQpyZXN0YXVyYW50cy5kZiA8LSByZWFkLmNzdigicmVzdGF1cmFudHMuY3N2IikKCmhlYWQocmVzdGF1cmFudHMuZGYpCmBgYAoKIyMjIyBBc3NpZ24gU2VydmVycyB0byBSZXN0YXVyYW50cwoKQXNzaWduIHRoZSBzZXJ2ZXJzIHRvIGEgcmVzdGF1cmFudCBhdCByYW5kb20gaWYgdGhlIHJlc3RhdXJhbnQgaGFzIHNlcnZpY2UuCgpgYGB7cn0KciA8LSBzYW1wbGUoMTpucm93KHJlc3RhdXJhbnRzLmRmKSwgbnJvdyhzZXJ2ZXJzLmRmKSwgcmVwbGFjZSA9IFQpCgpzZXJ2ZXJzLmRmJFJlc3RhdXJhbnQgPC0gaWZlbHNlKHJlc3RhdXJhbnRzLmRmJGhhc1NlcnZlcnNbcl0gPT0gInllcyIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlc3RhdXJhbnRzLmRmJFJlc3RhdXJhbnROYW1lW3JdLCAiIikKYGBgCgpUbyBtYWtlIGl0IG1vcmUgcmVhbGlzdGljLCB3ZSB3aWxsIG5vdyBhc3NpZ24gYSBmZXcgc2VydmVycyB0byBtb3JlIHRoYW4gb25lIHJlc3RhdXJhbnQuIFNvbWUgd2lsbCB3b3JrIGF0IGJvdGggcmVzdGF1cmFudHMgd2hpbGUgc29tZSB3aWxsIHdvcmsgYXQgdHdvIHJlc3RhdXJhbnRzLiBXZSBhcmUgdXNpbmcgc2VydmVycyBmcm9tIHRoZSBmaXJzdCByZXN0YXVyYW50IHRvIHdvcmsgYWxzbyBhdCB0aGUgc2Vjb25kIHJlc3RhdXJhbnQgYnV0IGF0IGEgaGlnaGVyIGhvdXJseSByYXRlLgoKYGBge3J9CnMgPC0gc2FtcGxlKHdoaWNoKHNlcnZlcnMuZGYkUmVzdGF1cmFudCA9PSByZXN0YXVyYW50cy5kZiRSZXN0YXVyYW50TmFtZVsxXSksIDMsIHJlcGxhY2UgPSBGKQoKc2VydmVycy5kZiA8LSByYmluZChzZXJ2ZXJzLmRmLHNlcnZlcnMuZGZbcyxdKQpzZXJ2ZXJzLmRmJFJlc3RhdXJhbnRbbnJvdyhzZXJ2ZXJzLmRmKS0yXSA8LSByZXN0YXVyYW50cy5kZiRSZXN0YXVyYW50TmFtZVsyXQpzZXJ2ZXJzLmRmJFJlc3RhdXJhbnRbbnJvdyhzZXJ2ZXJzLmRmKS0xXSA8LSByZXN0YXVyYW50cy5kZiRSZXN0YXVyYW50TmFtZVsyXQpzZXJ2ZXJzLmRmJFJlc3RhdXJhbnRbbnJvdyhzZXJ2ZXJzLmRmKV0gPC0gcmVzdGF1cmFudHMuZGYkUmVzdGF1cmFudE5hbWVbM10KCnNlcnZlcnMuZGYkSG91cmx5UmF0ZVtucm93KHNlcnZlcnMuZGYpLTFdIDwtIHNlcnZlcnMuZGYkSG91cmx5UmF0ZVtucm93KHNlcnZlcnMuZGYpLTFdICsgMS40NQpzZXJ2ZXJzLmRmJEhvdXJseVJhdGVbbnJvdyhzZXJ2ZXJzLmRmKV0gPC0gc2VydmVycy5kZiRIb3VybHlSYXRlW25yb3coc2VydmVycy5kZildICsgMi4xMAoKc2VydmVycy5kZiRFbmREYXRlSGlyZWRbbnJvdyhzZXJ2ZXJzLmRmKS0xXSA8LSAiIgpgYGAKCiMjIyMgVmlzaXQgVHJhbnNhY3Rpb25zCgojIyMjIyBBc3NpZ24gUmVzdGF1cmFudCBhbmQgU2VydmVyCgpBc3NpZ24gYSByZXN0YXVyYW50IGZvciB0aGUgdmlzaXQgdXNpbmcgYSBwcm9iYWJpbGl0eSB2ZWN0b3IgZnJvbSB0aGUgcGFyYW1ldGVycyBhbmQgY2hvb3NlIGEgc2VydmVyIGZvciB0aGF0IHJlc3RhdXJhbnQgaWYgdGhlIHJlc3RhdXJhbnQgaXMgYSAic2l0LWRvd24iIHJlc3RhdXJhbnQgYW5kIGhhcyB0YWJsZSBzZXJ2aWNlLiBBZGQgYWxsIG9mIHRoZSBhdmFpbGFibGUgc2VydmVyIGluZm9ybWF0aW9uLiBDaG9vc2UgZGlmZmVyZW50IGRlZmF1bHQgdmFsdWVzIGZvciBtaXNzaW5nIGluZm9ybWF0aW9uIHdoZW4gYSByZXN0YXVyYW50IGRvZXNuJ3QgaGF2ZSBzZXJ2aWNlLgoKYGBge3J9CmZvciAodCBpbiAxOm51bVR4bnMpIHsKICAjIyBhc3NpZ24gYSByYW5kb20gcmVzdGF1cmFudCBhc3N1bWluZyBhIHZpc2l0IHRvIGFueSByZXN0YXVyYW50IGlzIGVxdWFsbHkgbGlrZWx5CiAgciA8LSBzYW1wbGUoMTpucm93KHJlc3RhdXJhbnRzLmRmKSwgMSkKICBkZiRSZXN0YXVyYW50W3RdIDwtIHJlc3RhdXJhbnRzLmRmJFJlc3RhdXJhbnROYW1lW3JdCgogICMjIGFzc2lnbiBhIHJhbmRvbSBzZXJ2ZXIgZnJvbSB0aGF0IHJlc3RhdXJhbnQKICAjIyBhZGQgYWxsIHNlcnZlciBpbmZvcm1hdGlvbiB0byB0aGUgcm93CiAgaWYgKHJlc3RhdXJhbnRzLmRmJGhhc1NlcnZlcnNbcl0gPT0gIm5vIikgewogICAgIyMgcmVzdGF1cmFudCBoYXMgbm8gc2VydmVycwogICAgZGYkU2VydmVyRW1wSURbdF0gPC0gIiIKICAgIGRmJFNlcnZlck5hbWVbdF0gPC0gIk4vQSIKICAgIGRmJFN0YXJ0RGF0ZUhpcmVkW3RdIDwtICIwMDAwLTAwLTAwIgogICAgZGYkRW5kRGF0ZUhpcmVkW3RdIDwtICI5OTk5LTk5LTk5IgogICAgZGYkSG91cmx5UmF0ZVt0XSA8LSAwCiAgICBkZiRTZXJ2ZXJCaXJ0aERhdGVbdF0gPC0gIiIKICAgIGRmJFNlcnZlclRJTlt0XSA8LSAiIgogIH0gZWxzZSB7CiAgICAjIGNob29zZSBhIHJhbmRvbSBzZXJ2ZXIgYXQgdGhhdCByZXN0YXVyYW50CiAgICBlIDwtIHNhbXBsZSh3aGljaChzZXJ2ZXJzLmRmJFJlc3RhdXJhbnQgPT0gZGYkUmVzdGF1cmFudFt0XSksIDEpCiAgICBkZiRTZXJ2ZXJFbXBJRFt0XSA8LSBzZXJ2ZXJzLmRmJEVtcElEW2VdCiAgICBkZiRTZXJ2ZXJOYW1lW3RdIDwtIHNlcnZlcnMuZGYkU2VydmVyTmFtZVtlXQogICAgZGYkU3RhcnREYXRlSGlyZWRbdF0gPC0gc2VydmVycy5kZiRTdGFydERhdGVIaXJlZFtlXQogICAgZGYkRW5kRGF0ZUhpcmVkW3RdIDwtIHNlcnZlcnMuZGYkRW5kRGF0ZUhpcmVkW2VdCiAgICBkZiRIb3VybHlSYXRlW3RdIDwtIHNlcnZlcnMuZGYkSG91cmx5UmF0ZVtlXQogICAgZGYkU2VydmVyQmlydGhEYXRlW3RdIDwtIHNlcnZlcnMuZGYkQmlydGhEYXRlW2VdCiAgICBkZiRTZXJ2ZXJUSU5bdF0gPC0gc2VydmVycy5kZiRTU05bZV0KICB9Cn0KYGBgCgojIyMjIyBBc3NpZ24gYSBEYXRlIGZvciB0aGUgVmlzaXQKCkFzc2lnbiBhIHJhbmRvbSBkYXRlIGZvciB0aGUgdmlzaXQgdGhhdCBmYWxscyB3aXRoaW4gdGhlIHRpbWUgdGhhdCB0aGUgc2VydmVyIGFzc2lnbmVkIHRvIHRoYXQgdmlzaXQgd29ya3MgYXQgdGhhdCByZXN0YXVyYW50LiBBIHNlcnZlciBjYW4gd29yayBhdCBkaWZmZXJlbnQgdGltZXMgYXQgbW9yZSB0aGFuIG9uZSByZXN0YXVyYW50LiBBc3NpZ24gYSByYW5kb20gZGF0ZSBmb3IgYSByZXN0YXVyYW50IHdpdGhvdXQgc2VydmVycyAod2hlcmUgdGhlIGVuZCBkYXRlIGlzICI5OTk5LTk5LTk5IikuCgpgYGB7cn0KZm9yICh0IGluIDE6bnVtVHhucykgewogIGlmIChkZiRFbmREYXRlSGlyZWRbdF0gPT0gIjk5OTktOTktOTkiKSB7CiAgICAjIyBubyBzZXJ2ZXJzIGF0IHRoZSByZXN0YXVyYW50LCBzbyBwaWNrIGEgZGF0ZSBiZWZvcmUgdG9kYXkKICAgIG1vbnRoIDwtIHNhbXBsZSgxOjEyLDEpCiAgICBpZiAobW9udGggPCAxMCkgbW9udGggPC0gcGFzdGUwKCIwIiwgbW9udGgpCiAgICBkZiRWaXNpdERhdGVbdF0gPC0gcGFzdGUwKCIyMCIsIHNhbXBsZSgxOToyNCwgMSksICItIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbW9udGgsICItIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2FtcGxlKDEwOjI4LCAxKSkKICB9IGVsc2UgewogICAgIyMgZmluZCB0aGUgc2VydmVyJ3MgaGlyZSBzdGFydCBhbmQgZW5kIGFzc2lnbmVkIHRvIHRoYXQgdmlzaXQKICAgIHRoZVNlcnZlci5zdGFydCA8LSBhcy5EYXRlKGRmJFN0YXJ0RGF0ZUhpcmVkW3RdKQogIAogICAgdGhlU2VydmVyLmVuZCA8LSBhcy5EYXRlKGlmZWxzZShkZiRFbmREYXRlSGlyZWRbdF0gPT0gIiIsIFN5cy5EYXRlKCksIGRmJEVuZERhdGVIaXJlZFt0XSkpCiAgICAKICAgICMjIGNhbGN1bGF0ZSB0aGUgZGF5cyBiZXR3ZWVuIHRoZW4gYW5kIG5vdzsgaWYgdGhlcmUncyBubyBlbmQgZGF0ZSwgdXNlICJ0b2RheSIKICAgIG51bURheXMgPC0gYXMuaW50ZWdlcih0aGVTZXJ2ZXIuZW5kIC0gdGhlU2VydmVyLnN0YXJ0KQogICAgCiAgICAjIyBwaWNrIHJhbmRvbSBkYXRlIChoYWNrIGlzIG5lY2Vzc2FyeSB0byBjb2VyY2UgZGF0ZSB0byBhIHN0cmluZykKICAgIGRmJFZpc2l0RGF0ZVt0XSA8LSBwYXN0ZTAoIiIsIHRoZVNlcnZlci5zdGFydCArIHNhbXBsZSgwOm51bURheXMsIDEpKQogIH0KfQpgYGAKCiMjIyMjIEFzc2lnbiBhIFZpc2l0IFRpbWUKCkFzc2lnbmluZyBhIHZpc2l0IHRpbWUgZmlyc3QgcmVxdWlyZXMgZGV0ZXJtaW5pbmcgYSBtZWFsIHR5cGUsIHNvIHRoYXQgdGhlIHRpbWUgaXMgZHVyaW5nIHRoZSBjb21tb24gbWVhbCB0aW1lIGZvciB0aGF0IHR5cGUgb2YgbWVhbC4KCmBgYHtyfQpnZW5WaXNpdFRpbWUgPC0gZnVuY3Rpb24gKG1lYWxUeXBlKQp7CiAgbWludXRlICA8LSBzYW1wbGUoMTo1OSwxKQogIGhvdXIgPC0gc2FtcGxlKDU6MjMsMSkKICAKICBpZiAobWVhbFR5cGUgPT0gIkJyZWFrZmFzdCIpIGhvdXIgPC0gc2FtcGxlKDU6MTEsMSkKICBpZiAobWVhbFR5cGUgPT0gIkx1bmNoIikgaG91ciA8LSBzYW1wbGUoMTE6MTUsMSkKICBpZiAobWVhbFR5cGUgPT0gIkRpbm5lciIpIGhvdXIgPC0gc2FtcGxlKDE2OjIwLDEpCiAgCiAgaWYgKGhvdXIgPCAxMCkgaG91ciA8LSBwYXN0ZTAoIjAiLGhvdXIpCiAgCiAgaWYgKG1pbnV0ZSA8IDEwKSBtaW51dGUgPC0gcGFzdGUwKCIwIixtaW51dGUpCiAgCiAgcmV0dXJuIChwYXN0ZTAoaG91ciwiOiIsIG1pbnV0ZSkpCn0KCmZvciAodCBpbiAxOm51bVR4bnMpIHsKICBtZWFsVHlwZSA8LSBtZWFsVHlwZXNbc2FtcGxlKDE6bGVuZ3RoKG1lYWxUeXBlcyksMSldCiAgCiAgIyMgYWRkIHRoZSB2aXNpdCB0aW1lCiAgZGYkVmlzaXRUaW1lW3RdIDwtIGdlblZpc2l0VGltZShtZWFsVHlwZSkKICAKICAjIyBhZGQgdGhlIG1lYWwgdHlwZQogIGRmJE1lYWxUeXBlW3RdIDwtIG1lYWxUeXBlCn0KYGBgCgojIyMjIyBEYXRhIERvcGluZwoKV2UnbGwgaW50cm9kdWNlIHNvbWUgIm5vaXNlIiBieSByZW1vdmluZyBzb21lIHZpc2l0IHRpbWVzIHNvIHRoYXQgaXQgY2FuIGJlIHVzZWQgYXMgYW4gZXhhbXBsZSBmb3IgZGF0YSBpbXB1dGF0aW9uLgoKYGBge3J9Cm51bVZpc2l0VG9SZW1vdmUgPC0gbnJvdyhkZikgKiBwZXJjZW50VmlzaXRUaW1lc1RvUmVtb3ZlCgpkZiRWaXNpdFRpbWVbc2FtcGxlKDE6bnJvdyhkZiksIG51bVZpc2l0VG9SZW1vdmUpXSA8LSAiIgpgYGAKCiMjIyMjIEdlbmVyYXRlIFBhcnR5IFNpemUsIEdlbmRlcnMsIGFuZCBXYWl0IFRpbWUKCkdlbmVyYXRlIGEgcmFuZG9tIHNpemUgb2YgdGhlIHBhcnR5LCAqaS5lLiosIGhvdyBtYW55IGd1ZXN0cyBhcmUgZWF0aW5nIGluLiBGb3IgInRha2Utb3V0IiB0aGlzIGlzIHRoZSBudW1iZXIgb2YgbWVhbHMgb3JkZXJlZC4gQWRkIHNvbWUgcmFuZG9tIHdhaXQgdGltZXMgZm9yIHNvbWUgY3VzdG9tZXJzIGJ1dCBvbmx5IGF0IHRpbWVzIHdoZW4gaXQgaXMgbGlrZWx5IHRvIGJlIGJ1c3kgYW5kIGl0IGlzIG5vdCAidGFrZS1vdXQiLiBUaGUgcGFydHkgc2l6ZXMgYXJlIGJhc2VkIG9uIHByb2JhYmlsaXRpZXMgYXMgbm90IGFsbCBzaXplcyBvZiBwYXJ0aWVzIGFyZSBlcXVhbGx5IGxpa2VseS4gVGhlIHdhaXQgdGltZSBpcyBhc3N1bWVkIHRvIGJlIGRyYXduIGZyb20gYSBub3JtYWwgZGlzdHJpYnV0aW9uIHdpdGggdGhlIG1lYW4gYmVpbmcgYGF2Z1dhaXRUaW1lYCBhbmQgdGhlIHN0YW5kYXJkIGRldmlhdGlvbiBiZWluZyBhZGp1c3RlZCBmb3IgdGhlIG1lYWwgdHlwZS4gU2luY2UgdGhlIHJhbmRvbSBudW1iZXJzIGRyYXduIGNvdWxkIGJlIG5lZ2F0aXZlLCBhbiBhZGp1c3RtZW50IGlzIG1hZGUgdG8gZW5zdXJlIHRoZXkgYXJlIHBvc2l0aXZlLgoKVGhlIGdlbmRlcnMgZm9yIGVhY2ggbWVtYmVyIG9mIHRoZSBwYXJ0eSBvZiByYW5kb21seSBzZWxlY3RlZCBmcm9tIHRoZSBgZ2VuZGVyc2AgcGFyYW1ldGVyIHZlY3RvciBiYXNlZCBvbiBhIGNvbmZpZ3VyYWJsZSBwcm9iYWJpbGl0eSBkaXN0cmlidXRpb24gaW4gYGdlbmRlclByb2JzYC4KCmBgYHtyfQpnZW5XYWl0VGltZSA8LSBmdW5jdGlvbiAobWVhbFR5cGUsIHBhcnR5U2l6ZSwgdmlzaXRUaW1lKQp7CiAgIyMgbW9yZSBsaWtlbHkgdG8gd2FpdCBhdCBkaW5uZXIKICAjIyBtb3JlIHdhaXQgdGltZSBmb3IgcGFydGllcyA+IDQKICAjIyBhdmVyYWdlIHdhaXQgdGltZSBjb25maWd1cmFibGUKICAKICB0IDwtIDEKICBob3VyIDwtIGlmZWxzZSh2aXNpdFRpbWUgPT0gIiIsCiAgICAgICAgICAgICAgICAgMCwKICAgICAgICAgICAgICAgICBhcy5pbnRlZ2VyKHN1YnN0cmluZyh2aXNpdFRpbWUsIDAsIDIpKSkKICAKICBpZiAobWVhbFR5cGUgPT0gIkRpbm5lciIgJiBwYXJ0eVNpemUgPiA0KSB7CiAgICB0IDwtIHJub3JtKDEsIG1lYW4gPSBhdmdXYWl0VGltZSwgc2QgPSA0KSArIDEwCiAgfQogIAogIGlmIChtZWFsVHlwZSA9PSAiRGlubmVyIikgewogICAgdCA8LSBybm9ybSgxLCBtZWFuID0gYXZnV2FpdFRpbWUsIHNkID0gNCkKICB9CiAgCiAgaWYgKG1lYWxUeXBlID09ICJMdW5jaCIpIHsKICAgIHQgPC0gcm5vcm0oMSwgbWVhbiA9IGF2Z1dhaXRUaW1lLCBzZCA9IDMpIC0gMQogIH0KICAKICBpZiAoKGhvdXIgPCA3KSB8IChob3VyID09IDEwKSB8IChob3VyID49IDEzICYgaG91ciA8IDE4KSB8IChob3VyID49IDIwKSkgCiAgICB0IDwtIDAKICAKICByZXR1cm4gKHQpCn0KYGBgCgpOb3csIGxldCdzIGFzc2lnbiBhIHJhbmRvbSBwYXJ0eSBzaXplIChiYXNlZCBvbiBhbiBlbXBpcmljYWwgcHJvYmFiaWxpdHkgZGlzdHJpYnV0aW9uIG1ha2luZyBsYXJnZXIgcGFydGllcyBhIGxvdCBsZXNzIGxpa2VseSksIHJhbmRvbSBnZW5kZXJzLCBhbmQgYSByYW5kb20gd2FpdCB0aW1lIChiYXNlZCBvbiBtZWFsIHR5cGUgYW5kIHRpbWUgb2YgZGF5KS4KCmBgYHtyfQpmb3IgKHQgaW4gMTpudW1UeG5zKSB7CiAgIyMgcmFuZG9tIHBhcnR5IHNpemUKICBkZiRQYXJ0eVNpemVbdF0gPC0gc2FtcGxlKDE6bGVuZ3RoKHBhcnR5U2l6ZVByb2JzKSwgMSwgcHJvYiA9IHBhcnR5U2l6ZVByb2JzKQogIAogICMjIHJhbmRvbSBwYXJ0eSBnZW5kZXJzCiAgbWVtR2VuZGVycyA8LSAiIgogIGZvciAobSBpbiAxOmRmJFBhcnR5U2l6ZVt0XSkgewogICAgIyMgYXNzaWduIGEgcmFuZG9tIGdlbmRlciB0byBlYWNoIHBhcnR5IG1lbWJlcgogICAgbWVtR2VuZGVycyA8LSBwYXN0ZTAobWVtR2VuZGVycywgCiAgICAgICAgICAgICAgICAgICAgICAgICBnZW5kZXJzW3NhbXBsZSgxOmxlbmd0aChnZW5kZXJzKSwgMSwgcHJvYiA9IGdlbmRlclByb2JzKV0pCiAgfQoKICAjIyBhZGQgYSB2ZWN0b3IKICBkZiRHZW5kZXJzW3RdIDwtIG1lbUdlbmRlcnMKCiAgIyMgYWRkIGEgcmFuZG9tIHdhaXQgdGltZSBiYXNlZCBvbiBwYXJ0eSBzaXplLCB0aW1lIG9mIGRheSwgYW5kIHdoZXRoZXIgb3Igbm90CiAgIyMgdGFrZS1vdXQgb3Igc2l0LWRvd24gcmVzdGF1cmFudAogIGRmJFdhaXRUaW1lW3RdIDwtIHJvdW5kKGdlbldhaXRUaW1lKGRmJE1lYWxUeXBlW3RdLCBkZiRQYXJ0eVNpemVbdF0sIGRmJFZpc2l0VGltZVt0XSksMCkKfQpgYGAKCiMjIyMjIERhdGEgRG9waW5nCgpXZSdsbCBpbnRyb2R1Y2Ugc29tZSAibm9pc2UiIGJ5IG1ha2luZyBzb21lIHBhcnR5IHNpemVzIGFuICJvdXRsaWVyIiBvciAibm9uc2Vuc2ljYWwiIHZhbHVlIHNvIHRoYXQgaXQgY2FuIGJlIHVzZWQgYXMgYW4gZXhhbXBsZSBmb3IgYmFkIGRhdGEgZGV0ZWN0aW9uIGFuZCB1c2VkIGZvciBkYXRhIGltcHV0YXRpb24sIHBlcmhhcHMgYnkgcHJlZGljdGluZyB0aGVtIGZyb20gdGhlIG90aGVyIGZlYXR1cmVzIGFuZCB0aGUgdG90YWwgYmlsbCBhbW91bnQuIFdlJ2xsIHVzZSB0aGUgdmFsdWUgb2YgKjk5KiB0byBpbmRpY2F0ZSBhIG1pc3NpbmcgcGFydHkgc2l6ZSAtLSB0aGlzLCBvZiBjb3Vyc2UsIGlzIHBvb3IgZGF0YSBxdWFsaXR5IG1hbmFnZW1lbnQsIGJ1dCwgdW5mb3J0dW5hdGVseSwgcXVpdGUgY29tbW9uLgoKYGBge3J9Cm51bVBhcnR5U2l6ZXNUb1JlbW92ZSA8LSA4CgpkZiRQYXJ0eVNpemVbc2FtcGxlKDE6bnJvdyhkZiksIG51bVBhcnR5U2l6ZXNUb1JlbW92ZSldIDwtIDk5CmBgYAoKIyMjIyMgR2VuZXJhdGUgQ3VzdG9tZXIgSW5mb3JtYXRpb24KCkluIHRoaXMgc2VjdGlvbiwgd2Ugd2lsbCBnZW5lcmF0ZSBpbmZvcm1hdGlvbiBhYm91dCB0aGUgcGFydHkgdmlzaXRpbmcgdGhlIHJlc3RhdXJhbnQsICppLmUuKiwgdGhlICJjdXN0b21lciIuIEEgY3VzdG9tZXIgSUQsIG5hbWUsIHBob25lLCBhbmQgZW1haWwgd2VyZSBzeW50aGV0aWNhbGx5IGdlbmVyYXRlZCB1c2luZyBbZ2VuZXJhdGVkYXRhLmNvbV0oaHR0cDovL2dlbmVyYXRlZGF0YS5jb20pIGZvciBhYm91dCAyNTAwIGN1c3RvbWVycy4KCmBgYHtyIGxvYWRDdXN0b21lclN5bnRoZXRpY0RhdGF9CmN1c3RvbWVycy5kZiA8LSByZWFkLmNzdigiY3VzdG9tZXJzLmNzdiIpCmBgYAoKVGhlIGZ1bmN0aW9uIGJlbG93LCBzZWxlY3QgYSByYW5kb20gY3VzdG9tZXIsIGFsdGhvdWdoIGZvciBzb21lIGNvbmZpZ3VyYWJsZSBwZXJjZW50YWdlIG9mIGN1c3RvbWVycyBubyBjdXN0b21lciBpbmZvIGlzIGdlbmVyYXRlZCBhbmQgdGh1cyBpcyBsZWZ0IGJsYW5rLgoKYGBge3J9CiMjIGdlbmVyYXRlIGEgY3VzdG9tZXIgaW5mbyBmb3Igc29tZSBjdXN0b21lcnMgcmFuZG9tbHkKZ2VuQ3VzdG9tZXJJbmZvIDwtIGZ1bmN0aW9uICgpCnsKICBpZiAocnVuaWYoMSkgPiBwcm9iQ3VzdG9tZXJLbm93bikgewogICAgIyBubyBjdXN0b21lciBpbmZvCiAgICByZXR1cm4gKE5VTEwpCiAgfSBlbHNlIHsKICAgICMgcGljayBhIHJhbmRvbSBjdXN0b21lciBmcm9tIHRoZSBsaXN0IG9mIGN1c3RvbWVycwogICAgc2V0LnNlZWQoODA3NjMrKHJ1bmlmKDEpKjUwMCkpCiAgICBjdXN0SW5mbyA8LSBjdXN0b21lcnMuZGZbc2FtcGxlKDE6bnJvdyhjdXN0b21lcnMuZGYpLDEpLF0KICAgIHJldHVybiAoY3VzdEluZm8pCiAgfQp9CmBgYAoKTmV4dCwgd2UnbGwgYWRkIHRoZSBjdXN0b21lciBpbmZvcm1hdGlvbiB0byB0aGUgdmlzaXQuIElmIGEgY3VzdG9tZXIgd2FzIHNlbGVjdGVkLCB0aGVuIHdlIGFzc3VtZSB0aGF0IHRoaXMgbWVhbnMgdGhhdCB0aGV5IGFyZSBhIG1lbWJlciBvZiB0aGUgcmVzdGF1cmFudCBjaGFpbidzIGxveWFsdHkgcHJvZ3JhbS4gT2YgY291cnNlLCB0aGUgc2FtZSBjdXN0b21lciBjYW4gdmlzaXQgdGhlIHNhbWUgcmVzdGF1cmFudCBvciBkaWZmZXJlbnQgcmVzdGF1cmFudHMgbXVsdGlwbGUgdGltZXMuCgpgYGB7cn0KZm9yICh0IGluIDE6bnVtVHhucykgewogICMjIGNob29zZSBhIHJhbmRvbSBjdXN0b21lciBvciBubyBpbmZvcm1hdGlvbiAgCiAgY3VzdCA8LSBnZW5DdXN0b21lckluZm8oKQogIAogICMjIGFkZCB0byBkYXRhZnJhbWUgaWYgaW5mb3JtYXRpb24gd2FzIHNlbGVjdGVkCiAgaWYgKCFpcy5udWxsKGN1c3QpKSB7CiAgICBkZiRDdXN0b21lck5hbWVbdF0gPC0gY3VzdCRuYW1lCiAgICBkZiRDdXN0b21lclBob25lW3RdIDwtIGN1c3QkcGhvbmUKICAgIGRmJEN1c3RvbWVyRW1haWxbdF0gPC0gY3VzdCRlbWFpbAogICAgZGYkTG95YWx0eU1lbWJlclt0XSA8LSBUUlVFCiAgfSBlbHNlIHsKICAgIGRmJEN1c3RvbWVyTmFtZVt0XSA8LSAiIgogICAgZGYkQ3VzdG9tZXJQaG9uZVt0XSA8LSAiIgogICAgZGYkQ3VzdG9tZXJFbWFpbFt0XSA8LSAiIgogICAgZGYkTG95YWx0eU1lbWJlclt0XSA8LSBGQUxTRQogIH0KfQpgYGAKCiMjIyMjIEdlbmVyYXRlIEJpbGwsIFRpcCwgRGlzY291bnQsIGFuZCBQYXltZW50IFR5cGUKCk5leHQsIHdlJ2xsIGdlbmVyYXRlIHRoZSB0b3RhbCBhbW91bnQgZm9yIHRoZSBiaWxsLiBJdCBpcyBhIHJhbmRvbSB2YWx1ZSBiYXNlZCBvbiB0aGUgbWVhbCB0eXBlIGFuZCB0aGUgc2l6ZSBvZiB0aGUgcGFydHkuCgpUaGUgY29zdCBvZiBhIG1lYWwgKHBlciBwZXJzb24pIGlzIGJhc2VkIG9uIHBhcmFtZXRlcnMgZm9yIHRoZSBtZWFsIHR5cGUgYW5kIGlzIHRoZW4gc2VsZWN0ZWQgcmFuZG9tbHkgZnJvbSBhIG5vcm1hbCBkaXN0cmlidXRpb24uIFRoZSBjb2RlIGJlbG93IHNlbGVjdHMgY29zdHMgcmVwZWF0ZWRseSB1bnRpbCB0aGUgY29zdCBpcyBtb3JlIHRoYW4gXCQwLjkwLgoKYGBge3J9CmdlbkZvb2RCaWxsIDwtIGZ1bmN0aW9uIChtZWFsVHlwZSwgcGFydHlTaXplKQp7CiAgcmVwZWF0IHsKICAgIGMgPC0gcm5vcm0oMSwKICAgICAgICAgICAgICAgbWVhbiA9IHBlclBlcnNvbkNoZWNrJG1lYW5bd2hpY2gobWVhbFR5cGVzID09IG1lYWxUeXBlKV0sCiAgICAgICAgICAgICAgIHNkID0gcGVyUGVyc29uQ2hlY2skc2Rbd2hpY2gobWVhbFR5cGVzID09IG1lYWxUeXBlKV0pCiAgICAKICAgIGlmIChjID4gMC45KQogICAgICBicmVhawogIH0KICAKICAjIyBtdWx0aXBseSB0aGUgYXZlcmFnZSBtZWFsIGNvc3QgYnkgc2l6ZSBvZiBwYXJ0eQogICMjIGlmIHRoZSBzaXplIG9mIHBhcnR5IGlzIDk5LCB0aGVuIGl0IG1lYW5zIGl0IGlzIG1pc3NpbmcKICAjIyBhbmQgd2UnbGwgZ2VuZXJhdGUgYSByYW5kb20gcGFydHkgc2l6ZQogIGlmIChwYXJ0eVNpemUgPT0gOTkpCiAgICBwYXJ0eVNpemUgPC0gc2FtcGxlKDE6bGVuZ3RoKHBhcnR5U2l6ZVByb2JzKSwgMSwgcHJvYiA9IHBhcnR5U2l6ZVByb2JzKQogIAogIHJldHVybihyb3VuZChjICogcGFydHlTaXplLCAyKSkKfQpgYGAKClRoZSB0aXAgaXMgYSByYW5kb20gdmFsdWUgYmV0d2VlbiAxNSUgYW5kIDI1JSAtLSB0aGlzIGlzIGEgYml0IHVucmVhbGlzdGljIGFzIG1hbnkgY3VzdG9tZXJzIHdvdWxkIGFsc28gcm91bmQgdXAgdG8gYSB3aG9sZSBkb2xsYXIuIFRoZSB0aXAgYWxzbyB2YXJpZXMgYmFzZWQgb24gbWVhbCB0eXBlIGFuZCBwYXJ0eSBzaXplLgoKYGBge3IgZ2VuVGlwQW1vdW50fQpnZW5UaXBBbW91bnQgPC0gZnVuY3Rpb24gKG10LCBwcywgZ24pCnsKICB0aXBBbW91bnQgPC0gMC4xNSAgICAgICAgIyBzdGFuZGFyZCB0aXAgaXMgMTUlCiAgCiAgdGlwQW1vdW50IDwtIGlmZWxzZShtdCA9PSAiVGFrZS1PdXQiLCAwLAogICAgICAgICAgICAgICAgICBzYW1wbGUodGlwcGluZ1NjYWxlJGFtb3VudCwgMSwKICAgICAgICAgICAgICAgICAgICAgICAgIHByb2IgPSB0aXBwaW5nU2NhbGUkcHJvYikpCiAgCiAgcmV0dXJuKHRpcEFtb3VudCkKfQpgYGAKCklmIGEgY3VzdG9tZXIgaXMgcGFydCBvZiB0aGUgbG95YWx0eSBwcm9ncmFtLCB3ZSdsbCBnZW5lcmF0ZSBhbiBkaXNjb3VudCBmb3IgdGhlbSB0aGF0IGlzIGVpdGhlciAxMCUgb3IgMTUlLiBUaGUgcHJvYmFiaWxpdHkgdGhhdCB0aGUgZGlzY291bnQgaXMgMTAlIGlzIDgwJSwgMTUlIG90aGVyd2lzZS4KCmBgYHtyfQpnZW5EaXNjb3VudCA8LSBmdW5jdGlvbiAoZmxhZykKewogIGRpc2NvdW50IDwtIDAKICAKICAjIyBkaXNjb3VudCBvbmx5IGZvciBub24tbG95YWx0eSBwcm9ncmFtIG1lbWJlcnMKICBpZiAoZmxhZyA9PSBUUlVFKQogICAgZGlzY291bnQgPC0gaWZlbHNlKHJ1bmlmKDEpIDwgMC44LCAwLjEsIDAuMTUpIAogIAogIHJldHVybihkaXNjb3VudCkKfQpgYGAKCmBgYHtyfQpmb3IgKHQgaW4gMTpudW1UeG5zKSAKewogICMjIGdlbmVyYXRlIHRvdGFsIGFtb3VudCBmb3IgYmlsbCBiYXNlZCBvbiBtZWFsIHR5cGUgYW5kIHBhcnR5IHNpemUKICBkZiRGb29kQmlsbFt0XSA8LSBnZW5Gb29kQmlsbChkZiRNZWFsVHlwZVt0XSwgZGYkUGFydHlTaXplW3RdKQogIAogICMjIGFkZCBhIHJhbmRvbSB0aXAgYW1vdW50IGlmIGl0IGlzIG5vdCAidGFrZS1vdXQiCiAgZGYkVGlwQW1vdW50W3RdIDwtIGdlblRpcEFtb3VudChkZiRNZWFsVHlwZVt0XSwgZGYkUGFydHlTaXplW3RdKQogIAogICMjIGFwcGx5IGRpc2NvdW50IGlmIGluIGxveWFsdHkgcHJvZ3JhbQogIGRmJERpc2NvdW50QXBwbGllZFt0XSA8LSBnZW5EaXNjb3VudChkZiRMb3lhbHR5TWVtYmVyW3RdKQogIAogICMjIGFkZCBhIHJhbmRvbSBwYXltZW50IHR5cGUKICBkZiRQYXltZW50TWV0aG9kW3RdIDwtIHBheVR5cGVzW3NhbXBsZSgxOmxlbmd0aChwYXlUeXBlcyksIDEpXQp9CmBgYAoKIyMjIyMgQWRkIEFsY29ob2wKClJhbmRvbWx5IGFkZCBmbGFnIHdoZXRoZXIgcGFydHkgb3JkZXJlZCBhbGNvaG9sIChpZiBub3QgdGFrZS1vdXQpIGFuZCB0aGVuIGFkZCBhIHN1cmNoYXJnZSB0byB0aGUgcGFydHkgYW5kIHJlY2FsY3VsYXRlIGJpbGwgdG90YWwgYW5kIHRpcC4KClRoZSBmdW5jdGlvbiBlbmNvZGVzIHRoZSBmb2xsb3dpbmcgImRyaW5raW5nIHJ1bGVzIjoKCi0gICBOZXZlciBhbGNvaG9sIGlmIGJyZWFrZmFzdCBvciB0YWtlLW91dC4KLSAgIFNpbmdsZSBwZXJzb24gZm9yIGRpbm5lciBoYXMgcHJvYmFiaWxpdHkgb2YgZHJpbmtpbmcgYWxjb2hvbCBvZiA5MCUgb3IgbW9yZS4KLSAgIFNpbmdsZSBwZXJzb24gZm9yIGx1bmNoIGhhcyBwcm9iYWJpbGl0eSBvZiBkcmlua2luZyBhbGNvaG9sIG9mIDQwJSBvciBtb3JlLgotICAgSWYgcGFydHkgb2YgMiBvciBtb3JlIGZvciBkaW5uZXIgdGhlbiBwcm9iYWJpbGl0eSBvZiBkcmlua2luZyBpcyA2MCUgb3IgbW9yZS4KLSAgIEEgc2FtZS1zZXggcGFydHkgb2YgMiBvciBtb3JlIGZvciBkaW5uZXIgaGFzIHByb2JhYmlsaXR5IG9mIGRyaW5raW5nIG9mIDgwJSBvciBncmVhdGVyLgoKYGBge3IgZGV0ZXJtaW5lSWZBbGNvaG9sfQpnZW5BbGNvaG9sT3JkZXJlZCA8LSBmdW5jdGlvbiAocHMsIG10LCBnbikKewogICMjIG5vIGFsY29ob2wgb2YgdGFrZS1vdXQgb3IgYnJlYWtmYXN0CiAgaWYgKG10ID09ICJUYWtlLU91dCIgfHwgbXQgPT0gIkJyZWFrZmFzdCIpIHsKICAgIHJldHVybiAoIm5vIikKICB9CiAgCiAgaWYgKChtdCA9PSAiRGlubmVyIikgJiYgKHBzID09IDEpICYmIChydW5pZigxKSA8IDAuOSkpIHsKICAgICMjIHNpbmdsZSBwZXJzb24gZm9yIGRpbm5lcgogICAgcmV0dXJuICgieWVzIikKICB9CiAgCiAgaWYgKChtdCA9PSAiTHVuY2giKSAmJiAocHMgPT0gMSkgJiYgKHJ1bmlmKDEpIDwgMC40KSkgewogICAgIyMgc2luZ2xlIHBlcnNvbiBmb3IgbHVuY2gKICAgIHJldHVybiAoInllcyIpCiAgfQogIAogIGlmICgobXQgPT0gIkRpbm5lciIpICYmIChwcyA9PSAyKSAmJiAoZ24gPT0gIm1tIiB8fCBnbiA9PSAiZmYiKSAmJiAocnVuaWYoMSkgPCAwLjgpKSB7CiAgICAjIyBwYXJ0eSBvZiB0d28gb2Ygc2FtZS1zZXggZm9yIGRpbm5lcgogICAgcmV0dXJuICgieWVzIikKICB9CiAgCiAgaWYgKChtdCA9PSAiRGlubmVyIikgJiYgKHBzID4gMSkgJiYgKHJ1bmlmKDEpIDwgMC42KSkgewogICAgIyMgbGFyZ2UgcGFydHkgZm9yIGRpbm5lcgogICAgcmV0dXJuICgieWVzIikKICB9CiAgCiAgaWYgKChtdCA9PSAibHVuY2giKSAmJiAocnVuaWYoMSkgPCAwLjEpKSB7CiAgICByZXR1cm4gKCJ5ZXMiKQogIH0KICAKICAjIyBvdGhlcndpc2UgZGVmYXVsdCBpcyBubyBkcmlua2luZwogIHJldHVybiAoIm5vIikKfQpgYGAKClRoZSBhbW91bnQgc3BlbnQgb24gYWxjb2hvbCBzaG91bGQgZGVwZW5kIG9uIHRoZSBtZWFsIHR5cGUsIHRoZSBudW1iZXIgb2YgcGVvcGxlIGluIHRoZSBwYXJ0eSwgdGhlIGF2ZXJhZ2UgY29zdCBwZXIgZHJpbmssIGFuZCB0aGUgZ2VuZGVycy4gVGhlIGltcGxlbWVudGF0aW9uIGJlbG93IGlzIGEgZmlyc3QgcGFzcyBhdCB0aGlzLgoKYGBge3J9CmdlbkFsY29ob2xCaWxsIDwtIGZ1bmN0aW9uIChtdCwgcHMsIGduKQp7CiAgIyMgYWxjb2hvbCBiaWxsIGlzIG51bWJlciBvZiBwZW9wbGUgaW4gdGhlIHBhcnR5CiAgIyMgbXVsdGlwbGllZCBieSBhdmVyYWdlIGNvc3QgcGVyIGRyaW5rIGJ5IHJhbmRvbSBudW1iZXIgb2YgZHJpbmtzCiAgIyMgZm9yIHBlcmNlbnRhZ2Ugb2YgcGFydHkgd2hvIHdpbGwgZHJpbmsKICBpZiAocHMgPT0gOTkpCiAgICBwcyA8LSBzYW1wbGUoMTpsZW5ndGgocGFydHlTaXplUHJvYnMpLCAxLCBwcm9iID0gcGFydHlTaXplUHJvYnMpCiAgCiAgYWxjb2hvbEJpbGwgPC0gcHMgKiBhdmdDb3N0UGVyRHJpbmsgKiAxLjI1CiAgCiAgcmV0dXJuIChhbGNvaG9sQmlsbCkKfQpgYGAKCmBgYHtyfQpmb3IgKHQgaW4gMTpudW1UeG5zKSAKewogICMjIHJhbmRvbWx5IGFkZCBhbGNvaG9sCiAgZGYkb3JkZXJlZEFsY29ob2xbdF0gPC0gZ2VuQWxjb2hvbE9yZGVyZWQoZGYkUGFydHlTaXplW3RdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRmJE1lYWxUeXBlW3RdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRmJEdlbmRlcnNbdF0pCiAgCiAgIyMgaWYgYWxjb2hvbCB3YXMgY29uc3VtZWQsIHRoZW4gdXAgdGhlIGJpbGwgYnkgYSByYW5kb20gYW1vdW50CiAgIyMgYmFzZWQgb24gYSByYW5kb20gbnVtYmVyIG9mIHBlb3BsZSBkcmlua2luZwogIGlmIChkZiRvcmRlcmVkQWxjb2hvbFt0XSA9PSAieWVzIikgewogICAgIyMgYWRkIGEgc2VwYXJhdGUgYW1vdW50IGZvciBhbGNvaG9sCiAgICBkZiRBbGNvaG9sQmlsbFt0XSA8LSBnZW5BbGNvaG9sQmlsbChkZiRNZWFsVHlwZVt0XSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkZiRQYXJ0eVNpemVbdF0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkZiRHZW5kZXJzW3RdKQogICAgCiAgICAjIyBhZGp1c3QgdGlwIGFtb3VudCBiYXNlZCBvbiBuZXcgYmlsbAogICAgZGYkVGlwQW1vdW50W3RdIDwtIGdlblRpcEFtb3VudChkZiRNZWFsVHlwZVt0XSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRmJFBhcnR5U2l6ZVt0XSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZGYkR2VuZGVyc1t0XSkKICB9IGVsc2UgewogICAgZGYkQWxjb2hvbEJpbGxbdF0gPC0gMAogIH0KfQpgYGAKCiMjIyMjIEZpbmFsIExvb2sgYXQgRGF0YQoKYGBge3J9CmhlYWQoZGYsMTApCmBgYAoKIyMjIyBHZW5lcmF0ZSBEYXRhIEZpbGVzCgojIyMjIyBHZW5lcmF0ZSBDU1YKCmBgYHtyIGdlbkNTVkZpbGUtRnVsbH0Kd3JpdGUuY3N2KGRmLCAKICAgICAgICAgIGZpbGUgPSAicmVzdGF1cmFudC12aXNpdHMuY3N2IiwKICAgICAgICAgIHJvdy5uYW1lcyA9IEYsIAogICAgICAgICAgcXVvdGUgPSBUKQpgYGAKCiMjIyMjIEdlbmVyYXRlIFhNTAoKIyMjIyMjIFByZWFtYmxlCgpgYGB7ciB3cml0ZVNhbGVzRGF0YTJYTUx9CnhtbC5mbiA8LSAiUmVzdGF1cmFudFR4bnMueG1sIgoKeG1sIDwtICc8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCI/PlxuXG4nCnhtbCA8LSBwYXN0ZTAoeG1sLCAnPGRhdGFzZXQ+JywgJ1xuJykKYGBgCgojIyMjIyMgTWV0YSBEYXRhCgpgYGB7cn0KIyMgYWRkIG1ldGEgZGF0YTogc291cmNlLCBhdXRob3IsIGxpbmsgdG8gc291cmNlIChvciBwcm9ncmFtIHRoYXQgZ2VuZXJhdGVkIGRhdGEpLCBkYXRlCiMjIGZlYXR1cmVzIHdpdGggZGVmaW5pdGlvbnMsIGludGVuZGVkIHVzZSwgQ0M0Cgp4bWwgPC0gcGFzdGUwKHhtbCwgJzxtZXRhLWRhdGE+JywgJ1xuJykKeG1sIDwtIHBhc3RlMCh4bWwsICcgIDxkYXRlLWdlbmVyYXRlZD4nLCBTeXMuRGF0ZSgpLCAnPC9kYXRlLWdlbmVyYXRlZD4nLCAnXG4nKQp4bWwgPC0gcGFzdGUwKHhtbCwgJyAgPGF1dGhvcj4nLCAnXG4nKQp4bWwgPC0gcGFzdGUwKHhtbCwgJyAgICA8bmFtZT4nLCdNYXJ0aW4gU2NoZWRsYmF1ZXIsIFBoRCcsICc8L25hbWU+JywgJ1xuJykKeG1sIDwtIHBhc3RlMCh4bWwsICcgICAgPGNvbnRhY3Q+JywnbS5zY2hlZGxiYXVlckBub3J0aGVhc3Rlcm4uZWR1JywgJzwvY29udGFjdD4nLCAnXG4nKQp4bWwgPC0gcGFzdGUwKHhtbCwgJyAgPC9hdXRob3I+JywgJ1xuJykKeG1sIDwtIHBhc3RlMCh4bWwsICcgIDxzb3VyY2U+JywnU3ludGhldGljJywgJzwvc291cmNlPicsICdcbicpCnhtbCA8LSBwYXN0ZTAoeG1sLCAnICA8ZGVzY3JpcHRpb24+JywgJ1xuJykKeG1sIDwtIHBhc3RlMCh4bWwsICcgICAgICBBIGRhdGEgc2V0IGNvbnRhaW5pbmcgdmlzaXRzIHRvIGEgcmVzdGF1cmFudCB0aGF0IGlzIHBhcnQgb2YgYSBjaGFpblxuJykKeG1sIDwtIHBhc3RlMCh4bWwsICcgICAgICBvZiByZXN0YXVyYW50cy4nKQp4bWwgPC0gcGFzdGUwKHhtbCwgJyAgPC9kZXNjcmlwdGlvbj4nLCAnXG4nKQp4bWwgPC0gcGFzdGUwKHhtbCwgJyAgPHVzZS1jYXNlcz4nLCAnXG4nKQp4bWwgPC0gcGFzdGUwKHhtbCwgJyAgICAgPHVzZS1jYXNlPm1hY2hpbmUgbGVhcm5pbmc8L3VzZS1jYXNlPlxuJykKeG1sIDwtIHBhc3RlMCh4bWwsICcgICAgIDx1c2UtY2FzZT5kYXRhIGFuYWx5dGljczwvdXNlLWNhc2U+XG4nKQp4bWwgPC0gcGFzdGUwKHhtbCwgJyAgICAgPHVzZS1jYXNlPm1pc3NpbmcgdmFsdWVzLCBvdXRsaWVyLCBhbmQgZXh0cmVtZSB2YWx1ZSBoYW5kbGluZzwvdXNlLWNhc2U+XG4nKQp4bWwgPC0gcGFzdGUwKHhtbCwgJyAgICAgPHVzZS1jYXNlPm5vcm1hbGl6YXRpb24gd2l0aCBmdW5jdGlvbmFsIGRlcGVuZGVuY2llczwvdXNlLWNhc2U+XG4nKQp4bWwgPC0gcGFzdGUwKHhtbCwgJyAgICAgPHVzZS1jYXNlPmRhdGFiYXNlIGltcG9ydDwvdXNlLWNhc2U+XG4nKQp4bWwgPC0gcGFzdGUwKHhtbCwgJyAgICAgPHVzZS1jYXNlPkNTViBxdWVyaWVzIHdpdGggU1FMIHRocm91Z2ggc3FsZGY8L3VzZS1jYXNlPlxuJykKeG1sIDwtIHBhc3RlMCh4bWwsICcgICAgIDx1c2UtY2FzZT5YTUwgcXVlcmllcyB1c2luZyBYUGF0aDwvdXNlLWNhc2U+XG4nKQp4bWwgPC0gcGFzdGUwKHhtbCwgJyAgPC91c2UtY2FzZXM+JywgJ1xuJykKeG1sIDwtIHBhc3RlMCh4bWwsICcgIDxmaWVsZHM+JywgJ1xuJykKeG1sIDwtIHBhc3RlMCh4bWwsIAogICAgICAgICAgICAgICcgICAgPGZpZWxkIG5hbWU9IlZpc2l0SUQiIHR5cGU9Im51bWVyaWMiPicsCiAgICAgICAgICAgICAgJ1VuaXF1ZSBJRCBmb3IgdGhlIHZpc2l0LicsCiAgICAgICAgICAgICAgJyAgICA8L2ZpZWxkPicsICdcbicpCnhtbCA8LSBwYXN0ZTAoeG1sLCAnICAgIDxmaWVsZCBuYW1lPSJCaXJ0aERhdGUiIHR5cGU9ImRhdGUiPicpCnhtbCA8LSBwYXN0ZTAoeG1sLCAnQmlydGggZGF0ZSBvZiBzZXJ2ZXIvZW1wbG95ZWUuIEJsYW5rIGlmIG5vdCBrbm93bi4nKQp4bWwgPC0gcGFzdGUwKHhtbCwgJyAgICA8L2ZpZWxkPicsICdcbicpCnhtbCA8LSBwYXN0ZTAoeG1sLCAnICA8L2ZpZWxkcz4nLCAnXG4nKQp4bWwgPC0gcGFzdGUwKHhtbCwgJzwvbWV0YS1kYXRhPicsICdcbicpCmBgYAoKIyMjIyMjIERhdGEKCmBgYHtyIHdyaXRlRGF0YTJYTUx9CnhtbCA8LSBwYXN0ZTAoeG1sLCAnPHR4bnM+JywgJ1xuJykKCmZvciAociBpbiAxOm5yb3coZGYpKQp7CiAgeG1sIDwtIHBhc3RlMCh4bWwsICcgIDx2aXNpdD4nLCAnXG4nKQogIHhtbCA8LSBwYXN0ZTAoeG1sLCAnICA8L3Zpc2l0PicsICdcbicpCn0KCnhtbCA8LSBwYXN0ZTAoeG1sLCAnPC90eG5zPicsICdcbicpCmBgYAoKIyMjIyMjIFdyaXRlIFhNTCBGaWxlCgpgYGB7ciB3cml0ZVhNTEZpbGV9CgojIyBhZGQgZW5kIHRhZyBmb3IgZG9jdW1lbnQKeG1sIDwtIHBhc3RlMCh4bWwsICc8L2RhdGFzZXQ+JykKCmNvbm4gPC0gZmlsZSh4bWwuZm4pCndyaXRlTGluZXMoeG1sLCBjb25uKQpgYGAKCiMjIyBWYWxpZGF0aW9uCgpgYGB7cn0KZGYudmFsaWRhdGlvbiA8LSByZWFkLmNzdigicmVzdGF1cmFudC12aXNpdHMuY3N2IiwKICAgICAgICAgICAgICAgICAgICAgICAgICBoZWFkZXIgPSBULAogICAgICAgICAgICAgICAgICAgICAgICAgIHN0cmluZ3NBc0ZhY3RvcnMgPSBGKQoKcHJpbnQocGFzdGUwKG5yb3coZGYudmFsaWRhdGlvbiksICIgcm93cyByZWFkIHZzICIsCiAgICAgICAgICAgICBudW1UeG5zLCAiIHJvd3Mgd3JpdHRlbiIpKQoKcHJpbnQocGFzdGUwKCJOdW0gY29sdW1ucyByZWFkOiAiLCBuY29sKGRmLnZhbGlkYXRpb24pKSkKCmhlYWQoZGYudmFsaWRhdGlvbikKYGBgCgojIyBTdW1tYXJ5CgpTeW50aGV0aWMgZGF0YSBpcyBhcnRpZmljaWFsbHkgZ2VuZXJhdGVkIGluZm9ybWF0aW9uIHRoYXQgbWltaWNzIHJlYWwtd29ybGQgZGF0YSBpbiBzdHJ1Y3R1cmUgYW5kIGNoYXJhY3RlcmlzdGljcy4gSW4gdGhpcyBsZXNzb24sIHdlIHByZXNlbnRlZCBhIHdvcmtlZCBleGFtcGxlIHNob3dpbmcgc29tZSBvZiB0aGUgdGVjaG5pcXVlIGZvciBzeW50aGV0aWMgZGF0YSBnZW5lcmF0aW9uLiBJdCBpcyBlc3NlbnRpYWwgdG8ga2VlcCBpbiBtaW5kIHRoYXQgc3ludGhldGljIGRhdGEgaGFzIGxpbWl0YXRpb25zLCBpbmNsdWRpbmcgcmVkdWNlZCByZWFsaXNtLCByaXNrcyBvZiBvdmVyZml0dGluZywgdmFsaWRhdGlvbiBjaGFsbGVuZ2VzLCBhbmQgcG90ZW50aWFsIHJlZ3VsYXRvcnkgaXNzdWVzLgoKLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgojIyBGaWxlcyAmIFJlc291cmNlcwoKYGBge3IgemlwRmlsZXMsIGVjaG89RkFMU0V9CnppcE5hbWUgPSBzcHJpbnRmKCJMZXNzb25GaWxlcy0lcy0lcy56aXAiLCAKICAgICAgICAgICAgICAgICBwYXJhbXMkY2F0ZWdvcnksCiAgICAgICAgICAgICAgICAgcGFyYW1zJG51bWJlcikKCnRleHRBTGluayA9IHBhc3RlMCgiQWxsIEZpbGVzIGZvciBMZXNzb24gIiwgCiAgICAgICAgICAgICAgIHBhcmFtcyRjYXRlZ29yeSwiLiIscGFyYW1zJG51bWJlcikKCiMgZG93bmxvYWRGaWxlc0xpbmsoKSBpcyBpbmNsdWRlZCBmcm9tIF9pbnNlcnQyREIuUgprbml0cjo6cmF3X2h0bWwoZG93bmxvYWRGaWxlc0xpbmsoIi4iLCB6aXBOYW1lLCB0ZXh0QUxpbmspKQpgYGAKCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKIyMgUmVmZXJlbmNlcwoKR3JhbnZpbGxlLCBWLiAoMjAyNCkuICpTeW50aGV0aWMgRGF0YSBhbmQgR2VuZXJhdGl2ZSBBSSouIEVsc2V2aWVyLgoKU2luZ2gsIEouICgyMDIxKS4gVGhlIFJpc2Ugb2YgU3ludGhldGljIERhdGE6IEVuaGFuY2luZyBBSSBhbmQgTWFjaGluZSBMZWFybmluZyBNb2RlbCBUcmFpbmluZyB0byBBZGRyZXNzIERhdGEgU2NhcmNpdHkgYW5kIE1pdGlnYXRlIFByaXZhY3kgUmlza3MuICpKb3VybmFsIG9mIEFydGlmaWNpYWwgSW50ZWxsaWdlbmNlIFJlc2VhcmNoIGFuZCBBcHBsaWNhdGlvbnMqLCAxKDIpLCAyOTItMzMyLgoKSGFuc2VuLCBMLiwgU2VlZGF0LCBOLiwgdmFuIGRlciBTY2hhYXIsIE0uLCAmIFBldHJvdmljLCBBLiAoMjAyMykuIFJlaW1hZ2luaW5nIHN5bnRoZXRpYyB0YWJ1bGFyIGRhdGEgZ2VuZXJhdGlvbiB0aHJvdWdoIGRhdGEtY2VudHJpYyBBSTogQSBjb21wcmVoZW5zaXZlIGJlbmNobWFyay4gKkFkdmFuY2VzIGluIE5ldXJhbCBJbmZvcm1hdGlvbiBQcm9jZXNzaW5nIFN5c3RlbXMqLCAzNiwgMzM3ODEtMzM4MjMuCgojIyBFcnJhdGEKCk5vbmUgY29sbGVjdGVkIHlldC4gW0xldCB1cyBrbm93XShodHRwczovL2Zvcm0uam90Zm9ybS5jb20vMjEyMTg3MDcyNzg0MTU3KXt0YXJnZXQ9Il9ibGFuayJ9Lgo=