Drop the PTXQC dependency and bump to 1.3.0.9000
PTXQC was imported for exactly two small string functions, lcpCount()
and lcsCount(), both used only by queryStringToLabel(). It brought
rmzqc, jsonvalidate, seqinr and V8 with it, and V8 was the only
dependency in the whole tree that required a libv8 installation, which
made it by far the most expensive part of installing RKorAPClient on
Linux. The recursive dependency closure shrinks from 128 to 90 packages.
Both functions are pure R in PTXQC, so reimplementing them costs no
compiled code. The replacements determine the length of the common
prefix and suffix by binary search over vectorized startsWith() and
endsWith() calls, instead of comparing character by character (and, for
the suffix, reversing every string with strsplit/rev/paste first). That
makes them 10 to 65 times faster, depending on the number and length of
the strings:
PTXQC here
10 query strings, 37 chars prefix 183 us 16 us
suffix 127 us 13 us
121 vcs, 42 chars prefix 413 us 29 us
suffix 1029 us 26 us
50 strings, 563 chars prefix 1405 us 64 us
suffix 4362 us 67 us
Results are unchanged: the two functions were compared against their
PTXQC counterparts over 3020 inputs, and queryStringToLabel() over 8044
input/argument combinations, without a single difference. Empty vectors,
empty strings and the single string case (where a string is its own
prefix and suffix) behave exactly as before. The only deviation is for
NA input, where PTXQC's lcsCount() silently treats NA as the literal
string "NA"; queryStringToLabel() never reaches it in that case, because
the prefix function errors on NA in both implementations.
Since V8 is gone, and keyring no longer needs sodium either, libv8-dev
and libsodium-dev can be dropped from the Linux installation
instructions in the Readme.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Change-Id: Icad4bc76b773b1586922dd05458b82409d112932
diff --git a/R/misc.R b/R/misc.R
index ac70f47..6e7340f 100644
--- a/R/misc.R
+++ b/R/misc.R
@@ -57,6 +57,76 @@
mutate(f = .data$f * 10^2, conf.low = .data$conf.low * 10^2, conf.high = .data$conf.high * 10^2)
}
+#' Number of characters that all given strings share as a prefix
+#'
+#' Replaces `PTXQC::lcpCount()`, to avoid depending on PTXQC for two small
+#' string functions. The length is determined by binary search over vectorized
+#' [startsWith()] calls, which needs `log2(n)` instead of `n` comparisons for a
+#' common prefix of `n` characters.
+#'
+#' As in `PTXQC::lcpCount()`, a single string is its own prefix, and an empty
+#' vector has a common prefix of length 0.
+#'
+#' @param strings vector of strings
+#' @return number of characters common to the beginning of all `strings`
+#' @noRd
+longestCommonPrefixLength <- function(strings) {
+ strings <- as.character(strings)
+ if (length(strings) == 0) {
+ return(0L)
+ }
+ if (length(strings) == 1) {
+ return(nchar(strings[1]))
+ }
+
+ low <- 0L
+ high <- min(nchar(strings))
+ while (low < high) {
+ middle <- (low + high + 1L) %/% 2L
+ # isTRUE keeps NAs from breaking the condition, as in PTXQC, where they
+ # make the character comparison fail and thus end the common prefix
+ if (isTRUE(all(startsWith(strings, substr(strings[1], 1L, middle))))) {
+ low <- middle
+ } else {
+ high <- middle - 1L
+ }
+ }
+ low
+}
+
+#' Number of characters that all given strings share as a suffix
+#'
+#' Replaces `PTXQC::lcsCount()`, which reverses every string character by
+#' character before looking for the common prefix. Works like
+#' `longestCommonPrefixLength()`, but anchored at the end of the strings.
+#'
+#' @param strings vector of strings
+#' @return number of characters common to the end of all `strings`
+#' @noRd
+longestCommonSuffixLength <- function(strings) {
+ strings <- as.character(strings)
+ if (length(strings) == 0) {
+ return(0L)
+ }
+ if (length(strings) == 1) {
+ return(nchar(strings[1]))
+ }
+
+ first <- strings[1]
+ firstLength <- nchar(first)
+ low <- 0L
+ high <- min(nchar(strings))
+ while (low < high) {
+ middle <- (low + high + 1L) %/% 2L
+ if (isTRUE(all(endsWith(strings, substr(first, firstLength - middle + 1L, firstLength))))) {
+ low <- middle
+ } else {
+ high <- middle - 1L
+ }
+ }
+ low
+}
+
#' Convert query or vc strings to plot labels
#'
#' Converts a vector of query or vc strings to typically appropriate legend labels
@@ -74,9 +144,6 @@
#' queryStringToLabel(c("[marmot/m=mood:subj]", "[marmot/m=mood:ind]"))
#' queryStringToLabel(c("wegen dem [tt/p=NN]", "wegen des [tt/p=NN]"))
#'
-#' @importFrom PTXQC lcpCount
-#' @importFrom PTXQC lcsCount
-#'
#' @export
queryStringToLabel <- function(data, pubDateOnly = FALSE, excludePubDate = FALSE) {
if (pubDateOnly) {
@@ -84,11 +151,11 @@
} else if(excludePubDate) {
data <-substring(data, 1, regexpr("(pub|creation)Date", data))
}
- leftCommon = lcpCount(data)
+ leftCommon = longestCommonPrefixLength(data)
while (leftCommon > 0 && grepl("[[:alnum:]/=.*!]", substring(data[1], leftCommon, leftCommon))) {
leftCommon <- leftCommon - 1
}
- rightCommon = lcsCount(data)
+ rightCommon = longestCommonSuffixLength(data)
while (rightCommon > 0 && grepl("[[:alnum:]/=.*!]", substring(data[1], 1+nchar(data[1]) - rightCommon, 1+nchar(data[1]) - rightCommon))) {
rightCommon <- rightCommon - 1
}