diff --git a/CITATION.cff b/CITATION.cff index 2bccbda2..f940601e 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -11,7 +11,7 @@ authors: - family-names: "Shang" given-names: "Yi" title: "SGP: Student Growth Percentiles & Percentile Growth Trajectories" -version: 2.3-1.10 +version: 2.3-1.11 doi: 10.5281/zenodo.10037891 -date-released: 2026-8-24 +date-released: 2026-8-7 url: "https://sgp.io" diff --git a/DESCRIPTION b/DESCRIPTION index a8cd5323..54745f4b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Package: SGP Type: Package Title: Student Growth Percentiles & Percentile Growth Trajectories -Version: 2.3-1.10 -Date: 2026-8-24 +Version: 2.3-1.11 +Date: 2026-8-27 Authors@R: c(person(given=c("Damian", "W."), family="Betebenner", email="dbetebenner@nciea.org", role=c("aut", "cre"), comment=c(ORCID = "0000-0003-0476-5599")), person(given=c("Adam", "R."), family="Van Iwaarden", email="avaniwaarden@nciea.org", role="aut"), person(given="Ben", family="Domingue", email="ben.domingue@gmail.com", role="aut"), @@ -76,7 +76,7 @@ Authors@R: c(person(given=c("Damian", "W."), family="Betebenner", email="dbetebe Maintainer: Damian W. Betebenner Depends: R (>= 4.2.0) Suggests: SGPdata (>= 28.0-0), knitr, rmarkdown -Imports: Cairo, carrier, collapse, colorspace, crayon, curl, datasets, data.table (>= 1.14.0), digest, equate (>= 2.0-5), graphics, grid, grDevices, gridBase, gtools, jsonlite, matrixStats, methods, mirai, parallel, pkgsearch, quantreg, randomNames (>= 0.0-5), Rfast, rlang, sn (>= 1.0-0), splines, stats, svglite, toOrdinal, utils +Imports: Cairo, carrier, collapse, colorspace, crayon, curl, datasets, data.table (>= 1.14.0), digest, equate (>= 2.0-5), graphics, grid, grDevices, gridBase, jsonlite, methods, mirai, parallel, pkgsearch, quantreg, randomNames (>= 0.0-5), Rfast, rlang, sn (>= 1.0-0), splines, stats, svglite, toOrdinal, utils Description: An analytic framework for the calculation of norm- and criterion-referenced academic growth estimates using large scale, longitudinal education assessment data as developed in Betebenner (2009) . SystemRequirements: (PDF)LaTeX (https://www.latex-project.org/) with 'pdfpages' package for studentGrowthPlot option in visualizeSGP to bind together student growth plots into school catalogs URL: https://sgp.io, https://github.com/CenterForAssessment/SGP, https://CRAN.R-project.org/package=SGP @@ -84,5 +84,5 @@ BugReports: https://github.com/CenterForAssessment/SGP/issues VignetteBuilder: knitr LazyData: Yes LazyDataCompression: xz -License: GPL-3 +License: GPL-3 ByteCompile: TRUE diff --git a/NAMESPACE b/NAMESPACE index 1ef5b7d2..14419589 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -43,14 +43,12 @@ importFrom(equate,freqtab,equate) importFrom(graphics,par,plot,text) importFrom(grDevices,colorRampPalette,col2rgb,dev.off,extendrange,hsv,pdf,rgb,rgb2hsv) importFrom(gridBase,gridFIG) -importFrom(gtools,mixedsort) -importFrom(jsonlite,toJSON) -importFrom(matrixStats,weightedMedian) +importFrom(jsonlite,toJSON) # Still need? Only used in studentGrowthPlot_Syles now... importFrom(mirai, collect_mirai, daemons, mirai_map) importFrom(parallel, mclapply, detectCores) -importFrom(quantreg,rq) +importFrom(quantreg, rq.fit, rq.fit.br) importFrom(randomNames,randomNames) -importFrom(rlang,inject) +importFrom(rlang, inject) importFrom(Rfast, coldiffs, Rnorm, Round, rowSort, rowMaxs, rowMins, rowVars) importFrom(sn,rsn) importFrom(splines,bs) diff --git a/R/gtools.R b/R/gtools.R new file mode 100644 index 00000000..82d58a7b --- /dev/null +++ b/R/gtools.R @@ -0,0 +1,234 @@ +#' FUNCTIONS COPIED FROM THE `gtools` package +#' (8/6/26 - https://github.com/r-gregmisc/gtools/blob/master/R/mixedsort.R -- c8c3ce1) +#' Modified slightly only to remove Roman numeral use cases. +#' All `gtools` examples and exports have been removed from this text (internal SGP use only) + +#' Order or Sort strings with embedded numbers so that the numbers are in the +#' correct order +#' +#' These functions sort or order character strings containing embedded numbers +#' so that the numbers are numerically sorted rather than sorted by character +#' value. I.e. "Aspirin 50mg" will come before "Aspirin 100mg". In addition, +#' case of character strings is ignored so that "a", will come before "B" and +#' "C". +#' +#' I often have character vectors (e.g. factor labels), such as compound and +#' dose, that contain both text and numeric data. This function is useful for +#' sorting these character vectors into a logical order. +#' +#' It does so by splitting each character vector into a sequence of character +#' and numeric sections, and then sorting along these sections, with numbers +#' being sorted by numeric value (e.g. "50" comes before "100"), followed by +#' characters strings sorted by character value (e.g. "A" comes before "B") +#' \emph{ignoring case} (e.g. 'A' has the same sort order as 'a'). +#' +#' By default, sort order is ascending, empty strings are sorted to the front, +#' and \code{NA} values to the end. Setting \code{descending=TRUE} changes the +#' sort order to descending and reverses the meanings of \code{na.last} and +#' \code{blank.last}. +#' +#' Parsing looks for decimal numbers unless \code{numeric.type="roman"}, in +#' which parsing looks for roman numerals, with character case specified by +#' \code{roman.case}. +#' +#' @aliases mixedsort mixedorder +#' @param x Vector to be sorted. +#' @param decreasing logical. Should the sort be increasing or decreasing? +#' Note that \code{descending=TRUE} reverses the meanings of \code{na.last} and +#' \code{blanks.last}. +#' @param na.last for controlling the treatment of \code{NA} values. If +#' \code{TRUE}, missing values in the data are put last; if \code{FALSE}, they +#' are put first; if \code{NA}, they are removed. +#' @param blank.last for controlling the treatment of blank values. If +#' \code{TRUE}, blank values in the data are put last; if \code{FALSE}, they +#' are put first; if \code{NA}, they are removed. +#' @param numeric.type either "decimal" (default) or "roman". Are numeric +#' values represented as decimal numbers (\code{numeric.type="decimal"}) or as +#' Roman numerals (\code{numeric.type="roman"})? +#' @param roman.case one of "upper", "lower", or "both". Are roman numerals +#' represented using only capital letters ('IX') or lower-case letters ('ix') +#' or both? +#' @param scientific logical. Should exponential notation be allowed for numeric values. +#' @param hyphen.separator logical. Should hyphenated strings be treated as separators? (If not, they will be treated as potential components of numeric values.) +#' @return \code{mixedorder} returns a vector giving the sort order of the +#' input elements. \code{mixedsort} returns the sorted vector. +#' @author Gregory R. Warnes \email{greg@@warnes.net} +#' @seealso \code{\link[base]{sort}}, \code{\link[base]{order}} +#' @keywords univar manip + +mixedsort <- function(x, + decreasing = FALSE, + na.last = TRUE, + blank.last = FALSE, + numeric.type = c("decimal", "roman"), + roman.case = c("upper", "lower", "both"), + scientific = TRUE, + hyphen.separator = FALSE) { + ord <- mixedorder(x, + decreasing = decreasing, + na.last = na.last, + blank.last = blank.last, + numeric.type = numeric.type, + roman.case = roman.case, + scientific = scientific, + hyphen.separator = hyphen.separator + ) + x[ord] +} + +mixedorder <- function(x, + decreasing = FALSE, + na.last = TRUE, + blank.last = FALSE, + numeric.type = c("decimal", "roman"), + roman.case = c("upper", "lower", "both"), + scientific = TRUE, + hyphen.separator = FALSE) { + # - Split each each character string into an vector of strings and + # numbers + # - Separately rank numbers and strings + # - Combine orders so that strings follow numbers + + numeric.type <- match.arg(numeric.type) + roman.case <- match.arg(roman.case) + + if (length(x) < 1) { + return(NULL) + } else if (length(x) == 1) { + return(1) + } + + if (!is.character(x)) { + return(order(x, decreasing = decreasing, na.last = na.last)) + } + + delim <- "\\$\\@\\$" + + nums <- "[0123456789]" + pm <- if (hyphen.separator) "[+]" else "[+-]" + + if (numeric.type == "decimal") { + if (scientific) { + regex <- sprintf("((?:(?i)(?:%s?)(?:(?=[.]?%s)(?:%s*)(?:(?:[.])(?:%s{0,}))?)(?:(?:[eE])(?:(?:[-+]?)(?:%s+))|)))", pm, nums, nums, nums, nums) + } # uses PERL syntax + else { + regex <- sprintf("((?:(?i)(?:%s?)(?:(?=[.]?%s)(?:%s*)(?:(?:[.])(?:%s{0,}))?)))", pm, nums, nums, nums) + } # uses PERL syntax + + numeric <- function(x) as.numeric(x) + } ## No roman numeral support in SGP + # else if (numeric.type == "roman") { + # regex <- switch(roman.case, + # "both" = "([IVXCLDMivxcldm]+)", + # "upper" = "([IVXCLDM]+)", + # "lower" = "([ivxcldm]+)" + # ) + # numeric <- function(x) roman2int(x) + # } + else { + stop("Unknown value for numeric.type: ", numeric.type) + } + + nonnumeric <- function(x) { + ifelse(is.na(numeric(x)), toupper(x), NA) + } + + x <- as.character(x) + + which.nas <- which(is.na(x)) + which.blanks <- which(x == "") + + #### + # - Convert each character string into an vector containing single + # character and numeric values. + #### + + # find and mark numbers in the form of +1.23e+45.67 + delimited <- gsub(regex, + paste(delim, "\\1", delim, sep = ""), + x, + perl = TRUE + ) + + # separate out numbers + step1 <- strsplit(delimited, delim) + + # remove empty elements + step1 <- lapply(step1, function(x) x[x > ""]) + + # create numeric version of data + suppressWarnings(step1.numeric <- lapply(step1, numeric)) + + # create non-numeric version of data + suppressWarnings(step1.character <- lapply(step1, nonnumeric)) + + # now transpose so that 1st vector contains 1st element from each + # original string + maxelem <- max(sapply(step1, length)) + + step1.numeric.t <- lapply( + 1:maxelem, + function(i) { + sapply( + step1.numeric, + function(x) x[i] + ) + } + ) + + step1.character.t <- lapply( + 1:maxelem, + function(i) { + sapply( + step1.character, + function(x) x[i] + ) + } + ) + + # now order them + rank.numeric <- sapply(step1.numeric.t, rank) + rank.character <- sapply( + step1.character.t, + function(x) as.numeric(factor(x)) + ) + + # and merge + rank.numeric[!is.na(rank.character)] <- 0 # mask off string values + + rank.character <- t( + t(rank.character) + + apply(matrix(rank.numeric), 2, max, na.rm = TRUE) + ) + + rank.overall <- ifelse(is.na(rank.character), rank.numeric, rank.character) + + order.frame <- as.data.frame(rank.overall) + if (length(which.nas) > 0) { + if (is.na(na.last)) { + order.frame[which.nas, ] <- NA + } else if (na.last) { + order.frame[which.nas, ] <- Inf + } else { + order.frame[which.nas, ] <- -Inf + } + } + + if (length(which.blanks) > 0) { + if (is.na(blank.last)) { + order.frame[which.blanks, ] <- NA + } else if (blank.last) { + order.frame[which.blanks, ] <- 1e99 + } else { + order.frame[which.blanks, ] <- -1e99 + } + } + + order.frame <- as.list(order.frame) + order.frame$decreasing <- decreasing + order.frame$na.last <- NA + + retval <- do.call("order", order.frame) + + return(retval) +} diff --git a/R/testSGP.R b/R/testSGP.R index 98e02125..9831a6ec 100644 --- a/R/testSGP.R +++ b/R/testSGP.R @@ -1222,7 +1222,10 @@ # identical(mean(Demonstration_SGP@Data$SGP_STANDARD_ERROR, na.rm=TRUE) |> round(3), 15.515) # 8.509 # identical(digest(Demonstration_SGP@Data$SGP_STANDARD_ERROR), "cb81264f5933f0e4e22d515b5437f97f") # pre- parallel refactor key: ba125c9bb4a62a0e76bcbac55fca913b - if (identical(digest(Demonstration_SGP@Data$SGP_STANDARD_ERROR), "49c99d958745f7f5382ee8e7a2289560")) { + if (identical( + digest(Demonstration_SGP@Data$SGP_STANDARD_ERROR), + ifelse(.Platform$OS.type == "unix", "49c99d958745f7f5382ee8e7a2289560", "4db943ed3216372d4c54823134c4279d")) + ) { tmp.messages <- c(tmp.messages, "\tTest of variable SGP_STANDARD_ERROR (Omitting ALGEBRA_II): OK\n") } else { tmp.messages <- c(tmp.messages, "\tTest of variable SGP_STANDARD_ERROR (Omitting ALGEBRA_II): FAIL\n") diff --git a/R/zzz.R b/R/zzz.R index a684b3d3..28805fb3 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -32,7 +32,7 @@ function(libname, pkgname) { installed.version <- utils::packageDescription("SGP")[["Version"]] cran.version <- tryCatch({ # Add timeout to CRAN check - curl::curl_fetch_memory("https://cran.r-project.org/web/packages/SGP/index.html", + curl::curl_fetch_memory("https://cran.r-project.org/web/packages/SGP/index.html", handle = curl::new_handle(timeout = 2)) green("v", pkgsearch::cran_package("SGP")[["Version"]], sep="") }, error = function(e) red("Not Available"), @@ -41,7 +41,7 @@ function(libname, pkgname) { # Define a friendly startup message message_text <- paste0( - magenta(bold("\uD83C\uDF89 SGP v", installed.version, sep="")), " - ", toOrdinal::toOrdinalDate("2026-8-24"), "\n", + magenta(bold("\uD83C\uDF89 SGP v", installed.version, sep="")), " - ", toOrdinal::toOrdinalDate("2026-8-27"), "\n", strrep("\u2501", 40), "\n", bold("\U1F4E6 CRAN: "), cran.version, "\n", bold("\U1F527 Dev: "), dev.version, "\n", diff --git a/README.md b/README.md index ff545dba..bfef7096 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ SGP [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.6471237.svg)](https://doi.org/10.5281/zenodo.6471237) [![R-CMD-check](https://github.com/CenterForAssessment/SGP/workflows/R-CMD-check/badge.svg)](https://github.com/CenterForAssessment/SGP/actions) [![CRAN_Status_Badge](https://www.r-pkg.org/badges/version/SGP)](https://cran.r-project.org/package=SGP) -[![Development Version](https://img.shields.io/badge/devel-2.3--1.10-brightgreen.svg)](https://github.com/CenterForAssessment/SGP) +[![Development Version](https://img.shields.io/badge/devel-2.3--1.11-brightgreen.svg)](https://github.com/CenterForAssessment/SGP) [![Rstudio mirror downloads](https://cranlogs.r-pkg.org/badges/grand-total/SGP)](https://github.com/metacran/cranlogs.app) [![License](https://img.shields.io/badge/license-GPL%203-brightgreen.svg?style=flat)](https://github.com/CenterForAssessment/SGP/blob/master/LICENSE.md) diff --git a/data/SGPstateData.rda b/data/SGPstateData.rda index a56bf9f1..728411e7 100644 Binary files a/data/SGPstateData.rda and b/data/SGPstateData.rda differ diff --git a/inst/CITATION b/inst/CITATION index 2e97f11e..ecf04151 100644 --- a/inst/CITATION +++ b/inst/CITATION @@ -9,12 +9,12 @@ bibentry( person(given = c("Yi"), family = "Shang") ), year = "2026", - note = "R package version 2.3-1.10", + note = "R package version 2.3-1.11", url = "https://sgp.io", textVersion = paste( "Damian W. Betebenner, Adam R. Van Iwaarden, Benjamin Domingue and Yi Shang (2026).", "SGP: Student Growth Percentiles & Percentile Growth Trajectories.", - "(R package version 2.3-1.10)", + "(R package version 2.3-1.11)", "URL: https://sgp.io" ) ) diff --git a/man/SGP-package.Rd b/man/SGP-package.Rd index 35b10aca..5e9dcfde 100644 --- a/man/SGP-package.Rd +++ b/man/SGP-package.Rd @@ -19,8 +19,8 @@ growth projections to be calculated across assessment transitions by equating th \tabular{ll}{ Package: \tab SGP\cr Type: \tab Package\cr -Version: \tab 2.3-1.10\cr -Date: \tab 2026-8-24\cr +Version: \tab 2.3-1.11\cr +Date: \tab 2026-8-27\cr License: \tab GPL-3\cr LazyLoad: \tab yes\cr }