• Project Intro
  • Load helper library
  • Sentimental data Preprocessing
    • Search tweets contains certain keywords
    • Sentiment Analysis
    • Check the trend
  • Shiny APP
    • Query Tweets by keywords
    • Generate Word cloud and explore the word counts
    • Sentimental Analysis from 8 aspects
    • Temporal Anlaysis with tweets

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