├── .github ├── .gitignore ├── workflows │ ├── pkgdown.yaml │ ├── test-coverage.yaml │ ├── R-CMD-check.yaml │ └── pr-commands.yaml └── CODE_OF_CONDUCT.md ├── LICENSE ├── .gitignore ├── pkgsearch.png ├── gifs └── addin.gif ├── pkgdown ├── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-152x152.png │ └── apple-touch-icon-180x180.png └── extra.css ├── inst └── rstudio │ └── addins.dcf ├── R ├── my-errors.R ├── pkgsearch-package.R ├── isonline.R ├── rematch2.R ├── assert.R ├── aa-assertthat.R ├── utils.R ├── advanced_search.R ├── time-ago.R ├── tojson.R ├── date.R ├── http.R ├── print.R ├── aaa-rstudio-detect.R ├── api.R ├── crandb-public-api.R ├── compat-vctrs.R └── addin.R ├── tests ├── testthat │ ├── helper.R │ ├── test-cran-package-list.R │ ├── test-needs_packages.R │ ├── test-print.R │ ├── test-utils.R │ ├── test-time-zones.R │ ├── test-search.R │ ├── test-public-api.R │ ├── test-iso8601.R │ ├── test-tojson.R │ └── _snaps │ │ └── tojson.md └── testthat.R ├── codecov.yml ├── .Rbuildignore ├── pkgsearch.Rproj ├── NAMESPACE ├── man ├── cran_package_history.Rd ├── cran_top_downloaded.Rd ├── cran_package.Rd ├── cran_trending.Rd ├── cran_packages.Rd ├── pkgsearch-package.Rd ├── pkg_search_addin.Rd ├── cran_events.Rd ├── cran_new.Rd ├── advanced_search.Rd └── pkg_search.Rd ├── header.md ├── LICENSE.md ├── DESCRIPTION ├── tools └── badversions.R ├── _pkgdown.yml ├── NEWS.md └── README.Rmd /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2023 2 | COPYRIGHT HOLDER: pkgsearch authors 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tags 2 | .Rproj.user 3 | docs 4 | inst/doc 5 | /r-packages 6 | -------------------------------------------------------------------------------- /pkgsearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-hub/pkgsearch/HEAD/pkgsearch.png -------------------------------------------------------------------------------- /gifs/addin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-hub/pkgsearch/HEAD/gifs/addin.gif -------------------------------------------------------------------------------- /pkgdown/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-hub/pkgsearch/HEAD/pkgdown/favicon/favicon.ico -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-hub/pkgsearch/HEAD/pkgdown/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-hub/pkgsearch/HEAD/pkgdown/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /pkgdown/extra.css: -------------------------------------------------------------------------------- 1 | #sidebar .nav > li { 2 | padding: 0 0 0 20px; 3 | display: list-item; 4 | line-height: 15px; 5 | } -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-hub/pkgsearch/HEAD/pkgdown/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-hub/pkgsearch/HEAD/pkgdown/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-hub/pkgsearch/HEAD/pkgdown/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-hub/pkgsearch/HEAD/pkgdown/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-hub/pkgsearch/HEAD/pkgdown/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-hub/pkgsearch/HEAD/pkgdown/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /inst/rstudio/addins.dcf: -------------------------------------------------------------------------------- 1 | Name: CRAN Package Search 2 | Description: Search packages on CRAN 3 | Binding: pkg_search_addin 4 | Interactive: false 5 | -------------------------------------------------------------------------------- /R/my-errors.R: -------------------------------------------------------------------------------- 1 | 2 | new_no_package_error <- function(...) { 3 | cnd <- new_error(...) 4 | class(cnd) <- c("package_not_found_error", class(cnd)) 5 | cnd 6 | } 7 | -------------------------------------------------------------------------------- /tests/testthat/helper.R: -------------------------------------------------------------------------------- 1 | 2 | skip_if_offline <- function() { 3 | if (!is_online()) skip("Offline") 4 | } 5 | 6 | if (getOption("repos")["CRAN"] == "@CRAN@") { 7 | options(repos = structure(c(CRAN = "http://cran.rstudio.com"))) 8 | } 9 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | informational: true 10 | patch: 11 | default: 12 | target: auto 13 | threshold: 1% 14 | informational: true 15 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^\.travis\.yml$ 2 | ^tags$ 3 | ^README.Rmd$ 4 | ^Makefile$ 5 | ^appveyor.yml$ 6 | ^.*\.Rproj$ 7 | ^\.Rproj\.user$ 8 | ^_pkgdown\.yml$ 9 | ^\.Rprofile$ 10 | ^r-packages$ 11 | ^pkgdown$ 12 | ^header\.md$ 13 | ^pkgsearch\.png$ 14 | ^docs$ 15 | ^gifs$ 16 | ^\.github$ 17 | ^codecov\.yml$ 18 | ^LICENSE\.md$ 19 | -------------------------------------------------------------------------------- /R/pkgsearch-package.R: -------------------------------------------------------------------------------- 1 | #' @keywords internal 2 | "_PACKAGE" 3 | 4 | # The following block is used by usethis to automatically manage 5 | # roxygen namespace tags. Modify with care! 6 | ## usethis namespace: start 7 | ## usethis namespace: end 8 | NULL 9 | 10 | .onLoad <- function(libname, pkgname) { 11 | err$onload_hook() 12 | } 13 | -------------------------------------------------------------------------------- /pkgsearch.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | BuildType: Package 16 | PackageUseDevtools: Yes 17 | PackageInstallArgs: --no-multiarch --with-keep.source 18 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | # This file is part of the standard setup for testthat. 2 | # It is recommended that you do not modify it. 3 | # 4 | # Where should you do additional test configuration? 5 | # Learn more about the roles of various files in: 6 | # * https://r-pkgs.org/testing-design.html#sec-tests-files-overview 7 | # * https://testthat.r-lib.org/articles/special-files.html 8 | 9 | library(testthat) 10 | library(pkgsearch) 11 | 12 | test_check("pkgsearch") 13 | -------------------------------------------------------------------------------- /R/isonline.R: -------------------------------------------------------------------------------- 1 | 2 | is_rcmd_check <- function() { 3 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 4 | FALSE 5 | } else { 6 | Sys.getenv("_R_CHECK_PACKAGE_NAME_", "") != "" 7 | } 8 | } 9 | 10 | is_online <- local({ 11 | online <- TRUE 12 | expires <- Sys.time() 13 | function() { 14 | if (is_rcmd_check()) return(FALSE) 15 | t <- Sys.time() 16 | if (t >= expires) { 17 | online <<- pingr::is_online() 18 | expires <<- t + as.difftime(10, units = "secs") 19 | } 20 | online 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /tests/testthat/test-cran-package-list.R: -------------------------------------------------------------------------------- 1 | 2 | test_that("cran_packages works", { 3 | skip_if_offline() 4 | tab <- cran_packages(c("igraph", "pkgconfig@1.0.0")) 5 | expect_s3_class(tab, "tbl") 6 | expect_equal(tab$Package, c("igraph", "pkgconfig")) 7 | }) 8 | 9 | test_that("cran_package_histories works", { 10 | skip_if_offline() 11 | tab <- cran_package_history("igraph") 12 | expect_s3_class(tab, "tbl") 13 | expect_true(nrow(tab) >= 45) 14 | expect_true(all(tab$Package == "igraph")) 15 | expect_false(is.unsorted(package_version(tab$Version))) 16 | }) 17 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method("[",pkg_search_result) 4 | S3method(print,cran_event_list) 5 | S3method(print,pkg_search_result) 6 | S3method(print,pkgsearch_query_error) 7 | S3method(summary,cran_event_list) 8 | S3method(summary,pkg_search_result) 9 | export(advanced_search) 10 | export(cran_events) 11 | export(cran_new) 12 | export(cran_package) 13 | export(cran_package_history) 14 | export(cran_packages) 15 | export(cran_top_downloaded) 16 | export(cran_trending) 17 | export(more) 18 | export(pkg_search) 19 | export(pkg_search_addin) 20 | export(ps) 21 | importFrom(utils,capture.output) 22 | -------------------------------------------------------------------------------- /man/cran_package_history.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/crandb-public-api.R 3 | \name{cran_package_history} 4 | \alias{cran_package_history} 5 | \title{Query the history of a package} 6 | \usage{ 7 | cran_package_history(package) 8 | } 9 | \arguments{ 10 | \item{package}{Package name.} 11 | } 12 | \value{ 13 | A data frame, with one row per package version. 14 | } 15 | \description{ 16 | Query the history of a package 17 | } 18 | \examples{ 19 | \dontshow{if (identical(Sys.getenv("IN_PKGDOWN"), "true")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 20 | cran_package_history("igraph") 21 | \dontshow{\}) # examplesIf} 22 | } 23 | -------------------------------------------------------------------------------- /tests/testthat/test-needs_packages.R: -------------------------------------------------------------------------------- 1 | test_that("needs_packages works", { 2 | 3 | mockery::stub(what = "requireNamespace", 4 | where = needs_packages, 5 | how = FALSE) 6 | 7 | expect_error( 8 | needs_packages(c("memoise", "shiny", "shinyjs")), 9 | "are needed", class = "rlib_error") 10 | 11 | mockery::stub(what = "requireNamespace", 12 | where = needs_packages, 13 | how = function(x, ...) { 14 | if (x == "shiny") FALSE 15 | else TRUE 16 | }) 17 | 18 | expect_error( 19 | needs_packages(c("memoise", "shiny", "shinyjs")), 20 | "is needed", class = "rlib_error") 21 | 22 | }) 23 | -------------------------------------------------------------------------------- /man/cran_top_downloaded.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/crandb-public-api.R 3 | \name{cran_top_downloaded} 4 | \alias{cran_top_downloaded} 5 | \title{Top downloaded packages} 6 | \usage{ 7 | cran_top_downloaded() 8 | } 9 | \value{ 10 | Data frame of top downloaded packages. 11 | } 12 | \description{ 13 | Last week. 14 | } 15 | \details{ 16 | You can use the \href{https://r-hub.github.io/cranlogs/}{\code{cranlogs} package} 17 | to get more flexibility into what is returned. 18 | } 19 | \examples{ 20 | \dontshow{if (identical(Sys.getenv("IN_PKGDOWN"), "true")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 21 | cran_top_downloaded() 22 | \dontshow{\}) # examplesIf} 23 | } 24 | -------------------------------------------------------------------------------- /man/cran_package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/crandb-public-api.R 3 | \name{cran_package} 4 | \alias{cran_package} 5 | \title{Metadata about a CRAN package} 6 | \usage{ 7 | cran_package(name, version = NULL) 8 | } 9 | \arguments{ 10 | \item{name}{Name of the package.} 11 | 12 | \item{version}{The package version to query. If \code{NULL}, the latest 13 | version if returned.} 14 | } 15 | \value{ 16 | The package metadata, in a named list. 17 | } 18 | \description{ 19 | Metadata about a CRAN package 20 | } 21 | \examples{ 22 | \dontshow{if (identical(Sys.getenv("IN_PKGDOWN"), "true")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 23 | cran_package("pkgsearch") 24 | \dontshow{\}) # examplesIf} 25 | } 26 | -------------------------------------------------------------------------------- /man/cran_trending.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/crandb-public-api.R 3 | \name{cran_trending} 4 | \alias{cran_trending} 5 | \title{Trending R packages} 6 | \usage{ 7 | cran_trending() 8 | } 9 | \value{ 10 | Data frame of trending packages. 11 | } 12 | \description{ 13 | Trending packages are the ones that were downloaded at least 1000 times 14 | during last week, and that substantially increased their download 15 | counts, compared to the average weekly downloads in the previous 24 16 | weeks. The percentage of increase is also shown in the output. 17 | } 18 | \examples{ 19 | \dontshow{if (identical(Sys.getenv("IN_PKGDOWN"), "true")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 20 | cran_trending() 21 | \dontshow{\}) # examplesIf} 22 | } 23 | -------------------------------------------------------------------------------- /tests/testthat/test-print.R: -------------------------------------------------------------------------------- 1 | test_that("short", { 2 | skip_if_offline() 3 | 4 | x <- ps("csardi") 5 | out <- capture.output(print(x)) 6 | expect_equal(length(out), 12) 7 | expect_match(out[1], "csardi.*packages in.*seconds") 8 | expect_match(out[2], "#\\s+package\\s+version\\s+by\\s+@\\s+title") 9 | }) 10 | 11 | test_that("long", { 12 | skip_if_offline() 13 | 14 | x <- ps("csardi", "long") 15 | out <- capture.output(print(x)) 16 | expect_match(out[1], "csardi.*packages in.*seconds") 17 | expect_true(any(grepl("github.com/r-lib/crayon", out, fixed = TRUE))) 18 | expect_true(any(grepl("G.*bor Cs.*rdi", out))) 19 | }) 20 | 21 | test_that("left_right if they do not fit", { 22 | expect_equal( 23 | left_right("12345678901234567890", "1234567890", width = 20), 24 | " 1234567890\n12345678901234567890" 25 | ) 26 | }) 27 | -------------------------------------------------------------------------------- /header.md: -------------------------------------------------------------------------------- 1 | # Search and Query CRAN R Packages 2 | 3 | 4 | [![lifecycle](https://lifecycle.r-lib.org/articles/figures/lifecycle-stable.svg)](https://lifecycle.r-lib.org/articles/stages.html#stable-1) 5 | [![R-CMD-check](https://github.com/r-hub/pkgsearch/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/r-hub/pkgsearch/actions/workflows/R-CMD-check.yaml) 6 | [![CRAN status](https://www.r-pkg.org/badges/version/pkgsearch)](https://cran.r-project.org/package=pkgsearch) 7 | [![CRAN RStudio mirror downloads](https://cranlogs.r-pkg.org/badges/pkgsearch)](https://www.r-pkg.org/pkg/pkgsearch) 8 | [![Codecov test coverage](https://codecov.io/gh/r-hub/pkgsearch/branch/main/graph/badge.svg)](https://app.codecov.io/gh/r-hub/pkgsearch?branch=main) 9 | 10 | 11 | `pkgsearch` uses R-hub web services that munge CRAN metadata and let you 12 | access it through several lenses. 13 | -------------------------------------------------------------------------------- /R/rematch2.R: -------------------------------------------------------------------------------- 1 | 2 | re_match <- function(text, pattern, perl = TRUE, ...) { 3 | 4 | assert_that(is_string(pattern)) 5 | text <- as.character(text) 6 | 7 | match <- regexpr(pattern, text, perl = perl, ...) 8 | 9 | start <- as.vector(match) 10 | length <- attr(match, "match.length") 11 | end <- start + length - 1L 12 | 13 | matchstr <- substring(text, start, end) 14 | matchstr[ start == -1 ] <- NA_character_ 15 | 16 | res <- data_frame(.text = text, .match = matchstr) 17 | 18 | if (!is.null(attr(match, "capture.start"))) { 19 | 20 | gstart <- attr(match, "capture.start") 21 | glength <- attr(match, "capture.length") 22 | gend <- gstart + glength - 1L 23 | 24 | groupstr <- substring(text, gstart, gend) 25 | groupstr[ gstart == -1 ] <- NA_character_ 26 | dim(groupstr) <- dim(gstart) 27 | 28 | res <- cbind(groupstr, res, stringsAsFactors = FALSE) 29 | } 30 | names(res) <- c(attr(match, "capture.names"), ".text", ".match") 31 | class(res) <- c("tbl", class(res)) 32 | res 33 | } 34 | -------------------------------------------------------------------------------- /man/cran_packages.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/crandb-public-api.R 3 | \name{cran_packages} 4 | \alias{cran_packages} 5 | \title{Metadata about multiple CRAN packages} 6 | \usage{ 7 | cran_packages(names) 8 | } 9 | \arguments{ 10 | \item{names}{Package names. May also contain versions, separated by a 11 | \code{@} character.} 12 | } 13 | \value{ 14 | A data frame of package metadata, one package per row. 15 | } 16 | \description{ 17 | Metadata about multiple CRAN packages 18 | } 19 | \examples{ 20 | \dontshow{if (identical(Sys.getenv("IN_PKGDOWN"), "true")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 21 | # Get metadata about one package 22 | cran_packages("rhub") 23 | # Get metadata about two packages 24 | cran_packages(c("rhub", "testthat")) 25 | # Get metadata about two packages at given versions 26 | cran_packages(c("rhub@1.1.1", "testthat@2.2.1", "testthat@2.2.0")) 27 | # If a version does not exist nothing is returned 28 | cran_packages("rhub@notaversion") 29 | \dontshow{\}) # examplesIf} 30 | } 31 | -------------------------------------------------------------------------------- /tests/testthat/test-utils.R: -------------------------------------------------------------------------------- 1 | test_that("meta", { 2 | foo <- 42 3 | expect_identical(meta(foo), NULL) 4 | 5 | meta(foo)$key <- "value" 6 | meta(foo)$key2 <- "value2" 7 | 8 | expect_identical( 9 | meta(foo), 10 | list(key = "value", key2 = "value2")) 11 | 12 | expect_identical(meta(foo)$key, "value") 13 | expect_identical(meta(foo)$key2, "value2") 14 | }) 15 | 16 | test_that("check_count", { 17 | expect_error(check_count(NA_integer_)) 18 | expect_error(check_count(integer())) 19 | expect_error(check_count(1:2)) 20 | expect_error(check_count(-1)) 21 | expect_error(check_count(1.5)) 22 | expect_error(check_count(list(1))) 23 | 24 | expect_error(check_count(1L), NA) 25 | expect_error(check_count(1), NA) 26 | expect_error(check_count(0), NA) 27 | expect_error(check_count(101), NA) 28 | }) 29 | 30 | test_that("check_string", { 31 | expect_error(check_string(123)) 32 | expect_error(check_string(character())) 33 | expect_error(check_string(letters)) 34 | expect_error(check_string(NA_character_)) 35 | 36 | expect_error(check_string("foo"), NA) 37 | expect_error(check_string(""), NA) 38 | }) 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2023 pkgsearch authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /man/pkgsearch-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/pkgsearch-package.R 3 | \docType{package} 4 | \name{pkgsearch-package} 5 | \alias{pkgsearch} 6 | \alias{pkgsearch-package} 7 | \title{pkgsearch: Search and Query CRAN R Packages} 8 | \description{ 9 | Search CRAN metadata about packages by keyword, popularity, recent activity, package name and more. Uses the 'R-hub' search server, see \url{https://r-pkg.org} and the CRAN metadata database, that contains information about CRAN packages. Note that this is _not_ a CRAN project. 10 | } 11 | \seealso{ 12 | Useful links: 13 | \itemize{ 14 | \item \url{https://github.com/r-hub/pkgsearch} 15 | \item \url{https://r-hub.github.io/pkgsearch/} 16 | \item Report bugs at \url{https://github.com/r-hub/pkgsearch/issues} 17 | } 18 | 19 | } 20 | \author{ 21 | \strong{Maintainer}: Gábor Csárdi \email{csardi.gabor@gmail.com} 22 | 23 | Authors: 24 | \itemize{ 25 | \item Maëlle Salmon (\href{https://orcid.org/0000-0002-2815-0399}{ORCID}) 26 | } 27 | 28 | Other contributors: 29 | \itemize{ 30 | \item R Consortium [funder] 31 | } 32 | 33 | } 34 | \keyword{internal} 35 | -------------------------------------------------------------------------------- /man/pkg_search_addin.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/addin.R 3 | \name{pkg_search_addin} 4 | \alias{pkg_search_addin} 5 | \title{RStudio addin to search CRAN packages} 6 | \usage{ 7 | pkg_search_addin(query = "", viewer = c("dialog", "browser")) 8 | } 9 | \arguments{ 10 | \item{query}{Query string to start the addin with.} 11 | 12 | \item{viewer}{Whether to show the addin within RStudio (\code{"dialog"}), 13 | or in a web browser (\code{"browser"}).} 14 | } 15 | \description{ 16 | Call this function from RStudio for best results. You can also use it 17 | without RStudio, then it will run in the web browser. 18 | } 19 | \details{ 20 | The app has: 21 | \itemize{ 22 | \item A search tab for free text search, very much like the \code{\link[=pkg_search]{pkg_search()}} 23 | function. 24 | \item The list of recently updated packages. 25 | \item The list of top packages: most downloaded, most depended upon, 26 | and trending packages. 27 | \item Package list by maintainer. 28 | } 29 | } 30 | \section{Examples}{ 31 | 32 | 33 | \if{html}{\out{
}}\preformatted{pkg_search_addin() 34 | 35 | # Start with a search query 36 | pkg_search_addin("permutation test") 37 | }\if{html}{\out{
}} 38 | } 39 | 40 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: pkgsearch 2 | Title: Search and Query CRAN R Packages 3 | Version: 3.1.5.9000 4 | Authors@R: c( 5 | person("Gábor", "Csárdi", , "csardi.gabor@gmail.com", role = c("aut", "cre")), 6 | person("Maëlle", "Salmon", role = "aut", 7 | comment = c(ORCID = "0000-0002-2815-0399")), 8 | person("R Consortium", role = "fnd") 9 | ) 10 | Description: Search CRAN metadata about packages by keyword, popularity, 11 | recent activity, package name and more. Uses the 'R-hub' search 12 | server, see and the CRAN metadata database, that 13 | contains information about CRAN packages. Note that this is _not_ a 14 | CRAN project. 15 | License: MIT + file LICENSE 16 | URL: https://github.com/r-hub/pkgsearch, 17 | https://r-hub.github.io/pkgsearch/ 18 | BugReports: https://github.com/r-hub/pkgsearch/issues 19 | Imports: 20 | curl, 21 | jsonlite 22 | Suggests: 23 | covr, 24 | memoise, 25 | mockery, 26 | pillar, 27 | pingr (>= 2.0.0), 28 | rstudioapi, 29 | shiny, 30 | shinyjs, 31 | shinyWidgets, 32 | testthat (>= 3.0.0), 33 | whoami, 34 | withr 35 | Config/testthat/edition: 3 36 | Encoding: UTF-8 37 | Roxygen: list(markdown = TRUE) 38 | RoxygenNote: 7.3.1.9000 39 | -------------------------------------------------------------------------------- /man/cran_events.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/crandb-public-api.R, R/print.R 3 | \name{cran_events} 4 | \alias{cran_events} 5 | \alias{summary.cran_event_list} 6 | \alias{print.cran_event_list} 7 | \title{List of all CRAN events (new, updated, archived packages)} 8 | \usage{ 9 | cran_events(releases = TRUE, archivals = TRUE, limit = 10, from = 1) 10 | 11 | \method{summary}{cran_event_list}(object, ...) 12 | 13 | \method{print}{cran_event_list}(x, ...) 14 | } 15 | \arguments{ 16 | \item{releases}{Whether to include package releases.} 17 | 18 | \item{archivals}{Whether to include package archivals.} 19 | 20 | \item{limit}{Number of events to list.} 21 | 22 | \item{from}{Where to start the list, for pagination.} 23 | 24 | \item{object}{Object to summarize.} 25 | 26 | \item{...}{Additional arguments are ignored currently.} 27 | 28 | \item{x}{Object to print.} 29 | } 30 | \value{ 31 | List of events. 32 | } 33 | \description{ 34 | List of all CRAN events (new, updated, archived packages) 35 | } 36 | \examples{ 37 | \dontshow{if (identical(Sys.getenv("IN_PKGDOWN"), "true")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 38 | cran_events() 39 | cran_events(limit = 5, releases = FALSE) 40 | cran_events(limit = 5, archivals = FALSE) 41 | summary(cran_events(limit = 10)) 42 | \dontshow{\}) # examplesIf} 43 | } 44 | -------------------------------------------------------------------------------- /tools/badversions.R: -------------------------------------------------------------------------------- 1 | 2 | download_crandb <- function() { 3 | if (file.exists("cran-full.json.gz")) { 4 | system2("gzip", c("-d", "cran-full.json.gz")) 5 | } 6 | if (!file.exists("cran-full.json")) { 7 | download.file( 8 | "https://crandb.r-pkg.org:2053/cran/_all_docs?include_docs=true", 9 | "cran-full.json.tmp" 10 | ) 11 | file.rename("cran-full.json.tmp", "cran-full.json") 12 | } 13 | 14 | entries <- jsonlite::fromJSON("cran-full.json", simplifyVector = FALSE)$rows 15 | pkgs <- Filter(function(x) is.null(x$doc$type) && !startsWith(x$id, "_"), entries) 16 | 17 | pkgs 18 | } 19 | 20 | extract_versions <- function(pkgs) { 21 | vers <- lapply(pkgs, function(pkg) { 22 | unname(sapply(pkg$doc$versions, "[[", "Version")) 23 | }) 24 | names(vers) <- sapply(pkgs, "[[", "id") 25 | 26 | data.frame( 27 | stringsAsFactors = FALSE, 28 | package = rep(names(vers), lengths(vers)), 29 | version = unname(unlist(vers)) 30 | ) 31 | } 32 | 33 | find_bad_versions <- function(vers) { 34 | pv <- package_version(vers$version, strict = FALSE) 35 | bad <- vers[is.na(pv), ] 36 | tapply(bad$version, bad$package, c) 37 | } 38 | 39 | badversions_main <- function() { 40 | pkgs <- download_crandb() 41 | vers <- extract_versions(pkgs) 42 | bad <- find_bad_versions(vers) 43 | bad 44 | } 45 | 46 | if (is.null(sys.calls())) { 47 | badversions_main() 48 | } 49 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://r-hub.github.io/pkgsearch 2 | 3 | authors: 4 | R Consortium: 5 | href: "https://www.r-consortium.org/" 6 | html: "" 7 | 8 | destination: docs 9 | 10 | development: 11 | mode: auto 12 | 13 | toc: 14 | depth: 2 15 | 16 | reference: 17 | - title: Search packages by query 18 | desc: ~ 19 | contents: 20 | - '`pkg_search`' 21 | - '`advanced_search`' 22 | - title: Discover packages 23 | desc: ~ 24 | contents: 25 | - '`cran_top_downloaded`' 26 | - '`cran_trending`' 27 | - title: Get package metadata 28 | desc: ~ 29 | contents: 30 | - '`cran_package`' 31 | - '`cran_package_history`' 32 | - '`cran_packages`' 33 | - title: Get CRAN releases/archivals 34 | desc: ~ 35 | contents: 36 | - '`cran_new`' 37 | - '`cran_events`' 38 | - title: RStudio addin 39 | desc: ~ 40 | contents: 41 | - '`pkg_search_addin`' 42 | 43 | navbar: 44 | structure: 45 | left: 46 | - home 47 | - intro 48 | - reference 49 | - articles 50 | - tutorials 51 | - news 52 | right: github 53 | components: 54 | home: 55 | icon: fas fa-home fa-lg 56 | href: index.html 57 | reference: 58 | text: Reference 59 | href: reference/index.html 60 | news: 61 | text: Changelog 62 | href: news/index.html 63 | github: 64 | icon: fab fa-github fa-lg 65 | href: https://github.com/r-hub/pkgsearch 66 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | release: 8 | types: [published] 9 | workflow_dispatch: 10 | 11 | name: pkgdown.yaml 12 | 13 | permissions: read-all 14 | 15 | jobs: 16 | pkgdown: 17 | runs-on: ubuntu-latest 18 | # Only restrict concurrency for non-PR jobs 19 | concurrency: 20 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 21 | env: 22 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 23 | permissions: 24 | contents: write 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - uses: r-lib/actions/setup-pandoc@v2 29 | 30 | - uses: r-lib/actions/setup-r@v2 31 | with: 32 | use-public-rspm: true 33 | 34 | - uses: r-lib/actions/setup-r-dependencies@v2 35 | with: 36 | extra-packages: any::pkgdown, local::. 37 | needs: website 38 | 39 | - name: Build site 40 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 41 | shell: Rscript {0} 42 | 43 | - name: Deploy to GitHub pages 🚀 44 | if: github.event_name != 'pull_request' 45 | uses: JamesIves/github-pages-deploy-action@v4.5.0 46 | with: 47 | clean: false 48 | branch: gh-pages 49 | folder: docs 50 | -------------------------------------------------------------------------------- /tests/testthat/test-time-zones.R: -------------------------------------------------------------------------------- 1 | test_that("output time zone is always UTC", { 2 | expect_equal( 3 | attr(parse_iso_8601("2010-07-01"), "tzone"), 4 | "UTC") 5 | 6 | expect_equal( 7 | attr(parse_iso_8601("2010-07-01", default_tz = "CET"), "tzone"), 8 | "UTC") 9 | }) 10 | 11 | test_that("input time zone is respected (local CET)", { 12 | withr::local_timezone("CET") 13 | 14 | d <- parse_iso_8601("2010-07-01", default_tz = "CET") 15 | expect_equal(d, .POSIXct(as.POSIXct("2010-07-01", "CET"), "UTC")) 16 | 17 | d <- parse_iso_8601("2010-07-01", default_tz = "US/Pacific") 18 | expect_equal(d, .POSIXct(as.POSIXct("2010-07-01", "US/Pacific"), "UTC")) 19 | }) 20 | 21 | test_that("input time zone is respected (local US/Pacific)", { 22 | withr::local_timezone("US/Pacific") 23 | 24 | d <- parse_iso_8601("2010-07-01", default_tz = "CET") 25 | expect_equal(d, .POSIXct(as.POSIXct("2010-07-01", "CET"), "UTC")) 26 | 27 | d <- parse_iso_8601("2010-07-01", default_tz = "US/Pacific") 28 | expect_equal(d, .POSIXct(as.POSIXct("2010-07-01", "US/Pacific"), "UTC")) 29 | }) 30 | 31 | test_that("empty default time zone is the local time zone (CET)", { 32 | withr::local_timezone("CET") 33 | 34 | d <- parse_iso_8601("2010-07-01", default_tz = "") 35 | expect_equal(d, .POSIXct(as.POSIXct("2010-07-01", "CET"), "UTC")) 36 | }) 37 | 38 | test_that("empty default time zone is the local time zone (US/Pacific)", { 39 | withr::local_timezone("US/Pacific") 40 | 41 | d <- parse_iso_8601("2010-07-01", default_tz = "") 42 | expect_equal(d, .POSIXct(as.POSIXct("2010-07-01", "US/Pacific"), "UTC")) 43 | }) 44 | -------------------------------------------------------------------------------- /man/cran_new.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/crandb-public-api.R 3 | \name{cran_new} 4 | \alias{cran_new} 5 | \title{New CRAN packages} 6 | \usage{ 7 | cran_new(from = "last-week", to = "now", last = Inf) 8 | } 9 | \arguments{ 10 | \item{from}{Start of the time interval to query. Possible values: 11 | \itemize{ 12 | \item \code{"last-week"} 13 | \item \code{"last-month"} 14 | \item A \link{Date} object to be used as a start date. 15 | \item A \link{POSIXt} object to be used as the start date. 16 | \item A \link{difftime} object to used as the time interval until now. 17 | \item An integer scalar, the number of days until today. 18 | \item A character string that is converted to a start date using 19 | \code{\link[=as.POSIXct]{as.POSIXct()}}. 20 | }} 21 | 22 | \item{to}{End of the time interval to query. It accepts the same kinds 23 | of values as \code{from}, and additionally it can also be the string \code{"now"}, 24 | to specify the current date and time.} 25 | 26 | \item{last}{Integer to limit the number of returned packages.} 27 | } 28 | \value{ 29 | Data frame of package descriptions. 30 | } 31 | \description{ 32 | List the latest new CRAN packages. 33 | } 34 | \examples{ 35 | \dontshow{if (identical(Sys.getenv("IN_PKGDOWN"), "true")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 36 | # Last week 37 | cran_new("last-week") 38 | 39 | # Last month 40 | cran_new("last-month") 41 | 42 | # Last 5 days 43 | cran_new(from = 5) 44 | 45 | # From a given date, but at most 10 46 | cran_new(from = "2021-04-06", last = 10) 47 | 48 | # March of 2021 49 | cran_new(from = "2021-03-01", to = "2021-04-01") 50 | \dontshow{\}) # examplesIf} 51 | } 52 | -------------------------------------------------------------------------------- /tests/testthat/test-search.R: -------------------------------------------------------------------------------- 1 | test_that("search", { 2 | skip_if_offline() 3 | 4 | x <- ps("command line") 5 | expect_s3_class(x, "tbl") 6 | expect_s3_class(x, "pkg_search_result") 7 | 8 | expect_true( 9 | all(c("score", "package", "version", "title", "description", "date", 10 | "maintainer_name", "maintainer_email", "revdeps", 11 | "downloads_last_month", "license", "url", "bugreports") %in% 12 | colnames(x)) 13 | ) 14 | 15 | expect_true("cli" %in% x$package) 16 | }) 17 | 18 | test_that("again w/o previous search", { 19 | s_data$prev_q <- NULL 20 | expect_error(ps()) 21 | }) 22 | 23 | test_that("more w/o previous search", { 24 | s_data$prev_q <- NULL 25 | expect_error(more()) 26 | }) 27 | 28 | test_that("again", { 29 | skip_if_offline() 30 | 31 | x <- ps("csardi") 32 | expect_equal(meta(s_data$prev_q$result)$format, "short") 33 | expect_error(x2 <- ps(), NA) 34 | expect_equal(meta(s_data$prev_q$result)$format, "long") 35 | expect_error(x3 <- ps(), NA) 36 | expect_equal(meta(s_data$prev_q$result)$format, "short") 37 | 38 | expect_s3_class(x2, "tbl") 39 | expect_s3_class(x2, "pkg_search_result") 40 | expect_equal(x$package, x2$package) 41 | 42 | expect_s3_class(x3, "tbl") 43 | expect_s3_class(x3, "pkg_search_result") 44 | expect_equal(x$package, x3$package) 45 | }) 46 | 47 | test_that("more", { 48 | skip_if_offline() 49 | 50 | x <- ps("csardi") 51 | x2 <- more() 52 | 53 | expect_s3_class(x2, "tbl") 54 | expect_s3_class(x2, "pkg_search_result") 55 | expect_true(max(x2$score) <= min(x$score)) 56 | 57 | expect_true( 58 | all(c("score", "package", "version", "title", "description", "date", 59 | "maintainer_name", "maintainer_email", "revdeps", 60 | "downloads_last_month", "license", "url", "bugreports") %in% 61 | colnames(x2)) 62 | ) 63 | }) 64 | -------------------------------------------------------------------------------- /R/assert.R: -------------------------------------------------------------------------------- 1 | 2 | is_package_name <- function(string) { 3 | assert_that(is_string(string)) 4 | grepl("^[0-9a-zA-Z._]*$", string) 5 | } 6 | 7 | on_failure(is_package_name) <- function(call, env) { 8 | paste0(deparse(call$x), " is not a valid package name") 9 | } 10 | 11 | # via tools/badversions.R 12 | bad_versions <- list( 13 | dse = c("R2000.4-1", "R2000.6-1"), 14 | lme = c( 15 | "3.0-0 (1999/06/26)", 16 | "3.0b8a-2 (1999/06/07)", 17 | "3.1-0 (1999/08/03)" 18 | ), 19 | tframe = "R2000.6-1", 20 | VR = c( 21 | "5.3pl037-1 (1999/02/08)", 22 | "5.3pl037-2 (1999/02/08)", 23 | "6.1-4 (1999/08/15)" 24 | ) 25 | ) 26 | 27 | # base::.standard_regexps()$valid_package_version 28 | version_regex <- "^([[:digit:]]+[.-]){1,}[[:digit:]]+$" 29 | 30 | is_package_version <- function(string) { 31 | assert_that(is_string(string)) 32 | grep(version_regex, string) || string %in% unlist(bad_versions) 33 | } 34 | 35 | on_failure(is_package_version) <- function(call, env) { 36 | paste0(deparse(call$x), " is not a valid (package or R) version") 37 | } 38 | 39 | is_integerish <- function(x) { 40 | is.integer(x) || (is.numeric(x) && all(x == trunc(x)) && !is.na(x)) 41 | } 42 | 43 | is_positive_count <- function(x) { 44 | is_integerish(x) && length(x) == 1 && !is.na(x) && x > 0 45 | } 46 | 47 | on_failure(is_positive_count) <- function(call, env) { 48 | paste0(deparse(call$x), " is not a count (a single positive integer)") 49 | } 50 | 51 | is_flag <- function(x) { 52 | is.logical(x) && length(x) == 1 && !is.na(x) 53 | } 54 | 55 | on_failure(is_flag) <- function(call, env) { 56 | paste0(deparse(call$x), " is not a flag (a length one logical vector).") 57 | } 58 | 59 | is_string <- function(x) { 60 | is.character(x) && length(x) == 1 && !is.na(x) 61 | } 62 | 63 | on_failure(is_string) <- function(call, env) { 64 | paste0(deparse(call$x), " is not a string (a length one character vector).") 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | 8 | name: test-coverage.yaml 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | test-coverage: 14 | runs-on: ubuntu-latest 15 | env: 16 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - uses: r-lib/actions/setup-r@v2 22 | with: 23 | use-public-rspm: true 24 | 25 | - uses: r-lib/actions/setup-r-dependencies@v2 26 | with: 27 | extra-packages: any::covr, any::xml2 28 | needs: coverage 29 | 30 | - name: Test coverage 31 | run: | 32 | cov <- covr::package_coverage( 33 | quiet = FALSE, 34 | clean = FALSE, 35 | install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") 36 | ) 37 | covr::to_cobertura(cov) 38 | shell: Rscript {0} 39 | 40 | - uses: codecov/codecov-action@v4 41 | with: 42 | # Fail if error if not on PR, or if on PR and token is given 43 | fail_ci_if_error: ${{ github.event_name != 'pull_request' || secrets.CODECOV_TOKEN }} 44 | file: ./cobertura.xml 45 | plugin: noop 46 | disable_search: true 47 | token: ${{ secrets.CODECOV_TOKEN }} 48 | 49 | - name: Show testthat output 50 | if: always() 51 | run: | 52 | ## -------------------------------------------------------------------- 53 | find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true 54 | shell: bash 55 | 56 | - name: Upload test results 57 | if: failure() 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: coverage-test-failures 61 | path: ${{ runner.temp }}/package 62 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | # 4 | # NOTE: This workflow is overkill for most R packages and 5 | # check-standard.yaml is likely a better choice. 6 | # usethis::use_github_action("check-standard") will install it. 7 | on: 8 | push: 9 | branches: [main, master] 10 | pull_request: 11 | 12 | name: R-CMD-check.yaml 13 | 14 | permissions: read-all 15 | 16 | jobs: 17 | R-CMD-check: 18 | runs-on: ${{ matrix.config.os }} 19 | 20 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | config: 26 | - {os: macos-latest, r: 'release'} 27 | 28 | - {os: windows-latest, r: 'release'} 29 | # use 4.0 or 4.1 to check with rtools40's older compiler 30 | - {os: windows-latest, r: 'oldrel-4'} 31 | 32 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 33 | - {os: ubuntu-latest, r: 'release'} 34 | - {os: ubuntu-latest, r: 'oldrel-1'} 35 | - {os: ubuntu-latest, r: 'oldrel-2'} 36 | - {os: ubuntu-latest, r: 'oldrel-3'} 37 | - {os: ubuntu-latest, r: 'oldrel-4'} 38 | 39 | env: 40 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 41 | R_KEEP_PKG_SOURCE: yes 42 | 43 | steps: 44 | - uses: actions/checkout@v4 45 | 46 | - uses: r-lib/actions/setup-pandoc@v2 47 | 48 | - uses: r-lib/actions/setup-r@v2 49 | with: 50 | r-version: ${{ matrix.config.r }} 51 | http-user-agent: ${{ matrix.config.http-user-agent }} 52 | use-public-rspm: true 53 | 54 | - uses: r-lib/actions/setup-r-dependencies@v2 55 | with: 56 | extra-packages: any::rcmdcheck 57 | needs: check 58 | 59 | - uses: r-lib/actions/check-r-package@v2 60 | with: 61 | upload-snapshots: true 62 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' 63 | -------------------------------------------------------------------------------- /tests/testthat/test-public-api.R: -------------------------------------------------------------------------------- 1 | test_that("cran_package() works", { 2 | 3 | skip_if_offline() 4 | 5 | r1 <- cran_package("igraph0") 6 | 7 | expect_equal(sort(names(r1)), sort(c("Author", "crandb_file_date", "date", 8 | "Date", "Date/Publication", "Depends", "Description", "License", 9 | "Maintainer", "NeedsCompilation", "Package", "Packaged", "releases", 10 | "Repository", "Suggests", "SystemRequirements", "Title", "URL", 11 | "Version"))) 12 | 13 | expect_equal(r1$Version, "0.5.7") 14 | 15 | r2 <- cran_package("igraph", "0.5.5") 16 | 17 | expect_equal(sort(names(r2)), sort(c("Author", "crandb_file_date", "date", 18 | "Date", "Date/Publication", "Depends", "Description", "License", 19 | "Maintainer", "Package", "Packaged", "releases", "Repository", 20 | "Suggests", "SystemRequirements", "Title", "URL", "Version"))) 21 | 22 | r3 <- cran_package("igraph", "all") 23 | 24 | expect_equal(sort(names(r3)), c("archived", "latest", "name", "revdeps", 25 | "timeline", "title", "versions")) 26 | 27 | }) 28 | 29 | test_that("cran_events() works", { 30 | 31 | skip_if_offline() 32 | 33 | r1 <- cran_events() 34 | expect_true(is.null(names(r1))) 35 | expect_equal(length(r1), 10) 36 | expect_equal(names(r1[[1]]), c("date", "name", "event", "package")) 37 | r1 %>% 38 | sapply(pluck, "event") %>% 39 | isin(c("released", "archived")) %>% 40 | all() %>% 41 | expect_true() 42 | 43 | r2 <- cran_events(limit = 2) 44 | expect_equal(length(r2), 2) 45 | expect_equal(names(r2[[1]]), c("date", "name", "event", "package")) 46 | 47 | r3 <- cran_events(limit = 2, releases = FALSE) 48 | expect_equal(length(r3), 2) 49 | expect_equal(names(r3[[1]]), c("date", "name", "event", "package")) 50 | 51 | r4 <- cran_events(limit = 2, archivals = FALSE) 52 | expect_equal(length(r4), 2) 53 | expect_equal(names(r4[[1]]), c("date", "name", "event", "package")) 54 | 55 | }) 56 | 57 | test_that("cran_package_history() edge cases", { 58 | 59 | skip_if_offline() 60 | 61 | random_package <- paste( 62 | sample(letters, 20, replace = TRUE), 63 | collapse = "" 64 | ) 65 | expect_error(cran_package_history(random_package)) 66 | 67 | expect_error(cran_package_history("zzzzzzzz")) 68 | }) 69 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # pkgsearch (development version) 2 | 3 | # pkgsearch 3.1.5 4 | 5 | * `cran_new()` works correctly again. 6 | 7 | # pkgsearch 3.1.4 8 | 9 | * pkgsearch now uses the `timeout` option to set the limit for the total 10 | time of each HTTP request (#125, @gladkia). 11 | 12 | # pkgsearch 3.1.3 13 | 14 | * No user visible changes. 15 | 16 | # pkgsearch 3.1.2 17 | 18 | * `cran_new()` works again. 19 | 20 | # pkgsearch 3.1.1 21 | 22 | * pkgsearch gives nicer error messages now. 23 | 24 | # pkgsearch 3.1.0 25 | 26 | * pkgsearch functions return data frames now, instead of tibbles. 27 | The data frames have a `tbl` class, so they are still printed the 28 | same way as tibbles, as long as the pillar package is available. 29 | Otherwise they behave as data frames. 30 | 31 | * New `cran_new()` function to query new packages on CRAN. 32 | 33 | # pkgsearch 3.0.3 34 | 35 | * Fix dependency handling in the add-in (@salim-b, #101) 36 | 37 | * pkgsearch uses curl now for the HTTP calls, instead of httr, which makes 38 | it a bit more lightweight. 39 | 40 | # pkgsearch 3.0.2 41 | 42 | * The RStudio addin now gives a better error more missing dependencies 43 | (#84, @yonicd) 44 | 45 | * `cran_package_history()` now errors for non-existing packages, instead 46 | of returning `NULL` or the data for another package (#88). 47 | 48 | # pkgsearch 3.0.1 49 | 50 | * The "My packages" and "Most depended upon" items now work properly 51 | in the RStudio addin (#77). 52 | 53 | * The RStudio addin has a better window title when running in a 54 | browser (#79). 55 | 56 | * The addin now does not crash RStudio when closing the window (#78). 57 | 58 | # pkgsearch 3.0.0 59 | 60 | * New RStudio addin to search for packages in a GUI: 61 | `pkg_search_addin()`. 62 | 63 | * New `cran_package()`, `cran_packages()` and `cran_package_history()` 64 | functions to query metadata about certain packages. 65 | 66 | * New `cran_events()` function to list recent CRAN events, new, updated 67 | or archived packages. 68 | 69 | * New `cran_top_downloaded()` function to query packages with the most 70 | downloads. 71 | 72 | * New `cran_trending()` function to return the trending CRAN packages. 73 | 74 | * New function `advanced_search()` for more search flexibility. 75 | 76 | # pkgsearch 2.0.1 77 | 78 | * Fix a bug when a search hit does not have a 'downloads' field. 79 | (Because it is a brand new package.) 80 | 81 | # pkgsearch 2.0.0 82 | 83 | First release on CRAN. 84 | -------------------------------------------------------------------------------- /R/aa-assertthat.R: -------------------------------------------------------------------------------- 1 | 2 | assert_that <- function(..., env = parent.frame(), msg = NULL) { 3 | res <- see_if(..., env = env, msg = msg) 4 | if (res) return(TRUE) 5 | 6 | throw(new_assert_error(attr(res, "msg"))) 7 | } 8 | 9 | new_assert_error <- function (message, call = NULL) { 10 | cond <- new_error(message, call. = call) 11 | class(cond) <- c("assert_error", class(cond)) 12 | cond 13 | } 14 | 15 | see_if <- function(..., env = parent.frame(), msg = NULL) { 16 | asserts <- eval(substitute(alist(...))) 17 | 18 | for (assertion in asserts) { 19 | res <- tryCatch({ 20 | eval(assertion, env) 21 | }, new_assert_error = function(e) { 22 | structure(FALSE, msg = e$message) 23 | }) 24 | check_result(res) 25 | 26 | # Failed, so figure out message to produce 27 | if (!res) { 28 | if (is.null(msg)) 29 | msg <- get_message(res, assertion, env) 30 | return(structure(FALSE, msg = msg)) 31 | } 32 | } 33 | 34 | res 35 | } 36 | 37 | check_result <- function(x) { 38 | if (!is.logical(x)) 39 | throw(new_assert_error("assert_that: assertion must return a logical value")) 40 | if (any(is.na(x))) 41 | throw(new_assert_error("assert_that: missing values present in assertion")) 42 | if (length(x) != 1) { 43 | throw(new_assert_error("assert_that: length of assertion is not 1")) 44 | } 45 | 46 | TRUE 47 | } 48 | 49 | get_message <- function(res, call, env = parent.frame()) { 50 | stopifnot(is.call(call), length(call) >= 1) 51 | 52 | if (has_attr(res, "msg")) { 53 | return(attr(res, "msg")) 54 | } 55 | 56 | f <- eval(call[[1]], env) 57 | if (!is.primitive(f)) call <- match.call(f, call) 58 | fname <- deparse(call[[1]]) 59 | 60 | fail <- on_failure(f) %||% base_fs[[fname]] %||% fail_default 61 | fail(call, env) 62 | } 63 | 64 | # The default failure message works in the same way as stopifnot, so you can 65 | # continue to use any function that returns a logical value: you just won't 66 | # get a friendly error message. 67 | # The code below says you get the first 60 characters plus a ... 68 | fail_default <- function(call, env) { 69 | call_string <- deparse(call, width.cutoff = 60L) 70 | if (length(call_string) > 1L) { 71 | call_string <- paste0(call_string[1L], "...") 72 | } 73 | 74 | paste0(call_string, " is not TRUE") 75 | } 76 | 77 | on_failure <- function(x) attr(x, "fail") 78 | 79 | "on_failure<-" <- function(x, value) { 80 | stopifnot(is.function(x), identical(names(formals(value)), c("call", "env"))) 81 | attr(x, "fail") <- value 82 | x 83 | } 84 | 85 | has_attr <- function(x, which) !is.null(attr(x, which, exact = TRUE)) 86 | on_failure(has_attr) <- function(call, env) { 87 | paste0(deparse(call$x), " does not have attribute ", eval(call$which, env)) 88 | } 89 | "%has_attr%" <- has_attr 90 | 91 | base_fs <- new.env(parent = emptyenv()) 92 | -------------------------------------------------------------------------------- /.github/workflows/pr-commands.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | name: pr-commands.yaml 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | document: 13 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/document') }} 14 | name: document 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 18 | permissions: 19 | contents: write 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - uses: r-lib/actions/pr-fetch@v2 24 | with: 25 | repo-token: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - uses: r-lib/actions/setup-r@v2 28 | with: 29 | use-public-rspm: true 30 | 31 | - uses: r-lib/actions/setup-r-dependencies@v2 32 | with: 33 | extra-packages: any::roxygen2 34 | needs: pr-document 35 | 36 | - name: Document 37 | run: roxygen2::roxygenise() 38 | shell: Rscript {0} 39 | 40 | - name: commit 41 | run: | 42 | git config --local user.name "$GITHUB_ACTOR" 43 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 44 | git add man/\* NAMESPACE 45 | git commit -m 'Document' 46 | 47 | - uses: r-lib/actions/pr-push@v2 48 | with: 49 | repo-token: ${{ secrets.GITHUB_TOKEN }} 50 | 51 | style: 52 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/style') }} 53 | name: style 54 | runs-on: ubuntu-latest 55 | env: 56 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 57 | permissions: 58 | contents: write 59 | steps: 60 | - uses: actions/checkout@v4 61 | 62 | - uses: r-lib/actions/pr-fetch@v2 63 | with: 64 | repo-token: ${{ secrets.GITHUB_TOKEN }} 65 | 66 | - uses: r-lib/actions/setup-r@v2 67 | 68 | - name: Install dependencies 69 | run: install.packages("styler") 70 | shell: Rscript {0} 71 | 72 | - name: Style 73 | run: styler::style_pkg() 74 | shell: Rscript {0} 75 | 76 | - name: commit 77 | run: | 78 | git config --local user.name "$GITHUB_ACTOR" 79 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 80 | git add \*.R 81 | git commit -m 'Style' 82 | 83 | - uses: r-lib/actions/pr-push@v2 84 | with: 85 | repo-token: ${{ secrets.GITHUB_TOKEN }} 86 | -------------------------------------------------------------------------------- /man/advanced_search.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/advanced_search.R 3 | \name{advanced_search} 4 | \alias{advanced_search} 5 | \title{Advanced CRAN package search} 6 | \usage{ 7 | advanced_search( 8 | ..., 9 | json = NULL, 10 | format = c("short", "long"), 11 | from = 1, 12 | size = 10 13 | ) 14 | } 15 | \arguments{ 16 | \item{...}{Search terms. For named terms, the name specifies the field 17 | to search for. For unnamed ones, the term is taken as is. The 18 | individual terms are combined with the \code{AND} operator.} 19 | 20 | \item{json}{A character string that contains the query to 21 | send to Elastic. If this is not \code{NULL}, then you cannot specify 22 | any search terms in \code{...}.} 23 | 24 | \item{format}{Default formatting of the results. \emph{short} only 25 | outputs the name and title of the packages, \emph{long} also 26 | prints the author, last version, full description and URLs. 27 | Note that this only affects the default printing, and you can 28 | still inspect the full results, even if you specify \emph{short} 29 | here.} 30 | 31 | \item{from}{Where to start listing the results, for pagination.} 32 | 33 | \item{size}{The number of results to list.} 34 | } 35 | \value{ 36 | Search hits. 37 | } 38 | \description{ 39 | See the Elastic documentation for the syntax and features: 40 | https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html 41 | } 42 | \examples{ 43 | \dontshow{if (identical(Sys.getenv("IN_PKGDOWN"), "true")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 44 | # All orphaned packages 45 | advanced_search(Maintainer = "ORPHANED") 46 | 47 | # Packages with both Hester and Wickham as authors 48 | advanced_search(Author = "Hester", Author = "Wickham") 49 | advanced_search("Author: Hester AND Author: Wickham") 50 | 51 | # Packages with Hester but not Wickham as author 52 | advanced_search(Author = "Hester AND NOT Wickham") 53 | 54 | # Packages with Hester as an Author, and Wickham in any field 55 | advanced_search(Author = "Hester", "Wickham") 56 | 57 | # Packages with Hester as an Author and Wickham nowhere in the metadata 58 | advanced_search(Author = "Hester", "NOT Wickham") 59 | 60 | # Packages for permutation tests and permissive licenses 61 | advanced_search("permutation test AND NOT License: GPL OR GNU") 62 | 63 | # Packages that have a certain field 64 | advanced_search("_exists_" = "URL") 65 | 66 | # Packages that do not have a certain field: 67 | advanced_search("NOT _exists_: URL") 68 | 69 | # The same but as JSON query 70 | query <- '{ 71 | "query": { 72 | "bool": { 73 | "must_not": { 74 | "exists": { 75 | "field": "URL" 76 | } 77 | } 78 | } 79 | } 80 | }' 81 | advanced_search(json = query) 82 | 83 | # Regular expressions 84 | advanced_search(Author = "/Joh?nathan/") 85 | 86 | # Fuzzy search 87 | advanced_search(Author = "Johnathan~1") 88 | \dontshow{\}) # examplesIf} 89 | } 90 | -------------------------------------------------------------------------------- /man/pkg_search.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/api.R, R/print.R 3 | \name{pkg_search} 4 | \alias{pkg_search} 5 | \alias{ps} 6 | \alias{more} 7 | \alias{summary.pkg_search_result} 8 | \alias{print.pkg_search_result} 9 | \title{Search CRAN packages} 10 | \usage{ 11 | pkg_search(query = NULL, format = c("short", "long"), from = 1, size = 10) 12 | 13 | ps(query = NULL, format = c("short", "long"), from = 1, size = 10) 14 | 15 | more(format = NULL, size = NULL) 16 | 17 | \method{summary}{pkg_search_result}(object, ...) 18 | 19 | \method{print}{pkg_search_result}(x, ...) 20 | } 21 | \arguments{ 22 | \item{query}{Search query string. If this argument is missing or 23 | \code{NULL}, then the results of the last query are printed, in 24 | \emph{short} and \emph{long} formats, in turns for successive 25 | \code{pkg_search()} calls. If this argument is missing, then all 26 | other arguments are ignored.} 27 | 28 | \item{format}{Default formatting of the results. \emph{short} only 29 | outputs the name and title of the packages, \emph{long} also 30 | prints the author, last version, full description and URLs. 31 | Note that this only affects the default printing, and you can 32 | still inspect the full results, even if you specify \emph{short} 33 | here.} 34 | 35 | \item{from}{Where to start listing the results, for pagination.} 36 | 37 | \item{size}{The number of results to list.} 38 | 39 | \item{object}{Object to summarize.} 40 | 41 | \item{...}{Additional arguments, ignored currently.} 42 | 43 | \item{x}{Object to print.} 44 | } 45 | \value{ 46 | A data frame with columns: 47 | \itemize{ 48 | \item \code{score}: Score of the hit. See Section \emph{Scoring} for some details. 49 | \item \code{package}: Package name. 50 | \item \code{version}: Latest package version. 51 | \item \code{title}: Package title. 52 | \item \code{description}: Short package description. 53 | \item \code{date}: Time stamp of the last release. 54 | \item \code{maintainer_name}: Name of the package maintainer. 55 | \item \code{maintainer_email}: Email address of the package maintainer. 56 | \item \code{revdeps}: Number of (strong and weak) reverse dependencies of the 57 | package. 58 | \item \code{downloads_last_month}: Raw number of package downloads last month, 59 | from the RStudio CRAN mirror. 60 | \item \code{license}: Package license. 61 | \item \code{url}: Package URL(s). 62 | \item \code{bugreports}: URL of issue tracker, or email address for bug reports. 63 | } 64 | } 65 | \description{ 66 | \code{pkg_search()} starts a new search query, or shows the details of the 67 | previous query, if called without arguments. 68 | 69 | \code{ps()} is an alias to \code{pkg_search()}. 70 | 71 | \code{more()} retrieves that next page of results for the previous query. 72 | } 73 | \details{ 74 | Note that the search needs a working Internet connection. 75 | } 76 | \examples{ 77 | \dontshow{if (identical(Sys.getenv("IN_PKGDOWN"), "true")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 78 | # Example 79 | ps("survival") 80 | 81 | # Pagination 82 | ps("networks") 83 | more() 84 | 85 | # Details 86 | ps("visualization") 87 | ps() 88 | 89 | # See the underlying data frame 90 | ps("ropensci") 91 | ps()[] 92 | \dontshow{\}) # examplesIf} 93 | } 94 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | 2 | `%||%` <- function(l, r) if (is.null(l)) r else l 3 | 4 | `%|NA|%` <- function(l, r) ifelse(is.na(l), r, l) 5 | 6 | check_count <- function(x) { 7 | if (!is.numeric(x) || length(x) != 1 || as.integer(x) != x || 8 | is.na(x) || x < 0) { 9 | throw(new_error(x, " is not a count", call. = FALSE)) 10 | } 11 | } 12 | 13 | check_string <- function(x) { 14 | if (!is.character(x) || length(x) != 1 || is.na(x)) { 15 | throw(new_error(x, " is not a string", call. = FALSE)) 16 | } 17 | } 18 | 19 | `%+%` <- function(lhs, rhs) { 20 | check_string(lhs) 21 | check_string(rhs) 22 | paste0(lhs, rhs) 23 | } 24 | 25 | map <- function(.x, .f, ...) { 26 | lapply(.x, .f, ...) 27 | } 28 | 29 | map_mold <- function(.x, .f, .mold, ...) { 30 | out <- vapply(.x, .f, .mold, ..., USE.NAMES = FALSE) 31 | names(out) <- names(.x) 32 | out 33 | } 34 | 35 | map_int <- function(.x, .f, ...) { 36 | map_mold(.x, .f, integer(1), ...) 37 | } 38 | 39 | map_dbl <- function(.x, .f, ...) { 40 | map_mold(.x, .f, double(1), ...) 41 | } 42 | 43 | map_chr <- function(.x, .f, ...) { 44 | map_mold(.x, .f, character(1), ...) 45 | } 46 | 47 | map_lgl <- function(.x, .f, ...) { 48 | map_mold(.x, .f, logical(1), ...) 49 | } 50 | 51 | meta <- function(x) { 52 | attr(x, "metadata") 53 | } 54 | 55 | `meta<-` <- function(x, value) { 56 | attr(x, "metadata") <- value 57 | x 58 | } 59 | 60 | trim <- function (x) gsub("^\\s+|\\s+$", "", x) 61 | 62 | couchdb_uri <- function() { 63 | "https://crandb.r-pkg.org/" 64 | } 65 | 66 | add_class <- function(x, class_name) { 67 | if (! inherits(x, class_name)) { 68 | class(x) <- c(class_name, attr(x, "class")) 69 | } 70 | x 71 | } 72 | 73 | add_attr <- function(object, key, value) { 74 | attr(object, key) <- value 75 | object 76 | } 77 | 78 | contains <- function(x, y) y %in% x 79 | 80 | isin <- function(x, y) x %in% y 81 | 82 | remove_special <- function(list, level = 1) { 83 | 84 | assert_that(is_positive_count(level)) 85 | 86 | if (level == 1) { 87 | replace( 88 | grepl(pattern = "^_", names(list)), 89 | x = list, 90 | values = NULL 91 | ) 92 | } else { 93 | lapply(list, remove_special, level = level - 1) 94 | } 95 | 96 | } 97 | 98 | pluck <- function(list, idx) list[[idx]] 99 | 100 | needs_packages <- function(pkgs) { 101 | has <- map_lgl(pkgs, function(pkg) { 102 | requireNamespace(pkg, quietly = TRUE) 103 | }) 104 | 105 | if (!all(has)) { 106 | not_installed_pkgs <- pkgs[!has] 107 | 108 | if (length(not_installed_pkgs) == 1) { 109 | 110 | throw(new_error( 111 | "The ", 112 | sQuote(not_installed_pkgs), 113 | " package is needed for this addin.", 114 | call. = FALSE 115 | )) 116 | } else { 117 | 118 | throw(new_error( 119 | "The ", 120 | paste(sQuote(not_installed_pkgs), collapse = ", "), 121 | " packages are needed for this addin.", 122 | call. = FALSE 123 | )) 124 | } 125 | 126 | } 127 | } 128 | 129 | clean_description <- function(txt) { 130 | gsub("", " ", txt, fixed = TRUE) 131 | } 132 | 133 | zap_null <- function(x) { 134 | x[! map_lgl(x, is.null)] 135 | } 136 | 137 | drop_nulls <- function (x) { 138 | x[!map_lgl(x, is.null)] 139 | } 140 | -------------------------------------------------------------------------------- /R/advanced_search.R: -------------------------------------------------------------------------------- 1 | 2 | #' Advanced CRAN package search 3 | #' 4 | #' See the Elastic documentation for the syntax and features: 5 | #' https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html 6 | #' 7 | #' @param ... Search terms. For named terms, the name specifies the field 8 | #' to search for. For unnamed ones, the term is taken as is. The 9 | #' individual terms are combined with the `AND` operator. 10 | #' @param json A character string that contains the query to 11 | #' send to Elastic. If this is not `NULL`, then you cannot specify 12 | #' any search terms in `...`. 13 | #' @inheritParams pkg_search 14 | #' 15 | #' @return Search hits. 16 | #' @export 17 | #' @examplesIf identical(Sys.getenv("IN_PKGDOWN"), "true") 18 | #' # All orphaned packages 19 | #' advanced_search(Maintainer = "ORPHANED") 20 | #' 21 | #' # Packages with both Hester and Wickham as authors 22 | #' advanced_search(Author = "Hester", Author = "Wickham") 23 | #' advanced_search("Author: Hester AND Author: Wickham") 24 | #' 25 | #' # Packages with Hester but not Wickham as author 26 | #' advanced_search(Author = "Hester AND NOT Wickham") 27 | #' 28 | #' # Packages with Hester as an Author, and Wickham in any field 29 | #' advanced_search(Author = "Hester", "Wickham") 30 | #' 31 | #' # Packages with Hester as an Author and Wickham nowhere in the metadata 32 | #' advanced_search(Author = "Hester", "NOT Wickham") 33 | #' 34 | #' # Packages for permutation tests and permissive licenses 35 | #' advanced_search("permutation test AND NOT License: GPL OR GNU") 36 | #' 37 | #' # Packages that have a certain field 38 | #' advanced_search("_exists_" = "URL") 39 | #' 40 | #' # Packages that do not have a certain field: 41 | #' advanced_search("NOT _exists_: URL") 42 | #' 43 | #' # The same but as JSON query 44 | #' query <- '{ 45 | #' "query": { 46 | #' "bool": { 47 | #' "must_not": { 48 | #' "exists": { 49 | #' "field": "URL" 50 | #' } 51 | #' } 52 | #' } 53 | #' } 54 | #' }' 55 | #' advanced_search(json = query) 56 | #' 57 | #' # Regular expressions 58 | #' advanced_search(Author = "/Joh?nathan/") 59 | #' 60 | #' # Fuzzy search 61 | #' advanced_search(Author = "Johnathan~1") 62 | 63 | advanced_search <- function(..., json = NULL, format = c("short", "long"), 64 | from = 1, size = 10) { 65 | 66 | terms <- unlist(list(...)) 67 | format <- match.arg(format) 68 | 69 | if (!is.null(json) && length(terms) > 0) { 70 | throw(new_error("You cannot specify `json` together with search terms.")) 71 | } 72 | 73 | if (is.null(json)) { 74 | if (is.null(names(terms))) names(terms) <- rep("", length(terms)) 75 | 76 | q <- ifelse( 77 | names(terms) == "", 78 | terms, 79 | paste0("(", names(terms), ":", terms, ")") 80 | ) 81 | 82 | qstr <- tojson$write_str(list( 83 | query = list( 84 | query_string = list( 85 | query = paste0(q, collapse = " AND "), 86 | default_field = "*" 87 | ) 88 | ) 89 | ), opts = list(auto_unbox = TRUE, pretty = TRUE)) 90 | 91 | } else { 92 | qstr <- json 93 | } 94 | 95 | server <- Sys.getenv("R_PKG_SEARCH_SERVER", "https://search.r-pkg.org") 96 | 97 | resp <- do_query(qstr, server, from, size) 98 | 99 | result <- format_result( 100 | resp, 101 | "advanced search", 102 | format = format, 103 | from = from, 104 | size = size, 105 | server = server, 106 | qstr = qstr 107 | ) 108 | 109 | s_data$prev_q <- list(type = "advanced", result = result) 110 | 111 | result 112 | } 113 | -------------------------------------------------------------------------------- /R/time-ago.R: -------------------------------------------------------------------------------- 1 | 2 | format_time_ago <- local({ 3 | 4 | e <- expression 5 | 6 | `%s%` <- function(lhs, rhs) { 7 | assert_string(lhs) 8 | do.call( 9 | sprintf, 10 | c(list(lhs), as.list(rhs)) 11 | ) 12 | } 13 | 14 | assert_string <- function(x) { 15 | stopifnot(is.character(x), length(x) == 1L) 16 | } 17 | 18 | assert_diff_time <- function(x) { 19 | stopifnot(inherits(x, "difftime")) 20 | } 21 | 22 | vague_dt_default <- list( 23 | list(c = e(seconds < 10), s = "moments ago"), 24 | list(c = e(seconds < 45), s = "less than a minute ago"), 25 | list(c = e(seconds < 90), s = "about a minute ago"), 26 | list(c = e(minutes < 45), s = e("%d minutes ago" %s% round(minutes))), 27 | list(c = e(minutes < 90), s = "about an hour ago"), 28 | list(c = e(hours < 24), s = e("%d hours ago" %s% round(hours))), 29 | list(c = e(hours < 42), s = "a day ago"), 30 | list(c = e(days < 30), s = e("%d days ago" %s% round(days))), 31 | list(c = e(days < 45), s = "about a month ago"), 32 | list(c = e(days < 335), s = e("%d months ago" %s% round(days / 30))), 33 | list(c = e(years < 1.5), s = "about a year ago"), 34 | list(c = TRUE, s = e("%d years ago" %s% round(years))) 35 | ) 36 | 37 | vague_dt_short <- list( 38 | list(c = e(seconds < 50), s = "<1 min"), 39 | list(c = e(minutes < 50), s = e("%d min" %s% round(minutes))), 40 | list(c = e(hours < 1.5), s = "1 hour"), 41 | list(c = e(hours < 18), s = e("%d hours" %s% round(hours))), 42 | list(c = e(hours < 42), s = "1 day"), 43 | list(c = e(days < 30), s = e("%d day" %s% round(days))), 44 | list(c = e(days < 45), s = "1 mon"), 45 | list(c = e(days < 335), s = e("%d mon" %s% round(days / 30))), 46 | list(c = e(years < 1.5), s = "1 year"), 47 | list(c = TRUE, s = e("%d years" %s% round(years))) 48 | ) 49 | 50 | vague_dt_terse <- list( 51 | list(c = e(seconds < 50), s = e("%2ds" %s% round(seconds))), 52 | list(c = e(minutes < 50), s = e("%2dm" %s% round(minutes))), 53 | list(c = e(hours < 18), s = e("%2dh" %s% round(hours))), 54 | list(c = e(days < 30), s = e("%2dd" %s% round(days))), 55 | list(c = e(days < 335), s = e("%2dM" %s% round(days / 30))), 56 | list(c = TRUE, s = e("%2dy" %s% round(years))) 57 | ) 58 | 59 | vague_dt_formats <- list( 60 | "default" = vague_dt_default, 61 | "short" = vague_dt_short, 62 | "terse" = vague_dt_terse 63 | ) 64 | 65 | time_ago <- function(date, format = c("default", "short", "terse")) { 66 | 67 | date <- as.POSIXct(date) 68 | 69 | if (length(date) > 1) return(sapply(date, time_ago, format = format)) 70 | 71 | seconds <- difftime(Sys.time(), date, units = "secs") 72 | 73 | vague_dt(seconds, format = format) 74 | } 75 | 76 | vague_dt <- function(dt, format = c("default", "short", "terse")) { 77 | 78 | assert_diff_time(dt) 79 | 80 | units(dt) <- "secs" 81 | seconds <- as.vector(dt) 82 | 83 | ## Simplest to quit here for empty input 84 | if (!length(seconds)) return(character()) 85 | 86 | pieces <- list( 87 | minutes = seconds / 60, 88 | hours = seconds / 60 / 60, 89 | days = seconds / 60 / 60 / 24, 90 | years = seconds / 60 / 60 / 24 / 365.25 91 | ) 92 | 93 | format <- match.arg(format) 94 | 95 | for (p in vague_dt_formats[[format]]) { 96 | if (eval(p$c, pieces)) return(eval(p$s, pieces)) 97 | } 98 | } 99 | 100 | structure( 101 | list( 102 | .internal = environment(), 103 | time_ago = time_ago, 104 | vague_dt = vague_dt 105 | ), 106 | class = c("standalone_time_ago", "standalone") 107 | ) 108 | }) 109 | -------------------------------------------------------------------------------- /R/tojson.R: -------------------------------------------------------------------------------- 1 | tojson <- local({ 2 | map2 <- function(x, y, fn, ...) { 3 | mapply(fn, x, y, ..., SIMPLIFY = FALSE) 4 | } 5 | 6 | filter <- function(v, fn) { 7 | keep <- vapply(v, fn, logical(1)) 8 | v[keep] 9 | } 10 | 11 | # FIXME: is this escaping the right things? 12 | jq <- function(x) { 13 | encodeString(x, quote = "\"", justify = "none") 14 | } 15 | 16 | # 1. add a "key": at the begining of each element, unless is.null(key) 17 | # 2. add a comma after each elelemt, except the last one 18 | # Each element can be a character vector, so `key` is added to the first 19 | # element of the character vectors, and comma to the last ones. 20 | comma <- function(x, opts, key = NULL) { 21 | len <- length(x) 22 | stopifnot(len >= 1) 23 | 24 | if (!is.null(key)) { 25 | nokey <- is.na(key) | key == "" 26 | key[nokey] <- seq_along(x)[nokey] 27 | x <- map2(jq(key), x, function(k, el) { 28 | el[1] <- paste0(k, if (opts$pretty) ": " else ":", el[1]) 29 | el 30 | }) 31 | } 32 | 33 | # No commas needed for scalars 34 | if (len == 1) { 35 | return(x) 36 | } 37 | 38 | x2 <- lapply(x, function(el) { 39 | el[length(el)] <- paste0(el[length(el)], ",") 40 | el 41 | }) 42 | 43 | # Keep the last list element as is 44 | x2[[len]] <- x[[len]] 45 | x2 46 | } 47 | 48 | j_null <- function(x, opts) { 49 | "{}" 50 | } 51 | 52 | # Data frames are done row-wise. 53 | # Atomic columns are unboxed. Atomic NA values are omitted. 54 | # List columns remove the extra wrapping list. 55 | j_df <- function(x, opts) { 56 | sub <- unlist(comma( 57 | lapply(seq_len(nrow(x)), function(i) { 58 | row <- as.list(x[i, ]) 59 | row <- filter(row, function(v) !(is.atomic(v) && is.na(v))) 60 | row[] <- lapply(row, function(v) { 61 | if (is.atomic(v)) unbox(v) else if (is.list(v)) v[[1]] else v 62 | }) 63 | j_list(row, opts) 64 | }) 65 | )) 66 | if (opts$pretty) { 67 | c("[", paste0(" ", sub), "]") 68 | } else { 69 | paste0(c("[", sub, "]"), collapse = "") 70 | } 71 | } 72 | 73 | # Returns a character vector. Named lists are dictionaries, unnnamed 74 | # ones are lists. Missing dictionary keys are filled in. 75 | # Keys do _NOT_ need to be unique. 76 | j_list <- function(x, opts) { 77 | if (length(x) == 0L) { 78 | if (is.null(names(x))) "[]" else "{}" 79 | } else if (is.null(names(x))) { 80 | sub <- unlist(comma(lapply(x, j, opts), opts)) 81 | if (opts$pretty) { 82 | c("[", paste0(" ", sub), "]") 83 | } else { 84 | paste(c("[", sub, "]"), collapse = "") 85 | } 86 | } else { 87 | sub <- unlist(comma(lapply(x, j, opts), opts, names(x))) 88 | if (opts$pretty) { 89 | c("{", paste0(" ", sub), "}") 90 | } else { 91 | paste(c("{", sub, "}"), collapse = "") 92 | } 93 | } 94 | } 95 | 96 | # Atomic vectors are converted to lists, even if they have names. 97 | # The names are lost. Pretty formatting keeps a vector in one line 98 | # currently. NA is converted to null. 99 | j_atomic <- function(x, opts) { 100 | if (!typeof(x) %in% c("logical", "integer", "double", "character")) { 101 | stop("Cannot convert atomic ", typeof(x), " vectors to JSON.") 102 | } 103 | len <- length(x) 104 | 105 | if (len == 0) { 106 | return("[]") 107 | } 108 | 109 | unbox <- (opts$auto_unbox && len == 1) || "unbox" %in% class(x) 110 | 111 | if (is.character(x)) { 112 | x <- jq(enc2utf8(x)) 113 | } 114 | 115 | if (is.logical(x)) { 116 | # tolower() keeps NAs, we'll sub them later 117 | x <- tolower(x) 118 | } 119 | 120 | if (unbox) { 121 | if (is.na(x) || x == "NA") "null" else paste0(x) 122 | } else { 123 | x[is.na(x) | x == "NA"] <- "null" 124 | sep <- if (opts$pretty) " " else "" 125 | paste0("[", paste(comma(x), collapse = sep), "]") 126 | } 127 | } 128 | 129 | j <- function(x, opts) { 130 | if (is.null(x)) { 131 | j_null(x, opts) 132 | } else if (is.data.frame(x)) { 133 | j_df(x, opts) 134 | } else if (is.list(x)) { 135 | j_list(x, opts) 136 | } else if (is.atomic(x)) { 137 | j_atomic(x, opts) 138 | } else { 139 | stop("Cannot convert type ", typeof(x), " to JSON.") 140 | } 141 | } 142 | 143 | write_str <- function(x, opts = NULL) { 144 | paste0(write_lines(x, opts), collapse = "\n") 145 | } 146 | 147 | write_file <- function(x, file, opts = NULL) { 148 | writeLines(write_lines(x, opts), file) 149 | } 150 | 151 | write_lines <- function(x, opts = NULL) { 152 | opts <- list( 153 | auto_unbox = opts$auto_unbox %||% FALSE, 154 | pretty = opts$pretty %||% FALSE 155 | ) 156 | j(x, opts) 157 | } 158 | 159 | unbox <- function(x) { 160 | if (!is.atomic(x)) { 161 | stop("Can only unbox atomic scalar, not ", typeof(x), ".") 162 | } 163 | if (length(x) != 1) { 164 | stop("Cannot unbox vector of length ", length(x), ".") 165 | } 166 | class(x) <- c("unbox", class(x)) 167 | x 168 | } 169 | 170 | list( 171 | .envir = environment(), 172 | write_str = write_str, 173 | write_file = write_file, 174 | write_lines = write_lines, 175 | unbox = unbox 176 | ) 177 | }) 178 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at codeofconduct@posit.co. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.1, available at 118 | . 119 | 120 | Community Impact Guidelines were inspired by 121 | [Mozilla's code of conduct enforcement ladder][https://github.com/mozilla/inclusion]. 122 | 123 | For answers to common questions about this code of conduct, see the FAQ at 124 | . Translations are available at . 125 | 126 | [homepage]: https://www.contributor-covenant.org 127 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: 3 | md_document: 4 | variant: markdown_github 5 | toc: true 6 | toc_depth: 3 7 | includes: 8 | before_body: header.md 9 | --- 10 | 11 | 12 | 13 | ```{r, setup, echo = FALSE, message = FALSE} 14 | knitr::opts_chunk$set( 15 | comment = "#>", 16 | tidy = FALSE, 17 | error = FALSE, 18 | fig.width = 8, 19 | fig.height = 8) 20 | options(width = 90) 21 | options(max.print = 200) 22 | ``` 23 | 24 | ## Installation 25 | 26 | Install the latest pkgsearch release from CRAN: 27 | 28 | ```r 29 | install.packages("pkgsearch") 30 | ``` 31 | 32 | The development version is on GitHub: 33 | 34 | ```r 35 | pak::pak("r-hub/pkgsearch") 36 | ``` 37 | 38 | ## Usage 39 | 40 | ### Search relevant packages 41 | 42 | Do you need to find packages solving a particular problem, e.g. 43 | "permutation test"? 44 | 45 | ```{r search} 46 | library("pkgsearch") 47 | library("pillar") # nicer data frame printing 48 | pkg_search("permutation test") 49 | ``` 50 | 51 | pkgsearch uses an [R-hub](https://github.com/r-hub) web service and a careful 52 | ranking that puts popular packages before less frequently used ones. 53 | 54 | ### Do it all *clicking* 55 | 56 | For the search mentioned above, and other points of entry to CRAN metadata, 57 | you can use pkgsearch RStudio add-in! 58 | 59 | [![Addin screencast](https://raw.githubusercontent.com/r-hub/pkgsearch/main/gifs/addin.gif)](https://vimeo.com/375618736) 60 | 61 | Select the "CRAN package search" addin from the menu, or start it with 62 | `pkg_search_addin()`. 63 | 64 | ### Get package metadata 65 | 66 | Do you want to find the dependencies the first versions of `testthat` had 67 | and when each of these versions was released? 68 | 69 | ```{r history} 70 | cran_package_history("testthat") 71 | ``` 72 | 73 | ### Discover packages 74 | 75 | Do you want to know what packages are trending on CRAN these days? 76 | `pkgsearch` can help! 77 | 78 | ```{r trend} 79 | cran_trending() 80 | cran_top_downloaded() 81 | ``` 82 | 83 | ### Keep up with CRAN 84 | 85 | Are you curious about the latest releases or archivals? 86 | 87 | ```{r} 88 | cran_events() 89 | ``` 90 | 91 | ## Search features 92 | 93 | ### More details 94 | 95 | By default it returns a short summary of the ten best search hits. Their 96 | details can be printed by using the `format = "long"` option of `pkg_search()`, 97 | or just calling `pkg_search()` again, without any arguments, after a search: 98 | 99 | ```{r} 100 | library(pkgsearch) 101 | pkg_search("C++") 102 | ``` 103 | 104 | ```{r} 105 | pkg_search() 106 | ``` 107 | 108 | ### Pagination 109 | 110 | The `more()` function can be used to display the next batch of search hits, 111 | batches contain ten packages by default. `ps()` is a shorter alias to 112 | `pkg_search()`: 113 | 114 | ```{r} 115 | ps("google") 116 | ``` 117 | 118 | ```{r} 119 | more() 120 | ``` 121 | 122 | ### Stemming 123 | 124 | The search server uses the stems of the words in the indexed metadata, and 125 | the search phrase. This means that "colour" and "colours" deliver the 126 | exact same result. So do "coloring", "colored", etc. (Unless one is happen 127 | to be an exact package name or match another non-stemmed field.) 128 | 129 | ```{r} 130 | ps("colour", size = 3) 131 | ps("colours", size = 3) 132 | ``` 133 | 134 | ### Ranking 135 | 136 | The most important feature of a search engine is the ranking of the results. 137 | The best results should be listed first. pkgsearch uses weighted scoring, 138 | where a match in the package title gets a higher score than a match in the 139 | package description. It also uses the number of reverse dependencies and 140 | the number of downloads to weight the scores: 141 | 142 | ```{r} 143 | ps("colour")[, c("score", "package", "revdeps", "downloads_last_month")] 144 | ``` 145 | 146 | ### Preferring Phrases 147 | 148 | The search engine prefers matching whole phrases over single words. 149 | E.g. the search phrase "permutation test" will rank coin higher than 150 | testthat, even though testthat is a much better result for the single word 151 | "test". (In fact, at the time of writing testthat is not even on the first 152 | page of results.) 153 | 154 | ```{r} 155 | ps("permutation test") 156 | ``` 157 | 158 | If the whole phrase does not match, pkgsearch falls back to individual 159 | matching words. For example, a match from either words is enough here, 160 | to get on the first page of results: 161 | 162 | ```{r} 163 | ps("test http") 164 | ``` 165 | 166 | ### British vs American English 167 | 168 | The search engine uses a dictionary to make sure that package metadata 169 | and queries given in British and American English yield the same results. 170 | E.g. note the spelling of colour/color in the results: 171 | 172 | ```{r} 173 | ps("colour") 174 | ps("color") 175 | ``` 176 | 177 | ### Ascii Folding 178 | 179 | Especially when searching for package maintainer names, it is convenient 180 | to use the corresponding ASCII letters for non-ASCII characters in search 181 | phrases. E.g. the following two queries yield the same results. Note that 182 | case is also ignored. 183 | 184 | ```{r} 185 | ps("gabor", size = 5) 186 | ps("Gábor", size = 5) 187 | ``` 188 | 189 | ## Configuration 190 | 191 | ### Options 192 | 193 | * `timeout`: pkgsearch follows the `timeout` options for HTTP requests 194 | (i.e. for `pkg_search()` and `advanced_search()`. `timeout` is the limit 195 | for the total time of the HTTP request, and it is in seconds. See 196 | `?options` for details. 197 | 198 | ## More info 199 | 200 | See the [complete documentation](https://r-hub.github.io/pkgsearch/). 201 | 202 | ## License 203 | 204 | MIT @ [Gábor Csárdi](https://github.com/gaborcsardi), 205 | [RStudio](https://github.com/rstudio), 206 | [R Consortium](https://r-consortium.org/). 207 | -------------------------------------------------------------------------------- /R/date.R: -------------------------------------------------------------------------------- 1 | iso_8601 <- local({ 2 | format_iso_8601 <- function(date) { 3 | format(as.POSIXlt(date, tz = "UTC"), "%Y-%m-%dT%H:%M:%S+00:00") 4 | } 5 | 6 | milliseconds <- function(x) as.difftime(as.numeric(x) / 1000, units = "secs") 7 | seconds <- function(x) as.difftime(as.numeric(x), units = "secs") 8 | minutes <- function(x) as.difftime(as.numeric(x), units = "mins") 9 | hours <- function(x) as.difftime(as.numeric(x), units = "hours") 10 | days <- function(x) as.difftime(as.numeric(x), units = "days") 11 | weeks <- function(x) as.difftime(as.numeric(x), units = "weeks") 12 | wday <- function(x) as.POSIXlt(x, tz = "UTC")$wday + 1 13 | with_tz <- function(x, tzone = "") as.POSIXct(as.POSIXlt(x, tz = tzone)) 14 | ymd <- function(x) as.POSIXct(x, format = "%Y %m %d", tz = "UTC") 15 | yj <- function(x) as.POSIXct(x, format = "%Y %j", tz = "UTC") 16 | 17 | parse_iso_8601 <- function(dates, default_tz = "UTC") { 18 | if (default_tz == "") default_tz <- Sys.timezone() 19 | dates <- as.character(dates) 20 | match <- re_match(dates, iso_regex) 21 | matching <- !is.na(match$.match) 22 | result <- rep(.POSIXct(NA_real_, tz = ""), length.out = length(dates)) 23 | result[matching] <- parse_iso_parts(match[matching, ], default_tz) 24 | class(result) <- c("POSIXct", "POSIXt") 25 | with_tz(result, "UTC") 26 | } 27 | 28 | parse_iso_parts <- function(mm, default_tz) { 29 | num <- nrow(mm) 30 | 31 | ## ----------------------------------------------------------------- 32 | ## Date first 33 | 34 | date <- .POSIXct(rep(NA_real_, num), tz = "") 35 | 36 | ## Years-days 37 | fyd <- is.na(date) & mm$yearday != "" 38 | date[fyd] <- yj(paste(mm$year[fyd], mm$yearday[fyd])) 39 | 40 | ## Years-weeks-days 41 | fywd <- is.na(date) & mm$week != "" & mm$weekday != "" 42 | date[fywd] <- iso_week(mm$year[fywd], mm$week[fywd], mm$weekday[fywd]) 43 | 44 | ## Years-weeks 45 | fyw <- is.na(date) & mm$week != "" 46 | date[fyw] <- iso_week(mm$year[fyw], mm$week[fyw], "1") 47 | 48 | ## Years-months-days 49 | fymd <- is.na(date) & mm$month != "" & mm$day != "" 50 | date[fymd] <- ymd(paste(mm$year[fymd], mm$month[fymd], mm$day[fymd])) 51 | 52 | ## Years-months 53 | fym <- is.na(date) & mm$month != "" 54 | date[fym] <- ymd(paste(mm$year[fym], mm$month[fym], "01")) 55 | 56 | ## Years 57 | fy <- is.na(date) 58 | date[fy] <- ymd(paste(mm$year, "01", "01")) 59 | 60 | ## ----------------------------------------------------------------- 61 | ## Now the time 62 | 63 | th <- mm$hour != "" 64 | date[th] <- date[th] + hours(mm$hour[th]) 65 | 66 | tm <- mm$min != "" 67 | date[tm] <- date[tm] + minutes(mm$min[tm]) 68 | 69 | ts <- mm$sec != "" 70 | date[ts] <- date[ts] + seconds(mm$sec[ts]) 71 | 72 | ## ----------------------------------------------------------------- 73 | ## Fractional time 74 | 75 | frac <- as.numeric(sub(",", ".", mm$frac)) 76 | 77 | tfs <- !is.na(frac) & mm$sec != "" 78 | date[tfs] <- date[tfs] + milliseconds(round(frac[tfs] * 1000)) 79 | 80 | tfm <- !is.na(frac) & mm$sec == "" & mm$min != "" 81 | sec <- trunc(frac[tfm] * 60) 82 | mil <- round((frac[tfm] * 60 - sec) * 1000) 83 | date[tfm] <- date[tfm] + seconds(sec) + milliseconds(mil) 84 | 85 | tfh <- !is.na(frac) & mm$sec == "" & mm$min == "" 86 | min <- trunc(frac[tfh] * 60) 87 | sec <- trunc((frac[tfh] * 60 - min) * 60) 88 | mil <- round((((frac[tfh] * 60) - min) * 60 - sec) * 1000) 89 | date[tfh] <- date[tfh] + minutes(min) + seconds(sec) + milliseconds(mil) 90 | 91 | ## ----------------------------------------------------------------- 92 | ## Time zone 93 | 94 | ftzpm <- mm$tzpm != "" 95 | m <- ifelse(mm$tzpm[ftzpm] == "+", -1, 1) 96 | ftzpmh <- ftzpm & mm$tzhour != "" 97 | date[ftzpmh] <- date[ftzpmh] + m * hours(mm$tzhour[ftzpmh]) 98 | ftzpmm <- ftzpm & mm$tzmin != "" 99 | date[ftzpmm] <- date[ftzpmm] + m * minutes(mm$tzmin[ftzpmm]) 100 | 101 | ftzz <- mm$tz == "Z" 102 | date[ftzz] <- as.POSIXct(date[ftzz], "UTC") 103 | 104 | ftz <- mm$tz != "Z" & mm$tz != "" 105 | date[ftz] <- as.POSIXct(date[ftz], mm$tz[ftz]) 106 | 107 | if (default_tz != "UTC") { 108 | ftna <- mm$tzpm == "" & mm$tz == "" 109 | if (any(ftna)) { 110 | dd <- as.POSIXct(format_iso_8601(date[ftna]), 111 | "%Y-%m-%dT%H:%M:%S+00:00", 112 | tz = default_tz 113 | ) 114 | date[ftna] <- dd 115 | } 116 | } 117 | 118 | as.POSIXct(date, "UTC") 119 | } 120 | 121 | iso_regex <- paste0( 122 | "^\\s*", 123 | "(?[\\+-]?\\d{4}(?!\\d{2}\\b))", 124 | "(?:(?-?)", 125 | "(?:(?0[1-9]|1[0-2])", 126 | "(?:\\g{dash}(?[12]\\d|0[1-9]|3[01]))?", 127 | "|W(?[0-4]\\d|5[0-3])(?:-?(?[1-7]))?", 128 | "|(?00[1-9]|0[1-9]\\d|[12]\\d{2}|3", 129 | "(?:[0-5]\\d|6[1-6])))", 130 | "(?