7  Troubleshooting & FAQ

7.1 Installation

“could not find tools necessary to compile a package” / install fails

fast.string compiles C++ source on install (PCRE2 bindings, prepared literal search, RcppParallel). You need a working toolchain:

  • Windows: install Rtools matching your R version, and make sure it’s on PATH (the Rtools installer offers to do this). Verify with pkgbuild::has_build_tools(debug = TRUE).
  • macOS: install Xcode command-line tools (xcode-select --install) and PCRE2 (brew install pcre2).
  • Linux: a C++ compiler (gcc/clang) plus the PCRE2 development headers — libpcre2-dev on Debian/Ubuntu, pcre2-devel on Fedora/RHEL.

fast.string needs R 4.1.0 or later.

If pkgbuild::has_build_tools(debug = TRUE) returns FALSE, the underlying compiler invocation it prints is the fastest way to see exactly what’s missing.

Reinstalling after pulling new source changes

If you’re developing against a local checkout rather than installing a release, devtools::load_all() recompiles changed src/*.cpp files automatically; a plain library(fast.string) won’t pick up source changes without reinstalling first.

7.2 NA, empty strings, and encoding

Why does my function return NA for a non-NA input?

Every function in this package returns NA for NA input — but a few functions also return NA for specific valid-looking but unparseable inputs:

  • soundex()/nysiis()/refined_soundex()/double_metaphone() return NA for strings with no alphabetic characters at all (e.g. "123", "") — there’s no phonetic code for a string with nothing to encode phonetically. cologne() does the same, counting umlauts and ß as letters it can encode. caverphone() is the exception: it always returns a 10-character code for non-NA input (even "", which codes as "1111111111"), matching its reference implementation’s behaviour.
  • fas.Date() returns NA for any string that doesn’t match the specified format exactly (wrong length, wrong separator position, non-digit where a digit is expected) or has an out-of-range month/day. Unlike base::as.Date(), it does not attempt multiple formats or partial parsing.
  • fas.POSIXct() is stricter still: on top of the same exact-shape rule, it validates the whole calendar, so "2023-02-29 00:00:00" is NA even though every field is individually in range. This is deliberate and is the one place the timestamp functions are less permissive than the date functions — see Chapter 3.
soundex(c("Smith", "123", "", NA))
[1] "S530" NA     NA     NA    
fas.Date(c("2024-06-18", "2024/06/18", "2024-13-01"), "iso")  # 2nd: wrong separator, 3rd: month 13
[1] "2024-06-18" NA           NA          
fas.POSIXct(c("2024-06-18 09:15:00", "2023-02-29 00:00:00"))  # 2nd: not a leap year
[1] "2024-06-18 09:15:00 UTC" NA                       

fchartr() silently fell back to base::chartr() — why?

fchartr()’s fast path is a flat 256-entry byte lookup table, which only works correctly when every character in old and new is single-byte (ASCII). If either argument contains a multi-byte UTF-8 character (an accented letter, a curly quote, an emoji), the function detects that via a byte-count vs. char-count comparison and transparently delegates to base::chartr() instead — same result, just without the speedup, and without you needing to know in advance.

fchartr("e", "é", "test")  # "é" is multi-byte — falls back to base::chartr(), still correct
[1] "tést"

fnchar() bytes vs. chars

type = "bytes" counts bytes; type = "chars" counts Unicode codepoints. These differ for any non-ASCII input — get this backwards and a fixed-width field extraction (fsubstr()) on multi-byte data will be off by however many multi-byte characters are in the string.

fnchar("café", type = "bytes")  # 5 — "é" is 2 bytes in UTF-8
[1] 5
fnchar("café", type = "chars") # 4 — 4 visible characters
[1] 4

Similarity bytes vs. Unicode code points

Established edit-distance and Jaro-Winkler functions default to use_bytes = TRUE for compatibility. Set use_bytes = FALSE when one accented character or emoji should count as one comparison symbol. Fuzzy lookup defaults to code points. This does not perform Unicode normalization: precomposed and decomposed spellings can still compare differently.

Bytes-encoded R strings cannot be decoded as Unicode and produce an indexed error in code-point mode; pass use_bytes = TRUE when those bytes are the intended data.

7.3 Threading

Does nthreads affect later calls?

No. Functions that expose nthreads pass it to the native dispatcher as a per-call cap; they do not change RcppParallel’s process-wide options. NULL uses the RcppParallel default for that call, a positive integer limits it to at most that many threads, and nthreads = 1 forces strict serial execution. A later call without nthreads is unaffected.

The dispatcher can use fewer threads than the cap when the input contains too little useful work. String-utility, date, and phonetic functions keep their existing signatures and use the default scheduler for their own calls.

Does nthreads help on a machine with few cores?

Not much below 4 cores, and the package’s own size thresholds (Chapter 3) mean it won’t even try to parallelise small inputs regardless. If you’re running inside a container with a CPU limit lower than parallel::detectCores() reports (common in CI and some cloud environments), set nthreads explicitly to the actual limit — TBB doesn’t know about cgroup CPU quotas and will otherwise oversubscribe.

The supported Windows build always uses TBB. On a diagnostic build forced to RcppParallel’s TinyThread backend, values above one are advisory because that backend ignores the native thread-count argument; the dispatcher coarsens its ranges to limit useful parallel work instead. nthreads = 1 still runs strictly serially on either backend. Set the build variable FAST_STRING_TINYTHREAD=1 only when installing such a diagnostic build.

7.4 Regex behaviour

fsub()/fgsub() with perl = FALSE replaced a different match than base R

Where alternatives can match at the same position, base R’s default (TRE) engine takes the longest match and PCRE2 takes the first alternative that matches: sub("a|ab", "X", "abc") gives "Xc", fsub() gives "Xbc". Whether something matches (fgrepl()) is unaffected. List the longer alternative first ("ab|a") to get the same result from both. Everything else about perl = FALSE follows base R’s rules – see Chapter 3.

Patterns that only PCRE understands (lookarounds, atomic groups, possessive quantifiers, named groups) run with perl = TRUE semantics even when perl = FALSE, still on the fast path. fcount() delegates only an empty pattern to base::gregexpr(), because base R counts those matches in characters.

A warning says “PCRE2 error ‘match limit exceeded’ for element …”

The pattern backtracked too much on that element (see the next question). As in base R, that element counts as not matching – FALSE for fgrepl(), unchanged for fsub()/fgsub(), 0 for fcount() – and the rest of the vector is processed normally.

Catastrophic backtracking — am I safe?

Only partially. The prepared fixed = TRUE path treats the pattern as literal bytes, so regex backtracking cannot occur. PCRE2 (used for general regex) is not immune — it’s still a backtracking engine, in the same risk class as base R’s perl = TRUE. If you’re matching untrusted patterns against untrusted input, that risk exists regardless of which package you use; this package doesn’t add to it, but doesn’t remove it either. Prefer fixed = TRUE whenever the pattern is actually a literal string: it avoids regex backtracking and is also the fastest path in the package.

7.5 Getting help