Project Intro

This project focuses on analyzing the sentiments of Twitter data related to specific keywords. Utilizing the syuzhet package, it conducts sentiment scoring based on the comprehensive NRC Word-Emotion Association Lexicon. The analysis encompasses eight distinct emotional dimensions: anger, anticipation, disgust, fear, joy, sadness, surprise, and trust.

Additionally, the project features a visualization component that tracks the evolution of the overall sentiment score over time, offering a dynamic view of public sentiment trends in relation to the chosen keywords.

Load helper library

library(data.table);library(textstem);library(lubridate)
library(tm);library(syuzhet);library(ggplot2);library(hrbrthemes);library(wordcloud);library(wordcloud2)

Sentimental data Preprocessing

  • target: the polarity of the tweet (0 = negative, 2 = neutral, 4 = positive)
  • ids: The id of the tweet (2087)
  • date: the date of the tweet (Sat May 16 23:58:44 UTC 2009)
  • flag: The query (lyx). If there is no query, then this value is NO_QUERY.
  • user: the user that tweeted (robotickilldozr)
  • text: the text of the tweet (Lyx is cool)
data <- read.csv("./inputs.csv", header = FALSE, col.names = c("target", "ids", "date", "flag", "user", "text"), encoding = "UTF-8")
summary(data)
     target       ids                date               flag               user               text          
 Min.   :0   Min.   :1.468e+09   Length:1600000     Length:1600000     Length:1600000     Length:1600000    
 1st Qu.:0   1st Qu.:1.957e+09   Class :character   Class :character   Class :character   Class :character  
 Median :2   Median :2.002e+09   Mode  :character   Mode  :character   Mode  :character   Mode  :character  
 Mean   :2   Mean   :1.999e+09                                                                              
 3rd Qu.:4   3rd Qu.:2.177e+09                                                                              
 Max.   :4   Max.   :2.329e+09                                                                              

Search tweets contains certain keywords

search_tweets <- function(tweets_df, keyword, ignore.case = TRUE) {
  tryCatch({
    # Ensure tweets_df is a data.table
    setDT(tweets_df)
    # Vectorized search for keyword
    suppressWarnings(keyword_filtered <- tweets_df[grepl(keyword, text, ignore.case = ignore.case)])
    return(keyword_filtered)
  }, error = function(e) {
    stop("An error occurred: ", conditionMessage(e))
  })
}
test_result <- search_tweets(data,"apple")
summary(test_result)
     target           ids                date               flag               user               text          
 Min.   :0.000   Min.   :1.468e+09   Length:3732        Length:3732        Length:3732        Length:3732       
 1st Qu.:0.000   1st Qu.:1.969e+09   Class :character   Class :character   Class :character   Class :character  
 Median :0.000   Median :2.051e+09   Mode  :character   Mode  :character   Mode  :character   Mode  :character  
 Mean   :1.883   Mean   :2.030e+09                                                                              
 3rd Qu.:4.000   3rd Qu.:2.204e+09                                                                              
 Max.   :4.000   Max.   :2.329e+09                                                                              

Sentiment Analysis

Data Preprocessing

  • Lemmatization
    • Lemmatization is a linguistic process that reduces a word to its base or dictionary form, known as a lemma.
    • lemmatization considers the context and transforms words to their meaningful base forms. For example, “running,” “ran,” and “runs” would all be lemmatized to “run.”
    • Lemmatization is important in NLP because it groups together different inflected forms of a word, allowing them to be analyzed as a single item.
preprocess_text <- function(text_column) {
  text_column <- tolower(text_column)                   # Convert text to lowercase
  text_column <- removePunctuation(text_column)         # Remove punctuation
  text_column <- removeNumbers(text_column)             # Remove numbers
  text_column <- removeWords(text_column, stopwords("en")) # Remove stopwords
  text_column <- stripWhitespace(text_column)           # Strip extra whitespace
  text_column <- lemmatize_strings(text_column)         # Apply lemmatization
  return(text_column)
}

Calculating Sentiment Scores

  • The sentiment score is a measure of the emotional tone behind a series of words. It is determined by analyzing the presence of words and phrases that are positively or negatively expressed.
  • In our case, we’ll use the syuzhet package, which relies on the NRC Word-Emotion Association Lexicon. This lexicon associates words with emotions (like joy, sadness, anger, etc.) and sentiment valence (positive or negative).
analyze_sentiment <- function(preprocessed_text) {
  sentiment_scores <- get_nrc_sentiment(preprocessed_text)
  sentiment_scores$overall_sentiment <- sentiment_scores$positive - sentiment_scores$negative
  return(sentiment_scores)
}

Working with the data

# Preprocess the text
test_result[, text_clean := preprocess_text(text)]

# Calculate sentiment scores
sentiment_results <- analyze_sentiment(test_result$text_clean)

Visualize the results

## Word Cloud

# Create a corpus  
docs <- Corpus(VectorSource(test_result$text_clean))
dtm <- TermDocumentMatrix(docs) 
matrix <- as.matrix(dtm) 
words <- sort(rowSums(matrix),decreasing=TRUE) 
df_words <- data.frame(word = names(words),freq=words)

set.seed(1234) # for reproducibility 

# wordcloud(words = df_words$word, freq = df_words$freq, min.freq = 1,
#           max.words=200, random.order=FALSE, rot.per=0.15,
#           colors=brewer.pal(8, "Set2"))
wordcloud2(data=df_words, size=4, color='random-dark')
colnames(sentiment_results)
 [1] "anger"             "anticipation"      "disgust"           "fear"              "joy"               "sadness"          
 [7] "surprise"          "trust"             "negative"          "positive"          "overall_sentiment"
# Create a data frame with average scores for each emotion
emotion_averages <- data.frame(
  Anger = mean(sentiment_results$anger),
  Anticipation = mean(sentiment_results$anticipation),
  Disgust = mean(sentiment_results$disgust),
  Fear = mean(sentiment_results$fear),
  Joy = mean(sentiment_results$joy),
  Sadness = mean(sentiment_results$sadness),
  Surprise = mean(sentiment_results$surprise),
  Trust = mean(sentiment_results$trust)
)

# Add a row for the minimum value for each category for plotting
emotion_df <- rbind(rep(0, ncol(emotion_averages)),rep(1, ncol(emotion_averages)), emotion_averages)


# Create the radar chart
fmsb::radarchart(emotion_df, axistype = 1,
        #custom polygon
        pcol=rgb(0.2,0.5,0.5,0.9) , pfcol=rgb(0.2,0.5,0.5,0.5) , plwd=4,
        #custom the grid
        cglcol="grey", cglty=1, axislabcol="grey", caxislabels=seq(0,1,5), cglwd=0.8,
        #custom labels
        vlcex=1.2)

NA
NA
# Chart
p <- ggplot(sentiment_results, aes(x=x) ) +
  # Top
  geom_density( aes(x = positive, y = ..density..), fill="#69b3a2" ) +
  geom_label( aes(x=4.5, y=0.25, label="Positive"), color="#69b3a2") +
  # Bottom
  geom_density( aes(x = negative, y = -..density..), fill= "#404080") +
  geom_label( aes(x=4.5, y=-0.25, label="Negative"), color="#404080") +
  theme_ipsum() +
  xlab("Tweets Sentimental Score")
p

# basic histogram
p <- ggplot(sentiment_results, aes(x=overall_sentiment)) + 
  geom_histogram(binwidth = 1, fill="#69b3a2", color="#e9ecef", alpha=0.9) +
  theme_ipsum() + 
  xlab("Overall Sentimental Score")
p

Check the trend

Step 1: Parse the Dates

test_result$date_parsed <- as.Date(strptime(test_result$date, format = "%a %b %d %H:%M:%S PDT %Y"))
summary(test_result$date_parsed)
        Min.      1st Qu.       Median         Mean      3rd Qu.         Max. 
"2009-04-06" "2009-05-29" "2009-06-05" "2009-06-02" "2009-06-17" "2009-06-25" 

Step 2: Aggregate Daily Averages

test_result$overall_sentiment <- sentiment_results$overall_sentiment
daily_avg_sentiment <- aggregate(overall_sentiment ~ date_parsed, data = test_result, mean)
summary(daily_avg_sentiment)
  date_parsed         overall_sentiment
 Min.   :2009-04-06   Min.   :-0.6667  
 1st Qu.:2009-05-09   1st Qu.: 0.2020  
 Median :2009-05-27   Median : 0.3158  
 Mean   :2009-05-23   Mean   : 0.3783  
 3rd Qu.:2009-06-10   3rd Qu.: 0.5294  
 Max.   :2009-06-25   Max.   : 2.0000  

Step 3: Plot the Data

# Fit a linear model
model <- lm(overall_sentiment ~ date_parsed, data = daily_avg_sentiment)

# Extract slope and R^2 value
slope <- coef(model)[2]
r_squared <- summary(model)$r.squared

# Your original ggplot code
p <- ggplot(daily_avg_sentiment, aes(x = date_parsed, y = overall_sentiment)) +
  geom_point() +
  geom_smooth(method = "lm", se = FALSE) +
  labs(title = "Daily Average Overall Sentiment Score",
       x = "Date",
       y = "Average Sentiment Score") +
  theme_minimal()

# Add annotations for slope and R^2
p + annotate("text", x = as.Date(quantile(as.numeric(daily_avg_sentiment$date_parsed),0.75)), y = 1.2, 
             label = paste("Slope:", round(slope, 4), "\nR^2:", round(r_squared, 4)),
             hjust = 0, vjust = 0)

NA
NA

Shiny APP

Query Tweets by keywords

screen1
screen1

Generate Word cloud and explore the word counts

screen2
screen2

Sentimental Analysis from 8 aspects

screen3
screen3

Temporal Anlaysis with tweets

screen4
screen4
LS0tDQp0aXRsZTogIlR3ZWV0cyBTZW50aW1lbnRhbCBBbmFseXNpcyINCmF1dGhvcjogIllhbGluIFlhbmciDQpkYXRlOiAiYHIgU3lzLkRhdGUoKWAiDQpvdXRwdXQ6DQogIGh0bWxfbm90ZWJvb2s6DQogICAgdG9jOiBUUlVFDQogICAgdG9jX2Zsb2F0OiBUUlVFDQogIHdvcmRfZG9jdW1lbnQ6DQogICAgdG9jOiBubw0KICAgIHRvY19kZXB0aDogJzMnDQotLS0NCiMjIFByb2plY3QgSW50cm8NCg0KVGhpcyBwcm9qZWN0IGZvY3VzZXMgb24gYW5hbHl6aW5nIHRoZSBzZW50aW1lbnRzIG9mIFR3aXR0ZXIgZGF0YSByZWxhdGVkIHRvIHNwZWNpZmljIGtleXdvcmRzLiBVdGlsaXppbmcgdGhlIGBzeXV6aGV0YCBwYWNrYWdlLCBpdCBjb25kdWN0cyBzZW50aW1lbnQgc2NvcmluZyBiYXNlZCBvbiB0aGUgY29tcHJlaGVuc2l2ZSBOUkMgV29yZC1FbW90aW9uIEFzc29jaWF0aW9uIExleGljb24uIFRoZSBhbmFseXNpcyBlbmNvbXBhc3NlcyBlaWdodCBkaXN0aW5jdCBlbW90aW9uYWwgZGltZW5zaW9uczogYW5nZXIsIGFudGljaXBhdGlvbiwgZGlzZ3VzdCwgZmVhciwgam95LCBzYWRuZXNzLCBzdXJwcmlzZSwgYW5kIHRydXN0LiANCg0KQWRkaXRpb25hbGx5LCB0aGUgcHJvamVjdCBmZWF0dXJlcyBhIHZpc3VhbGl6YXRpb24gY29tcG9uZW50IHRoYXQgdHJhY2tzIHRoZSBldm9sdXRpb24gb2YgdGhlIG92ZXJhbGwgc2VudGltZW50IHNjb3JlIG92ZXIgdGltZSwgb2ZmZXJpbmcgYSBkeW5hbWljIHZpZXcgb2YgcHVibGljIHNlbnRpbWVudCB0cmVuZHMgaW4gcmVsYXRpb24gdG8gdGhlIGNob3NlbiBrZXl3b3Jkcy4gDQoNCiMjIExvYWQgaGVscGVyIGxpYnJhcnkNCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoZGF0YS50YWJsZSk7bGlicmFyeSh0ZXh0c3RlbSk7bGlicmFyeShsdWJyaWRhdGUpDQpsaWJyYXJ5KHRtKTtsaWJyYXJ5KHN5dXpoZXQpO2xpYnJhcnkoZ2dwbG90Mik7bGlicmFyeShocmJydGhlbWVzKTtsaWJyYXJ5KHdvcmRjbG91ZCk7bGlicmFyeSh3b3JkY2xvdWQyKQ0KYGBgDQoNCg0KIyMgW1NlbnRpbWVudGFsIGRhdGFdKGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vZGF0YXNldHMva2F6YW5vdmEvc2VudGltZW50MTQwKSBQcmVwcm9jZXNzaW5nDQoNCiogKip0YXJnZXQqKjogdGhlIHBvbGFyaXR5IG9mIHRoZSB0d2VldCAoMCA9IG5lZ2F0aXZlLCAyID0gbmV1dHJhbCwgNCA9IHBvc2l0aXZlKQ0KKiAqKmlkcyoqOiBUaGUgaWQgb2YgdGhlIHR3ZWV0ICgyMDg3KQ0KKiAqKmRhdGUqKjogdGhlIGRhdGUgb2YgdGhlIHR3ZWV0IChTYXQgTWF5IDE2IDIzOjU4OjQ0IFVUQyAyMDA5KQ0KKiAqKmZsYWcqKjogVGhlIHF1ZXJ5IChgbHl4YCkuIElmIHRoZXJlIGlzIG5vIHF1ZXJ5LCB0aGVuIHRoaXMgdmFsdWUgaXMgYE5PX1FVRVJZYC4NCiogKip1c2VyKio6IHRoZSB1c2VyIHRoYXQgdHdlZXRlZCAoYHJvYm90aWNraWxsZG96cmApDQoqICoqdGV4dCoqOiB0aGUgdGV4dCBvZiB0aGUgdHdlZXQgKGBMeXhgIGlzIGNvb2wpDQoNCmBgYHtyfQ0KZGF0YSA8LSByZWFkLmNzdigiLi9pbnB1dHMuY3N2IiwgaGVhZGVyID0gRkFMU0UsIGNvbC5uYW1lcyA9IGMoInRhcmdldCIsICJpZHMiLCAiZGF0ZSIsICJmbGFnIiwgInVzZXIiLCAidGV4dCIpLCBlbmNvZGluZyA9ICJVVEYtOCIpDQpzdW1tYXJ5KGRhdGEpDQpgYGANCg0KIyMjIFNlYXJjaCB0d2VldHMgY29udGFpbnMgY2VydGFpbiBrZXl3b3Jkcw0KDQpgYGB7cn0NCnNlYXJjaF90d2VldHMgPC0gZnVuY3Rpb24odHdlZXRzX2RmLCBrZXl3b3JkLCBpZ25vcmUuY2FzZSA9IFRSVUUpIHsNCiAgdHJ5Q2F0Y2goew0KICAgICMgRW5zdXJlIHR3ZWV0c19kZiBpcyBhIGRhdGEudGFibGUNCiAgICBzZXREVCh0d2VldHNfZGYpDQogICAgIyBWZWN0b3JpemVkIHNlYXJjaCBmb3Iga2V5d29yZA0KICAgIHN1cHByZXNzV2FybmluZ3Moa2V5d29yZF9maWx0ZXJlZCA8LSB0d2VldHNfZGZbZ3JlcGwoa2V5d29yZCwgdGV4dCwgaWdub3JlLmNhc2UgPSBpZ25vcmUuY2FzZSldKQ0KICAgIHJldHVybihrZXl3b3JkX2ZpbHRlcmVkKQ0KICB9LCBlcnJvciA9IGZ1bmN0aW9uKGUpIHsNCiAgICBzdG9wKCJBbiBlcnJvciBvY2N1cnJlZDogIiwgY29uZGl0aW9uTWVzc2FnZShlKSkNCiAgfSkNCn0NCmBgYA0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KdGVzdF9yZXN1bHQgPC0gc2VhcmNoX3R3ZWV0cyhkYXRhLCJhcHBsZSIpDQpzdW1tYXJ5KHRlc3RfcmVzdWx0KQ0KYGBgDQoNCiMjIyBTZW50aW1lbnQgQW5hbHlzaXMNCg0KIyMjIyBEYXRhIFByZXByb2Nlc3NpbmcNCg0KKiBMZW1tYXRpemF0aW9uDQogICogYExlbW1hdGl6YXRpb25gIGlzIGEgbGluZ3Vpc3RpYyBwcm9jZXNzIHRoYXQgcmVkdWNlcyBhIHdvcmQgdG8gaXRzIGJhc2Ugb3IgZGljdGlvbmFyeSBmb3JtLCBrbm93biBhcyBhIGBsZW1tYWAuDQogICogYGxlbW1hdGl6YXRpb25gIGNvbnNpZGVycyB0aGUgY29udGV4dCBhbmQgdHJhbnNmb3JtcyB3b3JkcyB0byB0aGVpciBtZWFuaW5nZnVsIGJhc2UgZm9ybXMuIEZvciBleGFtcGxlLCAicnVubmluZywiICJyYW4sIiBhbmQgInJ1bnMiIHdvdWxkIGFsbCBiZSBsZW1tYXRpemVkIHRvICJydW4uIg0KICAqIGBMZW1tYXRpemF0aW9uYCBpcyBpbXBvcnRhbnQgaW4gTkxQIGJlY2F1c2UgaXQgZ3JvdXBzIHRvZ2V0aGVyIGRpZmZlcmVudCBpbmZsZWN0ZWQgZm9ybXMgb2YgYSB3b3JkLCBhbGxvd2luZyB0aGVtIHRvIGJlIGFuYWx5emVkIGFzIGEgc2luZ2xlIGl0ZW0uDQoNCmBgYHtyfQ0KcHJlcHJvY2Vzc190ZXh0IDwtIGZ1bmN0aW9uKHRleHRfY29sdW1uKSB7DQogIHRleHRfY29sdW1uIDwtIHRvbG93ZXIodGV4dF9jb2x1bW4pICAgICAgICAgICAgICAgICAgICMgQ29udmVydCB0ZXh0IHRvIGxvd2VyY2FzZQ0KICB0ZXh0X2NvbHVtbiA8LSByZW1vdmVQdW5jdHVhdGlvbih0ZXh0X2NvbHVtbikgICAgICAgICAjIFJlbW92ZSBwdW5jdHVhdGlvbg0KICB0ZXh0X2NvbHVtbiA8LSByZW1vdmVOdW1iZXJzKHRleHRfY29sdW1uKSAgICAgICAgICAgICAjIFJlbW92ZSBudW1iZXJzDQogIHRleHRfY29sdW1uIDwtIHJlbW92ZVdvcmRzKHRleHRfY29sdW1uLCBzdG9wd29yZHMoImVuIikpICMgUmVtb3ZlIHN0b3B3b3Jkcw0KICB0ZXh0X2NvbHVtbiA8LSBzdHJpcFdoaXRlc3BhY2UodGV4dF9jb2x1bW4pICAgICAgICAgICAjIFN0cmlwIGV4dHJhIHdoaXRlc3BhY2UNCiAgdGV4dF9jb2x1bW4gPC0gbGVtbWF0aXplX3N0cmluZ3ModGV4dF9jb2x1bW4pICAgICAgICAgIyBBcHBseSBsZW1tYXRpemF0aW9uDQogIHJldHVybih0ZXh0X2NvbHVtbikNCn0NCmBgYA0KDQojIyMjIENhbGN1bGF0aW5nIFNlbnRpbWVudCBTY29yZXMNCg0KKiBUaGUgc2VudGltZW50IHNjb3JlIGlzIGEgbWVhc3VyZSBvZiB0aGUgZW1vdGlvbmFsIHRvbmUgYmVoaW5kIGEgc2VyaWVzIG9mIHdvcmRzLiBJdCBpcyBkZXRlcm1pbmVkIGJ5IGFuYWx5emluZyB0aGUgcHJlc2VuY2Ugb2Ygd29yZHMgYW5kIHBocmFzZXMgdGhhdCBhcmUgYHBvc2l0aXZlbHlgIG9yIGBuZWdhdGl2ZWx5YCBleHByZXNzZWQuDQoqIEluIG91ciBjYXNlLCB3ZSdsbCB1c2UgdGhlIGBzeXV6aGV0YCBwYWNrYWdlLCB3aGljaCByZWxpZXMgb24gdGhlIE5SQyBXb3JkLUVtb3Rpb24gQXNzb2NpYXRpb24gTGV4aWNvbi4gVGhpcyBsZXhpY29uIGFzc29jaWF0ZXMgd29yZHMgd2l0aCBgZW1vdGlvbnNgICgqKmxpa2Ugam95LCBzYWRuZXNzLCBhbmdlciwgZXRjLioqKSBhbmQgYHNlbnRpbWVudGAgdmFsZW5jZSAoKipwb3NpdGl2ZSBvciBuZWdhdGl2ZSoqKS4NCg0KYGBge3J9DQphbmFseXplX3NlbnRpbWVudCA8LSBmdW5jdGlvbihwcmVwcm9jZXNzZWRfdGV4dCkgew0KICBzZW50aW1lbnRfc2NvcmVzIDwtIGdldF9ucmNfc2VudGltZW50KHByZXByb2Nlc3NlZF90ZXh0KQ0KICBzZW50aW1lbnRfc2NvcmVzJG92ZXJhbGxfc2VudGltZW50IDwtIHNlbnRpbWVudF9zY29yZXMkcG9zaXRpdmUgLSBzZW50aW1lbnRfc2NvcmVzJG5lZ2F0aXZlDQogIHJldHVybihzZW50aW1lbnRfc2NvcmVzKQ0KfQ0KYGBgDQoNCiMjIyMgV29ya2luZyB3aXRoIHRoZSBkYXRhDQoNCmBgYHtyfQ0KIyBQcmVwcm9jZXNzIHRoZSB0ZXh0DQp0ZXN0X3Jlc3VsdFssIHRleHRfY2xlYW4gOj0gcHJlcHJvY2Vzc190ZXh0KHRleHQpXQ0KDQojIENhbGN1bGF0ZSBzZW50aW1lbnQgc2NvcmVzDQpzZW50aW1lbnRfcmVzdWx0cyA8LSBhbmFseXplX3NlbnRpbWVudCh0ZXN0X3Jlc3VsdCR0ZXh0X2NsZWFuKQ0KYGBgDQoNCiMjIyMgVmlzdWFsaXplIHRoZSByZXN1bHRzDQoNCmBgYHtyfQ0KIyMgV29yZCBDbG91ZA0KDQojIENyZWF0ZSBhIGNvcnB1cyAgDQpkb2NzIDwtIENvcnB1cyhWZWN0b3JTb3VyY2UodGVzdF9yZXN1bHQkdGV4dF9jbGVhbikpDQpkdG0gPC0gVGVybURvY3VtZW50TWF0cml4KGRvY3MpIA0KbWF0cml4IDwtIGFzLm1hdHJpeChkdG0pIA0Kd29yZHMgPC0gc29ydChyb3dTdW1zKG1hdHJpeCksZGVjcmVhc2luZz1UUlVFKSANCmRmX3dvcmRzIDwtIGRhdGEuZnJhbWUod29yZCA9IG5hbWVzKHdvcmRzKSxmcmVxPXdvcmRzKQ0KYGBgDQoNCmBgYHtyIGZpZy5oZWlnaHQ9NSwgZmlnLndpZHRoPTV9DQoNCnNldC5zZWVkKDEyMzQpICMgZm9yIHJlcHJvZHVjaWJpbGl0eSANCg0KIyB3b3JkY2xvdWQod29yZHMgPSBkZl93b3JkcyR3b3JkLCBmcmVxID0gZGZfd29yZHMkZnJlcSwgbWluLmZyZXEgPSAxLA0KIyAgICAgICAgICAgbWF4LndvcmRzPTIwMCwgcmFuZG9tLm9yZGVyPUZBTFNFLCByb3QucGVyPTAuMTUsDQojICAgICAgICAgICBjb2xvcnM9YnJld2VyLnBhbCg4LCAiU2V0MiIpKQ0Kd29yZGNsb3VkMihkYXRhPWRmX3dvcmRzLCBzaXplPTQsIGNvbG9yPSdyYW5kb20tZGFyaycpDQpgYGANCg0KDQoNCmBgYHtyfQ0KY29sbmFtZXMoc2VudGltZW50X3Jlc3VsdHMpDQpgYGANCg0KDQpgYGB7ciBmaWcuaGVpZ2h0PTUsIGZpZy53aWR0aD01fQ0KIyBDcmVhdGUgYSBkYXRhIGZyYW1lIHdpdGggYXZlcmFnZSBzY29yZXMgZm9yIGVhY2ggZW1vdGlvbg0KZW1vdGlvbl9hdmVyYWdlcyA8LSBkYXRhLmZyYW1lKA0KICBBbmdlciA9IG1lYW4oc2VudGltZW50X3Jlc3VsdHMkYW5nZXIpLA0KICBBbnRpY2lwYXRpb24gPSBtZWFuKHNlbnRpbWVudF9yZXN1bHRzJGFudGljaXBhdGlvbiksDQogIERpc2d1c3QgPSBtZWFuKHNlbnRpbWVudF9yZXN1bHRzJGRpc2d1c3QpLA0KICBGZWFyID0gbWVhbihzZW50aW1lbnRfcmVzdWx0cyRmZWFyKSwNCiAgSm95ID0gbWVhbihzZW50aW1lbnRfcmVzdWx0cyRqb3kpLA0KICBTYWRuZXNzID0gbWVhbihzZW50aW1lbnRfcmVzdWx0cyRzYWRuZXNzKSwNCiAgU3VycHJpc2UgPSBtZWFuKHNlbnRpbWVudF9yZXN1bHRzJHN1cnByaXNlKSwNCiAgVHJ1c3QgPSBtZWFuKHNlbnRpbWVudF9yZXN1bHRzJHRydXN0KQ0KKQ0KDQojIEFkZCBhIHJvdyBmb3IgdGhlIG1pbmltdW0gdmFsdWUgZm9yIGVhY2ggY2F0ZWdvcnkgZm9yIHBsb3R0aW5nDQplbW90aW9uX2RmIDwtIHJiaW5kKHJlcCgwLCBuY29sKGVtb3Rpb25fYXZlcmFnZXMpKSxyZXAoMSwgbmNvbChlbW90aW9uX2F2ZXJhZ2VzKSksIGVtb3Rpb25fYXZlcmFnZXMpDQoNCg0KIyBDcmVhdGUgdGhlIHJhZGFyIGNoYXJ0DQpmbXNiOjpyYWRhcmNoYXJ0KGVtb3Rpb25fZGYsIGF4aXN0eXBlID0gMSwNCiAgICAgICAgI2N1c3RvbSBwb2x5Z29uDQogICAgICAgIHBjb2w9cmdiKDAuMiwwLjUsMC41LDAuOSkgLCBwZmNvbD1yZ2IoMC4yLDAuNSwwLjUsMC41KSAsIHBsd2Q9NCwNCiAgICAgICAgI2N1c3RvbSB0aGUgZ3JpZA0KICAgICAgICBjZ2xjb2w9ImdyZXkiLCBjZ2x0eT0xLCBheGlzbGFiY29sPSJncmV5IiwgY2F4aXNsYWJlbHM9c2VxKDAsMSw1KSwgY2dsd2Q9MC44LA0KICAgICAgICAjY3VzdG9tIGxhYmVscw0KICAgICAgICB2bGNleD0xLjIpDQoNCg0KYGBgDQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQojIENoYXJ0DQpwIDwtIGdncGxvdChzZW50aW1lbnRfcmVzdWx0cywgYWVzKHg9eCkgKSArDQogICMgVG9wDQogIGdlb21fZGVuc2l0eSggYWVzKHggPSBwb3NpdGl2ZSwgeSA9IC4uZGVuc2l0eS4uKSwgZmlsbD0iIzY5YjNhMiIgKSArDQogIGdlb21fbGFiZWwoIGFlcyh4PTQuNSwgeT0wLjI1LCBsYWJlbD0iUG9zaXRpdmUiKSwgY29sb3I9IiM2OWIzYTIiKSArDQogICMgQm90dG9tDQogIGdlb21fZGVuc2l0eSggYWVzKHggPSBuZWdhdGl2ZSwgeSA9IC0uLmRlbnNpdHkuLiksIGZpbGw9ICIjNDA0MDgwIikgKw0KICBnZW9tX2xhYmVsKCBhZXMoeD00LjUsIHk9LTAuMjUsIGxhYmVsPSJOZWdhdGl2ZSIpLCBjb2xvcj0iIzQwNDA4MCIpICsNCiAgdGhlbWVfaXBzdW0oKSArDQogIHhsYWIoIlR3ZWV0cyBTZW50aW1lbnRhbCBTY29yZSIpDQpwDQpgYGANCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCiMgYmFzaWMgaGlzdG9ncmFtDQpwIDwtIGdncGxvdChzZW50aW1lbnRfcmVzdWx0cywgYWVzKHg9b3ZlcmFsbF9zZW50aW1lbnQpKSArIA0KICBnZW9tX2hpc3RvZ3JhbShiaW53aWR0aCA9IDEsIGZpbGw9IiM2OWIzYTIiLCBjb2xvcj0iI2U5ZWNlZiIsIGFscGhhPTAuOSkgKw0KICB0aGVtZV9pcHN1bSgpICsgDQogIHhsYWIoIk92ZXJhbGwgU2VudGltZW50YWwgU2NvcmUiKQ0KcA0KYGBgDQojIyMgQ2hlY2sgdGhlIHRyZW5kDQoNCiMjIyMgU3RlcCAxOiBQYXJzZSB0aGUgRGF0ZXMNCmBgYHtyfQ0KdGVzdF9yZXN1bHQkZGF0ZV9wYXJzZWQgPC0gYXMuRGF0ZShzdHJwdGltZSh0ZXN0X3Jlc3VsdCRkYXRlLCBmb3JtYXQgPSAiJWEgJWIgJWQgJUg6JU06JVMgUERUICVZIikpDQpzdW1tYXJ5KHRlc3RfcmVzdWx0JGRhdGVfcGFyc2VkKQ0KYGBgDQojIyMjIFN0ZXAgMjogQWdncmVnYXRlIERhaWx5IEF2ZXJhZ2VzDQoNCmBgYHtyfQ0KdGVzdF9yZXN1bHQkb3ZlcmFsbF9zZW50aW1lbnQgPC0gc2VudGltZW50X3Jlc3VsdHMkb3ZlcmFsbF9zZW50aW1lbnQNCmRhaWx5X2F2Z19zZW50aW1lbnQgPC0gYWdncmVnYXRlKG92ZXJhbGxfc2VudGltZW50IH4gZGF0ZV9wYXJzZWQsIGRhdGEgPSB0ZXN0X3Jlc3VsdCwgbWVhbikNCnN1bW1hcnkoZGFpbHlfYXZnX3NlbnRpbWVudCkNCmBgYA0KIyMjIyBTdGVwIDM6IFBsb3QgdGhlIERhdGENCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCiMgRml0IGEgbGluZWFyIG1vZGVsDQptb2RlbCA8LSBsbShvdmVyYWxsX3NlbnRpbWVudCB+IGRhdGVfcGFyc2VkLCBkYXRhID0gZGFpbHlfYXZnX3NlbnRpbWVudCkNCg0KIyBFeHRyYWN0IHNsb3BlIGFuZCBSXjIgdmFsdWUNCnNsb3BlIDwtIGNvZWYobW9kZWwpWzJdDQpyX3NxdWFyZWQgPC0gc3VtbWFyeShtb2RlbCkkci5zcXVhcmVkDQoNCiMgWW91ciBvcmlnaW5hbCBnZ3Bsb3QgY29kZQ0KcCA8LSBnZ3Bsb3QoZGFpbHlfYXZnX3NlbnRpbWVudCwgYWVzKHggPSBkYXRlX3BhcnNlZCwgeSA9IG92ZXJhbGxfc2VudGltZW50KSkgKw0KICBnZW9tX3BvaW50KCkgKw0KICBnZW9tX3Ntb290aChtZXRob2QgPSAibG0iLCBzZSA9IEZBTFNFKSArDQogIGxhYnModGl0bGUgPSAiRGFpbHkgQXZlcmFnZSBPdmVyYWxsIFNlbnRpbWVudCBTY29yZSIsDQogICAgICAgeCA9ICJEYXRlIiwNCiAgICAgICB5ID0gIkF2ZXJhZ2UgU2VudGltZW50IFNjb3JlIikgKw0KICB0aGVtZV9taW5pbWFsKCkNCg0KIyBBZGQgYW5ub3RhdGlvbnMgZm9yIHNsb3BlIGFuZCBSXjINCnAgKyBhbm5vdGF0ZSgidGV4dCIsIHggPSBhcy5EYXRlKHF1YW50aWxlKGFzLm51bWVyaWMoZGFpbHlfYXZnX3NlbnRpbWVudCRkYXRlX3BhcnNlZCksMC43NSkpLCB5ID0gMS4yLCANCiAgICAgICAgICAgICBsYWJlbCA9IHBhc3RlKCJTbG9wZToiLCByb3VuZChzbG9wZSwgNCksICJcblJeMjoiLCByb3VuZChyX3NxdWFyZWQsIDQpKSwNCiAgICAgICAgICAgICBoanVzdCA9IDAsIHZqdXN0ID0gMCkNCg0KDQpgYGANCg0KIyMgU2hpbnkgQVBQDQoNCiMjIyBRdWVyeSBUd2VldHMgYnkga2V5d29yZHMNCg0KIVtzY3JlZW4xXShhc3NldHMvU2VjMS5wbmcpDQoNCiMjIyBHZW5lcmF0ZSBXb3JkIGNsb3VkIGFuZCBleHBsb3JlIHRoZSB3b3JkIGNvdW50cw0KDQohW3NjcmVlbjJdKGFzc2V0cy9TZWMyLnBuZykNCg0KIyMjIFNlbnRpbWVudGFsIEFuYWx5c2lzIGZyb20gOCBhc3BlY3RzDQoNCiFbc2NyZWVuM10oYXNzZXRzL1NlYzMucG5nKQ0KDQojIyMgVGVtcG9yYWwgQW5sYXlzaXMgd2l0aCB0d2VldHMNCg0KIVtzY3JlZW40XShhc3NldHMvU2VjNC5wbmcpDQoNCg0KDQoNCg0KDQoNCg0K