Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
bb960a1
better connection error handling
mountainMath Sep 16, 2025
1bef259
new and cran comments
mountainMath Sep 16, 2025
adb42bc
more improvements on error handling
mountainMath Nov 15, 2025
1288fb1
fail more gracefully when statcan servers are down and cube list can'…
mountainMath Nov 15, 2025
6eeb3d4
tidyselect tweaks
mountainMath Nov 15, 2025
99a4db3
fix doi badge
mountainMath Nov 15, 2025
6cf8f57
fix dio badge
mountainMath Nov 15, 2025
e3b4e4b
toggle visibility of main logo on pkgdown site
mountainMath Nov 15, 2025
ff894bc
move css into pkgdown/extra.css
mountainMath Nov 15, 2025
7ef661d
refine error handling when statcan is down
mountainMath Jan 11, 2026
67c9ad1
update hex sticker code
mountainMath Jan 11, 2026
cb3e98a
Add .claude and CLAUDE.md to .Rbuildignore
mountainMath Jan 21, 2026
22f2988
fix: Critical bug fixes and typo corrections
dshkol Jan 22, 2026
320147e
fix: Edge case and error handling improvements (H8, H9, H10, M3, M4, …
dshkol Jan 22, 2026
1adbc72
fix: Normalization and factor conversion fixes (M1, M10)
dshkol Jan 22, 2026
8453b35
perf: Hot path optimizations (P2, P5, P13, partial P1)
dshkol Jan 22, 2026
41c035d
perf: Caching and I/O optimizations (P3, P10)
dshkol Jan 22, 2026
bc33d62
perf: Lookups and vectorization optimizations (P4, P8, P11)
dshkol Jan 22, 2026
5486032
refactor: Replace deprecated mutate_at/vars with across() (M11)
dshkol Jan 22, 2026
4cc3383
revert: Remove P4/P11 hash table optimization
dshkol Jan 22, 2026
f335ae0
Optimize date parsing in normalize_cansim_values using lookup tables
mountainMath Jan 23, 2026
c8974c3
Optimize factor conversion in normalize_cansim_values
mountainMath Jan 23, 2026
7daeee9
Merge pull request #167 from mountainMath/master
mountainMath Mar 28, 2026
e176e33
Merge pull request #154 from mountainMath/fix/critical-bug-fixes
mountainMath Mar 28, 2026
bb4eb9a
symbols for french language tables
mountainMath Apr 8, 2026
df13266
Fix cube metadata and table templates for vectors of table numbers
mountainMath Aug 16, 2026
946f718
build and git ignore
mountainMath Aug 16, 2026
c2e17c2
Merge pull request #160 from mountainMath/refactor/api-consistency
mountainMath Aug 16, 2026
be7e2f3
Merge pull request #159 from mountainMath/perf/lookups-vectorization
mountainMath Aug 16, 2026
08124b8
Merge pull request #165 from mountainMath/performance/factor-conversi…
mountainMath Aug 16, 2026
991396e
Merge pull request #156 from mountainMath/fix/normalization-factor-co…
mountainMath Aug 16, 2026
cee614b
Merge pull request #158 from mountainMath/perf/caching-io
mountainMath Aug 16, 2026
683041d
Merge pull request #164 from mountainMath/performance/date-parsing-op…
mountainMath Aug 16, 2026
466a96e
Merge pull request #157 from mountainMath/perf/hot-paths
mountainMath Aug 16, 2026
97d2fc1
Merge pull request #155 from mountainMath/fix/edge-case-error-handling
mountainMath Aug 16, 2026
0ef2512
Optimize table template construction with expand_grid (#163)
mountainMath Aug 16, 2026
e25b2b2
Optimize hierarchy building and coordinate splitting (from #161)
mountainMath Aug 16, 2026
f6be8c2
Document SQLite helpers as deprecated, announce planned removal
mountainMath Aug 16, 2026
ba56b26
Fix dplyr 1.2.0 case_when deprecation in geography column names
mountainMath Aug 16, 2026
0715f28
Note the case_when deprecation fix in NEWS
mountainMath Aug 16, 2026
e4e45ff
Add UOM/UOM_ID to vector and coordinate data (#170)
mountainMath Aug 16, 2026
29cd36e
Repair non-breaking spaces and control characters in names (#169)
mountainMath Aug 16, 2026
8da3d73
Show the offending code point in the repair warning, and shorten it
mountainMath Aug 16, 2026
4239a03
Split only unique coordinates when renaming duplicate factor levels
mountainMath Aug 16, 2026
e5af662
Return NULL with a warning when StatCan is unavailable
mountainMath Aug 16, 2026
bc9e1a0
Prepare 0.4.5 for CRAN
mountainMath Aug 16, 2026
2db64b9
Resolve coordinate metadata for all coordinates at once
mountainMath Aug 16, 2026
2ea2caf
Repair non-breaking spaces in member labels in the data
mountainMath Aug 16, 2026
3c3a47c
Record the package version a cache was parsed under
mountainMath Aug 16, 2026
3ca0ceb
Keep the cache timestamp and package version in one file
mountainMath Aug 16, 2026
e06b2eb
Wrap repair warnings to the console width
mountainMath Aug 16, 2026
0713424
Validate the language argument and standardize its default (#152)
mountainMath Aug 16, 2026
7fa90f7
Drop "an" as a name for English
mountainMath Aug 16, 2026
d7996d7
Drop the unreachable else branch in metadata parsing (#151)
mountainMath Aug 16, 2026
670c6ad
Say in the repair warning that StatCan is the one who can fix it
mountainMath Aug 16, 2026
f67263c
Add an internal scanner for the characters StatCan publishes (#169)
mountainMath Aug 16, 2026
7ca71d3
Deprecate disconnect_cansim_sqlite in favour of disconnect_cansim_con…
mountainMath Aug 16, 2026
700277c
Fix the issues found in the pre-release code scan
mountainMath Aug 17, 2026
5fe8bf3
perf: avoid repeated vector batch binding
dshkol Aug 17, 2026
16b5852
Batch every WDS list request and stop passing failed records off as data
mountainMath Aug 17, 2026
65bd0bc
Move to httr2, expose the changed series methods, and time out on sil…
mountainMath Aug 17, 2026
e001458
Hold back the changed series list method until StatCan can answer it
mountainMath Aug 17, 2026
c741996
add new functions to index
mountainMath Aug 17, 2026
8d3f481
update docs
mountainMath Aug 17, 2026
622123f
Label named vectors in the changed series method however they are spe…
mountainMath Aug 17, 2026
f87dd58
Take the default period count from the API bound rather than a guess
mountainMath Aug 17, 2026
1a80629
Export the changed series list method after all
mountainMath Aug 18, 2026
a89f365
version bump
mountainMath Aug 19, 2026
1e8f90d
regenerate docs, getting ready for CRAN once they come back from thei…
mountainMath Aug 19, 2026
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: 4 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ fix_meta.sh
^CRAN-SUBMISSION$
^\.github$
R-CMD-check-old.yaml
^\.claude$
^CLAUDE\.md$
^\.positai$
^benchmarks$
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ inst/doc
.DS_Store
CRAN-RELEASE
CRAN-SUBMISSION
.claude/settings.local.json
.positai
9 changes: 5 additions & 4 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: cansim
Type: Package
Title: Accessing Statistics Canada Data Table and Vectors
Version: 0.4.4
Version: 0.5.0
Authors@R: c(
person("Jens", "von Bergmann", email = "jens@mountainmath.ca", role = c("aut","cre")),
person("Dmitry", "Shkolnik", email = "shkolnikd@gmail.com", role = c("aut")))
Expand All @@ -21,7 +21,7 @@ LazyData: true
Depends: R (>= 4.1)
Imports: digest (>= 0.6),
dplyr (>= 1.1),
httr (>= 1.0.0),
httr2 (>= 1.1.0),
tidyr (>= 1.3),
readr (>= 2.1),
rlang (>= 1.1),
Expand All @@ -33,15 +33,16 @@ Imports: digest (>= 0.6),
RSQLite (>= 2.3),
utils (>= 4.3),
dbplyr (>= 2.5)
RoxygenNote: 7.3.2
Suggests:
knitr,
rmarkdown,
ggplot2,
scales,
testthat (>= 3.0.0)
testthat (>= 3.0.0),
withr
URL: https://github.com/mountainMath/cansim, https://mountainmath.github.io/cansim/, https://www.statcan.gc.ca/
BugReports: https://github.com/mountainMath/cansim/issues
VignetteBuilder: knitr
Language: en-CA
Config/testthat/edition: 3
Config/roxygen2/version: 8.0.0
4 changes: 4 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ export(cansim_old_to_new)
export(cansim_repartition_cached_table)
export(categories_for_level)
export(collect_and_normalize)
export(disconnect_cansim_connection)
export(disconnect_cansim_sqlite)
export(get_cansim)
export(get_cansim_changed_series_data_for_coordinates)
export(get_cansim_changed_series_data_for_vectors)
export(get_cansim_changed_series_list)
export(get_cansim_changed_tables)
export(get_cansim_code_set)
export(get_cansim_column_categories)
Expand Down
212 changes: 212 additions & 0 deletions NEWS.md

Large diffs are not rendered by default.

321 changes: 201 additions & 120 deletions R/cansim.R

Large diffs are not rendered by default.

188 changes: 188 additions & 0 deletions R/cansim_changed_series.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# StatCan reports a request naming only series that did not change with an HTTP 404 carrying a
# "Not data found" body. Nothing went wrong in that case, the answer is simply that there is nothing,
# so the two methods that name the series they ask about have that status come back as an empty result
# rather than as the failure the shared handling would otherwise make of it.
CHANGED_SERIES_NO_DATA_STATUS <- 404L

#' Retrieve the series that changed today
#'
#' Retrieve the list of data series Statistics Canada changed today, as vectors together with the
#' table and coordinate they belong to. Where \code{get_cansim_changed_tables()} reports which tables
#' were touched, this reports the individual series inside them, which is the finer grained way to
#' decide what needs re-downloading.
#'
#' StatCan serves this for the current day only and fills it during the daily update window that ends
#' at 8:30am Eastern. Unlike the changed tables method there is no way to ask for an earlier day,
#' StatCan answers a request naming a date with an HTTP 404.
#'
#' How long this takes depends entirely on how much StatCan released that morning. The method takes no
#' parameters, so there is no way to ask for a smaller slice of a busy day, and StatCan works out a
#' whole response before sending any of it. On a quiet day the answer arrives in well under a second;
#' on a heavy one the series changing can number in the hundreds of thousands and the request has been
#' seen to outlive StatCan's own gateway, coming back as an HTTP 504 after some nine minutes of
#' silence. That is a limit at StatCan's end which raising \code{timeout} cannot lift, so on such a day
#' \code{get_cansim_changed_tables()} is the question worth asking instead.
#'
#' @param timeout (Optional) Number of seconds StatCan is allowed to go without sending data before
#' the download is abandoned. The default is set high because this method is silent while it works.
#'
#' @return A tibble with one row per changed series, carrying the vector, the table number, the
#' coordinate and the release time
#'
#' Returns \code{NULL} if the data could not be retrieved because StatCan is unavailable.
#' @examples
#' \dontrun{
#' get_cansim_changed_series_list()
#' }
#' @export
get_cansim_changed_series_list <- function(timeout=600){
url <- "https://www150.statcan.gc.ca/t1/wds/rest/getChangedSeriesList"

# no `empty_status` here, unlike the two data methods below. Their 404 means that none of the series
# asked about changed, but this method names no series, so nothing it could report as absent. A 404
# from it is StatCan not serving the route, which is worth telling the caller about rather than
# quietly passing off as a day on which nothing changed.
response <- get_with_timeout_retry(url,timeout=timeout)
if (is.null(response)) return(NULL)

data <- statcan_response_json(response)
if (length(data$object)==0) return(empty_changed_series_list())

# this method has been seen to answer both in the wrapped shape the other list methods use, each
# series sitting in the `object` of a record carrying its own status, and with the series themselves
# as the entries of `object`. The presence of that per-record status is what tells the two apart,
# and it has to be settled before asking for the successful records, because putting the unwrapped
# shape through that check would report every series in it as a failure
wrapped <- !is.null(data$object[[1]]$status)
series <- if (wrapped) {
purrr::map(successful_wds_records(data$object,"changed series"),\(x) x$object)
} else {
Filter(\(x) is.list(x) && !is.null(x$vectorId),data$object)
}

if (length(series)==0) return(empty_changed_series_list())
changed_series_tibble(series)
}

empty_changed_series_list <- function(){
tibble::tibble(VECTOR=character(0),cansimTableNumber=character(0),
COORDINATE=character(0),releaseTime=character(0))
}

changed_series_tibble <- function(series){
field <- function(x,name,default=NA_character_){
value <- x[[name]]
if (length(value)!=1 || is.null(value)) return(default)
as.character(value)
}
tibble::tibble(
VECTOR=paste0("v",purrr::map_chr(series,field,"vectorId")),
cansimTableNumber=purrr::map_chr(series,\(x){
product_id <- field(x,"productId")
if (is.na(product_id)) NA_character_ else cleaned_ndm_table_number(product_id)
}),
COORDINATE=gsub("(\\.0)+$","",purrr::map_chr(series,field,"coordinate")),
releaseTime=purrr::map_chr(series,field,"releaseTime"))
}

# Shared body of the two methods that fetch the data of series that changed. They differ only in the
# endpoint they call and in how the caller names the series it is asking about.
changed_series_data <- function(url,bodies,vectors,language,timeout,factors,
default_month,default_day){
cleaned_language <- cleaned_ndm_language(language)

batches <- batch_items(bodies)
batch_results <- vector("list", length(batches))
for (batch_number in seq_along(batches)) {
addition <- if (length(batches)>1) paste0(" (batch ",batch_number," of ",length(batches),")") else ""
message(paste0("Accessing changed CANSIM NDM series from Statistics Canada",addition))

body <- paste0("[",paste(batches[[batch_number]],collapse=", "),"]")
response <- post_with_timeout_retry(url,body=body,timeout=timeout,
empty_status=CHANGED_SERIES_NO_DATA_STATUS)
if (is.null(response)) return(NULL)
# none of the series in this batch changed, which says nothing about the other batches
if (statcan_no_data(response)) next

records <- successful_wds_records(statcan_response_json(response),"changed series data")
if (length(records)>0) batch_results[[batch_number]] <- extract_vector_data(records)
}

result <- bind_rows(batch_results)
# an empty answer here means nothing changed, which is an ordinary thing to report rather than the
# sign of a problem the vector methods warn about
finalize_vector_data(result,vectors,cleaned_language,factors,default_month,default_day,
warn_if_empty=FALSE)
}

#' Retrieve data for series that changed, by vector
#'
#' Retrieve the data points Statistics Canada changed for the given vectors. Series among the ones
#' asked about that did not change contribute no rows, and if none of them changed the result is an
#' empty table rather than an error. The StatCan API can only process 300 vectors at a time, if more
#' than 300 vectors are specified the function will batch the requests to the API.
#'
#' @param vectors The list of vectors to retrieve changed data for
#' @param language \code{"english"} (the default) or \code{"french"}. Short forms such as \code{"en"}, \code{"eng"}, \code{"fr"} or \code{"fra"} are accepted, as are the French names \code{"anglais"} and \code{"francais"}; case and accents are ignored
#' @param timeout (Optional) Number of seconds StatCan is allowed to go without sending data before the download is abandoned, to work around scenarios where StatCan servers drop the network connection. This does not limit how long a download may take overall, a transfer that keeps delivering data is left alone. StatCan prepares a whole response before sending any of it, which for large requests can take the better part of a minute, so values much below the default of 200 risk cutting off legitimate requests.
#' @param factors (Optional) Logical value indicating if dimensions should be converted to factors. (Default set to \code{TRUE}).
#' @param default_month The default month that should be used when creating Date objects for annual data (default set to "07")
#' @param default_day The default day of the month that should be used when creating Date objects for monthly data (default set to "01")
#'
#' @return A tibble with the changed data for the specified vector(s)
#'
#' Returns \code{NULL} if the data could not be retrieved because StatCan is unavailable.
#' @examples
#' \dontrun{
#' get_cansim_changed_series_data_for_vectors("v41690973")
#' }
#' @export
get_cansim_changed_series_data_for_vectors <- function(vectors, language="english", timeout=200,
factors=TRUE, default_month="07",
default_day="01"){
naked_vectors <- gsub("^v","",vectors) # allow for leading "v" by conditionally stripping it
bodies <- paste0('{"vectorId":',naked_vectors,'}')

# the naked vectors, which keep their names through the gsub above, are what rename_vectors()
# further down expects; handing it the prefixed originals would look up "vv..." and label nothing
changed_series_data("https://www150.statcan.gc.ca/t1/wds/rest/getChangedSeriesDataFromVector",
bodies,naked_vectors,language,timeout,factors,default_month,default_day)
}

#' Retrieve data for series that changed, by table and coordinate
#'
#' Retrieve the data points Statistics Canada changed for the given coordinates of a table.
#' Coordinates among the ones asked about that did not change contribute no rows, and if none of them
#' changed the result is an empty table rather than an error. The StatCan API can only process 300
#' coordinates at a time, if more than 300 coordinates are specified the function will batch the
#' requests to the API.
#'
#' @param cansimTableNumber The table number the coordinates belong to
#' @param coordinates The coordinates to retrieve changed data for
#' @param language \code{"english"} (the default) or \code{"french"}. Short forms such as \code{"en"}, \code{"eng"}, \code{"fr"} or \code{"fra"} are accepted, as are the French names \code{"anglais"} and \code{"francais"}; case and accents are ignored
#' @param timeout (Optional) Number of seconds StatCan is allowed to go without sending data before the download is abandoned, to work around scenarios where StatCan servers drop the network connection. This does not limit how long a download may take overall, a transfer that keeps delivering data is left alone. StatCan prepares a whole response before sending any of it, which for large requests can take the better part of a minute, so values much below the default of 200 risk cutting off legitimate requests.
#' @param factors (Optional) Logical value indicating if dimensions should be converted to factors. (Default set to \code{TRUE}).
#' @param default_month The default month that should be used when creating Date objects for annual data (default set to "07")
#' @param default_day The default day of the month that should be used when creating Date objects for monthly data (default set to "01")
#'
#' @return A tibble with the changed data for the specified coordinates
#'
#' Returns \code{NULL} if the data could not be retrieved because StatCan is unavailable.
#' @examples
#' \dontrun{
#' get_cansim_changed_series_data_for_coordinates("34-10-0013","1.1")
#' }
#' @export
get_cansim_changed_series_data_for_coordinates <- function(cansimTableNumber, coordinates,
language="english", timeout=200,
factors=TRUE, default_month="07",
default_day="01"){
validate_single_table_number(cansimTableNumber)
product_id <- naked_ndm_table_number(cleaned_ndm_table_number(cansimTableNumber))
# the API wants coordinates spelled out to all ten dimensions
coordinates <- normalize_coordinates(coordinates)
bodies <- paste0('{"productId":',product_id,', "coordinate":"',coordinates,'"}')

changed_series_data("https://www150.statcan.gc.ca/t1/wds/rest/getChangedSeriesDataFromCubePidCoord",
bodies,NULL,language,timeout,factors,default_month,default_day)
}
Loading
Loading