Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -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"
8 changes: 4 additions & 4 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -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"),
Expand Down Expand Up @@ -76,13 +76,13 @@ Authors@R: c(person(given=c("Damian", "W."), family="Betebenner", email="dbetebe
Maintainer: Damian W. Betebenner <dbetebenner@nciea.org>
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) <doi:10.1111/j.1745-3992.2009.00161.x>.
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
BugReports: https://github.com/CenterForAssessment/SGP/issues
VignetteBuilder: knitr
LazyData: Yes
LazyDataCompression: xz
License: GPL-3
License: GPL-3
ByteCompile: TRUE
8 changes: 3 additions & 5 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
234 changes: 234 additions & 0 deletions R/gtools.R
Original file line number Diff line number Diff line change
@@ -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)
}
5 changes: 4 additions & 1 deletion R/testSGP.R
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions R/zzz.R
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Binary file modified data/SGPstateData.rda
Binary file not shown.
4 changes: 2 additions & 2 deletions inst/CITATION
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
)
4 changes: 2 additions & 2 deletions man/SGP-package.Rd
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading