NEWS


cppally 1.1.0.9000

Breaking changes

Bug fixes

r_function

random_stream

r_date & r_psxct

A new complete set of fast and parallelisable thread-safe member and free functions for r_date and r_psxct, which respectively represent R dates and date-times. Currently, all r_date and r_psxct member functions are thread-safe except for r_date::date_str() and r_psxct::datetime_str().

Date and date-time arithmetic

To perform time arithmetic with dates and date-times, use add<>, e.g. x.add<"days">(1) adds 1 day to x, and x.add<"months">(-3) subtracts 3 months from x.

The on_impossible_date argument of add<> allows you to control how to handle impossible dates that arise when performing month-based arithmetic, e.g. 31 Jan + 1 month = ?

All roll options:

enum roll : uint8_t {
    none = 0,       // does not roll and impossible dates are returned as NA
    backward = 1,   // rolls backwards until the last day of the current month is reached
    forward = 2,    // rolls to first day of next month
    away = 3,       // rolls forward when adding and backward when subtracting
    nearest = 4     // rolls backward when adding and forward when subtracting
};

Sub-day units are accepted in both r_date and r_psxct arithmetic. For r_date specifically, fractional dates are produced when using sub-day units (such as "hours") for date arithmetic. r_date::today().add<"hours">(12) will produce a fractional date representing noon of the current day and r_date::today().as_datetime().add<"hours">(12) will produce a date-time r_psxct also representing noon of the current day.

I would generally recommend against using sub-day units with r_date and to use r_psxct if you ever need sub-day granularity.

To add sub-second units such as milliseconds and microseconds, simply supply fractional seconds, e.g. x.add<"seconds">(1.0/1000.0) adds 1 millisecond to x. It's worth noting that loss in floating-point precision can occur when working with small microseconds, though this is inherent in R's POSIXct class as well.

Date and date-time rounding

To round dates and date-times, use member functions floor, ceiling and round.

For example, x.floor<"months">() floors x to the start of the current month. x.ceiling<"months">() advances x to the start of the next month, unless it is already at the start of the month.

Rounding functions accept a week_start argument, which is only relevant for rounding "weeks". It follows the same convention as wday(), where 1 is Monday and 7 is Sunday (the default). For example, x.floor<"weeks">() floors x to the most recent past Sunday, whereas x.floor<"weeks">(1) floors x to the most recent past Monday.

When the date is exactly at the midpoint between boundaries, round() will floor the date, so for example if we are rounding a date-time to the nearest day and the time is at noon, the date-time gets floored to midnight on the same day.

Date and date-time differences

To calculate the time difference between two dates or date-times, use time_diff<>.

For example, time_diff<"months">(x, y) calculates the difference in months between x and y. If x and y do not share the same day-of-the-month, then a fractional difference is returned. Use cppally::floor() to floor the result if you always need a whole difference.

To return time differences using n time unit periods (the default is 1), just pass it to the n argument, e.g. time_diff<"months">(x, y, /*n=*/ 3) returns the difference in quarters between x and y, and similarly time_diff<"days">(x, y, /*n=*/ 7) is the same as time_diff<"weeks">(x, y).

For monthly time differences, sometimes impossible dates can arise when either x or y has a day-of-the-month that is larger than the other's number of days in the month. While the default time_diff<"months">() would return NA, we can control the internal impossible date rolling with the on_impossible_date arg.

For example, assuming x is 31 Jan 2024 and y is 29 Feb 2024, time_diff<"months">(x, y, 1, roll::nearest) rolls the impossible 31 Feb date back to 29th February, resulting in a difference of exactly 1 month. If we specified roll::away the date becomes 1st March instead, giving a fractional answer that is slightly less than 1 month.

Improvements

Other new features

cppally 1.1.0 (2026-07-12)

Breaking changes

Bug fixes

r_lgl (logical) semantics

Arithmetic

Improvements

cppally 1.0.0 (2026-07-02)

First major release. cppally's public API is now considered stable. While there may be structural changes to the r_df and r_raw classes in the future, cppally's vector and scalar classes are considered stable.

Breaking changes

For example, in the below pseudo-code, when x is r_null of type r_sexp, r_sexp_visit will disambiguate it as r_vec<r_sexp>(r_null), preserving its data as R's NULL but assigning its type as r_vec<r_sexp> (list).

 r_sexp_visit(x, [&]<RVector T>(const T& vec) -> bool {
  return vec.is_null();
 });

This preservation behaviour is not new, in fact all r_vec<T> vectors preserve r_null by design, allowing for efficient and easier attribute manipulation with vectors that may or may not be r_null. What is new is that previously r_null was not a visitable r_sexp object and now it is.

Data frames

std::vector

Any coercion between std::vector and cppally::r_vec is possible so long as the element coercions are supported by cppally::as

Regular sequences

Named vector hash lookups

For named vectors, lookup by name has been dramatically improved in C++ by introducing a hashing approach. It works in the following way: the first time a lookup is requested, a linear scan is done to find the named value. The second time triggers the hash map of name-value pairs to be built and cached with the vector. That second lookup is completed using the cached hash map and all subsequent lookups also use the hash map. The rationale for hashing on second lookup is covered in the 'Automatic Names Hashing' vignette.

A similar hashing approach is also used for r_factors, making conversions of strings to and from factor codes fast and analytically viable.

Copy-on-modify

cppally now supports copy-on-modify as an opt-in feature. This feature prevents accidentally overwriting data between shared objects, just like R. To opt-in, run cppally::use_copy_on_modify or set the copy_on_modify to TRUE in cpp_source.

The major downside of this feature is significantly slower element setting as every set must verify the object is not referenced by another object. This check is single-threaded and thus nearly all parallel cppally code is disabled as a safety precaution. If using copy-on-modify, it is recommended to avoid writing cppally registered R functions that rely on in-place modification.

pmap

Inspired by purrr::pmap and base::mapply, cppally::pmap is a C++ variadic function that supports applying custom C++ lambda functions across corresponding elements of multiple vectors.

With pmap it is trivial to calculate parallel statistics like max, min, etc. Example of C++ version of base::pmax applied to two vectors.

template <RVector T, RVector U>
requires requires(typename T::data_type a, typename U::data_type b) { max(a, b); }
[[cppally::register]]
auto cpp_pmax2(T x, U y){
  return pmap([](auto a, auto b){ return max(a, b); }, x, y);
}

reduce

A left-fold reduction functional that successively applies a binary function along the elements of the vector (from left-to-right).

Example: maximum value across vector of doubles

[[cppally::register]]
r_dbl cpp_max(r_vec<r_dbl> x){
  return x.reduce([](auto acc, auto curr){ return max(acc, curr); });
}

Other changes

Bug fixes

cppally 0.1.0 (2026-04-28)