zufast is a C library first: its R functions exist to test, benchmark
and demonstrate the headers, and they are a convenient way to see what
the C layer does. This article runs every exported function. All of them
are vectorised, keep NA as NA, and turn
invalid input into NA (or NULL in a list)
without a warning. Arguments of the wrong type raise a condition of
class zufast_invalid_argument, which inherits from
zufast_error.
Package information
fast_info() reports, from compiled code, the version of
the headers zufast’s own shared object was built with, the vendored
library versions and the compiler.
str(fast_info())
#> List of 7
#> $ version : chr "0.1.0"
#> $ version_major: int 0
#> $ version_minor: int 1
#> $ version_patch: int 0
#> $ compiler : chr "gcc 13.3.0"
#> $ build : Named chr [1:7] "202000" "true" "true" "3" ...
#> ..- attr(*, "names")= chr [1:7] "c_standard" "optimized" "ndebug" "fortify_source" ...
#> $ vendored : Named chr [1:3] "26.09.01" "v2.0" "0.8.4"
#> ..- attr(*, "names")= chr [1:3] "ffc" "ryu" "xxhash"Numbers
fast_parse_double()
Decimal and scientific notation, Inf,
Infinity and NaN in any case, with a leading
- or +, after trimming ASCII whitespace.
Anything else, including trailing characters and hexadecimal, is
NA.
fast_parse_double(c("1.5", " -2e-3 ", "+inf", "nan", ".5", "1.", "1e400", "1e-400"))
#> [1] 1.500 -0.002 Inf NaN 0.500 1.000 Inf 0.000
fast_parse_double(c("0x10", "1,5", "12abc", "", NA))
#> [1] NA NA NA NA NAThe result is correctly rounded: the nearest double, ties to even.
R’s as.numeric() is not, and the two can differ in the last
bit:
x <- "0.799012"
fast_parse_double(x) == as.numeric(x)
#> [1] FALSE
sprintf("%a", c(fast_parse_double(x), as.numeric(x)))
#> [1] "0x1.991819d2391d5p-1" "0x1.991819d2391d6p-1"
fast_parse_integer()
Decimal integers only, in R’s integer range. Unlike
as.integer(), nothing goes through a double, so a fraction,
an exponent or an overflow is NA rather than truncated.
fast_parse_integer(c("42", " +7 ", "-2147483647", "2147483648", "1.0", "1e3", "0x1A"))
#> [1] 42 7 -2147483647 NA NA NA
#> [7] NA
as.integer(c("1.9", "1e3")) # base R, for comparison
#> [1] 1 1000
fast_format_double()
The fewest significant digits that read back to exactly the same
double, in ECMAScript Number.prototype.toString()
notation.
x <- c(1, 0.1, 0.1 + 0.2, 1 / 3, 1e21, 1e-7, 123456.789, -0, NA, NaN, Inf)
fast_format_double(x)
#> [1] "1" "0.1" "0.30000000000000004"
#> [4] "0.3333333333333333" "1e+21" "1e-7"
#> [7] "123456.789" "-0" NA
#> [10] "NaN" "Inf"
as.character(x) # base R: fifteen significant digits
#> [1] "1" "0.1" "0.3"
#> [4] "0.333333333333333" "1e+21" "1e-07"
#> [7] "123456.789" "0" NA
#> [10] "NaN" "Inf"
identical(fast_parse_double(fast_format_double(x)), x)
#> [1] TRUEscientific = TRUE always writes d.ddde+x;
trailing_zero = TRUE keeps integral values recognisably
floating point.
fast_format_double(c(1, 123.456, 1e-3), scientific = TRUE)
#> [1] "1e+0" "1.23456e+2" "1e-3"
fast_format_double(c(1, 2.5, 1e20), trailing_zero = TRUE)
#> [1] "1.0" "2.5"
#> [3] "100000000000000000000.0"Dates and timestamps
fast_parse_date()
YYYY-MM-DD with full validation in the proleptic
Gregorian calendar.
fast_parse_date(c("2024-02-29", "2023-02-29", "1900-02-29", "2000-02-29",
"2024-13-01", "2024-1-5", "2024-01-01T00:00", NA))
#> [1] "2024-02-29" NA NA "2000-02-29" NA
#> [6] NA NA NA
fast_parse_datetime()
RFC 3339 and the common ISO 8601 forms: T,
t or a space between date and time, optional seconds, 1 to
9 fraction digits, and Z or a numeric offset. The result is
a POSIXct in UTC; an offset is applied, and a timestamp
without one is taken to be UTC. A bare date is midnight.
x <- c("2024-03-01T12:30:00Z",
"2024-03-01 12:30:00.25+01:00",
"2024-03-01t12:30-0530",
"2016-12-31T23:59:60Z", # leap second: the next minute
"2024-03-01",
"2024-03-01T24:00:00Z", # rejected
"2024-03-01T12:30:00,5Z") # comma fractions are rejected
fast_parse_datetime(x)
#> [1] "2024-03-01 12:30:00 UTC" "2024-03-01 11:30:00 UTC"
#> [3] "2024-03-01 18:00:00 UTC" "2017-01-01 00:00:00 UTC"
#> [5] "2024-03-01 00:00:00 UTC" NA
#> [7] NA
fast_datetime_fields()
The parsed fields themselves, for a caller that wants to apply a time zone of its own or tell “no offset” apart from “UTC”.
fast_datetime_fields(c("2024-03-01T12:30:00.123456789-05:00",
"2024-03-01T12:30:00",
"2024-03-01",
"not a date"))
#> year month day hour minute second nanosecond offset has_time has_offset
#> 1 2024 3 1 12 30 0 123456789 -18000 TRUE TRUE
#> 2 2024 3 1 12 30 0 0 NA TRUE FALSE
#> 3 2024 3 1 NA NA NA NA NA FALSE FALSE
#> 4 NA NA NA NA NA NA NA NA NA NA
fast_format_datetime()
RFC 3339 in UTC for POSIXct, YYYY-MM-DD for
Date.
t <- as.POSIXct(c("2024-03-01 12:30:00", "2024-03-01 12:30:00.25"), tz = "UTC")
fast_format_datetime(t)
#> [1] "2024-03-01T12:30:00Z" "2024-03-01T12:30:00.250Z"
fast_format_datetime(t, digits = 0)
#> [1] "2024-03-01T12:30:00Z" "2024-03-01T12:30:00Z"
fast_format_datetime(t, digits = 6)
#> [1] "2024-03-01T12:30:00.000000Z" "2024-03-01T12:30:00.250000Z"
fast_format_datetime(as.Date(c("2024-03-01", "0001-01-01", NA)))
#> [1] "2024-03-01" "0001-01-01" NAFormatting and parsing round-trip:
t <- as.POSIXct("1969-07-20 20:17:40", tz = "UTC")
identical(fast_parse_datetime(fast_format_datetime(t)), t)
#> [1] TRUEBytes and text
fast_hex_encode() and
fast_hex_decode()
A raw vector encodes to one string; a character vector encodes each
element’s bytes. Decoding accepts either case and returns a list of raw
vectors, NULL where the input is NA or not
hex.
fast_hex_encode(as.raw(c(0xde, 0xad, 0xbe, 0xef)))
#> [1] "deadbeef"
fast_hex_encode(c("zu", "fast", NA), upper = TRUE)
#> [1] "7A75" "66617374" NA
fast_hex_decode(c("DEADbeef", "abc", "zz", NA))
#> [[1]]
#> [1] de ad be ef
#>
#> [[2]]
#> NULL
#>
#> [[3]]
#> NULL
#>
#> [[4]]
#> NULL
fast_base64_encode() and
fast_base64_decode()
Standard or URL-safe alphabet, with or without padding.
fast_base64_encode(c("f", "fo", "foo", "foob"))
#> [1] "Zg==" "Zm8=" "Zm9v" "Zm9vYg=="
fast_base64_encode(as.raw(c(0xfb, 0xff, 0xbf)), url = TRUE)
#> [1] "-_-_"
fast_base64_encode("hello", url = TRUE, pad = FALSE)
#> [1] "aGVsbG8"Decoding is strict, so that every accepted input has exactly one encoding: whitespace, the other alphabet, partial padding and non-zero unused bits are all rejected. Padding itself is optional.
decoded <- fast_base64_decode(c("aGVsbG8=", "aGVsbG8", "aGVsbG9=", "aGVs bG8=", NA))
lapply(decoded, function(r) if (is.null(r)) NULL else rawToChar(r))
#> [[1]]
#> [1] "hello"
#>
#> [[2]]
#> [1] "hello"
#>
#> [[3]]
#> NULL
#>
#> [[4]]
#> NULL
#>
#> [[5]]
#> NULL
fast_base64_decode("-_-_", url = TRUE)
#> [[1]]
#> [1] fb ff bf
fast_hash()
XXH3, 64 or 128 bits, as lower-case hex in xxHash’s canonical form. The values match the reference implementation. XXH3 is not a cryptographic hash: use it for caches, deduplication and change detection, never against an adversary.
fast_hash(charToRaw("hello"))
#> [1] "9555e8555c62dcfd"
fast_hash(c("hello", "world", NA))
#> [1] "9555e8555c62dcfd" "d6476c25083d69be" NA
fast_hash("hello", bits = 128)
#> [1] "b5e9c1ad071b3e7fc779cfaa5e523818"
fast_hash("hello", seed = 42)
#> [1] "bafa072f07db7937"A string and its bytes hash alike:
fast_utf8_valid()
Well-formed UTF-8: no overlong forms, no surrogates, nothing above U+10FFFF. The declared encoding is ignored; only the bytes are examined.
cafe <- rawToChar(as.raw(c(0x63, 0x61, 0x66, 0xc3, 0xa9))) # "cafe" with an acute e
latin1 <- rawToChar(as.raw(c(0x63, 0x61, 0x66, 0xe9))) # the same in Latin-1
fast_utf8_valid(c("plain", cafe, latin1, NA))
#> [1] TRUE TRUE FALSE NA
fast_utf8_valid(as.raw(c(0xc0, 0xaf))) # overlong "/"
#> [1] FALSE
fast_utf8_valid(as.raw(c(0xed, 0xa0, 0x80))) # a surrogate
#> [1] FALSEErrors
Argument problems are classed conditions, so they can be caught precisely:
tryCatch(fast_parse_double(1.5), zufast_invalid_argument = function(e) conditionMessage(e))
#> [1] "`x` must be a character vector."
tryCatch(fast_hash("x", bits = 32), zufast_error = function(e) class(e))
#> [1] "zufast_invalid_argument" "zufast_error"
#> [3] "error" "condition"