Overview

The goal of this lab is to practice parsing through a text file to obtain chess tournament data, divided by individual players. Some skills used are regex matching, file handling, and data handling functions.

library(tidyverse)
library(openintro)
library(RCurl)
library(stringr)

Load tournament info txt from cloud hosted source

Start by loading the text file called “tournamentinfo.txt” to be parsed. Split the text into lines to make it easier to identify each player. Indices are saved to denote which lines belong to a player and simplify the look up process later. The regex matches lines that start with a pair number and has “|” in between each piece of information. Note that the pair number is not saved because as long as the lines are parsed in order, it can be derived.

tournament_info_url <- 'https://raw.githubusercontent.com/Megabuster/Data607/refs/heads/main/data/project1/tournamentinfo.txt'
raw_text <- getURL(tournament_info_url)
lines <- readLines(textConnection((raw_text)))
indices <- str_which(lines, '([1-9])(.)+(|)(.)+(|)(.)+([.])(.)+(|)(.)+(|)(.)+(|)(.)+(|)(.)+(|)(.)+(|)')

Create data frame columns

As the tournament info text file is not in a usable data set form, we need to parse it for Player’s Name, Player’s State, Total Number of Points, Player’s Pre-Rating, and Average Pre Chess Rating of Opponents. Each category can be saved with its own vector. The “opponent_ids” variable is notably not part of the columns of the final csv file. It is there to help fascilitate the creating of the average opponent pre-ratings column.

players <- vector()
states <- vector()
points <- numeric()
pre_ratings <- numeric()
opp_pre_ratings <- numeric()

opponent_ids <- list()

Parse raw text

These steps are regex heavy to identify each key piece of information. The loop iterates once per player using the “indices” saved in the initial step. “Player_index” is incremented manually to match each player’s pair number. A “line” is equal to “lines[i]” which means the index of a line containing a player’s pair number, name, and match data. The format of the original file has the remaining player info including Elo on the following line which is labeled below as “line2”.

The ratings section of the player’s information always has a “->” to denote the pre and post ratings. This means the desired number, the pre-rating, will be on the left of the arrow. The next regex consideration is that some ratings have a “P” in them. We only want the number that comes before the P. A notable difference between the “P” and non-P ratings is that “P” ratings are always attached to the Elo rating directly while non-P ratings have a variable amount of space in between. The “(\s)*” pattern match accounts for that.

The last 7 digits of the first player line contains all the matches and the opponents’ pair numbers. The results are outside of the scope of this project, so collect just the numbers and store them all in “opponent_ids”.

player_index = 1
for (i in indices) {
  line <- lines[i]
  line2 <- lines[i+1]
  new_split <- trimws(unlist(strsplit(line, '\\|')))
  new_split2 <- trimws(unlist(strsplit(line2, '\\|')))

  players <- append(players, new_split[2])
  states <- append(states, new_split2[1])
  points <- append(points, new_split[3])

  if (str_detect(new_split2[2], '[0-9]+?(?=P)')) {
    pre_ratings <- append(pre_ratings, str_extract(new_split2[2], '[0-9]+?(?=P)'))
  } else {
    pre_ratings <- append(pre_ratings, str_extract(new_split2[2], '[0-9]+?(?=(\\s)*->)'))
  }
  
  new_opponent_ids <- new_split[4:10]
  new_opponent_ids_vec <- numeric()

  for (match in new_opponent_ids) {
    if (str_detect(match, '[0-9]+')) {
      new_opponent_ids_vec <- append(new_opponent_ids_vec, str_extract(match, '[0-9]+'))
    }
  }

  opponent_ids[[player_index]] <- new_opponent_ids_vec
  player_index <- player_index + 1
}

Calculate average opponent ratings

Using “opponent_ids”, look up the ratings for every opponent associated with a player’s pair number. For example, Gary Hua is the first player and faced 7 opponents. None of the ratings had decimals in them, so I opted to round each mean and convert them into integers before saving the column.

for (player_opp_ids in 1:length(opponent_ids)) {

  ratings_vec <- numeric()
  for (opp_id in opponent_ids[player_opp_ids]) {
    ratings_vec <- append(ratings_vec, as.numeric(pre_ratings[as.numeric(opp_id)]))
  }
  avg_opp_rating <- as.integer(round(mean(ratings_vec)))
  opp_pre_ratings <- append(opp_pre_ratings, avg_opp_rating)
}

Collect the data in a data frame

With all of the data already organized into individual columns, create the final data frame. A sample of the data is shown below.

tournament_players <- data.frame(
  name = players,
  state = states,
  total_points = points,
  pre_rating = pre_ratings,
  avg_opp_pre_rating = opp_pre_ratings
)
head(tournament_players, 10)
##                   name state total_points pre_rating avg_opp_pre_rating
## 1             GARY HUA    ON          6.0       1794               1605
## 2      DAKSHESH DARURI    MI          6.0       1553               1469
## 3         ADITYA BAJAJ    MI          6.0       1384               1564
## 4  PATRICK H SCHILLING    MI          5.5       1716               1574
## 5           HANSHI ZUO    MI          5.5       1655               1501
## 6          HANSEN SONG    OH          5.0       1686               1519
## 7    GARY DEE SWATHELL    MI          5.0       1649               1372
## 8     EZEKIEL HOUGHTON    MI          5.0       1641               1468
## 9          STEFANO LEE    ON          5.0       1411               1523
## 10           ANVIT RAO    MI          5.0       1365               1554

Save to csv

Save the results to a csv without any quotes around the strings and removing the pair number via the row.names argument.

write.csv(x = tournament_players, file = 'tournament_players.csv', quote = FALSE, row.names = FALSE)

Conclusions

Many assumptions were made according to the exact layout of “tournamentinfo.txt”. Rows were laid out consistently for each player. Columns always had a “|” between them. Total points always had a “.” regardless of the number.

This kind of parsing is not good for reusability, but it also represents a realistic scenario. When scraping data, the format it is in might not always be convenient. There will often be times where a custom solution is needed to collect that information.

LS0tDQp0aXRsZTogIkRBVEEgNjA3IFByb2plY3QgMSINCmF1dGhvcjogIkxhd3JlbmNlIFl1Ig0KZGF0ZTogImByIFN5cy5EYXRlKClgIg0Kb3V0cHV0OiBvcGVuaW50cm86OmxhYl9yZXBvcnQNCi0tLQ0KDQojIyMgT3ZlcnZpZXcNCg0KVGhlIGdvYWwgb2YgdGhpcyBsYWIgaXMgdG8gcHJhY3RpY2UgcGFyc2luZyB0aHJvdWdoIGEgdGV4dCBmaWxlIHRvIG9idGFpbiBjaGVzcyB0b3VybmFtZW50IGRhdGEsIGRpdmlkZWQgYnkgaW5kaXZpZHVhbCBwbGF5ZXJzLiBTb21lIHNraWxscyB1c2VkIGFyZSByZWdleCBtYXRjaGluZywgZmlsZSBoYW5kbGluZywgYW5kIGRhdGEgaGFuZGxpbmcgZnVuY3Rpb25zLg0KYGBge3IgbG9hZC1wYWNrYWdlcywgbWVzc2FnZT1GQUxTRX0NCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeShvcGVuaW50cm8pDQpsaWJyYXJ5KFJDdXJsKQ0KbGlicmFyeShzdHJpbmdyKQ0KYGBgDQoNCiMjIyBMb2FkIHRvdXJuYW1lbnQgaW5mbyB0eHQgZnJvbSBjbG91ZCBob3N0ZWQgc291cmNlDQoNClN0YXJ0IGJ5IGxvYWRpbmcgdGhlIHRleHQgZmlsZSBjYWxsZWQgInRvdXJuYW1lbnRpbmZvLnR4dCIgdG8gYmUgcGFyc2VkLiBTcGxpdCB0aGUgdGV4dCBpbnRvIGxpbmVzIHRvIG1ha2UgaXQgZWFzaWVyIHRvIGlkZW50aWZ5IGVhY2ggcGxheWVyLiBJbmRpY2VzIGFyZSBzYXZlZCB0byBkZW5vdGUgd2hpY2ggbGluZXMgYmVsb25nIHRvIGEgcGxheWVyIGFuZCBzaW1wbGlmeSB0aGUgbG9vayB1cCBwcm9jZXNzIGxhdGVyLiBUaGUgcmVnZXggbWF0Y2hlcyBsaW5lcyB0aGF0IHN0YXJ0IHdpdGggYSBwYWlyIG51bWJlciBhbmQgaGFzICJ8IiBpbiBiZXR3ZWVuIGVhY2ggcGllY2Ugb2YgaW5mb3JtYXRpb24uIE5vdGUgdGhhdCB0aGUgcGFpciBudW1iZXIgaXMgbm90IHNhdmVkIGJlY2F1c2UgYXMgbG9uZyBhcyB0aGUgbGluZXMgYXJlIHBhcnNlZCBpbiBvcmRlciwgaXQgY2FuIGJlIGRlcml2ZWQuDQpgYGB7ciBnZXQtdG91cm5hbWVudC1pbmZvfQ0KdG91cm5hbWVudF9pbmZvX3VybCA8LSAnaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL01lZ2FidXN0ZXIvRGF0YTYwNy9yZWZzL2hlYWRzL21haW4vZGF0YS9wcm9qZWN0MS90b3VybmFtZW50aW5mby50eHQnDQpyYXdfdGV4dCA8LSBnZXRVUkwodG91cm5hbWVudF9pbmZvX3VybCkNCmxpbmVzIDwtIHJlYWRMaW5lcyh0ZXh0Q29ubmVjdGlvbigocmF3X3RleHQpKSkNCmluZGljZXMgPC0gc3RyX3doaWNoKGxpbmVzLCAnKFsxLTldKSguKSsofCkoLikrKHwpKC4pKyhbLl0pKC4pKyh8KSguKSsofCkoLikrKHwpKC4pKyh8KSguKSsofCkoLikrKHwpJykNCmBgYA0KDQojIyMgQ3JlYXRlIGRhdGEgZnJhbWUgY29sdW1ucw0KDQpBcyB0aGUgdG91cm5hbWVudCBpbmZvIHRleHQgZmlsZSBpcyBub3QgaW4gYSB1c2FibGUgZGF0YSBzZXQgZm9ybSwgd2UgbmVlZCB0byBwYXJzZSBpdCBmb3IgUGxheWVy4oCZcyBOYW1lLCBQbGF5ZXLigJlzIFN0YXRlLCBUb3RhbCBOdW1iZXIgb2YgUG9pbnRzLCBQbGF5ZXLigJlzIFByZS1SYXRpbmcsIGFuZCBBdmVyYWdlIFByZSBDaGVzcyBSYXRpbmcgb2YgT3Bwb25lbnRzLiBFYWNoIGNhdGVnb3J5IGNhbiBiZSBzYXZlZCB3aXRoIGl0cyBvd24gdmVjdG9yLiBUaGUgIm9wcG9uZW50X2lkcyIgdmFyaWFibGUgaXMgbm90YWJseSBub3QgcGFydCBvZiB0aGUgY29sdW1ucyBvZiB0aGUgZmluYWwgY3N2IGZpbGUuIEl0IGlzIHRoZXJlIHRvIGhlbHAgZmFzY2lsaXRhdGUgdGhlIGNyZWF0aW5nIG9mIHRoZSBhdmVyYWdlIG9wcG9uZW50IHByZS1yYXRpbmdzIGNvbHVtbi4NCmBgYHtyIGNyZWF0ZS1jb2x1bW5zfQ0KcGxheWVycyA8LSB2ZWN0b3IoKQ0Kc3RhdGVzIDwtIHZlY3RvcigpDQpwb2ludHMgPC0gbnVtZXJpYygpDQpwcmVfcmF0aW5ncyA8LSBudW1lcmljKCkNCm9wcF9wcmVfcmF0aW5ncyA8LSBudW1lcmljKCkNCg0Kb3Bwb25lbnRfaWRzIDwtIGxpc3QoKQ0KYGBgDQoNCiMjIyBQYXJzZSByYXcgdGV4dA0KDQpUaGVzZSBzdGVwcyBhcmUgcmVnZXggaGVhdnkgdG8gaWRlbnRpZnkgZWFjaCBrZXkgcGllY2Ugb2YgaW5mb3JtYXRpb24uIFRoZSBsb29wIGl0ZXJhdGVzIG9uY2UgcGVyIHBsYXllciB1c2luZyB0aGUgImluZGljZXMiIHNhdmVkIGluIHRoZSBpbml0aWFsIHN0ZXAuICJQbGF5ZXJfaW5kZXgiIGlzIGluY3JlbWVudGVkIG1hbnVhbGx5IHRvIG1hdGNoIGVhY2ggcGxheWVyJ3MgcGFpciBudW1iZXIuIEEgImxpbmUiIGlzIGVxdWFsIHRvICJsaW5lc1tpXSIgd2hpY2ggbWVhbnMgdGhlIGluZGV4IG9mIGEgbGluZSBjb250YWluaW5nIGEgcGxheWVyJ3MgcGFpciBudW1iZXIsIG5hbWUsIGFuZCBtYXRjaCBkYXRhLiBUaGUgZm9ybWF0IG9mIHRoZSBvcmlnaW5hbCBmaWxlIGhhcyB0aGUgcmVtYWluaW5nIHBsYXllciBpbmZvIGluY2x1ZGluZyBFbG8gb24gdGhlIGZvbGxvd2luZyBsaW5lIHdoaWNoIGlzIGxhYmVsZWQgYmVsb3cgYXMgImxpbmUyIi4gDQoNClRoZSByYXRpbmdzIHNlY3Rpb24gb2YgdGhlIHBsYXllcidzIGluZm9ybWF0aW9uIGFsd2F5cyBoYXMgYSAiLT4iIHRvIGRlbm90ZSB0aGUgcHJlIGFuZCBwb3N0IHJhdGluZ3MuIFRoaXMgbWVhbnMgdGhlIGRlc2lyZWQgbnVtYmVyLCB0aGUgcHJlLXJhdGluZywgd2lsbCBiZSBvbiB0aGUgbGVmdCBvZiB0aGUgYXJyb3cuIFRoZSBuZXh0IHJlZ2V4IGNvbnNpZGVyYXRpb24gaXMgdGhhdCBzb21lIHJhdGluZ3MgaGF2ZSBhICJQIiBpbiB0aGVtLiBXZSBvbmx5IHdhbnQgdGhlIG51bWJlciB0aGF0IGNvbWVzIGJlZm9yZSB0aGUgUC4gQSBub3RhYmxlIGRpZmZlcmVuY2UgYmV0d2VlbiB0aGUgIlAiIGFuZCBub24tUCByYXRpbmdzIGlzIHRoYXQgIlAiIHJhdGluZ3MgYXJlIGFsd2F5cyBhdHRhY2hlZCB0byB0aGUgRWxvIHJhdGluZyBkaXJlY3RseSB3aGlsZSBub24tUCByYXRpbmdzIGhhdmUgYSB2YXJpYWJsZSBhbW91bnQgb2Ygc3BhY2UgaW4gYmV0d2Vlbi4gVGhlICIoXFxzKSoiIHBhdHRlcm4gbWF0Y2ggYWNjb3VudHMgZm9yIHRoYXQuDQoNClRoZSBsYXN0IDcgZGlnaXRzIG9mIHRoZSBmaXJzdCBwbGF5ZXIgbGluZSBjb250YWlucyBhbGwgdGhlIG1hdGNoZXMgYW5kIHRoZSBvcHBvbmVudHMnIHBhaXIgbnVtYmVycy4gVGhlIHJlc3VsdHMgYXJlIG91dHNpZGUgb2YgdGhlIHNjb3BlIG9mIHRoaXMgcHJvamVjdCwgc28gY29sbGVjdCBqdXN0IHRoZSBudW1iZXJzIGFuZCBzdG9yZSB0aGVtIGFsbCBpbiAib3Bwb25lbnRfaWRzIi4NCmBgYHtyIHRvdXJuYW1lbnQtaW5mby1jb2xsZWN0fQ0KcGxheWVyX2luZGV4ID0gMQ0KZm9yIChpIGluIGluZGljZXMpIHsNCiAgbGluZSA8LSBsaW5lc1tpXQ0KICBsaW5lMiA8LSBsaW5lc1tpKzFdDQogIG5ld19zcGxpdCA8LSB0cmltd3ModW5saXN0KHN0cnNwbGl0KGxpbmUsICdcXHwnKSkpDQogIG5ld19zcGxpdDIgPC0gdHJpbXdzKHVubGlzdChzdHJzcGxpdChsaW5lMiwgJ1xcfCcpKSkNCg0KICBwbGF5ZXJzIDwtIGFwcGVuZChwbGF5ZXJzLCBuZXdfc3BsaXRbMl0pDQogIHN0YXRlcyA8LSBhcHBlbmQoc3RhdGVzLCBuZXdfc3BsaXQyWzFdKQ0KICBwb2ludHMgPC0gYXBwZW5kKHBvaW50cywgbmV3X3NwbGl0WzNdKQ0KDQogIGlmIChzdHJfZGV0ZWN0KG5ld19zcGxpdDJbMl0sICdbMC05XSs/KD89UCknKSkgew0KICAgIHByZV9yYXRpbmdzIDwtIGFwcGVuZChwcmVfcmF0aW5ncywgc3RyX2V4dHJhY3QobmV3X3NwbGl0MlsyXSwgJ1swLTldKz8oPz1QKScpKQ0KICB9IGVsc2Ugew0KICAgIHByZV9yYXRpbmdzIDwtIGFwcGVuZChwcmVfcmF0aW5ncywgc3RyX2V4dHJhY3QobmV3X3NwbGl0MlsyXSwgJ1swLTldKz8oPz0oXFxzKSotPiknKSkNCiAgfQ0KICANCiAgbmV3X29wcG9uZW50X2lkcyA8LSBuZXdfc3BsaXRbNDoxMF0NCiAgbmV3X29wcG9uZW50X2lkc192ZWMgPC0gbnVtZXJpYygpDQoNCiAgZm9yIChtYXRjaCBpbiBuZXdfb3Bwb25lbnRfaWRzKSB7DQogICAgaWYgKHN0cl9kZXRlY3QobWF0Y2gsICdbMC05XSsnKSkgew0KICAgICAgbmV3X29wcG9uZW50X2lkc192ZWMgPC0gYXBwZW5kKG5ld19vcHBvbmVudF9pZHNfdmVjLCBzdHJfZXh0cmFjdChtYXRjaCwgJ1swLTldKycpKQ0KICAgIH0NCiAgfQ0KDQogIG9wcG9uZW50X2lkc1tbcGxheWVyX2luZGV4XV0gPC0gbmV3X29wcG9uZW50X2lkc192ZWMNCiAgcGxheWVyX2luZGV4IDwtIHBsYXllcl9pbmRleCArIDENCn0NCmBgYA0KDQojIyMgQ2FsY3VsYXRlIGF2ZXJhZ2Ugb3Bwb25lbnQgcmF0aW5ncw0KDQpVc2luZyAib3Bwb25lbnRfaWRzIiwgbG9vayB1cCB0aGUgcmF0aW5ncyBmb3IgZXZlcnkgb3Bwb25lbnQgYXNzb2NpYXRlZCB3aXRoIGEgcGxheWVyJ3MgcGFpciBudW1iZXIuIEZvciBleGFtcGxlLCBHYXJ5IEh1YSBpcyB0aGUgZmlyc3QgcGxheWVyIGFuZCBmYWNlZCA3IG9wcG9uZW50cy4gTm9uZSBvZiB0aGUgcmF0aW5ncyBoYWQgZGVjaW1hbHMgaW4gdGhlbSwgc28gSSBvcHRlZCB0byByb3VuZCBlYWNoIG1lYW4gYW5kIGNvbnZlcnQgdGhlbSBpbnRvIGludGVnZXJzIGJlZm9yZSBzYXZpbmcgdGhlIGNvbHVtbi4NCmBgYHtyIGNhbGN1bGF0ZS1hdmctb3BwLXJhdGluZ3N9DQpmb3IgKHBsYXllcl9vcHBfaWRzIGluIDE6bGVuZ3RoKG9wcG9uZW50X2lkcykpIHsNCg0KICByYXRpbmdzX3ZlYyA8LSBudW1lcmljKCkNCiAgZm9yIChvcHBfaWQgaW4gb3Bwb25lbnRfaWRzW3BsYXllcl9vcHBfaWRzXSkgew0KICAgIHJhdGluZ3NfdmVjIDwtIGFwcGVuZChyYXRpbmdzX3ZlYywgYXMubnVtZXJpYyhwcmVfcmF0aW5nc1thcy5udW1lcmljKG9wcF9pZCldKSkNCiAgfQ0KICBhdmdfb3BwX3JhdGluZyA8LSBhcy5pbnRlZ2VyKHJvdW5kKG1lYW4ocmF0aW5nc192ZWMpKSkNCiAgb3BwX3ByZV9yYXRpbmdzIDwtIGFwcGVuZChvcHBfcHJlX3JhdGluZ3MsIGF2Z19vcHBfcmF0aW5nKQ0KfQ0KYGBgDQoNCiMjIyBDb2xsZWN0IHRoZSBkYXRhIGluIGEgZGF0YSBmcmFtZQ0KDQpXaXRoIGFsbCBvZiB0aGUgZGF0YSBhbHJlYWR5IG9yZ2FuaXplZCBpbnRvIGluZGl2aWR1YWwgY29sdW1ucywgY3JlYXRlIHRoZSBmaW5hbCBkYXRhIGZyYW1lLiBBIHNhbXBsZSBvZiB0aGUgZGF0YSBpcyBzaG93biBiZWxvdy4NCmBgYHtyIG1ha2UtZGF0YS1mcmFtZX0NCnRvdXJuYW1lbnRfcGxheWVycyA8LSBkYXRhLmZyYW1lKA0KICBuYW1lID0gcGxheWVycywNCiAgc3RhdGUgPSBzdGF0ZXMsDQogIHRvdGFsX3BvaW50cyA9IHBvaW50cywNCiAgcHJlX3JhdGluZyA9IHByZV9yYXRpbmdzLA0KICBhdmdfb3BwX3ByZV9yYXRpbmcgPSBvcHBfcHJlX3JhdGluZ3MNCikNCmhlYWQodG91cm5hbWVudF9wbGF5ZXJzLCAxMCkNCmBgYA0KDQojIyMgU2F2ZSB0byBjc3YNCg0KU2F2ZSB0aGUgcmVzdWx0cyB0byBhIGNzdiB3aXRob3V0IGFueSBxdW90ZXMgYXJvdW5kIHRoZSBzdHJpbmdzIGFuZCByZW1vdmluZyB0aGUgcGFpciBudW1iZXIgdmlhIHRoZSByb3cubmFtZXMgYXJndW1lbnQuIA0KYGBge3Igc2F2ZS10by1jc3Z9DQoNCndyaXRlLmNzdih4ID0gdG91cm5hbWVudF9wbGF5ZXJzLCBmaWxlID0gJ3RvdXJuYW1lbnRfcGxheWVycy5jc3YnLCBxdW90ZSA9IEZBTFNFLCByb3cubmFtZXMgPSBGQUxTRSkNCg0KYGBgDQoNCg0KIyMjIENvbmNsdXNpb25zDQoNCk1hbnkgYXNzdW1wdGlvbnMgd2VyZSBtYWRlIGFjY29yZGluZyB0byB0aGUgZXhhY3QgbGF5b3V0IG9mICJ0b3VybmFtZW50aW5mby50eHQiLiBSb3dzIHdlcmUgbGFpZCBvdXQgY29uc2lzdGVudGx5IGZvciBlYWNoIHBsYXllci4gQ29sdW1ucyBhbHdheXMgaGFkIGEgInwiIGJldHdlZW4gdGhlbS4gVG90YWwgcG9pbnRzIGFsd2F5cyBoYWQgYSAiLiIgcmVnYXJkbGVzcyBvZiB0aGUgbnVtYmVyLiANCg0KVGhpcyBraW5kIG9mIHBhcnNpbmcgaXMgbm90IGdvb2QgZm9yIHJldXNhYmlsaXR5LCBidXQgaXQgYWxzbyByZXByZXNlbnRzIGEgcmVhbGlzdGljIHNjZW5hcmlvLiBXaGVuIHNjcmFwaW5nIGRhdGEsIHRoZSBmb3JtYXQgaXQgaXMgaW4gbWlnaHQgbm90IGFsd2F5cyBiZSBjb252ZW5pZW50LiBUaGVyZSB3aWxsIG9mdGVuIGJlIHRpbWVzIHdoZXJlIGEgY3VzdG9tIHNvbHV0aW9uIGlzIG5lZWRlZCB0byBjb2xsZWN0IHRoYXQgaW5mb3JtYXRpb24uDQo=