├── .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 | [](https://lifecycle.r-lib.org/articles/stages.html#stable-1)
5 | [](https://github.com/r-hub/pkgsearch/actions/workflows/R-CMD-check.yaml)
6 | [](https://cran.r-project.org/package=pkgsearch)
7 | [](https://www.r-pkg.org/pkg/pkgsearch)
8 | [](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 | [](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 | "(?