Spaces:
Sleeping
Sleeping
Download R/scores_functions.R from WVUBaseball/App: direct link, hf CLI and curl.
- Browser
- Download file 66.3 kB
-
https://huggingface.co/spaces/WVUBaseball/App/resolve/main/R/scores_functions.R
- Command line
-
hf download hf://spaces/WVUBaseball/App/R/scores_functions.R
-
curl -L -o scores_functions.R https://huggingface.co/spaces/WVUBaseball/App/resolve/main/R/scores_functions.R
66.3 kB
sort_scoreboard_games <- function(games) {
if (nrow(games) == 0) {
return(games)
}
if (!("start_datetime_utc" %in% names(games))) {
games$start_datetime_utc <- NA_character_
}
if (!("start_time" %in% names(games))) {
games$start_time <- NA_character_
}
if (!("start_sort_key" %in% names(games))) {
games$start_sort_key <- NA_real_
}
if (!("start_time_minutes" %in% names(games))) {
games$start_time_minutes <- NA_real_
}
games %>%
dplyr::mutate(
sort_bucket = dplyr::case_when(
status_category == "live" ~ 0L,
status_category == "upcoming" ~ 1L,
status_category == "final" ~ 2L,
status_category == "canceled" ~ 3L,
TRUE ~ 4L
),
start_sort_abs = dplyr::coalesce(
as.numeric(start_sort_key),
vapply(start_datetime_utc, parse_start_datetime_numeric, numeric(1))
),
start_sort_time = dplyr::coalesce(
as.numeric(start_time_minutes),
vapply(start_time, parse_time_label_minutes, numeric(1))
),
start_sort_missing_abs = is.na(start_sort_abs),
start_sort = dplyr::if_else(start_sort_missing_abs, start_sort_time, start_sort_abs),
start_sort = ifelse(is.na(start_sort), Inf, start_sort)
) %>%
dplyr::arrange(sort_bucket, start_sort_missing_abs, start_sort, away, home) %>%
dplyr::select(-sort_bucket, -start_sort_abs, -start_sort_time, -start_sort_missing_abs, -start_sort)
}
cache_live_result <- function(key, expr, ttl_seconds) {
cache_get_or_set(
key = key,
expr = expr,
namespace = "live_data",
ttl = ttl_seconds
)
}
fetch_espn_scoreboard <- function(game_date = Sys.Date(), limit = 200) {
date_value <- as.Date(game_date)
if (is.na(date_value)) {
empty <- tibble::tibble()
attr(empty, "fetch_error") <- "Invalid date."
return(empty)
}
cache_key <- make_cache_key("espn_scoreboard", format(date_value, "%Y%m%d"), as.integer(limit))
ttl_seconds <- get_cache_ttl_seconds("WVU_APP_CACHE_TTL_SCORES", default_seconds = 120)
cache_live_result(cache_key, {
date_key <- format(date_value, "%Y%m%d")
url <- sprintf(
"https://site.api.espn.com/apis/site/v2/sports/baseball/college-baseball/scoreboard?dates=%s&limit=%d",
date_key,
as.integer(limit)
)
payload <- tryCatch(
jsonlite::fromJSON(url, simplifyVector = FALSE),
error = function(e) {
NULL
}
)
if (is.null(payload)) {
empty <- tibble::tibble()
attr(empty, "fetch_error") <- "Could not reach ESPN scoreboard API."
return(empty)
}
events <- payload$events %||% list()
if (length(events) == 0) {
return(tibble::tibble())
}
games <- purrr::map_dfr(events, parse_espn_score_event)
if (nrow(games) == 0) {
return(tibble::tibble())
}
sort_scoreboard_games(games)
}, ttl_seconds = ttl_seconds)
}
is_standings_entry <- function(entry) {
is.list(entry) && (!is.null(entry$team) || !is.null(entry$stats) || !is.null(entry$records))
}
collect_standings_groups <- function(node, inherited_conference = NA_character_) {
groups <- list()
if (!is.list(node) || length(node) == 0) {
return(groups)
}
local_conference <- as.character(
node$name %||% node$displayName %||% node$shortDisplayName %||% inherited_conference %||% NA_character_
)
entry_sets <- list(
node$standings$entries %||% list(),
node$entries %||% list()
)
for (entries in entry_sets) {
if (length(entries) == 0) {
next
}
valid <- vapply(entries, is_standings_entry, logical(1))
if (any(valid)) {
conference_name <- if (!is.na(local_conference) && nzchar(local_conference)) {
local_conference
} else {
"Other"
}
groups[[length(groups) + 1]] <- list(
conference = conference_name,
entries = entries[valid]
)
}
}
if (!is.null(names(node))) {
for (name in names(node)) {
child <- node[[name]]
if (!is.list(child) || length(child) == 0 || identical(name, "entries")) {
next
}
groups <- c(groups, collect_standings_groups(child, local_conference))
}
} else {
for (child in node) {
if (!is.list(child) || length(child) == 0) {
next
}
groups <- c(groups, collect_standings_groups(child, local_conference))
}
}
groups
}
fetch_espn_standings <- function(season = NULL, seasontype = 2L) {
cache_key <- make_cache_key("espn_standings", season %||% "<NULL>", seasontype)
ttl_seconds <- get_cache_ttl_seconds("WVU_APP_CACHE_TTL_STANDINGS", default_seconds = 300)
cache_live_result(cache_key, {
base_url <- "https://site.api.espn.com/apis/site/v2/sports/baseball/college-baseball/standings"
query <- c(
if (!is.null(season) && !is.na(season)) paste0("season=", as.integer(season)) else NULL,
paste0("seasontype=", as.integer(seasontype))
)
url <- if (length(query) > 0) paste0(base_url, "?", paste(query, collapse = "&")) else base_url
payload <- tryCatch(
jsonlite::fromJSON(url, simplifyVector = FALSE),
error = function(e) NULL
)
if (is.null(payload)) {
empty <- tibble::tibble()
attr(empty, "fetch_error") <- "Could not reach ESPN standings API."
return(empty)
}
groups <- collect_standings_groups(payload)
standings <- purrr::map_dfr(groups, function(group) {
entries <- group$entries %||% list()
conf_name <- as.character(group$conference %||% "Other")
purrr::map_dfr(seq_along(entries), function(i) {
parse_espn_standings_entry(entries[[i]], conf_name, i)
})
})
if (nrow(standings) == 0) {
empty <- tibble::tibble()
attr(empty, "fetch_error") <- "No standings found."
return(empty)
}
team_lookup_conf <- vapply(standings$team, lookup_conference_from_team_name, character(1))
team_lookup_conf[!nzchar(team_lookup_conf)] <- NA_character_
standings %>%
dplyr::mutate(
conference = dplyr::coalesce(team_lookup_conf, conference, "Other"),
team_key = vapply(team, normalize_team_name, character(1)),
conf_key = vapply(conference, normalize_team_name, character(1)),
rank_sort = dplyr::coalesce(as.numeric(rank), as.numeric(default_rank), Inf),
wins_sort = dplyr::coalesce(as.numeric(overall_wins), -Inf),
losses_sort = dplyr::coalesce(as.numeric(overall_losses), Inf)
) %>%
dplyr::arrange(conf_key, team_key, rank_sort, dplyr::desc(wins_sort), losses_sort, team) %>%
dplyr::group_by(conf_key, team_key) %>%
dplyr::slice(1) %>%
dplyr::ungroup() %>%
dplyr::arrange(conference, rank_sort, dplyr::desc(wins_sort), losses_sort, team) %>%
dplyr::group_by(conference) %>%
dplyr::mutate(display_rank = dplyr::row_number()) %>%
dplyr::ungroup() %>%
dplyr::select(-team_lookup_conf, -team_key, -conf_key, -rank_sort, -wins_sort, -losses_sort)
}, ttl_seconds = ttl_seconds)
}
extract_html_table_rows <- function(table_node) {
row_nodes <- xml2::xml_find_all(table_node, ".//tr")
if (length(row_nodes) == 0) {
return(list())
}
rows <- lapply(row_nodes, function(row_node) {
cell_nodes <- xml2::xml_find_all(row_node, "./th|./td")
if (length(cell_nodes) == 0) {
return(character())
}
values <- trimws(xml2::xml_text(cell_nodes, trim = TRUE))
values[is.na(values)] <- ""
values
})
rows[lengths(rows) > 0]
}
extract_html_table_data_frame <- function(table_node) {
if (requireNamespace("rvest", quietly = TRUE)) {
tbl <- tryCatch(
rvest::html_table(table_node, fill = TRUE),
error = function(e) NULL
)
if (!is.null(tbl) && nrow(tbl) > 0 && ncol(tbl) > 0) {
tbl <- as.data.frame(tbl, stringsAsFactors = FALSE)
nm <- trimws(as.character(names(tbl)))
nm[is.na(nm) | !nzchar(nm)] <- paste0("col", which(is.na(nm) | !nzchar(nm)))
names(tbl) <- make.unique(nm)
return(tbl)
}
}
rows <- extract_html_table_rows(table_node)
if (length(rows) == 0) {
return(data.frame(stringsAsFactors = FALSE))
}
has_header <- any(grepl("team|school|program|overall|record|conf|rank", tolower(rows[[1]]), perl = TRUE))
if (has_header) {
header <- rows[[1]]
body_rows <- rows[-1]
} else {
width <- max(lengths(rows))
header <- paste0("col", seq_len(width))
body_rows <- rows
}
if (length(body_rows) == 0) {
return(data.frame(stringsAsFactors = FALSE))
}
width <- max(c(length(header), lengths(body_rows)))
header <- c(header, rep("", max(0, width - length(header))))
header <- trimws(header)
blank_header <- !nzchar(header) | is.na(header)
header[blank_header] <- paste0("col", which(blank_header))
header <- make.unique(header)
matrix_data <- matrix(NA_character_, nrow = length(body_rows), ncol = width)
for (i in seq_along(body_rows)) {
values <- body_rows[[i]]
if (length(values) > 0) {
matrix_data[i, seq_along(values)] <- values
}
}
tbl <- as.data.frame(matrix_data, stringsAsFactors = FALSE)
names(tbl) <- header
tbl
}
extract_d1_table_heading <- function(table_node) {
candidates <- c(
trimws(xml2::xml_text(xml2::xml_find_first(table_node, ".//caption"), trim = TRUE)),
trimws(xml2::xml_text(xml2::xml_find_first(table_node, "preceding::*[self::h2 or self::h3 or self::h4 or self::h5][1]"), trim = TRUE))
)
candidates <- candidates[!is.na(candidates) & nzchar(candidates)]
if (length(candidates) == 0) {
return(NA_character_)
}
heading <- candidates[[1]]
heading <- gsub("(?i)conference\\s+standings", "", heading, perl = TRUE)
heading <- gsub("(?i)standings", "", heading, perl = TRUE)
heading <- trimws(heading)
if (!nzchar(heading)) {
return(NA_character_)
}
heading
}
normalize_record_text <- function(values) {
text <- trimws(as.character(values %||% ""))
text[is.na(text)] <- ""
text <- gsub("[\u2012\u2013\u2014\u2212]", "-", text, perl = TRUE)
text <- gsub("\\s+", "", text, perl = TRUE)
matches <- regmatches(text, regexec("([0-9]+)-([0-9]+)", text, perl = TRUE))
out <- rep(NA_character_, length(text))
for (i in seq_along(matches)) {
if (length(matches[[i]]) >= 3) {
out[[i]] <- paste0(matches[[i]][2], "-", matches[[i]][3])
}
}
out
}
extract_record_components <- function(record_values) {
values <- normalize_record_text(record_values)
wins <- rep(NA_integer_, length(values))
losses <- rep(NA_integer_, length(values))
for (i in seq_along(values)) {
wl <- extract_wl_from_record(values[[i]])
wins[[i]] <- suppressWarnings(as.integer(wl[[1]]))
losses[[i]] <- suppressWarnings(as.integer(wl[[2]]))
}
list(record = values, wins = wins, losses = losses)
}
extract_d1_logo_src <- function(node) {
if (inherits(node, "xml_missing") || length(node) == 0) {
return(NA_character_)
}
attr_names <- c("src", "data-src", "data-lazy-src", "data-original", "srcset", "data-srcset")
for (attr_name in attr_names) {
value <- trimws(as.character(xml2::xml_attr(node, attr_name)))
if (is.na(value) || !nzchar(value)) {
next
}
if (attr_name %in% c("srcset", "data-srcset")) {
first_src <- strsplit(value, ",", fixed = TRUE)[[1]][1]
value <- trimws(strsplit(first_src, "\\s+", perl = TRUE)[[1]][1])
}
if (!nzchar(value)) {
next
}
if (startsWith(value, "//")) {
value <- paste0("https:", value)
} else if (!grepl("^https?://", value, ignore.case = TRUE)) {
base_url <- tryCatch(xml2::xml_url(node), error = function(e) NA_character_)
if (!is.na(base_url) && nzchar(base_url)) {
value <- tryCatch(xml2::url_absolute(value, base_url), error = function(e) value)
}
}
if (nzchar(value)) {
return(value)
}
}
NA_character_
}
extract_d1_team_text_from_row <- function(row_node) {
cells <- xml2::xml_find_all(row_node, "./td")
if (length(cells) == 0) {
return(NA_character_)
}
texts <- trimws(xml2::xml_text(cells, trim = TRUE))
texts <- texts[!is.na(texts) & nzchar(texts)]
if (length(texts) == 0) {
return(NA_character_)
}
invalid_exact <- c(
"w", "l", "pct", "gb", "strk", "streak", "team", "record", "rank", "rk",
"overall", "conference", "conf", "wins", "losses"
)
for (text in texts) {
key <- normalize_standing_key(text)
if (!nzchar(key) || key %in% invalid_exact) {
next
}
if (!grepl("[A-Za-z]", text, perl = TRUE)) {
next
}
if (grepl("^[0-9]+$", text, perl = TRUE)) {
next
}
if (grepl("^[0-9]+\\s*[-\u2013\u2014]\\s*[0-9]+$", text, perl = TRUE)) {
next
}
if (grepl("^\\.?[0-9]{3}$", text, perl = TRUE)) {
next
}
cleaned <- gsub("^#?[0-9]+\\s+", "", text, perl = TRUE)
cleaned <- gsub("\\s+\\([0-9]+\\)$", "", cleaned, perl = TRUE)
cleaned <- trimws(cleaned)
if (nzchar(cleaned)) {
return(cleaned)
}
}
NA_character_
}
extract_d1_team_logo_map <- function(table_node) {
row_nodes <- xml2::xml_find_all(table_node, ".//tr[td]")
if (length(row_nodes) == 0) {
return(tibble::tibble(team_key = character(), team_logo = character()))
}
rows <- lapply(row_nodes, function(row_node) {
logo_node <- xml2::xml_find_first(row_node, ".//img")
logo_url <- extract_d1_logo_src(logo_node)
team_name <- extract_d1_team_text_from_row(row_node)
team_key <- normalize_team_name(team_name)
if (is.na(logo_url) || !nzchar(logo_url) || !nzchar(team_key)) {
return(NULL)
}
tibble::tibble(team_key = team_key, team_logo = logo_url)
})
logo_tbl <- dplyr::bind_rows(rows)
if (nrow(logo_tbl) == 0) {
return(tibble::tibble(team_key = character(), team_logo = character()))
}
logo_tbl %>%
dplyr::filter(!is.na(team_logo), nzchar(team_logo)) %>%
dplyr::group_by(team_key) %>%
dplyr::summarise(team_logo = dplyr::first(team_logo), .groups = "drop")
}
promote_standings_subheader_row <- function(tbl) {
if (nrow(tbl) == 0 || ncol(tbl) == 0) {
return(tbl)
}
first_row <- vapply(tbl[1, , drop = FALSE], function(x) trimws(as.character(x[[1]])), character(1))
first_key <- normalize_standing_key(first_row)
if (length(first_key) == 0) {
return(tbl)
}
header_like <- grepl("^(team|school|program|w|l|pct|gb|overall|record|conference|conf|wins|losses)$", first_key, perl = TRUE) |
grepl("(overall|conference|conf|wins|losses|record|pct|gb)", first_key, perl = TRUE)
if (mean(header_like, na.rm = TRUE) < 0.4) {
return(tbl)
}
new_names <- names(tbl)
for (i in seq_along(new_names)) {
sub <- trimws(as.character(first_row[[i]]))
if (!is.na(sub) && nzchar(sub)) {
base_key <- normalize_standing_key(new_names[[i]])
sub_key <- normalize_standing_key(sub)
if (nzchar(sub_key) && !identical(base_key, sub_key)) {
new_names[[i]] <- paste(new_names[[i]], sub)
}
}
}
names(tbl) <- make.unique(trimws(new_names))
tbl <- tbl[-1, , drop = FALSE]
rownames(tbl) <- NULL
tbl
}
parse_integer_column <- function(values) {
text <- trimws(as.character(values %||% ""))
text[is.na(text)] <- ""
text <- gsub("[^0-9-]", "", text, perl = TRUE)
text[!nzchar(text)] <- NA_character_
suppressWarnings(as.integer(text))
}
extract_record_from_wl_columns <- function(tbl, w_col, l_col) {
n <- nrow(tbl)
if (is.na(w_col) || is.na(l_col) || w_col < 1 || l_col < 1 || w_col > ncol(tbl) || l_col > ncol(tbl)) {
return(list(record = rep(NA_character_, n), wins = rep(NA_integer_, n), losses = rep(NA_integer_, n)))
}
wins <- parse_integer_column(tbl[[w_col]])
losses <- parse_integer_column(tbl[[l_col]])
record <- ifelse(!is.na(wins) & !is.na(losses), paste0(wins, "-", losses), NA_character_)
list(record = record, wins = wins, losses = losses)
}
find_wl_pair <- function(col_keys, include_pattern = NULL, exclude_pattern = NULL) {
idx <- seq_along(col_keys)
if (!is.null(include_pattern) && nzchar(include_pattern)) {
idx <- idx[grepl(include_pattern, col_keys[idx], perl = TRUE)]
}
if (!is.null(exclude_pattern) && nzchar(exclude_pattern)) {
idx <- idx[!grepl(exclude_pattern, col_keys[idx], perl = TRUE)]
}
if (length(idx) == 0) {
return(c(NA_integer_, NA_integer_))
}
win_idx <- idx[grepl("(wins?|w)$", col_keys[idx], perl = TRUE)]
loss_idx <- idx[grepl("(losses?|l)$", col_keys[idx], perl = TRUE)]
if (length(win_idx) == 0 || length(loss_idx) == 0) {
return(c(NA_integer_, NA_integer_))
}
best <- c(NA_integer_, NA_integer_)
best_score <- Inf
for (w in win_idx) {
for (l in loss_idx) {
score <- abs(l - w) + ifelse(l < w, 0.5, 0)
if (score < best_score) {
best <- c(w, l)
best_score <- score
}
}
}
best
}
find_adjacent_wl_pairs <- function(col_keys) {
pairs <- list()
if (length(col_keys) < 2) {
return(pairs)
}
for (i in seq_len(length(col_keys) - 1)) {
if (grepl("(wins?|w)$", col_keys[[i]], perl = TRUE) && grepl("(losses?|l)$", col_keys[[i + 1]], perl = TRUE)) {
pairs[[length(pairs) + 1]] <- c(i, i + 1)
}
}
pairs
}
is_missing_record_vector <- function(x) {
all(is.na(x) | !nzchar(trimws(as.character(x))))
}
find_standings_column <- function(col_keys, exact = character(), contains = character(), candidates = seq_along(col_keys), prefer = NULL) {
if (length(candidates) == 0) {
return(NA_integer_)
}
exact_norm <- normalize_standing_key(exact)
contains_norm <- normalize_standing_key(contains)
prefer_norm <- normalize_standing_key(prefer %||% character())
candidate_keys <- col_keys[candidates]
exact_hits <- candidates[candidate_keys %in% exact_norm]
if (length(exact_hits) > 0) {
return(exact_hits[[1]])
}
for (term in contains_norm) {
idx <- candidates[grepl(term, candidate_keys, fixed = TRUE)]
if (length(idx) > 0) {
return(idx[[1]])
}
}
for (term in prefer_norm) {
idx <- candidates[grepl(term, candidate_keys, fixed = TRUE)]
if (length(idx) > 0) {
return(idx[[1]])
}
}
NA_integer_
}
parse_d1baseball_standings_table <- function(table_node, fallback_conference = NA_character_) {
logo_map <- extract_d1_team_logo_map(table_node)
tbl <- extract_html_table_data_frame(table_node)
if (nrow(tbl) == 0 || ncol(tbl) == 0) {
return(tibble::tibble())
}
tbl <- promote_standings_subheader_row(tbl)
if (nrow(tbl) == 0 || ncol(tbl) == 0) {
return(tibble::tibble())
}
col_keys <- normalize_standing_key(names(tbl))
record_score <- vapply(tbl, function(col) {
values <- trimws(as.character(col))
values <- values[!is.na(values) & nzchar(values)]
if (length(values) == 0) {
return(0)
}
mean(grepl("^[0-9]+\\s*[-\u2013\u2014]\\s*[0-9]+", values, perl = TRUE))
}, numeric(1))
team_col <- find_standings_column(
col_keys,
exact = c("team", "school", "program"),
contains = c("team", "school", "program")
)
if (is.na(team_col)) {
alpha_score <- vapply(tbl, function(col) {
values <- as.character(col)
values <- values[!is.na(values) & nzchar(trimws(values))]
if (length(values) == 0) {
return(0)
}
mean(grepl("[A-Za-z]", values, perl = TRUE))
}, numeric(1))
candidate <- which.max(alpha_score)
if (length(candidate) == 0 || alpha_score[[candidate]] < 0.4) {
return(tibble::tibble())
}
team_col <- candidate
}
rank_col <- find_standings_column(
col_keys,
exact = c("rk", "rank", "pos"),
contains = c("rank", "rk", "position", "pos")
)
record_cols <- which(record_score >= 0.3)
conf_record_col <- find_standings_column(
col_keys,
exact = c("conf", "conferencerecord", "leaguerecord"),
contains = c("conf", "conference", "league"),
candidates = record_cols
)
overall_record_col <- find_standings_column(
col_keys,
exact = c("overall", "record", "overallrecord"),
contains = c("overall", "record", "total"),
candidates = setdiff(record_cols, conf_record_col)
)
if (is.na(overall_record_col)) {
overall_record_col <- find_standings_column(
col_keys,
exact = c("overall", "record", "overallrecord"),
contains = c("overall", "record", "total")
)
}
conf_name_col <- find_standings_column(
col_keys,
exact = c("conference", "conferencename", "league", "leaguename"),
contains = c("conference", "league")
)
team <- trimws(as.character(tbl[[team_col]]))
team <- gsub("^#?[0-9]+\\s+", "", team, perl = TRUE)
team <- gsub("\\s+\\([0-9]+\\)$", "", team, perl = TRUE)
team[!nzchar(team)] <- NA_character_
rank <- if (!is.na(rank_col)) {
suppressWarnings(as.integer(gsub("[^0-9]", "", as.character(tbl[[rank_col]]), perl = TRUE)))
} else {
rep(NA_integer_, nrow(tbl))
}
conf_parts <- if (!is.na(conf_record_col)) {
extract_record_components(tbl[[conf_record_col]])
} else {
list(
record = rep(NA_character_, nrow(tbl)),
wins = rep(NA_integer_, nrow(tbl)),
losses = rep(NA_integer_, nrow(tbl))
)
}
overall_parts <- if (!is.na(overall_record_col)) {
extract_record_components(tbl[[overall_record_col]])
} else {
list(
record = rep(NA_character_, nrow(tbl)),
wins = rep(NA_integer_, nrow(tbl)),
losses = rep(NA_integer_, nrow(tbl))
)
}
conf_wl_pair <- find_wl_pair(col_keys, include_pattern = "(conf|conference|league)")
overall_wl_pair <- find_wl_pair(col_keys, include_pattern = "(overall|total)")
adjacent_pairs <- find_adjacent_wl_pairs(col_keys)
if ((is.na(conf_wl_pair[[1]]) || is.na(conf_wl_pair[[2]])) && length(adjacent_pairs) > 0) {
conf_wl_pair <- adjacent_pairs[[1]]
}
if ((is.na(overall_wl_pair[[1]]) || is.na(overall_wl_pair[[2]])) && length(adjacent_pairs) > 1) {
overall_wl_pair <- adjacent_pairs[[2]]
}
if (is_missing_record_vector(conf_parts$record)) {
conf_from_wl <- extract_record_from_wl_columns(tbl, conf_wl_pair[[1]], conf_wl_pair[[2]])
if (!is_missing_record_vector(conf_from_wl$record)) {
conf_parts <- conf_from_wl
}
}
need_overall_wl <- is_missing_record_vector(overall_parts$record)
if (!need_overall_wl && !is_missing_record_vector(conf_parts$record)) {
same_record <- sum(!is.na(overall_parts$record) & !is.na(conf_parts$record) & overall_parts$record == conf_parts$record)
compared <- sum(!is.na(overall_parts$record) & !is.na(conf_parts$record))
need_overall_wl <- compared > 0 && same_record == compared && length(adjacent_pairs) > 1
}
if (need_overall_wl) {
overall_from_wl <- extract_record_from_wl_columns(tbl, overall_wl_pair[[1]], overall_wl_pair[[2]])
if (!is_missing_record_vector(overall_from_wl$record) && !identical(overall_from_wl$record, conf_parts$record)) {
overall_parts <- overall_from_wl
}
}
row_conference <- if (!is.na(conf_name_col)) {
trimws(as.character(tbl[[conf_name_col]]))
} else {
rep(as.character(fallback_conference), nrow(tbl))
}
row_conference[!nzchar(row_conference)] <- NA_character_
out <- tibble::tibble(
conference = row_conference,
team = team,
team_logo = NA_character_,
rank = rank,
default_rank = seq_len(nrow(tbl)),
overall_record = overall_parts$record,
conference_record = conf_parts$record,
overall_wins = suppressWarnings(as.integer(overall_parts$wins)),
overall_losses = suppressWarnings(as.integer(overall_parts$losses)),
conference_wins = suppressWarnings(as.integer(conf_parts$wins)),
conference_losses = suppressWarnings(as.integer(conf_parts$losses))
) %>%
dplyr::filter(
!is.na(team),
!normalize_standing_key(team) %in% c("team", "school", "program"),
!is.na(overall_record) | !is.na(conference_record)
)
if (nrow(out) > 0 && nrow(logo_map) > 0) {
out <- out %>%
dplyr::mutate(team_key = vapply(team, normalize_team_name, character(1))) %>%
dplyr::left_join(logo_map, by = "team_key", suffix = c("", "_mapped")) %>%
dplyr::mutate(team_logo = dplyr::coalesce(team_logo_mapped, team_logo)) %>%
dplyr::select(-team_key, -team_logo_mapped)
}
out
}
fetch_d1baseball_standings <- function(season = NULL) {
cache_key <- make_cache_key("d1baseball_standings", season %||% "<NULL>")
ttl_seconds <- get_cache_ttl_seconds("WVU_APP_CACHE_TTL_STANDINGS", default_seconds = 300)
cache_live_result(cache_key, {
if (!requireNamespace("xml2", quietly = TRUE)) {
empty <- tibble::tibble()
attr(empty, "fetch_error") <- "xml2 package is required for D1Baseball standings."
return(empty)
}
normalize_conference_slug <- function(value) {
slug <- tolower(trimws(as.character(value %||% "")))
slug <- iconv(slug, to = "ASCII//TRANSLIT", sub = "")
slug[is.na(slug)] <- ""
slug <- gsub("&", " and ", slug, fixed = TRUE)
slug <- gsub("[^a-z0-9]+", "-", slug, perl = TRUE)
slug <- gsub("(^-|-$)", "", slug, perl = TRUE)
slug
}
conference_slug_candidates <- function(conference_name) {
conf_key <- normalize_standing_key(conference_name)
base_slug <- normalize_conference_slug(conference_name)
if (!nzchar(base_slug)) {
return(character())
}
explicit <- switch(
conf_key,
"acc" = c("acc", "atlantic-coast-conference"),
"atlanticcoast" = c("acc", "atlantic-coast-conference"),
"americaeast" = c("america-east-conference", "america-east"),
"americanathletic" = c("american-athletic-conference", "american-athletic", "aac"),
"atlanticsun" = c("atlantic-sun-conference", "atlantic-sun", "asun"),
"atlantic10" = c("atlantic-10-conference", "atlantic-10", "a10"),
"big12" = c("big-12-conference", "big-12"),
"bigeast" = c("big-east-conference", "big-east"),
"bigsouth" = c("big-south-conference", "big-south"),
"bigten" = c("big-ten-conference", "big-ten"),
"bigwest" = c("big-west-conference", "big-west"),
"colonialathleticassociation" = c("colonial-athletic-association", "caa"),
"conferenceusa" = c("conference-usa", "cusa"),
"horizonleague" = c("horizon-league"),
"ivyleague" = c("ivy-league"),
"metroatlanticathletic" = c("metro-atlantic-athletic-conference", "maac"),
"midamerican" = c("mid-american-conference", "mac"),
"mideasternathletic" = c("mid-eastern-athletic-conference", "meac"),
"missourivalley" = c("missouri-valley-conference", "mvc"),
"mountainwest" = c("mountain-west-conference", "mwc"),
"ncaadivisioniindependents" = c("ncaa-division-i-independents", "division-i-independents", "independents"),
"northeast" = c("northeast-conference", "nec"),
"ohiovalley" = c("ohio-valley-conference", "ovc"),
"pac12" = c("pac-12-conference", "pac-12"),
"pacific10" = c("pac-10-conference", "pac-10"),
"patriotleague" = c("patriot-league"),
"sec" = c("southeastern-conference", "sec"),
"southeastern" = c("southeastern-conference", "sec"),
"southern" = c("southern-conference", "socon"),
"southland" = c("southland-conference"),
"southwesternathletic" = c("southwestern-athletic-conference", "swac"),
"summitleague" = c("summit-league"),
"sunbelt" = c("sun-belt-conference", "sun-belt"),
"westcoast" = c("west-coast-conference", "wcc"),
"westernathletic" = c("western-athletic-conference", "wac"),
NULL
)
unique(c(
explicit,
paste0(base_slug, "-conference"),
base_slug
))
}
read_d1_doc <- function(url, ua) {
out <- list(doc = NULL, reached = FALSE, error = NULL)
result <- tryCatch(
{
if (requireNamespace("httr", quietly = TRUE)) {
response <- httr::GET(url, httr::user_agent(ua), httr::timeout(20))
if (httr::http_error(response)) {
out$reached <- TRUE
out
} else {
html_text <- httr::content(response, as = "text", encoding = "UTF-8")
out$doc <- xml2::read_html(html_text)
out$reached <- TRUE
out
}
} else {
out$doc <- xml2::read_html(url)
out$reached <- TRUE
out
}
},
error = function(e) {
out$error <- conditionMessage(e)
out
}
)
result
}
season_year <- suppressWarnings(as.integer(season))
if (length(season_year) == 0 || is.na(season_year)) {
season_year <- suppressWarnings(as.integer(format(Sys.Date(), "%Y")))
}
user_agent_text <- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
standings_by_conference <- list()
reached_source <- FALSE
parse_error <- NULL
for (conference_name in get_all_conferences()) {
slug_candidates <- conference_slug_candidates(conference_name)
if (length(slug_candidates) == 0) {
next
}
conf_tbl <- tibble::tibble()
for (slug in slug_candidates) {
conf_url <- sprintf("https://d1baseball.com/conference/%s/%d/", slug, season_year)
fetch_result <- read_d1_doc(conf_url, user_agent_text)
reached_source <- reached_source || isTRUE(fetch_result$reached)
if (!is.null(fetch_result$error) && nzchar(fetch_result$error)) {
parse_error <- fetch_result$error
}
if (is.null(fetch_result$doc)) {
next
}
tables <- xml2::xml_find_all(fetch_result$doc, ".//table")
if (length(tables) == 0) {
next
}
conf_tbl <- purrr::map_dfr(seq_along(tables), function(i) {
table_node <- tables[[i]]
table_conf <- extract_d1_table_heading(table_node)
fallback_conf <- if (!is.na(table_conf) && nzchar(table_conf)) table_conf else conference_name
parse_d1baseball_standings_table(table_node, fallback_conference = fallback_conf)
})
if (nrow(conf_tbl) > 0) {
break
}
}
if (nrow(conf_tbl) > 0) {
standings_by_conference[[length(standings_by_conference) + 1]] <- conf_tbl
}
}
standings <- dplyr::bind_rows(standings_by_conference)
if (nrow(standings) == 0) {
for (url in c("https://d1baseball.com/standings/", "https://www.d1baseball.com/standings/")) {
fetch_result <- read_d1_doc(url, user_agent_text)
reached_source <- reached_source || isTRUE(fetch_result$reached)
if (!is.null(fetch_result$error) && nzchar(fetch_result$error)) {
parse_error <- fetch_result$error
}
if (is.null(fetch_result$doc)) {
next
}
tables <- xml2::xml_find_all(fetch_result$doc, ".//table")
if (length(tables) == 0) {
next
}
standings <- purrr::map_dfr(seq_along(tables), function(i) {
table_node <- tables[[i]]
table_conf <- extract_d1_table_heading(table_node)
parse_d1baseball_standings_table(table_node, fallback_conference = table_conf)
})
if (nrow(standings) > 0) {
break
}
}
}
if (nrow(standings) > 0) {
team_lookup_conf <- vapply(standings$team, lookup_conference_from_team_name, character(1))
team_lookup_conf[!nzchar(team_lookup_conf)] <- NA_character_
standings <- standings %>%
dplyr::mutate(
conference = trimws(as.character(conference)),
conference = dplyr::if_else(is.na(conference) | !nzchar(conference), NA_character_, conference),
conference = dplyr::coalesce(team_lookup_conf, conference, "Other"),
overall_record = dplyr::if_else(is.na(overall_record) | !nzchar(overall_record), "-", overall_record),
conference_record = dplyr::if_else(is.na(conference_record) | !nzchar(conference_record), "-", conference_record),
team_key = vapply(team, normalize_team_name, character(1)),
conf_key = vapply(conference, normalize_team_name, character(1)),
rank_sort = dplyr::coalesce(as.numeric(rank), as.numeric(default_rank), Inf),
wins_sort = dplyr::coalesce(as.numeric(overall_wins), -Inf),
losses_sort = dplyr::coalesce(as.numeric(overall_losses), Inf)
) %>%
dplyr::arrange(conf_key, team_key, rank_sort, dplyr::desc(wins_sort), losses_sort, team) %>%
dplyr::group_by(conf_key, team_key) %>%
dplyr::slice(1) %>%
dplyr::ungroup() %>%
dplyr::arrange(conference, rank_sort, dplyr::desc(wins_sort), losses_sort, team) %>%
dplyr::group_by(conference) %>%
dplyr::mutate(display_rank = dplyr::row_number()) %>%
dplyr::ungroup() %>%
dplyr::select(
conference, team, team_logo, rank, default_rank, overall_record, conference_record,
overall_wins, overall_losses, conference_wins, conference_losses, display_rank
)
return(standings)
}
empty <- tibble::tibble()
if (!reached_source) {
attr(empty, "fetch_error") <- "Could not reach D1Baseball standings."
} else {
attr(empty, "fetch_error") <- "No standings found."
}
if (!is.null(parse_error) && nzchar(parse_error)) {
attr(empty, "fetch_error_detail") <- parse_error
}
empty
}, ttl_seconds = ttl_seconds)
}
fetch_local_standings <- function(season = NULL) {
cache_key <- make_cache_key("local_standings", season %||% "<NULL>")
ttl_seconds <- get_cache_ttl_seconds("WVU_APP_CACHE_TTL_STANDINGS", default_seconds = 300)
cache_live_result(cache_key, {
pbp_path <- resolve_scores_file("ncaa_pbp.parquet")
if (is.na(pbp_path)) {
empty <- tibble::tibble()
attr(empty, "fetch_error") <- "No standings found."
return(empty)
}
if (!requireNamespace("arrow", quietly = TRUE)) {
empty <- tibble::tibble()
attr(empty, "fetch_error") <- "No standings found."
return(empty)
}
pbp <- tryCatch(
arrow::read_parquet(pbp_path),
error = function(e) NULL
)
if (is.null(pbp) || nrow(pbp) == 0) {
empty <- tibble::tibble()
attr(empty, "fetch_error") <- "No standings found."
return(empty)
}
required_cols <- c("game_id", "game_date", "away_team", "home_team", "away_score_after", "home_score_after")
if (!all(required_cols %in% names(pbp))) {
empty <- tibble::tibble()
attr(empty, "fetch_error") <- "No standings found."
return(empty)
}
games <- pbp %>%
dplyr::select(dplyr::all_of(required_cols)) %>%
dplyr::mutate(game_date = as.character(game_date))
season_year <- suppressWarnings(as.integer(season))
if (length(season_year) == 0) {
season_year <- NA_integer_
}
if (!is.na(season_year)) {
games <- games %>%
dplyr::filter(substr(game_date, 1, 4) == as.character(season_year))
} else if (nrow(games) == 0) {
all_years <- suppressWarnings(as.integer(substr(as.character(pbp$game_date), 1, 4)))
all_years <- all_years[!is.na(all_years)]
latest_year <- if (length(all_years) > 0) max(all_years) else NA_integer_
if (!is.na(latest_year)) {
games <- pbp %>%
dplyr::select(dplyr::all_of(required_cols)) %>%
dplyr::mutate(game_date = as.character(game_date)) %>%
dplyr::filter(substr(game_date, 1, 4) == as.character(latest_year))
}
}
if (nrow(games) == 0) {
empty <- tibble::tibble()
attr(empty, "fetch_error") <- "No standings found."
return(empty)
}
finals <- games %>%
dplyr::group_by(game_id, game_date, away_team, home_team) %>%
dplyr::summarise(
away_final = suppressWarnings(max(as.numeric(away_score_after), na.rm = TRUE)),
home_final = suppressWarnings(max(as.numeric(home_score_after), na.rm = TRUE)),
.groups = "drop"
) %>%
dplyr::filter(is.finite(away_final), is.finite(home_final), away_final != home_final)
if (nrow(finals) == 0) {
empty <- tibble::tibble()
attr(empty, "fetch_error") <- "No standings found."
return(empty)
}
finals <- finals %>%
dplyr::mutate(
away_conference = vapply(away_team, lookup_conference_from_team_name, character(1)),
home_conference = vapply(home_team, lookup_conference_from_team_name, character(1))
)
away_rows <- finals %>%
dplyr::transmute(
team = away_team,
conference = away_conference,
overall_wins = as.integer(away_final > home_final),
overall_losses = as.integer(away_final < home_final),
conference_wins = as.integer(!is.na(away_conference) & !is.na(home_conference) & nzchar(away_conference) & nzchar(home_conference) & away_conference == home_conference & away_final > home_final),
conference_losses = as.integer(!is.na(away_conference) & !is.na(home_conference) & nzchar(away_conference) & nzchar(home_conference) & away_conference == home_conference & away_final < home_final)
)
home_rows <- finals %>%
dplyr::transmute(
team = home_team,
conference = home_conference,
overall_wins = as.integer(home_final > away_final),
overall_losses = as.integer(home_final < away_final),
conference_wins = as.integer(!is.na(home_conference) & !is.na(away_conference) & nzchar(home_conference) & nzchar(away_conference) & home_conference == away_conference & home_final > away_final),
conference_losses = as.integer(!is.na(home_conference) & !is.na(away_conference) & nzchar(home_conference) & nzchar(away_conference) & home_conference == away_conference & home_final < away_final)
)
standings <- dplyr::bind_rows(away_rows, home_rows) %>%
dplyr::mutate(
conference = dplyr::if_else(is.na(conference) | !nzchar(conference), "Other", conference)
) %>%
dplyr::group_by(conference, team) %>%
dplyr::summarise(
overall_wins = sum(overall_wins, na.rm = TRUE),
overall_losses = sum(overall_losses, na.rm = TRUE),
conference_wins = sum(conference_wins, na.rm = TRUE),
conference_losses = sum(conference_losses, na.rm = TRUE),
.groups = "drop"
) %>%
dplyr::mutate(
team_logo = NA_character_,
rank = NA_integer_,
default_rank = NA_integer_,
overall_record = paste0(overall_wins, "-", overall_losses),
conference_record = paste0(conference_wins, "-", conference_losses),
rank_sort = Inf,
wins_sort = as.numeric(overall_wins),
losses_sort = as.numeric(overall_losses)
) %>%
dplyr::arrange(conference, dplyr::desc(wins_sort), losses_sort, team) %>%
dplyr::group_by(conference) %>%
dplyr::mutate(display_rank = dplyr::row_number()) %>%
dplyr::ungroup() %>%
dplyr::select(
conference, team, team_logo, rank, default_rank, overall_record, conference_record,
overall_wins, overall_losses, conference_wins, conference_losses, display_rank
)
if (nrow(standings) == 0) {
empty <- tibble::tibble()
attr(empty, "fetch_error") <- "No standings found."
return(empty)
}
standings
}, ttl_seconds = ttl_seconds)
}
normalize_standing_key <- function(value) {
text <- tolower(trimws(as.character(value %||% "")))
text[is.na(text)] <- ""
gsub("[^a-z0-9]+", "", text)
}
extract_standing_stat <- function(stats, keys) {
if (length(stats) == 0) {
return(list(value = NA_character_, numeric = NA_real_))
}
norm_keys <- unique(normalize_standing_key(keys))
for (stat in stats) {
candidates <- c(
stat$name,
stat$displayName,
stat$shortDisplayName,
stat$abbreviation,
stat$description,
stat$type
)
norm_candidates <- unique(normalize_standing_key(candidates))
if (any(norm_candidates %in% norm_keys)) {
value <- as.character(stat$displayValue %||% stat$summary %||% stat$value %||% NA_character_)
num <- suppressWarnings(as.numeric(stat$value %||% stat$displayValue %||% NA_real_))
return(list(value = value, numeric = num))
}
}
list(value = NA_character_, numeric = NA_real_)
}
extract_record_summary <- function(records, key_patterns) {
if (length(records) == 0) {
return(NA_character_)
}
for (rec in records) {
rec_name <- tolower(trimws(as.character(rec$name %||% rec$type %||% rec$description %||% "")))
if (!nzchar(rec_name)) {
next
}
if (any(vapply(key_patterns, function(p) grepl(p, rec_name), logical(1)))) {
summary <- as.character(rec$summary %||% NA_character_)
if (!is.na(summary) && nzchar(summary)) {
return(summary)
}
wins <- suppressWarnings(as.integer(rec$wins %||% NA_integer_))
losses <- suppressWarnings(as.integer(rec$losses %||% NA_integer_))
ties <- suppressWarnings(as.integer(rec$ties %||% NA_integer_))
if (!is.na(wins) && !is.na(losses)) {
if (!is.na(ties) && ties > 0) {
return(sprintf("%d-%d-%d", wins, losses, ties))
}
return(sprintf("%d-%d", wins, losses))
}
}
}
NA_character_
}
extract_wl_from_record <- function(record_text) {
text <- trimws(as.character(record_text %||% ""))
if (!nzchar(text)) {
return(c(NA_integer_, NA_integer_))
}
parts <- regmatches(text, regexec("^([0-9]+)-([0-9]+)", text, perl = TRUE))[[1]]
if (length(parts) < 3) {
return(c(NA_integer_, NA_integer_))
}
c(suppressWarnings(as.integer(parts[2])), suppressWarnings(as.integer(parts[3])))
}
extract_team_logo_from_standing <- function(team) {
first_logo <- NULL
if (is.list(team$logos) && length(team$logos) > 0) {
first_logo <- team$logos[[1]]$href %||% team$logos[[1]]$url
}
candidates <- list(
first_logo,
team$logo
)
for (candidate in candidates) {
if (is.null(candidate) || length(candidate) == 0) {
next
}
value <- trimws(as.character(candidate[[1]]))
if (!is.na(value) && nzchar(value)) {
return(value)
}
}
NA_character_
}
parse_espn_standings_entry <- function(entry, conference_name, default_rank = NA_integer_) {
team_obj <- entry$team %||% list()
stats <- entry$stats %||% list()
records <- entry$records %||% list()
overall_record <- extract_record_summary(records, c("overall"))
conference_record <- extract_record_summary(records, c("conference", "conf", "league"))
if (is.na(overall_record)) {
overall_record <- extract_standing_stat(stats, c("overall", "record", "overallrecord"))$value
}
if (is.na(conference_record)) {
conference_record <- extract_standing_stat(stats, c("conference", "conf", "leaguerecord", "conferencerecord"))$value
}
wins <- extract_standing_stat(stats, c("wins", "w"))$numeric
losses <- extract_standing_stat(stats, c("losses", "l"))$numeric
if (is.na(wins) || is.na(losses)) {
wl <- extract_wl_from_record(overall_record)
if (is.na(wins)) {
wins <- wl[1]
}
if (is.na(losses)) {
losses <- wl[2]
}
}
conf_wins <- extract_standing_stat(stats, c("conferencewins", "confwins"))$numeric
conf_losses <- extract_standing_stat(stats, c("conferencelosses", "conflosses"))$numeric
if (is.na(conf_wins) || is.na(conf_losses)) {
cwl <- extract_wl_from_record(conference_record)
if (is.na(conf_wins)) {
conf_wins <- cwl[1]
}
if (is.na(conf_losses)) {
conf_losses <- cwl[2]
}
}
rank_value <- suppressWarnings(as.integer(
entry$rank %||% extract_standing_stat(stats, c("rank", "seed", "playoffseed"))$numeric %||% NA_integer_
))
tibble::tibble(
conference = as.character(conference_name %||% "Other"),
team = as.character(team_obj$displayName %||% team_obj$shortDisplayName %||% team_obj$name %||% "Team"),
team_logo = as.character(extract_team_logo_from_standing(team_obj)),
rank = rank_value,
default_rank = as.integer(default_rank),
overall_record = as.character(overall_record %||% "-"),
conference_record = as.character(conference_record %||% "-"),
overall_wins = suppressWarnings(as.integer(wins)),
overall_losses = suppressWarnings(as.integer(losses)),
conference_wins = suppressWarnings(as.integer(conf_wins)),
conference_losses = suppressWarnings(as.integer(conf_losses))
)
}
extract_competitor_rank <- function(competitor) {
candidates <- list(
competitor$curatedRank$current,
competitor$curatedRank$currentRank,
competitor$curatedRank$rank,
competitor$curatedRank,
competitor$rank,
competitor$team$curatedRank$current,
competitor$team$curatedRank$currentRank,
competitor$team$curatedRank$rank,
competitor$team$rank
)
for (candidate in candidates) {
if (is.null(candidate) || length(candidate) == 0) {
next
}
value <- suppressWarnings(as.integer(candidate[[1]]))
if (!is.na(value) && value > 0) {
return(value)
}
}
NA_integer_
}
extract_bool <- function(value) {
if (is.null(value) || length(value) == 0) {
return(NA)
}
first <- value[[1]]
bool <- suppressWarnings(as.logical(first))
if (!is.na(bool)) {
return(bool)
}
text <- tolower(as.character(first))
if (text %in% c("true", "t", "1", "yes", "y")) {
return(TRUE)
}
if (text %in% c("false", "f", "0", "no", "n")) {
return(FALSE)
}
NA
}
resolve_scores_file <- function(filename) {
candidates <- c(
file.path("Data", filename),
file.path("..", "Data", filename),
filename
)
existing <- candidates[file.exists(candidates)]
if (length(existing) == 0) {
return(NA_character_)
}
existing[[1]]
}
HF_REPO_ID <- "WVUBaseball/Data"
hf_read <- function(
repo_id,
path,
type = c("parquet", "csv", "rds", "rdata", "raw"),
token_env = "api_key",
rev = "main"
) {
type <- match.arg(type)
url <- paste0("https://huggingface.co/datasets/", repo_id, "/resolve/", rev, "/", path)
token <- Sys.getenv(token_env, unset = "")
headers <- if (nzchar(token)) {
httr::add_headers(Authorization = paste("Bearer", token))
} else {
NULL
}
resp <- if (is.null(headers)) {
httr::GET(url)
} else {
httr::GET(url, headers)
}
if (httr::status_code(resp) != 200) {
stop(sprintf("HF download failed (%s): %s", httr::status_code(resp), url), call. = FALSE)
}
ext <- switch(
type,
parquet = ".parquet",
csv = ".csv",
rds = ".rds",
rdata = ".RData",
raw = ""
)
tmp <- tempfile(fileext = ext)
writeBin(httr::content(resp, "raw"), tmp)
switch(
type,
parquet = arrow::read_parquet(tmp) |> tibble::as_tibble(),
csv = readr::read_csv(
tmp,
show_col_types = FALSE,
locale = readr::locale(encoding = "UTF-8")
),
rds = readRDS(tmp),
rdata = {
e <- new.env(parent = emptyenv())
load(tmp, envir = e)
e
},
raw = readBin(tmp, what = "raw", n = file.info(tmp)$size)
)
}
read_scores_csv <- function(filename) {
remote <- tryCatch(
hf_read(HF_REPO_ID, filename, type = "csv"),
error = function(e) NULL
)
if (!is.null(remote)) {
return(as.data.frame(remote, stringsAsFactors = FALSE))
}
local_path <- resolve_scores_file(filename)
if (is.na(local_path)) {
return(data.frame(stringsAsFactors = FALSE))
}
tryCatch(
utils::read.csv(local_path, stringsAsFactors = FALSE),
error = function(e) data.frame(stringsAsFactors = FALSE)
)
}
get_all_conferences <- local({
cache <- NULL
function() {
if (!is.null(cache)) {
return(cache)
}
conferences <- character()
conf_tbl <- read_scores_csv("conferences.csv")
if ("conference" %in% names(conf_tbl)) {
conferences <- c(conferences, as.character(conf_tbl$conference))
}
teams_tbl <- read_scores_csv("teams.csv")
if ("conference" %in% names(teams_tbl)) {
conferences <- c(conferences, as.character(teams_tbl$conference))
}
conferences <- trimws(conferences)
conferences <- conferences[!is.na(conferences) & nzchar(conferences)]
conferences <- sort(unique(conferences))
cache <<- conferences
conferences
}
})
normalize_team_name <- function(value) {
text <- trimws(as.character(value %||% ""))
if (!nzchar(text)) {
return("")
}
text <- tolower(text)
text <- gsub("&", " and ", text, fixed = TRUE)
text <- gsub("[^a-z0-9]+", " ", text)
text <- gsub("\\s+", " ", text)
trimws(text)
}
build_conference_lookup <- function() {
teams <- read_scores_csv("teams.csv")
conv <- read_scores_csv("team_conversions.csv")
if (!all(c("conference", "team_name") %in% names(teams))) {
return(tibble::tibble(alias_key = character(), conference = character()))
}
if ("BRef" %in% names(conv)) {
keep_cols <- intersect(c("BRef", "NCAA", "D1Baseball"), names(conv))
base <- teams %>%
dplyr::select(conference, team_name) %>%
dplyr::left_join(conv %>% dplyr::select(dplyr::all_of(keep_cols)), by = c("team_name" = "BRef"))
} else {
base <- teams %>% dplyr::select(conference, team_name)
base$NCAA <- NA_character_
base$D1Baseball <- NA_character_
}
alias_tbl <- dplyr::bind_rows(
tibble::tibble(conference = base$conference, alias = base$team_name),
tibble::tibble(conference = base$conference, alias = base$NCAA),
tibble::tibble(conference = base$conference, alias = base$D1Baseball)
) %>%
dplyr::mutate(
conference = trimws(as.character(conference)),
alias_key = vapply(alias, normalize_team_name, character(1))
) %>%
dplyr::filter(!is.na(conference), nzchar(conference), nzchar(alias_key)) %>%
dplyr::distinct(alias_key, conference)
if (nrow(alias_tbl) == 0) {
return(tibble::tibble(alias_key = character(), conference = character()))
}
ambiguous <- alias_tbl %>%
dplyr::count(alias_key, name = "n_conferences") %>%
dplyr::filter(n_conferences > 1)
if (nrow(ambiguous) > 0) {
alias_tbl <- alias_tbl %>%
dplyr::filter(!(alias_key %in% ambiguous$alias_key))
}
alias_tbl %>% dplyr::distinct(alias_key, conference)
}
get_conference_lookup <- local({
cache <- NULL
function() {
if (is.null(cache)) {
cache <<- build_conference_lookup()
}
cache
}
})
lookup_conference_from_team <- function(competitor) {
lookup <- get_conference_lookup()
if (nrow(lookup) == 0) {
return(NA_character_)
}
team <- competitor$team %||% list()
candidates <- c(
as.character(team$displayName %||% NA_character_),
as.character(team$shortDisplayName %||% NA_character_),
as.character(team$name %||% NA_character_),
as.character(team$location %||% NA_character_),
if (!is.null(team$location) && !is.null(team$name)) {
as.character(paste(team$location, team$name))
} else {
NA_character_
}
)
for (candidate in candidates) {
key <- normalize_team_name(candidate)
if (!nzchar(key)) {
next
}
matched <- lookup %>% dplyr::filter(alias_key == key)
if (nrow(matched) > 0) {
value <- trimws(as.character(matched$conference[[1]]))
if (!is.na(value) && nzchar(value)) {
return(value)
}
}
}
NA_character_
}
lookup_conference_from_team_name <- function(team_name) {
lookup <- get_conference_lookup()
if (nrow(lookup) == 0) {
return(NA_character_)
}
key <- normalize_team_name(team_name)
if (!nzchar(key)) {
return(NA_character_)
}
matched <- lookup %>% dplyr::filter(alias_key == key)
if (nrow(matched) == 0) {
return(NA_character_)
}
value <- trimws(as.character(matched$conference[[1]]))
if (is.na(value) || !nzchar(value)) {
return(NA_character_)
}
value
}
extract_conference_name <- function(competitor) {
lookup_value <- lookup_conference_from_team(competitor)
if (!is.na(lookup_value) && nzchar(lookup_value)) {
return(lookup_value)
}
candidates <- list(
competitor$team$conferenceName,
competitor$team$conferenceShortName,
competitor$team$conference$displayName,
competitor$team$conference$name,
competitor$team$conference$shortDisplayName,
competitor$team$conference$shortName,
competitor$team$groups[[1]]$displayName,
competitor$team$groups[[1]]$name,
competitor$team$groups[[1]]$shortName,
competitor$team$group$displayName,
competitor$team$group$name,
competitor$team$group$shortName,
competitor$conference$displayName,
competitor$conference$name,
competitor$conference$shortDisplayName,
competitor$conference$shortName,
competitor$groups[[1]]$displayName,
competitor$groups[[1]]$name,
competitor$groups[[1]]$shortName
)
for (candidate in candidates) {
if (is.null(candidate) || length(candidate) == 0) {
next
}
value <- trimws(as.character(candidate[[1]]))
if (!is.na(value) && nzchar(value)) {
return(value)
}
}
NA_character_
}
extract_logo_url <- function(competitor) {
team_id <- as.character(competitor$team$id %||% NA_character_)
team_logo_by_id <- if (!is.na(team_id) && nzchar(team_id)) {
sprintf("https://a.espncdn.com/i/teamlogos/ncaa/500/%s.png", team_id)
} else {
NA_character_
}
first_logo <- NULL
if (is.list(competitor$team$logos) && length(competitor$team$logos) > 0) {
first_logo <- competitor$team$logos[[1]]$href %||% competitor$team$logos[[1]]$url
}
alt_logo <- NULL
if (is.list(competitor$logos) && length(competitor$logos) > 0) {
alt_logo <- competitor$logos[[1]]$href %||% competitor$logos[[1]]$url
}
candidates <- list(
first_logo,
competitor$team$logo,
alt_logo,
competitor$logo,
team_logo_by_id
)
for (candidate in candidates) {
if (is.null(candidate) || length(candidate) == 0) {
next
}
value <- trimws(as.character(candidate[[1]]))
if (!is.na(value) && nzchar(value)) {
return(value)
}
}
NA_character_
}
extract_game_location <- function(competition, event) {
venue <- competition$venue %||% event$venue %||% list()
venue_name <- trimws(as.character(venue$fullName %||% venue$name %||% venue$shortName %||% NA_character_))
city <- trimws(as.character(venue$address$city %||% NA_character_))
state <- trimws(as.character(venue$address$state %||% venue$address$stateAbbrev %||% NA_character_))
city_state_parts <- c(city, state)
city_state_parts <- city_state_parts[!is.na(city_state_parts) & nzchar(city_state_parts)]
city_state <- if (length(city_state_parts) > 0) {
paste(city_state_parts, collapse = ", ")
} else {
NA_character_
}
if (!is.na(venue_name) && nzchar(venue_name) && !is.na(city_state) && nzchar(city_state)) {
return(paste(venue_name, city_state, sep = ", "))
}
if (!is.na(venue_name) && nzchar(venue_name)) {
return(venue_name)
}
if (!is.na(city_state) && nzchar(city_state)) {
return(city_state)
}
NA_character_
}
extract_game_start_time <- function(status_short_detail, status_detail, competition, event) {
time_pattern <- "([0-9]{1,2}:[0-9]{2}\\s*[APap][Mm](?:\\s*[A-Za-z]{1,4})?)"
candidates <- c(
as.character(status_short_detail %||% ""),
as.character(status_detail %||% "")
)
for (candidate in candidates) {
text <- trimws(candidate)
if (is.na(text) || !nzchar(text)) {
next
}
m <- regexpr(time_pattern, text, perl = TRUE)
if (m[1] != -1) {
out <- regmatches(text, m)[1]
out <- toupper(gsub("\\s+", " ", trimws(out)))
return(out)
}
}
start_raw <- as.character(competition$date %||% event$date %||% NA_character_)
if (!is.na(start_raw) && nzchar(start_raw)) {
parsed <- suppressWarnings(as.POSIXct(start_raw, tz = "America/New_York"))
if (!is.na(parsed)) {
out <- format(parsed, "%I:%M %p")
out <- sub("^0", "", out)
return(paste(out, "ET"))
}
}
NA_character_
}
parse_start_datetime_numeric <- function(value) {
text <- trimws(as.character(value %||% ""))
if (!nzchar(text)) {
return(NA_real_)
}
fmts <- c(
"%Y-%m-%dT%H:%M:%OSZ",
"%Y-%m-%dT%H:%MZ",
"%Y-%m-%dT%H:%M:%OS%z",
"%Y-%m-%dT%H:%M%z"
)
for (fmt in fmts) {
parsed <- suppressWarnings(as.POSIXct(strptime(text, fmt, tz = "UTC"), tz = "UTC"))
if (!is.na(parsed)) {
return(as.numeric(parsed))
}
}
parsed_fallback <- suppressWarnings(as.POSIXct(text, tz = "UTC"))
if (!is.na(parsed_fallback)) {
return(as.numeric(parsed_fallback))
}
NA_real_
}
parse_time_label_minutes <- function(value) {
text <- toupper(trimws(as.character(value %||% "")))
if (!nzchar(text)) {
return(NA_real_)
}
m <- regexec("([0-9]{1,2}):([0-9]{2})\\s*([AP]M)", text, perl = TRUE)
parts <- regmatches(text, m)[[1]]
if (length(parts) != 4) {
return(NA_real_)
}
hour <- suppressWarnings(as.integer(parts[2]))
minute <- suppressWarnings(as.integer(parts[3]))
ampm <- parts[4]
if (is.na(hour) || is.na(minute)) {
return(NA_real_)
}
hour <- hour %% 12L
if (identical(ampm, "PM")) {
hour <- hour + 12L
}
as.numeric(hour * 60L + minute)
}
extract_scoreboard_team_name <- function(competitor, fallback = "Team") {
team <- competitor$team %||% list()
candidates <- c(
as.character(team$shortDisplayName %||% NA_character_),
as.character(team$location %||% NA_character_),
as.character(team$name %||% NA_character_),
as.character(team$displayName %||% NA_character_)
)
candidates <- trimws(candidates)
candidates <- candidates[!is.na(candidates) & nzchar(candidates)]
if (length(candidates) == 0) {
return(as.character(fallback))
}
candidates[[1]]
}
sanitize_scoreboard_record <- function(value) {
text <- trimws(as.character(value %||% ""))
if (!nzchar(text)) {
return(NA_character_)
}
m <- regexpr("[0-9]+-[0-9]+(?:-[0-9]+)?", text, perl = TRUE)
if (!is.na(m[[1]]) && m[[1]] != -1) {
return(regmatches(text, m)[[1]])
}
text
}
extract_scoreboard_record <- function(competitor) {
records <- competitor$records %||% list()
if (length(records) == 0) {
return(NA_character_)
}
record_value <- function(record_entry) {
summary <- as.character(
record_entry$summary %||%
record_entry$displayValue %||%
record_entry$value %||%
NA_character_
)
summary <- sanitize_scoreboard_record(summary)
if (!is.na(summary) && nzchar(summary)) {
return(summary)
}
wins <- suppressWarnings(as.integer(record_entry$wins %||% NA_integer_))
losses <- suppressWarnings(as.integer(record_entry$losses %||% NA_integer_))
ties <- suppressWarnings(as.integer(record_entry$ties %||% NA_integer_))
if (!is.na(wins) && !is.na(losses)) {
if (!is.na(ties) && ties > 0) {
return(sprintf("%d-%d-%d", wins, losses, ties))
}
return(sprintf("%d-%d", wins, losses))
}
NA_character_
}
overall_idx <- which(vapply(records, function(rec) {
rec_name <- tolower(trimws(as.character(rec$name %||% rec$type %||% rec$description %||% "")))
grepl("overall|total|season", rec_name)
}, logical(1)))
if (length(overall_idx) > 0) {
for (idx in overall_idx) {
value <- record_value(records[[idx]])
if (!is.na(value) && nzchar(value)) {
return(value)
}
}
}
for (rec in records) {
value <- record_value(rec)
if (!is.na(value) && nzchar(value)) {
return(value)
}
}
NA_character_
}
parse_espn_score_event <- function(event) {
competition <- event$competitions[[1]] %||% list()
competitors <- competition$competitors %||% list()
away <- purrr::detect(competitors, ~ identical(.x$homeAway, "away")) %||% list()
home <- purrr::detect(competitors, ~ identical(.x$homeAway, "home")) %||% list()
status <- event$status$type %||% competition$status$type %||% list()
away_score <- suppressWarnings(as.integer(away$score %||% NA_character_))
home_score <- suppressWarnings(as.integer(home$score %||% NA_character_))
inning <- suppressWarnings(as.integer(event$status$period %||% competition$status$period %||% NA_integer_))
away_conf_id <- as.character(away$team$conferenceId %||% away$team$conference$id %||% away$conference$id %||% NA_character_)
home_conf_id <- as.character(home$team$conferenceId %||% home$team$conference$id %||% home$conference$id %||% NA_character_)
away_rank <- extract_competitor_rank(away)
home_rank <- extract_competitor_rank(home)
away_conference <- extract_conference_name(away)
home_conference <- extract_conference_name(home)
if (is.na(away_conference) || !nzchar(away_conference)) {
away_conference <- if (!is.na(away_conf_id) && nzchar(away_conf_id)) paste0("Conference ", away_conf_id) else NA_character_
}
if (is.na(home_conference) || !nzchar(home_conference)) {
home_conference <- if (!is.na(home_conf_id) && nzchar(home_conf_id)) paste0("Conference ", home_conf_id) else NA_character_
}
away_logo <- extract_logo_url(away)
home_logo <- extract_logo_url(home)
location <- extract_game_location(competition, event)
top25_game <- (!is.na(away_rank) && away_rank <= 25L) || (!is.na(home_rank) && home_rank <= 25L)
away_school <- extract_scoreboard_team_name(away, fallback = away$team$displayName %||% "Away")
home_school <- extract_scoreboard_team_name(home, fallback = home$team$displayName %||% "Home")
away_record <- extract_scoreboard_record(away)
home_record <- extract_scoreboard_record(home)
away_name <- away$team$displayName %||% away$team$shortDisplayName %||% "Away"
home_name <- home$team$displayName %||% home$team$shortDisplayName %||% "Home"
status_desc <- as.character(status$description %||% "Scheduled")
status_detail <- as.character(status$detail %||% status_desc)
status_short_detail <- as.character(status$shortDetail %||% competition$status$type$shortDetail %||% status_detail)
start_datetime_utc <- as.character(competition$date %||% event$date %||% NA_character_)
start_time <- extract_game_start_time(status_short_detail, status_detail, competition, event)
start_sort_key <- parse_start_datetime_numeric(start_datetime_utc)
start_time_minutes <- parse_time_label_minutes(start_time)
status_state <- tolower(as.character(status$state %||% ""))
status_name <- toupper(as.character(status$name %||% ""))
status_blob <- toupper(paste(status_name, status_desc, status_detail, status_short_detail))
canceled <- grepl("CANCEL", status_blob)
live <- identical(status_state, "in") || grepl("\\bLIVE\\b", status_blob)
final <- isTRUE(status$completed) || identical(status_state, "post")
conference_flag <- extract_bool(competition$conferenceCompetition %||% event$conferenceCompetition)
conference_by_team <- !is.na(away_conf_id) && !is.na(home_conf_id) && nzchar(away_conf_id) && nzchar(home_conf_id) && identical(away_conf_id, home_conf_id)
conference_game <- if (!is.na(conference_flag)) conference_flag else conference_by_team
status_category <- dplyr::case_when(
canceled ~ "canceled",
live ~ "live",
final ~ "final",
TRUE ~ "upcoming"
)
status_label <- dplyr::case_when(
status_category == "canceled" ~ "Canceled",
status_category == "live" ~ "Live",
status_category == "final" ~ "Final",
TRUE ~ "Upcoming"
)
winner <- dplyr::case_when(
status_category == "final" & !is.na(away_score) & !is.na(home_score) & away_score > home_score ~ "away",
status_category == "final" & !is.na(away_score) & !is.na(home_score) & home_score > away_score ~ "home",
TRUE ~ NA_character_
)
status_display <- dplyr::case_when(
status_category == "live" ~ paste0("LIVE - ", status_detail),
status_category == "final" ~ "Final",
status_category == "canceled" ~ "Canceled",
TRUE ~ status_detail
)
inning_arrow <- dplyr::case_when(
status_category != "live" ~ "",
grepl("\\bTOP\\b|\\bTOP OF\\b", toupper(status_short_detail)) ~ "up",
grepl("\\bBOT\\b|\\bBOTTOM\\b", toupper(status_short_detail)) ~ "down",
TRUE ~ ""
)
inning_display <- dplyr::case_when(
status_category == "final" & is.na(inning) ~ "F",
status_category == "final" & inning == 9L ~ "F",
status_category == "final" ~ paste0("F/", inning),
status_category == "live" & !is.na(inning) ~ as.character(inning),
TRUE ~ "-"
)
live_late <- status_category == "live" && !is.na(inning) && inning >= 7
live_close <- status_category == "live" && !is.na(away_score) && !is.na(home_score) && abs(away_score - home_score) <= 2
tibble::tibble(
game_id = as.character(event$id %||% NA_character_),
away = as.character(away_name),
home = as.character(home_name),
away_school = as.character(away_school),
home_school = as.character(home_school),
away_record = as.character(away_record),
home_record = as.character(home_record),
away_rank = away_rank,
home_rank = home_rank,
away_conference = away_conference,
home_conference = home_conference,
away_conference_id = away_conf_id,
home_conference_id = home_conf_id,
away_logo = away_logo,
home_logo = home_logo,
location = as.character(location),
top25_game = top25_game,
conference_game = conference_game,
inning = inning,
away_score = away_score,
home_score = home_score,
away_score_display = ifelse(is.na(away_score), "-", as.character(away_score)),
home_score_display = ifelse(is.na(home_score), "-", as.character(home_score)),
status = as.character(status_desc),
detail = as.character(status_detail),
start_datetime_utc = as.character(start_datetime_utc),
start_time = as.character(start_time),
start_sort_key = as.numeric(start_sort_key),
start_time_minutes = as.numeric(start_time_minutes),
status_label = as.character(status_label),
status_category = as.character(status_category),
status_display = as.character(status_display),
inning_display = as.character(inning_display),
inning_arrow = as.character(inning_arrow),
live = status_category == "live",
final = status_category == "final",
live_late = live_late,
live_close = live_close,
winner = winner
)
}
sort_scoreboard_games <- function(games) {
if (nrow(games) == 0) {
return(games)
}
if (!("start_datetime_utc" %in% names(games))) {
games$start_datetime_utc <- NA_character_
}
if (!("start_time" %in% names(games))) {
games$start_time <- NA_character_
}
if (!("start_sort_key" %in% names(games))) {
games$start_sort_key <- NA_real_
}
if (!("start_time_minutes" %in% names(games))) {
games$start_time_minutes <- NA_real_
}
games %>%
dplyr::mutate(
sort_bucket = dplyr::case_when(
status_category == "live" ~ 0L,
status_category == "upcoming" ~ 1L,
status_category == "final" ~ 2L,
status_category == "canceled" ~ 3L,
TRUE ~ 4L
),
start_sort_abs = dplyr::coalesce(
as.numeric(start_sort_key),
vapply(start_datetime_utc, parse_start_datetime_numeric, numeric(1))
),
start_sort_time = dplyr::coalesce(
as.numeric(start_time_minutes),
vapply(start_time, parse_time_label_minutes, numeric(1))
),
start_sort_missing_abs = is.na(start_sort_abs),
start_sort = dplyr::if_else(start_sort_missing_abs, start_sort_time, start_sort_abs),
start_sort = ifelse(is.na(start_sort), Inf, start_sort)
) %>%
dplyr::arrange(sort_bucket, start_sort_missing_abs, start_sort, away, home) %>%
dplyr::select(-sort_bucket, -start_sort_abs, -start_sort_time, -start_sort_missing_abs, -start_sort)
}
cache_live_result <- function(key, expr, ttl_seconds) {
cache_get_or_set(
key = key,
expr = expr,
namespace = "live_data",
ttl = ttl_seconds
)
}
fetch_espn_scoreboard <- function(game_date = Sys.Date(), limit = 200) {
date_value <- as.Date(game_date)
if (is.na(date_value)) {
empty <- tibble::tibble()
attr(empty, "fetch_error") <- "Invalid date."
return(empty)
}
cache_key <- make_cache_key("espn_scoreboard", format(date_value, "%Y%m%d"), as.integer(limit))
ttl_seconds <- get_cache_ttl_seconds("WVU_APP_CACHE_TTL_SCORES", default_seconds = 120)
cache_live_result(cache_key, {
date_key <- format(date_value, "%Y%m%d")
url <- sprintf(
"https://site.api.espn.com/apis/site/v2/sports/baseball/college-baseball/scoreboard?dates=%s&limit=%d",
date_key,
as.integer(limit)
)
payload <- tryCatch(
jsonlite::fromJSON(url, simplifyVector = FALSE),
error = function(e) {
NULL
}
)
if (is.null(payload)) {
empty <- tibble::tibble()
attr(empty, "fetch_error") <- "Could not reach ESPN scoreboard API."
return(empty)
}
events <- payload$events %||% list()
if (length(events) == 0) {
return(tibble::tibble())
}
games <- purrr::map_dfr(events, parse_espn_score_event)
if (nrow(games) == 0) {
return(tibble::tibble())
}
sort_scoreboard_games(games)
}, ttl_seconds = ttl_seconds)
}
is_standings_entry <- function(entry) {
is.list(entry) && (!is.null(entry$team) || !is.null(entry$stats) || !is.null(entry$records))