diff --git a/.Rbuildignore b/.Rbuildignore new file mode 100644 index 0000000..c50f4b3 --- /dev/null +++ b/.Rbuildignore @@ -0,0 +1,21 @@ +^renv$ +^renv\.lock$ +^.*\.Rproj$ +^\.Rproj\.user$ +^doc$ +^Meta$ +^_pkgdown\.yml$ +^docs$ +^pkgdown$ +^\.travis\.yml$ +^travis-tool\.sh\.cmd$ +^README\.Rmd$ +^README\.md$ +^codecov\.yml$ +^cran-comments\.md$ +^\.github$ +^\.github\/$ +^.github$ +.github +^CRAN-RELEASE$ +^LICENSE\.md$ diff --git a/.Rprofile b/.Rprofile new file mode 100644 index 0000000..81b960f --- /dev/null +++ b/.Rprofile @@ -0,0 +1 @@ +source("renv/activate.R") diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml new file mode 100644 index 0000000..aa14e64 --- /dev/null +++ b/.github/workflows/codecov.yml @@ -0,0 +1,29 @@ +name: Run tests and upload coverage report to Codecov + +on: + push: + branches: [dev, main] + +jobs: + coverage: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up R + uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - name: Install R dependencies + run: | + Rscript -e "install.packages(c('covr', 'devtools', 'remotes'))" + Rscript -e "devtools::install_deps(dependencies = TRUE)" + + - name: Run tests and upload coverage to Codecov + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + run: | + Rscript -e "covr::codecov(token = Sys.getenv('CODECOV_TOKEN'))" diff --git a/.github/workflows/pkgdown.yml b/.github/workflows/pkgdown.yml new file mode 100644 index 0000000..7e0ce6c --- /dev/null +++ b/.github/workflows/pkgdown.yml @@ -0,0 +1,41 @@ +name: Build and deploy pkgdown site to GitHub Pages + +on: + push: + branches: [main] + +jobs: + pkgdown: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Pandoc + uses: r-lib/actions/setup-pandoc@v2 + + - name: Set up R + uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - name: Install R dependencies + uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::pkgdown, local::. + needs: website + + - name: Build pkgdown site + run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) + shell: Rscript {0} + + - name: Deploy site to gh-pages branch + uses: JamesIves/github-pages-deploy-action@v4 + with: + clean: false + branch: gh-pages + folder: docs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..234f028 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.Rproj.user +.Rhistory +.RData +.Ruserdata +docs diff --git a/DESCRIPTION b/DESCRIPTION new file mode 100644 index 0000000..4cdcadd --- /dev/null +++ b/DESCRIPTION @@ -0,0 +1,47 @@ +Package: EFSATools +Type: Package +Title: EFSA ensemble of data collections tools +Version: 1.0.0 +Maintainer: Luca Belmonte +Authors@R: + c(person(given = "Lorenzo", + family = "Copelli", + role = "aut", + comment = c(ORCID = "0009-0002-4305-065X")), + person(given = "Luca", + family = "Belmonte", + role = c("aut", "cre"), + email = "luca.belmonte@efsa.europa.eu", + comment = c(ORCID = "0000-0002-7977-9170"))) +Description: The EFSATools package brings together all the functions developed + for EFSA's ad hoc data collections, providing tools for dataset operations as + well as utilities designed to preserve data history. +License: file LICENSE +URL: https://openefsa.github.io/EFSATools +BugReports: https://github.com/openefsa/EFSATools/issues +Depends: + R (>= 4.0.0) +Imports: + checkmate (>= 2.3.1), + dplyr (>= 1.1.4), + glue (>= 1.7.0), + rlang (>= 1.1.4), + stringr (>= 1.5.1), + tidyr (>= 1.3.1) +Suggests: + devtools (>= 2.4.5), + tibble (>= 3.3.0), + covr (>= 3.6.4), + knitr (>= 1.0), + rmarkdown (>= 2.0), + testthat (>= 3.0.0), + usethis (>= 2.2.3), + roxygen2 (>= 7.2.1) +Encoding: UTF-8 +LazyData: true +RoxygenNote: 7.3.3 +Config/testthat/edition: 3 +VignetteBuilder: knitr +Config/Needs/website: pkgdown +Repository: CRAN +Roxygen: list(markdown = TRUE) diff --git a/EFSATools.Rproj b/EFSATools.Rproj new file mode 100644 index 0000000..584b854 --- /dev/null +++ b/EFSATools.Rproj @@ -0,0 +1,19 @@ +Version: 1.0 + +RestoreWorkspace: Default +SaveWorkspace: Default +AlwaysSaveHistory: Default + +EnableCodeIndexing: Yes +UseSpacesForTab: Yes +NumSpacesForTab: 2 +Encoding: UTF-8 + +RnwWeave: Sweave +LaTeX: pdfLaTeX + +BuildType: Package +PackageUseDevtools: Yes +PackageInstallArgs: --no-multiarch --with-keep.source +PackageCheckArgs: --as-cran +PackageRoxygenize: rd,collate,namespace diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c29ce2f --- /dev/null +++ b/LICENSE @@ -0,0 +1,287 @@ + EUROPEAN UNION PUBLIC LICENCE v. 1.2 + EUPL © the European Union 2007, 2016 + +This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined +below) which is provided under the terms of this Licence. Any use of the Work, +other than as authorised under this Licence is prohibited (to the extent such +use is covered by a right of the copyright holder of the Work). + +The Work is provided under the terms of this Licence when the Licensor (as +defined below) has placed the following notice immediately following the +copyright notice for the Work: + + Licensed under the EUPL + +or has expressed by any other means his willingness to license under the EUPL. + +1. Definitions + +In this Licence, the following terms have the following meaning: + +- ‘The Licence’: this Licence. + +- ‘The Original Work’: the work or software distributed or communicated by the + Licensor under this Licence, available as Source Code and also as Executable + Code as the case may be. + +- ‘Derivative Works’: the works or software that could be created by the + Licensee, based upon the Original Work or modifications thereof. This Licence + does not define the extent of modification or dependence on the Original Work + required in order to classify a work as a Derivative Work; this extent is + determined by copyright law applicable in the country mentioned in Article 15. + +- ‘The Work’: the Original Work or its Derivative Works. + +- ‘The Source Code’: the human-readable form of the Work which is the most + convenient for people to study and modify. + +- ‘The Executable Code’: any code which has generally been compiled and which is + meant to be interpreted by a computer as a program. + +- ‘The Licensor’: the natural or legal person that distributes or communicates + the Work under the Licence. + +- ‘Contributor(s)’: any natural or legal person who modifies the Work under the + Licence, or otherwise contributes to the creation of a Derivative Work. + +- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of + the Work under the terms of the Licence. + +- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, + renting, distributing, communicating, transmitting, or otherwise making + available, online or offline, copies of the Work or providing access to its + essential functionalities at the disposal of any other natural or legal + person. + +2. Scope of the rights granted by the Licence + +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +sublicensable licence to do the following, for the duration of copyright vested +in the Original Work: + +- use the Work in any circumstance and for all usage, +- reproduce the Work, +- modify the Work, and make Derivative Works based upon the Work, +- communicate to the public, including the right to make available or display + the Work or copies thereof to the public and perform publicly, as the case may + be, the Work, +- distribute the Work or copies thereof, +- lend and rent the Work or copies thereof, +- sublicense rights in the Work or copies thereof. + +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. + +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make effective +the licence of the economic rights here above listed. + +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to +any patents held by the Licensor, to the extent necessary to make use of the +rights granted on the Work under this Licence. + +3. Communication of the Source Code + +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, in +a notice following the copyright notice attached to the Work, a repository where +the Source Code is easily and freely accessible for as long as the Licensor +continues to distribute or communicate the Work. + +4. Limitations on copyright + +Nothing in this Licence is intended to deprive the Licensee of the benefits from +any exception or limitation to the exclusive rights of the rights owners in the +Work, of the exhaustion of those rights or of other applicable limitations +thereto. + +5. Obligations of the Licensee + +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: + +Attribution right: The Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices and a +copy of the Licence with every copy of the Work he/she distributes or +communicates. The Licensee must cause any Derivative Work to carry prominent +notices stating that the Work has been modified and the date of modification. + +Copyleft clause: If the Licensee distributes or communicates copies of the +Original Works or Derivative Works, this Distribution or Communication will be +done under the terms of this Licence or of a later version of this Licence +unless the Original Work is expressly distributed only under this version of the +Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee +(becoming Licensor) cannot offer or impose any additional terms or conditions on +the Work or Derivative Work that alter or restrict the terms of the Licence. + +Compatibility clause: If the Licensee Distributes or Communicates Derivative +Works or copies thereof based upon both the Work and another work licensed under +a Compatible Licence, this Distribution or Communication can be done under the +terms of this Compatible Licence. For the sake of this clause, ‘Compatible +Licence’ refers to the licences listed in the appendix attached to this Licence. +Should the Licensee's obligations under the Compatible Licence conflict with +his/her obligations under this Licence, the obligations of the Compatible +Licence shall prevail. + +Provision of Source Code: When distributing or communicating copies of the Work, +the Licensee will provide a machine-readable copy of the Source Code or indicate +a repository where this Source will be easily and freely available for as long +as the Licensee continues to distribute or communicate the Work. + +Legal Protection: This Licence does not grant permission to use the trade names, +trademarks, service marks, or names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + +6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she brings +to the Work are owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. + +7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +Contributors. It is not a finished work and may therefore contain defects or +‘bugs’ inherent to this type of development. + +For the above reason, the Work is provided under the Licence on an ‘as is’ basis +and without warranties of any kind concerning the Work, including without +limitation merchantability, fitness for a particular purpose, absence of defects +or errors, accuracy, non-infringement of intellectual property rights other than +copyright as stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a condition +for the grant of any rights to the Work. + +8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the use +of the Work, including without limitation, damages for loss of goodwill, work +stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such damage. +However, the Licensor will be liable under statutory product liability laws as +far such laws apply to the Work. + +9. Additional agreements + +While distributing the Work, You may choose to conclude an additional agreement, +defining obligations or services consistent with this Licence. However, if +accepting obligations, You may act only on your own behalf and on your sole +responsibility, not on behalf of the original Licensor or any other Contributor, +and only if You agree to indemnify, defend, and hold each Contributor harmless +for any liability incurred by, or claims asserted against such Contributor by +the fact You have accepted any warranty or additional liability. + +10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ +placed under the bottom of a window displaying the text of this Licence or by +affirming consent in any other similar way, in accordance with the rules of +applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this Licence, +such as the use of the Work, the creation by You of a Derivative Work or the +Distribution or Communication by You of the Work or copies thereof. + +11. Information to the public + +In case of any Distribution or Communication of the Work by means of electronic +communication by You (for example, by offering to download the Work from a +remote location) the distribution channel or media (for example, a website) must +at least provide to the public the information requested by the applicable law +regarding the Licensor, the Licence and the way it may be accessible, concluded, +stored and reproduced by the Licensee. + +12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. + +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. + +13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work. + +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed or reformed so as necessary to make it +valid and enforceable. + +The European Commission may publish other linguistic versions or new versions of +this Licence or updated versions of the Appendix, so far this is required and +reasonable, without reducing the scope of the rights granted by the Licence. New +versions of the Licence will be published with a unique version number. + +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. + +14. Jurisdiction + +Without prejudice to specific agreement between parties, + +- any litigation resulting from the interpretation of this License, arising + between the European Union institutions, bodies, offices or agencies, as a + Licensor, and any Licensee, will be subject to the jurisdiction of the Court + of Justice of the European Union, as laid down in article 272 of the Treaty on + the Functioning of the European Union, + +- any litigation arising between other parties and resulting from the + interpretation of this License, will be subject to the exclusive jurisdiction + of the competent court where the Licensor resides or conducts its primary + business. + +15. Applicable Law + +Without prejudice to specific agreement between parties, + +- this Licence shall be governed by the law of the European Union Member State + where the Licensor has his seat, resides or has his registered office, + +- this licence shall be governed by Belgian law if the Licensor has no seat, + residence or registered office inside a European Union Member State. + +Appendix + +‘Compatible Licences’ according to Article 5 EUPL are: + +- GNU General Public License (GPL) v. 2, v. 3 +- GNU Affero General Public License (AGPL) v. 3 +- Open Software License (OSL) v. 2.1, v. 3.0 +- Eclipse Public License (EPL) v. 1.0 +- CeCILL v. 2.0, v. 2.1 +- Mozilla Public Licence (MPL) v. 2 +- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for + works other than software +- European Union Public Licence (EUPL) v. 1.1, v. 1.2 +- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong + Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the above +licences without producing a new version of the EUPL, as long as they provide +the rights granted in Article 2 of this Licence and protect the covered Source +Code from exclusive appropriation. + +All other changes or additions to this Appendix require the production of a new +EUPL version. \ No newline at end of file diff --git a/NAMESPACE b/NAMESPACE new file mode 100644 index 0000000..23beaea --- /dev/null +++ b/NAMESPACE @@ -0,0 +1,33 @@ +# Generated by roxygen2: do not edit by hand + +export(SCD2) +export(SSCD2) +export(dropEmpty) +export(enrich) +export(removeReplicatedColumns) +importFrom(checkmate,assert_data_frame) +importFrom(checkmate,assert_names) +importFrom(checkmate,assert_string) +importFrom(checkmate,assert_vector) +importFrom(dplyr,across) +importFrom(dplyr,anti_join) +importFrom(dplyr,any_of) +importFrom(dplyr,bind_cols) +importFrom(dplyr,bind_rows) +importFrom(dplyr,filter) +importFrom(dplyr,inner_join) +importFrom(dplyr,left_join) +importFrom(dplyr,mutate) +importFrom(dplyr,mutate_all) +importFrom(dplyr,na_if) +importFrom(dplyr,right_join) +importFrom(dplyr,select) +importFrom(dplyr,starts_with) +importFrom(dplyr,where) +importFrom(glue,glue) +importFrom(rlang,":=") +importFrom(rlang,.data) +importFrom(rlang,sym) +importFrom(stringr,regex) +importFrom(stringr,str_replace_all) +importFrom(tidyr,unite) diff --git a/R/EFSATools-package.R b/R/EFSATools-package.R new file mode 100644 index 0000000..a65cf64 --- /dev/null +++ b/R/EFSATools-package.R @@ -0,0 +1,6 @@ +#' @keywords internal +"_PACKAGE" + +## usethis namespace: start +## usethis namespace: end +NULL diff --git a/R/SCD2.R b/R/SCD2.R new file mode 100644 index 0000000..0f53f74 --- /dev/null +++ b/R/SCD2.R @@ -0,0 +1,66 @@ +#' Implement a Slowly Changing Dimension Type 2. +#' +#' This function implements a Slowly Changing Dimension Type 2 to merge new and +#' current data while maintaining historical records. The function deactivates +#' the old records and activates new ones, ensuring a history-preserving update +#' strategy. Only the changing records are marked as not active and replaced by +#' new active ones. +#' +#' @param newData `data.frame`. The data frame containing new records. +#' +#' @param currentData `data.frame`. The data frame containing existing records. +#' +#' @param key `character` (vector). The columns to be used as key. +#' +#' @return A combined data frame with old data marked as not active and new data +#' marked as active. +#' +#' @importFrom checkmate assert_data_frame assert_vector +#' @importFrom dplyr bind_rows inner_join right_join anti_join +#' +#' @details +#' The function: +#' 1. Separates active and inactive records from the current data. +#' 2. Gets the old records that are still present in the new data (i.e., +#' the ones that can remain active). +#' 3. Gets the records present in new data but not present in still active +#' current data (i.e., the records to activate) and activates them. +#' 4. Gets the current active records that are not present in the new data +#' (i.e., the records to deactivate) and deactivates them. +#' +#' @examplesIf FALSE +#' mergedData <- SCD2(newData = iris, currentData = iris) +#' +#' @export +#' +SCD2 <- function(newData, currentData, key = names(newData)) { + + assert_data_frame(newData) + assert_data_frame(currentData) + assert_vector(key) + + currentInactiveData_ <- currentData |> + filter(IS_ACTIVE = FALSE) + + currentActiveData_ <- currentData |> + filter(IS_ACTIVE = TRUE) + + stillActiveData_ <- inner_join( + unique(currentActiveData_), + unique(newData), + by = key) + + newlyActiveData_ <- anti_join(newData, currentActiveData_, by = key) + newlyActiveData_ <- .activate(dataframe = newlyActiveData_) + + deactivatedData_ <- anti_join(currentActiveData_, newData, by = key) + deactivatedData_ <- .deactivate(dataframe = deactivatedData_) + + mergedData_ <- bind_rows( + stillActiveData_, + newlyActiveData_, + deactivatedData_, + currentInactiveData_) + + return(mergedData_) +} diff --git a/R/SSCD2.R b/R/SSCD2.R new file mode 100644 index 0000000..6c6887f --- /dev/null +++ b/R/SSCD2.R @@ -0,0 +1,36 @@ +#' Implement a "Simple" Slowly Changing Dimension Type 2. +#' +#' This function implements a Simplified version of Slowly Changing Dimension +#' Type 2 to merge new and current data while maintaining historical records. +#' The function deactivates all the old records and activates new ones, ensuring +#' a history-preserving update strategy. The difference between a standard SCD2 +#' is that this simplified version applies no checks on the data, deactivating +#' all the old records and activating the new ones, even if some of the old +#' records are still active. +#' +#' @param newData `data.frame`. The data frame containing new records. +#' +#' @param currentData `data.frame`. The data frame containing existing records. +#' +#' @return A combined data frame with all old data marked as not active and new +#' data marked as active. +#' +#' @importFrom checkmate assert_data_frame +#' @importFrom dplyr bind_rows +#' +#' @examplesIf FALSE +#' mergedData <- SSCD2(newData = iris, currentData = iris) +#' +#' @export +#' +SSCD2 <- function(newData, currentData) { + + assert_data_frame(newData) + assert_data_frame(currentData) + + mergedData_ <- bind_rows( + .deactivate(currentData), + .activate(newData)) + + return(mergedData_) +} diff --git a/R/activate.R b/R/activate.R new file mode 100644 index 0000000..6a4e84e --- /dev/null +++ b/R/activate.R @@ -0,0 +1,31 @@ +#' Activate the records of the specified data frame. +#' +#' This helper function is used in the context of a Slowly Changing Dimension +#' (SCD) to mark new records of a data frame as active with a start date. +#' +#' @param dataframe `data.frame`. The data frame to activate. +#' +#' @return The specified data frame with ACTIVE set to TRUE, START_DATE set to +#' current time, and END_DATE set to NA. +#' +#' @importFrom checkmate assert_data_frame +#' @importFrom dplyr mutate +#' +#' @examplesIf FALSE +#' activatedDataframe <- .activate(dataframe = dataframe) +#' +#' @keywords internal +#' @noRd +#' +.activate <- function(dataframe) { + + assert_data_frame(dataframe) + + activatedDataframe_ <- dataframe |> mutate( + IS_ACTIVE = TRUE, + START_DATE = Sys.time(), + END_DATE = as.Date(NA) + ) + + return(activatedDataframe_) +} diff --git a/R/deactivate.R b/R/deactivate.R new file mode 100644 index 0000000..fc4051c --- /dev/null +++ b/R/deactivate.R @@ -0,0 +1,30 @@ +#' Deactivate the records of the specified data frame. +#' +#' This helper function is used in the context of a Slowly Changing Dimension +#' (SCD) to mark active records of a data frame as not active with an end date. +#' +#' @param dataframe `data.frame`. The data frame to deactivate. +#' +#' @return The specified data frame with IS_ACTIVE set to FALSE and END_DATE set +#' to current time. +#' +#' @importFrom checkmate assert_data_frame +#' @importFrom dplyr mutate +#' +#' @examplesIf FALSE +#' deactivatedDataframe <- .deactivate(dataframe = dataframe) +#' +#' @keywords internal +#' @noRd +#' +.deactivate <- function(dataframe) { + + assert_data_frame(dataframe) + + deactivatedDataframe_ <- dataframe |> mutate( + IS_ACTIVE = FALSE, + END_DATE = Sys.time() + ) + + return(deactivatedDataframe_) +} diff --git a/R/dropEmpty.R b/R/dropEmpty.R new file mode 100644 index 0000000..29ffc4d --- /dev/null +++ b/R/dropEmpty.R @@ -0,0 +1,38 @@ +#' Drop empty lines and columns from the specified data frame. +#' +#' This function drops all the empty lines and columns from the specified data +#' frame, i.e. all the rows and columns that contain only NAs. +#' +#' @param dataframe `data.frame`. The data frame from which to remove the empty +#' lines and columns. +#' +#' @return The provided data frame without empty lines and columns and all the +#' types transformed to string. +#' +#' @importFrom checkmate assert_data_frame +#' @importFrom dplyr filter select where mutate_all +#' +#' @examplesIf FALSE +#' # The first row is going to be dropped. +#' iris[1, ] <- NA +#' iris <- dropEmpty(iris) +#' +#' # The Species column is going to be dropped. +#' iris$Species <- NA +#' iris <- dropEmpty(iris) +#' +#' @export +#' +dropEmpty <- function(dataframe) { + + assert_data_frame(dataframe) + + rowSum_ <- rowSums(is.na(dataframe)) + + dataframe <- dataframe |> + filter(rowSum_ != ncol(dataframe)) |> + select(where(~ !all(is.na(.)))) |> + mutate_all(as.character) + + return(dataframe) +} diff --git a/R/enrich.R b/R/enrich.R new file mode 100644 index 0000000..b1cde48 --- /dev/null +++ b/R/enrich.R @@ -0,0 +1,56 @@ +#' Enrich a data frame with an EFSA catalogue. +#' +#' This function takes a data frame and joins it with an EFSA catalog. The EFSA +#' catalog must be itself a data frame. +#' +#' @param dataframe `data.frame`. The data frame to be enriched. +#' +#' @param catalogue `data.frame`. The data frame that contains the EFSA +#' catalogue to be used for the enrichment. It must contain at least two +#' columns, namely: NAME and CODE. +#' +#' @param joinBy `character` (string). The variable to be used as the join key. +#' +#' @param enrichedColumnName `character` (string). The name of the column added +#' to the original data. +#' +#' @return The specified data frame enriched with the catalogue data. +#' +#' @importFrom checkmate assert_data_frame assert_names assert_string +#' @importFrom dplyr select mutate left_join +#' @importFrom rlang .data := +#' +#' @examplesIf FALSE +#' dataframe_ <- iris |> dplyr::rename(CODE = Species) +#' +#' catalogue_ <- iris |> +#' dplyr::rename(CODE = Species) |> +#' dplyr::mutate(NAME = "data") |> +#' dplyr::select(CODE, NAME) |> +#' unique() +#' +#' enriched_ <- enrich( +#' dataframe = dataframe_, +#' catalogue = catalogue_, +#' joinBy = "CODE", +#' enrichedColumnName = "enrichedColumn") +#' +#' @export +#' +enrich <- function(dataframe, catalogue, joinBy, enrichedColumnName) { + + assert_data_frame(dataframe) + assert_data_frame(catalogue) + assert_names(names(catalogue), must.include = c("NAME", "CODE")) + assert_string(joinBy) + assert_string(enrichedColumnName) + + catalogue <- catalogue |> + select("NAME", "CODE") |> + mutate(!!joinBy := .data$CODE) + + enriched_ <- left_join(dataframe, catalogue, by = joinBy) |> + select(!!enrichedColumnName := "NAME", names(dataframe)) + + return(enriched_) +} diff --git a/R/removeReplicatedColumns.R b/R/removeReplicatedColumns.R new file mode 100644 index 0000000..4aba464 --- /dev/null +++ b/R/removeReplicatedColumns.R @@ -0,0 +1,78 @@ +#' Drop and merge replicated columns from the specified data frame. +#' +#' This function drops and merges all the replicated columns from the specified +#' data frame. +#' +#' @param dataframe `data.frame`. The data frame from which to drop the +#' replicated columns. +#' +#' @param prefix `character` (string). The prefix with which the name of the +#' replicated columns starts. +#' +#' @returns The specified data frame with an additional deduplicated column and +#' all the types transformed to string. +#' +#' @importFrom dplyr mutate across starts_with select bind_cols na_if any_of +#' @importFrom tidyr unite +#' @importFrom stringr str_replace_all regex +#' @importFrom glue glue +#' @importFrom checkmate assert_data_frame assert_string +#' @importFrom rlang := sym +#' +#' @details +#' All the occurrences of "N/A", "NA", and empty strings (case insensitive) +#' inside the provided data frame are replaced with NAs of type character. +#' Then, all and only the columns starting with the specified prefix are +#' selected and united into a single column with name ending per +#' "_deduplicated". All empty entries in the new deduplicated column are +#' replaced with NAs. Finally, the new column is bound with the other columns of +#' the initial dataframe. +#' +#' @examplesIf FALSE +#' iris$Species_1 <- iris$Species +#' iris$Species_2 <- iris$Species +#' iris$Species <- NULL +#' +#' deduplicatedDataframe_ <- removeReplicatedColumns( +#' dataframe = iris, +#' prefix = "Species_") +#' +#' @export +#' +removeReplicatedColumns <- function(dataframe, prefix) { + + assert_data_frame(dataframe) + assert_string(prefix) + + dataframe <- dataframe |> + + mutate(across(starts_with(prefix), as.character)) |> + + mutate(across(starts_with(prefix), + \(x) str_replace_all( + x, + regex("^N/a$|^Na$|^$", ignore_case = TRUE), + NA_character_))) + + dataframe_prefix_columns_ <- dataframe |> + select(starts_with(prefix)) + + dataframe_prefix_columns_ <- dataframe_prefix_columns_ |> + + unite(!!glue("{prefix}_deduplicated"), + 1:ncol(dataframe_prefix_columns_), + sep = "", + na.rm = TRUE) |> + + mutate(!!glue("{prefix}_deduplicated") + := na_if(!!sym(glue("{prefix}_deduplicated")), "")) |> + + bind_cols( + dataframe |> + select(-starts_with(prefix)) |> + select(any_of(names(dataframe)))) |> + + mutate_all(as.character) + + return(dataframe_prefix_columns_) +} diff --git a/README.md b/README.md index cec48bd..efb5377 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,45 @@ -# EFSATools -EFSA ensemble of data collections tools. +# EFSATools + +[![Lifecycle: stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html#stable) [![codecov](https://codecov.io/gh/openefsa/EFSATools/branch/main/graph/badge.svg?token=X2HZIV0B1D)](https://codecov.io/gh/openefsa/EFSATools) + +## Overview + +The **EFSATools** package brings together all the functions developed for EFSA's ad hoc data collections, providing tools for dataset operations as well as utilities designed to preserve data history. + +The package is intended for researchers, analysts, and practitioners who require convenient programmatic access to data collection utilities. + +## Installation + +### From CRAN + +```r +install.packages("EFSATools") +``` + +### Development version (from GitHub) + +To install the latest development version: + +```r +# install.packages("devtools") +devtools::install_github("openefsa/EFSATools") +``` + +## Usage + +Once installed, load the package as usual: + +```r +library(EFSATools) +``` + +Basic usage examples and full documentation are available in the package [vignette](vignettes/EFSATools.Rmd): + +```r +vignette("EFSATools") +``` + +## Links + +- **Source code:** [GitHub – openefsa/EFSATools](https://github.com/openefsa/EFSATools) +- **Bug reports:** [Issues on GitHub](https://github.com/openefsa/EFSATools/issues) \ No newline at end of file diff --git a/_pkgdown.yml b/_pkgdown.yml new file mode 100644 index 0000000..107bd78 --- /dev/null +++ b/_pkgdown.yml @@ -0,0 +1,8 @@ +template: + bootstrap: 5 + +development: + mode: auto + +url: https://openefsa.github.io/EFSATools +destination: docs diff --git a/man/EFSATools-package.Rd b/man/EFSATools-package.Rd new file mode 100644 index 0000000..e45f452 --- /dev/null +++ b/man/EFSATools-package.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/EFSATools-package.R +\docType{package} +\name{EFSATools-package} +\alias{EFSATools} +\alias{EFSATools-package} +\title{EFSATools: EFSA ensemble of data collections tools} +\description{ +\if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} + +The EFSATools package brings together all the functions developed for EFSA's ad hoc data collections, providing tools for dataset operations as well as utilities designed to preserve data history. +} +\seealso{ +Useful links: +\itemize{ + \item \url{https://openefsa.github.io/EFSATools} + \item Report bugs at \url{https://github.com/openefsa/EFSATools/issues} +} + +} +\author{ +\strong{Maintainer}: Luca Belmonte \email{luca.belmonte@efsa.europa.eu} (\href{https://orcid.org/0000-0002-7977-9170}{ORCID}) + +Authors: +\itemize{ + \item Lorenzo Copelli (\href{https://orcid.org/0009-0002-4305-065X}{ORCID}) +} + +} +\keyword{internal} diff --git a/man/SCD2.Rd b/man/SCD2.Rd new file mode 100644 index 0000000..cf67afe --- /dev/null +++ b/man/SCD2.Rd @@ -0,0 +1,43 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/SCD2.R +\name{SCD2} +\alias{SCD2} +\title{Implement a Slowly Changing Dimension Type 2.} +\usage{ +SCD2(newData, currentData, key = names(newData)) +} +\arguments{ +\item{newData}{\code{data.frame}. The data frame containing new records.} + +\item{currentData}{\code{data.frame}. The data frame containing existing records.} + +\item{key}{\code{character} (vector). The columns to be used as key.} +} +\value{ +A combined data frame with old data marked as not active and new data +marked as active. +} +\description{ +This function implements a Slowly Changing Dimension Type 2 to merge new and +current data while maintaining historical records. The function deactivates +the old records and activates new ones, ensuring a history-preserving update +strategy. Only the changing records are marked as not active and replaced by +new active ones. +} +\details{ +The function: +\enumerate{ +\item Separates active and inactive records from the current data. +\item Gets the old records that are still present in the new data (i.e., +the ones that can remain active). +\item Gets the records present in new data but not present in still active +current data (i.e., the records to activate) and activates them. +\item Gets the current active records that are not present in the new data +(i.e., the records to deactivate) and deactivates them. +} +} +\examples{ +\dontshow{if (FALSE) withAutoprint(\{ # examplesIf} +mergedData <- SCD2(newData = iris, currentData = iris) +\dontshow{\}) # examplesIf} +} diff --git a/man/SSCD2.Rd b/man/SSCD2.Rd new file mode 100644 index 0000000..dea4a39 --- /dev/null +++ b/man/SSCD2.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/SSCD2.R +\name{SSCD2} +\alias{SSCD2} +\title{Implement a "Simple" Slowly Changing Dimension Type 2.} +\usage{ +SSCD2(newData, currentData) +} +\arguments{ +\item{newData}{\code{data.frame}. The data frame containing new records.} + +\item{currentData}{\code{data.frame}. The data frame containing existing records.} +} +\value{ +A combined data frame with all old data marked as not active and new +data marked as active. +} +\description{ +This function implements a Simplified version of Slowly Changing Dimension +Type 2 to merge new and current data while maintaining historical records. +The function deactivates all the old records and activates new ones, ensuring +a history-preserving update strategy. The difference between a standard SCD2 +is that this simplified version applies no checks on the data, deactivating +all the old records and activating the new ones, even if some of the old +records are still active. +} +\examples{ +\dontshow{if (FALSE) withAutoprint(\{ # examplesIf} +mergedData <- SSCD2(newData = iris, currentData = iris) +\dontshow{\}) # examplesIf} +} diff --git a/man/dropEmpty.Rd b/man/dropEmpty.Rd new file mode 100644 index 0000000..7160bde --- /dev/null +++ b/man/dropEmpty.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dropEmpty.R +\name{dropEmpty} +\alias{dropEmpty} +\title{Drop empty lines and columns from the specified data frame.} +\usage{ +dropEmpty(dataframe) +} +\arguments{ +\item{dataframe}{\code{data.frame}. The data frame from which to remove the empty +lines and columns.} +} +\value{ +The provided data frame without empty lines and columns and all the +types transformed to string. +} +\description{ +This function drops all the empty lines and columns from the specified data +frame, i.e. all the rows and columns that contain only NAs. +} +\examples{ +\dontshow{if (FALSE) withAutoprint(\{ # examplesIf} +# The first row is going to be dropped. +iris[1, ] <- NA +iris <- dropEmpty(iris) + +# The Species column is going to be dropped. +iris$Species <- NA +iris <- dropEmpty(iris) +\dontshow{\}) # examplesIf} +} diff --git a/man/enrich.Rd b/man/enrich.Rd new file mode 100644 index 0000000..ef02c2a --- /dev/null +++ b/man/enrich.Rd @@ -0,0 +1,44 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/enrich.R +\name{enrich} +\alias{enrich} +\title{Enrich a data frame with an EFSA catalogue.} +\usage{ +enrich(dataframe, catalogue, joinBy, enrichedColumnName) +} +\arguments{ +\item{dataframe}{\code{data.frame}. The data frame to be enriched.} + +\item{catalogue}{\code{data.frame}. The data frame that contains the EFSA +catalogue to be used for the enrichment. It must contain at least two +columns, namely: NAME and CODE.} + +\item{joinBy}{\code{character} (string). The variable to be used as the join key.} + +\item{enrichedColumnName}{\code{character} (string). The name of the column added +to the original data.} +} +\value{ +The specified data frame enriched with the catalogue data. +} +\description{ +This function takes a data frame and joins it with an EFSA catalog. The EFSA +catalog must be itself a data frame. +} +\examples{ +\dontshow{if (FALSE) withAutoprint(\{ # examplesIf} +dataframe_ <- iris |> dplyr::rename(CODE = Species) + +catalogue_ <- iris |> + dplyr::rename(CODE = Species) |> + dplyr::mutate(NAME = "data") |> + dplyr::select(CODE, NAME) |> + unique() + +enriched_ <- enrich( + dataframe = dataframe_, + catalogue = catalogue_, + joinBy = "CODE", + enrichedColumnName = "enrichedColumn") +\dontshow{\}) # examplesIf} +} diff --git a/man/figures/logo.png b/man/figures/logo.png new file mode 100644 index 0000000..8bbfcbf Binary files /dev/null and b/man/figures/logo.png differ diff --git a/man/removeReplicatedColumns.Rd b/man/removeReplicatedColumns.Rd new file mode 100644 index 0000000..75b169b --- /dev/null +++ b/man/removeReplicatedColumns.Rd @@ -0,0 +1,43 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/removeReplicatedColumns.R +\name{removeReplicatedColumns} +\alias{removeReplicatedColumns} +\title{Drop and merge replicated columns from the specified data frame.} +\usage{ +removeReplicatedColumns(dataframe, prefix) +} +\arguments{ +\item{dataframe}{\code{data.frame}. The data frame from which to drop the +replicated columns.} + +\item{prefix}{\code{character} (string). The prefix with which the name of the +replicated columns starts.} +} +\value{ +The specified data frame with an additional deduplicated column and +all the types transformed to string. +} +\description{ +This function drops and merges all the replicated columns from the specified +data frame. +} +\details{ +All the occurrences of "N/A", "NA", and empty strings (case insensitive) +inside the provided data frame are replaced with NAs of type character. +Then, all and only the columns starting with the specified prefix are +selected and united into a single column with name ending per +"_deduplicated". All empty entries in the new deduplicated column are +replaced with NAs. Finally, the new column is bound with the other columns of +the initial dataframe. +} +\examples{ +\dontshow{if (FALSE) withAutoprint(\{ # examplesIf} +iris$Species_1 <- iris$Species +iris$Species_2 <- iris$Species +iris$Species <- NULL + +deduplicatedDataframe_ <- removeReplicatedColumns( + dataframe = iris, + prefix = "Species_") +\dontshow{\}) # examplesIf} +} diff --git a/pkgdown/favicon/apple-touch-icon.png b/pkgdown/favicon/apple-touch-icon.png new file mode 100644 index 0000000..0cf3a40 Binary files /dev/null and b/pkgdown/favicon/apple-touch-icon.png differ diff --git a/pkgdown/favicon/favicon-96x96.png b/pkgdown/favicon/favicon-96x96.png new file mode 100644 index 0000000..6cb8f6e Binary files /dev/null and b/pkgdown/favicon/favicon-96x96.png differ diff --git a/pkgdown/favicon/favicon.ico b/pkgdown/favicon/favicon.ico new file mode 100644 index 0000000..4233e74 Binary files /dev/null and b/pkgdown/favicon/favicon.ico differ diff --git a/pkgdown/favicon/favicon.svg b/pkgdown/favicon/favicon.svg new file mode 100644 index 0000000..40776e4 --- /dev/null +++ b/pkgdown/favicon/favicon.svg @@ -0,0 +1 @@ +RealFaviconGeneratorhttps://realfavicongenerator.net \ No newline at end of file diff --git a/pkgdown/favicon/site.webmanifest b/pkgdown/favicon/site.webmanifest new file mode 100644 index 0000000..4ebda26 --- /dev/null +++ b/pkgdown/favicon/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/pkgdown/favicon/web-app-manifest-192x192.png b/pkgdown/favicon/web-app-manifest-192x192.png new file mode 100644 index 0000000..12009c4 Binary files /dev/null and b/pkgdown/favicon/web-app-manifest-192x192.png differ diff --git a/pkgdown/favicon/web-app-manifest-512x512.png b/pkgdown/favicon/web-app-manifest-512x512.png new file mode 100644 index 0000000..fc1b860 Binary files /dev/null and b/pkgdown/favicon/web-app-manifest-512x512.png differ diff --git a/renv.lock b/renv.lock new file mode 100644 index 0000000..ababfa0 --- /dev/null +++ b/renv.lock @@ -0,0 +1,1857 @@ +{ + "R": { + "Version": "4.4.0", + "Repositories": [ + { + "Name": "CRAN", + "URL": "https://packagemanager.posit.co/cran/latest" + } + ] + }, + "Packages": { + "DBI": { + "Package": "DBI", + "Version": "1.2.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods" + ], + "Hash": "065ae649b05f1ff66bb0c793107508f5" + }, + "DT": { + "Package": "DT", + "Version": "0.34.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "crosstalk", + "htmltools", + "htmlwidgets", + "jquerylib", + "jsonlite", + "magrittr", + "promises" + ], + "Hash": "a9d00f59f9c7e613dd4c3e0d27135a03" + }, + "KernSmooth": { + "Package": "KernSmooth", + "Version": "2.23-22", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "stats" + ], + "Hash": "2fecebc3047322fa5930f74fae5de70f" + }, + "MASS": { + "Package": "MASS", + "Version": "7.3-60.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "graphics", + "methods", + "stats", + "utils" + ], + "Hash": "2f342c46163b0b54d7b64d1f798e2c78" + }, + "Matrix": { + "Package": "Matrix", + "Version": "1.7-0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "graphics", + "grid", + "lattice", + "methods", + "stats", + "utils" + ], + "Hash": "1920b2f11133b12350024297d8a4ff4a" + }, + "R6": { + "Package": "R6", + "Version": "2.6.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "d4335fe7207f1c01ab8c41762f5840d4" + }, + "Rcpp": { + "Package": "Rcpp", + "Version": "1.1.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "methods", + "utils" + ], + "Hash": "0d3d8867aa41b0d9ad113b50c169ecb2" + }, + "askpass": { + "Package": "askpass", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "sys" + ], + "Hash": "c39f4155b3ceb1a9a2799d700fbd4b6a" + }, + "backports": { + "Package": "backports", + "Version": "1.5.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "e1e1b9d75c37401117b636b7ae50827a" + }, + "base64enc": { + "Package": "base64enc", + "Version": "0.1-3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "543776ae6848fde2f48ff3816d0628bc" + }, + "bit": { + "Package": "bit", + "Version": "4.6.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "ebe86ffd194564abdc895d87742d5a29" + }, + "bit64": { + "Package": "bit64", + "Version": "4.6.0-1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "bit", + "graphics", + "methods", + "stats", + "utils" + ], + "Hash": "4f572fbc586294afff277db583b9060f" + }, + "blob": { + "Package": "blob", + "Version": "1.2.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "methods", + "rlang", + "vctrs" + ], + "Hash": "40415719b5a479b87949f3aa0aee737c" + }, + "boot": { + "Package": "boot", + "Version": "1.3-30", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "graphics", + "stats" + ], + "Hash": "96abeed416a286d4a0f52e550b612343" + }, + "brew": { + "Package": "brew", + "Version": "1.0-10", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "8f4a384e19dccd8c65356dc096847b76" + }, + "brio": { + "Package": "brio", + "Version": "1.1.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "c1ee497a6d999947c2c224ae46799b1a" + }, + "bslib": { + "Package": "bslib", + "Version": "0.9.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "base64enc", + "cachem", + "fastmap", + "grDevices", + "htmltools", + "jquerylib", + "jsonlite", + "lifecycle", + "memoise", + "mime", + "rlang", + "sass" + ], + "Hash": "70a6489cc254171fb9b4a7f130f44dca" + }, + "cachem": { + "Package": "cachem", + "Version": "1.1.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "fastmap", + "rlang" + ], + "Hash": "cd9a672193789068eb5a2aad65a0dedf" + }, + "callr": { + "Package": "callr", + "Version": "3.7.6", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "processx", + "utils" + ], + "Hash": "d7e13f49c19103ece9e58ad2d83a7354" + }, + "checkmate": { + "Package": "checkmate", + "Version": "2.3.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "backports", + "utils" + ], + "Hash": "92b3b84e5789dcde37cb0c409c54553a" + }, + "class": { + "Package": "class", + "Version": "7.3-22", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "MASS", + "R", + "stats", + "utils" + ], + "Hash": "f91f6b29f38b8c280f2b9477787d4bb2" + }, + "cli": { + "Package": "cli", + "Version": "3.6.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "utils" + ], + "Hash": "16850760556401a2eeb27d39bd11c9cb" + }, + "clipr": { + "Package": "clipr", + "Version": "0.8.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "utils" + ], + "Hash": "3f038e5ac7f41d4ac41ce658c85e3042" + }, + "cluster": { + "Package": "cluster", + "Version": "2.1.6", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "graphics", + "stats", + "utils" + ], + "Hash": "0aaa05204035dc43ea0004b9c76611dd" + }, + "codetools": { + "Package": "codetools", + "Version": "0.2-20", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "61e097f35917d342622f21cdc79c256e" + }, + "commonmark": { + "Package": "commonmark", + "Version": "2.0.0", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "8cba62334c1088d21689d353a7e87663" + }, + "covr": { + "Package": "covr", + "Version": "3.6.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "digest", + "httr", + "jsonlite", + "methods", + "rex", + "stats", + "utils", + "withr", + "yaml" + ], + "Hash": "aa76c4e2fe7c9973b3f4fbd01a0710ff" + }, + "cpp11": { + "Package": "cpp11", + "Version": "0.5.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "2720e3fd3dad08f34b19b56b3d6f073d" + }, + "crayon": { + "Package": "crayon", + "Version": "1.5.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "grDevices", + "methods", + "utils" + ], + "Hash": "859d96e65ef198fd43e82b9628d593ef" + }, + "credentials": { + "Package": "credentials", + "Version": "2.0.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "askpass", + "curl", + "jsonlite", + "openssl", + "sys" + ], + "Hash": "bda033f0a309540bb66d5e85872c6973" + }, + "crosstalk": { + "Package": "crosstalk", + "Version": "1.2.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R6", + "htmltools", + "jsonlite", + "lazyeval" + ], + "Hash": "8b008bc619e0bbebb5646b3d37efdad1" + }, + "curl": { + "Package": "curl", + "Version": "7.0.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "aa27e963d3deccf4bade44d06b976977" + }, + "dbplyr": { + "Package": "dbplyr", + "Version": "2.5.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "DBI", + "R", + "R6", + "blob", + "cli", + "dplyr", + "glue", + "lifecycle", + "magrittr", + "methods", + "pillar", + "purrr", + "rlang", + "tibble", + "tidyr", + "tidyselect", + "utils", + "vctrs", + "withr" + ], + "Hash": "e38d7151c767b00b919091be4686e274" + }, + "desc": { + "Package": "desc", + "Version": "1.4.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "cli", + "utils" + ], + "Hash": "99b79fcbd6c4d1ce087f5c5c758b384f" + }, + "devtools": { + "Package": "devtools", + "Version": "2.4.6", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "desc", + "ellipsis", + "fs", + "lifecycle", + "memoise", + "miniUI", + "pkgbuild", + "pkgdown", + "pkgload", + "profvis", + "rcmdcheck", + "remotes", + "rlang", + "roxygen2", + "rversions", + "sessioninfo", + "stats", + "testthat", + "tools", + "urlchecker", + "usethis", + "utils", + "withr" + ], + "Hash": "4eea2ae2b112624ebc0da375a47d3bb5" + }, + "diffobj": { + "Package": "diffobj", + "Version": "0.3.6", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "crayon", + "methods", + "stats", + "tools", + "utils" + ], + "Hash": "e036ce354ab60e705ac5f40bac87e8cb" + }, + "digest": { + "Package": "digest", + "Version": "0.6.39", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "utils" + ], + "Hash": "d18028e978a88b2b16ef8d400cb49adf" + }, + "downlit": { + "Package": "downlit", + "Version": "0.4.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "brio", + "desc", + "digest", + "evaluate", + "fansi", + "memoise", + "rlang", + "vctrs", + "withr", + "yaml" + ], + "Hash": "13795617f8a6dfa59b74baf7083aeeb2" + }, + "dplyr": { + "Package": "dplyr", + "Version": "1.1.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "cli", + "generics", + "glue", + "lifecycle", + "magrittr", + "methods", + "pillar", + "rlang", + "tibble", + "tidyselect", + "utils", + "vctrs" + ], + "Hash": "fedd9d00c2944ff00a0e2696ccf048ec" + }, + "ellipsis": { + "Package": "ellipsis", + "Version": "0.3.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "rlang" + ], + "Hash": "bb0eec2fe32e88d9e2836c2f73ea2077" + }, + "evaluate": { + "Package": "evaluate", + "Version": "1.0.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "94cf2c54237f6841cee68e3ba4ab5a14" + }, + "fansi": { + "Package": "fansi", + "Version": "1.0.7", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "grDevices", + "utils" + ], + "Hash": "17986671536180cabd08d6309755af85" + }, + "fastmap": { + "Package": "fastmap", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "aa5e1cd11c2d15497494c5292d7ffcc8" + }, + "fontawesome": { + "Package": "fontawesome", + "Version": "0.5.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "htmltools", + "rlang" + ], + "Hash": "bd1297f9b5b1fc1372d19e2c4cd82215" + }, + "foreign": { + "Package": "foreign", + "Version": "0.8-86", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods", + "stats", + "utils" + ], + "Hash": "550170303dbb19d07b2bcc288068e7dc" + }, + "fs": { + "Package": "fs", + "Version": "1.6.6", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods" + ], + "Hash": "7eb1e342eee7e0a7449c49cdaa526d39" + }, + "generics": { + "Package": "generics", + "Version": "0.1.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods" + ], + "Hash": "4b29bf698d0c7bdb9f1e4976e7ade41d" + }, + "gert": { + "Package": "gert", + "Version": "2.2.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "askpass", + "credentials", + "openssl", + "rstudioapi", + "sys", + "zip" + ], + "Hash": "0fa6acb04daaafc762fe9524fa6f3457" + }, + "gh": { + "Package": "gh", + "Version": "1.5.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "gitcreds", + "glue", + "httr2", + "ini", + "jsonlite", + "lifecycle", + "rlang" + ], + "Hash": "d92acb7afad09df6839e4e456e538d03" + }, + "gitcreds": { + "Package": "gitcreds", + "Version": "0.1.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "ab08ac61f3e1be454ae21911eb8bc2fe" + }, + "glue": { + "Package": "glue", + "Version": "1.8.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods" + ], + "Hash": "5899f1eaa825580172bb56c08266f37c" + }, + "highr": { + "Package": "highr", + "Version": "0.11", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "xfun" + ], + "Hash": "d65ba49117ca223614f71b60d85b8ab7" + }, + "hms": { + "Package": "hms", + "Version": "1.1.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "cli", + "lifecycle", + "methods", + "pkgconfig", + "rlang", + "vctrs" + ], + "Hash": "2799ac720626cec589478b191e48b88b" + }, + "htmltools": { + "Package": "htmltools", + "Version": "0.5.9", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "base64enc", + "digest", + "fastmap", + "grDevices", + "rlang", + "utils" + ], + "Hash": "102298e238c14eb830cc4b5edd23c3e8" + }, + "htmlwidgets": { + "Package": "htmlwidgets", + "Version": "1.6.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "grDevices", + "htmltools", + "jsonlite", + "knitr", + "rmarkdown", + "yaml" + ], + "Hash": "04291cc45198225444a397606810ac37" + }, + "httpuv": { + "Package": "httpuv", + "Version": "1.6.16", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "Rcpp", + "later", + "promises", + "utils" + ], + "Hash": "6c3c8728e40326de6529a5c46e377e5c" + }, + "httr": { + "Package": "httr", + "Version": "1.4.7", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "curl", + "jsonlite", + "mime", + "openssl" + ], + "Hash": "ac107251d9d9fd72f0ca8049988f1d7f" + }, + "httr2": { + "Package": "httr2", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "cli", + "curl", + "glue", + "lifecycle", + "magrittr", + "openssl", + "rappdirs", + "rlang", + "vctrs", + "withr" + ], + "Hash": "6e29f1ed132b927f7d52e9fd8869f0ea" + }, + "ini": { + "Package": "ini", + "Version": "0.3.1", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "6154ec2223172bce8162d4153cda21f7" + }, + "janitor": { + "Package": "janitor", + "Version": "2.2.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "dplyr", + "hms", + "lifecycle", + "lubridate", + "magrittr", + "purrr", + "rlang", + "snakecase", + "stringi", + "stringr", + "tidyr", + "tidyselect" + ], + "Hash": "64f308bf1fbf5f856cdf4b4c7c0ce51b" + }, + "jquerylib": { + "Package": "jquerylib", + "Version": "0.1.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "htmltools" + ], + "Hash": "5aab57a3bd297eee1c1d862735972182" + }, + "jsonlite": { + "Package": "jsonlite", + "Version": "2.0.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "methods" + ], + "Hash": "b0776f526d36d8bd4a3344a88fe165c4" + }, + "knitr": { + "Package": "knitr", + "Version": "1.51", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "evaluate", + "highr", + "methods", + "tools", + "xfun", + "yaml" + ], + "Hash": "27682babb50f03b6eb7939ea69ec79ca" + }, + "later": { + "Package": "later", + "Version": "1.4.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "Rcpp", + "rlang" + ], + "Hash": "a729883bff94b606cd4f0445aeed95b5" + }, + "lattice": { + "Package": "lattice", + "Version": "0.22-6", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "graphics", + "grid", + "stats", + "utils" + ], + "Hash": "cc5ac1ba4c238c7ca9fa6a87ca11a7e2" + }, + "lazyeval": { + "Package": "lazyeval", + "Version": "0.2.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "d908914ae53b04d4c0c0fd72ecc35370" + }, + "lifecycle": { + "Package": "lifecycle", + "Version": "1.0.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "glue", + "rlang" + ], + "Hash": "b8552d117e1b808b09a832f589b79035" + }, + "lubridate": { + "Package": "lubridate", + "Version": "1.9.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "generics", + "methods", + "timechange" + ], + "Hash": "be38bc740fc51783a78edb5a157e4104" + }, + "magrittr": { + "Package": "magrittr", + "Version": "2.0.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "8b3b213aa0c54a0dec254288ccb545d1" + }, + "memoise": { + "Package": "memoise", + "Version": "2.0.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "cachem", + "rlang" + ], + "Hash": "e2817ccf4a065c5d9d7f2cfbe7c1d78c" + }, + "mgcv": { + "Package": "mgcv", + "Version": "1.9-1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "Matrix", + "R", + "graphics", + "methods", + "nlme", + "splines", + "stats", + "utils" + ], + "Hash": "110ee9d83b496279960e162ac97764ce" + }, + "mime": { + "Package": "mime", + "Version": "0.13", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "tools" + ], + "Hash": "0ec19f34c72fab674d8f2b4b1c6410e1" + }, + "miniUI": { + "Package": "miniUI", + "Version": "0.1.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "htmltools", + "shiny", + "utils" + ], + "Hash": "96b7fedb0964cc6b86631fb2fee23afa" + }, + "nlme": { + "Package": "nlme", + "Version": "3.1-164", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "graphics", + "lattice", + "stats", + "utils" + ], + "Hash": "a623a2239e642806158bc4dc3f51565d" + }, + "nnet": { + "Package": "nnet", + "Version": "7.3-19", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "stats", + "utils" + ], + "Hash": "2c797b46eea7fb58ede195bc0b1f1138" + }, + "odbc": { + "Package": "odbc", + "Version": "1.6.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "DBI", + "R", + "Rcpp", + "bit64", + "blob", + "cli", + "hms", + "lifecycle", + "methods", + "rlang" + ], + "Hash": "212720b6e1282128e7c156e200c0a5cb" + }, + "openssl": { + "Package": "openssl", + "Version": "2.3.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "askpass" + ], + "Hash": "d5811dade9cd3a34a9038a45f1a59bae" + }, + "otel": { + "Package": "otel", + "Version": "0.2.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "627d6993db1043703c0b084fa432f21f" + }, + "pillar": { + "Package": "pillar", + "Version": "1.11.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "cli", + "glue", + "lifecycle", + "rlang", + "utf8", + "utils", + "vctrs" + ], + "Hash": "1395e64f2689ffd503657778e810cee2" + }, + "pkgbuild": { + "Package": "pkgbuild", + "Version": "1.4.8", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "callr", + "cli", + "desc", + "processx" + ], + "Hash": "fc9fc4162e79a94f760aac8d328ee6c9" + }, + "pkgconfig": { + "Package": "pkgconfig", + "Version": "2.0.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "utils" + ], + "Hash": "01f28d4278f15c76cddbea05899c5d6f" + }, + "pkgdown": { + "Package": "pkgdown", + "Version": "2.2.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "bslib", + "callr", + "cli", + "desc", + "downlit", + "fontawesome", + "fs", + "httr2", + "jsonlite", + "lifecycle", + "openssl", + "purrr", + "ragg", + "rlang", + "rmarkdown", + "tibble", + "whisker", + "withr", + "xml2", + "yaml" + ], + "Hash": "46bf69a508df4a09e5ac1db2ce24fc3a" + }, + "pkgload": { + "Package": "pkgload", + "Version": "1.4.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "desc", + "fs", + "glue", + "lifecycle", + "methods", + "pkgbuild", + "processx", + "rlang", + "rprojroot", + "utils" + ], + "Hash": "6f2060ae5ad112cc61df8876f93993de" + }, + "praise": { + "Package": "praise", + "Version": "1.0.0", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "a555924add98c99d2f411e37e7d25e9f" + }, + "prettyunits": { + "Package": "prettyunits", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "6b01fc98b1e86c4f705ce9dcfd2f57c7" + }, + "processx": { + "Package": "processx", + "Version": "3.8.6", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "ps", + "utils" + ], + "Hash": "720161b280b0a35f4d1490ead2fe81d0" + }, + "profvis": { + "Package": "profvis", + "Version": "0.4.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "htmlwidgets", + "rlang", + "vctrs" + ], + "Hash": "bffa126bf92987e677c12cfb5651fc1d" + }, + "promises": { + "Package": "promises", + "Version": "1.5.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "fastmap", + "later", + "lifecycle", + "magrittr", + "otel", + "rlang" + ], + "Hash": "62cb899ed5fff70d4e918ec1b762bf7c" + }, + "ps": { + "Package": "ps", + "Version": "1.9.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "utils" + ], + "Hash": "093688087b0bacce6ba2f661f36328e2" + }, + "purrr": { + "Package": "purrr", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "lifecycle", + "magrittr", + "rlang", + "vctrs" + ], + "Hash": "5150f0e38b8150315ac853acade2740e" + }, + "ragg": { + "Package": "ragg", + "Version": "1.5.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "systemfonts", + "textshaping" + ], + "Hash": "cdb40b21711a8870f305b24226698f9f" + }, + "rappdirs": { + "Package": "rappdirs", + "Version": "0.3.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "5e3c5dc0b071b21fa128676560dbe94d" + }, + "rcmdcheck": { + "Package": "rcmdcheck", + "Version": "1.4.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R6", + "callr", + "cli", + "curl", + "desc", + "digest", + "pkgbuild", + "prettyunits", + "rprojroot", + "sessioninfo", + "utils", + "withr", + "xopen" + ], + "Hash": "8f25ebe2ec38b1f2aef3b0d2ef76f6c4" + }, + "remotes": { + "Package": "remotes", + "Version": "2.5.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods", + "stats", + "tools", + "utils" + ], + "Hash": "3ee025083e66f18db6cf27b56e23e141" + }, + "renv": { + "Package": "renv", + "Version": "1.0.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "utils" + ], + "Hash": "41b847654f567341725473431dd0d5ab" + }, + "rex": { + "Package": "rex", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "lazyeval" + ], + "Hash": "ae34cd56890607370665bee5bd17812f" + }, + "rlang": { + "Package": "rlang", + "Version": "1.1.7", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "utils" + ], + "Hash": "34c0d101f4613098abc538b82e0d86c5" + }, + "rmarkdown": { + "Package": "rmarkdown", + "Version": "2.30", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "bslib", + "evaluate", + "fontawesome", + "htmltools", + "jquerylib", + "jsonlite", + "knitr", + "methods", + "tinytex", + "tools", + "utils", + "xfun", + "yaml" + ], + "Hash": "efe19db0fde0fff13cea7eec6f695021" + }, + "roxygen2": { + "Package": "roxygen2", + "Version": "7.3.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "brew", + "cli", + "commonmark", + "cpp11", + "desc", + "knitr", + "methods", + "pkgload", + "purrr", + "rlang", + "stringi", + "stringr", + "utils", + "withr", + "xml2" + ], + "Hash": "44dd0ef95afe841ad84d88e22ce1ae1b" + }, + "rpart": { + "Package": "rpart", + "Version": "4.1.23", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "graphics", + "stats" + ], + "Hash": "b3d390424f41d04174cccf84d49676c2" + }, + "rprojroot": { + "Package": "rprojroot", + "Version": "2.1.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "b2453de2d29aa646afe4781defdc7903" + }, + "rstudioapi": { + "Package": "rstudioapi", + "Version": "0.17.1", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "5f90cd73946d706cfe26024294236113" + }, + "rversions": { + "Package": "rversions", + "Version": "3.0.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "curl" + ], + "Hash": "e1ebbf6f3ffcf42e12f84d4ebab350d0" + }, + "sass": { + "Package": "sass", + "Version": "0.4.10", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R6", + "fs", + "htmltools", + "rappdirs", + "rlang" + ], + "Hash": "3fb78d066fb92299b1d13f6a7c9a90a8" + }, + "sessioninfo": { + "Package": "sessioninfo", + "Version": "1.2.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "tools", + "utils" + ], + "Hash": "bf169c6e52cdbded916e448dc1254913" + }, + "shiny": { + "Package": "shiny", + "Version": "1.11.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "bslib", + "cachem", + "cli", + "commonmark", + "fastmap", + "fontawesome", + "glue", + "grDevices", + "htmltools", + "httpuv", + "jsonlite", + "later", + "lifecycle", + "methods", + "mime", + "promises", + "rlang", + "sourcetools", + "tools", + "utils", + "withr", + "xtable" + ], + "Hash": "1c1a2437351984ff592639ae28cda09e" + }, + "snakecase": { + "Package": "snakecase", + "Version": "0.11.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "stringi", + "stringr" + ], + "Hash": "58767e44739b76965332e8a4fe3f91f1" + }, + "sourcetools": { + "Package": "sourcetools", + "Version": "0.1.7-1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "5f5a7629f956619d519205ec475fe647" + }, + "spatial": { + "Package": "spatial", + "Version": "7.3-17", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "graphics", + "stats", + "utils" + ], + "Hash": "1229a01b4ec059e9f2396724f2ec9010" + }, + "stringi": { + "Package": "stringi", + "Version": "1.8.7", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "stats", + "tools", + "utils" + ], + "Hash": "2b56088e23bdd58f89aebf43a0913457" + }, + "stringr": { + "Package": "stringr", + "Version": "1.6.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "magrittr", + "rlang", + "stringi", + "vctrs" + ], + "Hash": "d47392652eedc68bf916657347ff2526" + }, + "survival": { + "Package": "survival", + "Version": "3.5-8", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "Matrix", + "R", + "graphics", + "methods", + "splines", + "stats", + "utils" + ], + "Hash": "184d7799bca4ba8c3be72ea396f4b9a3" + }, + "sys": { + "Package": "sys", + "Version": "3.4.3", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "de342ebfebdbf40477d0758d05426646" + }, + "systemfonts": { + "Package": "systemfonts", + "Version": "1.3.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "base64enc", + "cpp11", + "grid", + "jsonlite", + "lifecycle", + "tools", + "utils" + ], + "Hash": "4fae513a3bb1a155943d1e9d1ae467ec" + }, + "testthat": { + "Package": "testthat", + "Version": "3.3.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "brio", + "callr", + "cli", + "desc", + "evaluate", + "jsonlite", + "lifecycle", + "magrittr", + "methods", + "pkgload", + "praise", + "processx", + "ps", + "rlang", + "utils", + "waldo", + "withr" + ], + "Hash": "8305f01766c61fbe99c5e37ae0e8bedf" + }, + "textshaping": { + "Package": "textshaping", + "Version": "1.0.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cpp11", + "lifecycle", + "stats", + "stringi", + "systemfonts", + "utils" + ], + "Hash": "477d70e23914730aa7f5c5fd351f4721" + }, + "tibble": { + "Package": "tibble", + "Version": "3.3.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "lifecycle", + "magrittr", + "methods", + "pillar", + "pkgconfig", + "rlang", + "utils", + "vctrs" + ], + "Hash": "784b27d0801c3829de602105757b2cd7" + }, + "tidyr": { + "Package": "tidyr", + "Version": "1.3.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "cpp11", + "dplyr", + "glue", + "lifecycle", + "magrittr", + "purrr", + "rlang", + "stringr", + "tibble", + "tidyselect", + "utils", + "vctrs" + ], + "Hash": "915fb7ce036c22a6a33b5a8adb712eb1" + }, + "tidyselect": { + "Package": "tidyselect", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "rlang", + "vctrs", + "withr" + ], + "Hash": "829f27b9c4919c16b593794a6344d6c0" + }, + "timechange": { + "Package": "timechange", + "Version": "0.3.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cpp11" + ], + "Hash": "c5f3c201b931cd6474d17d8700ccb1c8" + }, + "tinytex": { + "Package": "tinytex", + "Version": "0.58", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "xfun" + ], + "Hash": "03af3ad04817f3019de8ee5c73dab807" + }, + "urlchecker": { + "Package": "urlchecker", + "Version": "1.0.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "curl", + "tools", + "xml2" + ], + "Hash": "409328b8e1253c8d729a7836fe7f7a16" + }, + "usethis": { + "Package": "usethis", + "Version": "3.2.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "clipr", + "crayon", + "curl", + "desc", + "fs", + "gert", + "gh", + "glue", + "jsonlite", + "lifecycle", + "purrr", + "rappdirs", + "rlang", + "rprojroot", + "rstudioapi", + "stats", + "tools", + "utils", + "whisker", + "withr", + "yaml" + ], + "Hash": "caa93b7eada0b69e299f61db5984faa7" + }, + "utf8": { + "Package": "utf8", + "Version": "1.2.6", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "d526d558be176e9ceb68c3d1e83479b7" + }, + "vctrs": { + "Package": "vctrs", + "Version": "0.6.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "rlang" + ], + "Hash": "c03fa420630029418f7e6da3667aac4a" + }, + "waldo": { + "Package": "waldo", + "Version": "0.6.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "diffobj", + "glue", + "methods", + "rlang" + ], + "Hash": "b7b91294ee9418a54527a9a9d8677597" + }, + "whisker": { + "Package": "whisker", + "Version": "0.4.1", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "c6abfa47a46d281a7d5159d0a8891e88" + }, + "withr": { + "Package": "withr", + "Version": "3.0.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "grDevices", + "graphics" + ], + "Hash": "cc2d62c76458d425210d1eb1478b30b4" + }, + "xfun": { + "Package": "xfun", + "Version": "0.54", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "grDevices", + "stats", + "tools" + ], + "Hash": "d23c8a31e000d9865a8a6572f61da9ee" + }, + "xml2": { + "Package": "xml2", + "Version": "1.5.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "methods", + "rlang" + ], + "Hash": "df249a568f7fbfa996016695d1da7ea1" + }, + "xopen": { + "Package": "xopen", + "Version": "1.0.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "processx" + ], + "Hash": "423df1e86d5533fcb73c6b02b4923b49" + }, + "xtable": { + "Package": "xtable", + "Version": "1.8-4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "stats", + "utils" + ], + "Hash": "b8acdf8af494d9ec19ccb2481a9b11c2" + }, + "yaml": { + "Package": "yaml", + "Version": "2.3.11", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "fd01df43bb8d6c9e0c8968440bb46d34" + }, + "zip": { + "Package": "zip", + "Version": "2.3.3", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "6ebe4b1dc74c3e50e74e316323629583" + } + } +} diff --git a/renv/.gitignore b/renv/.gitignore new file mode 100644 index 0000000..0ec0cbb --- /dev/null +++ b/renv/.gitignore @@ -0,0 +1,7 @@ +library/ +local/ +cellar/ +lock/ +python/ +sandbox/ +staging/ diff --git a/renv/activate.R b/renv/activate.R new file mode 100644 index 0000000..cb5401f --- /dev/null +++ b/renv/activate.R @@ -0,0 +1,1180 @@ + +local({ + + # the requested version of renv + version <- "1.0.3" + attr(version, "sha") <- NULL + + # the project directory + project <- getwd() + + # use start-up diagnostics if enabled + diagnostics <- Sys.getenv("RENV_STARTUP_DIAGNOSTICS", unset = "FALSE") + if (diagnostics) { + start <- Sys.time() + profile <- tempfile("renv-startup-", fileext = ".Rprof") + utils::Rprof(profile) + on.exit({ + utils::Rprof(NULL) + elapsed <- signif(difftime(Sys.time(), start, units = "auto"), digits = 2L) + writeLines(sprintf("- renv took %s to run the autoloader.", format(elapsed))) + writeLines(sprintf("- Profile: %s", profile)) + print(utils::summaryRprof(profile)) + }, add = TRUE) + } + + # figure out whether the autoloader is enabled + enabled <- local({ + + # first, check config option + override <- getOption("renv.config.autoloader.enabled") + if (!is.null(override)) + return(override) + + # next, check environment variables + # TODO: prefer using the configuration one in the future + envvars <- c( + "RENV_CONFIG_AUTOLOADER_ENABLED", + "RENV_AUTOLOADER_ENABLED", + "RENV_ACTIVATE_PROJECT" + ) + + for (envvar in envvars) { + envval <- Sys.getenv(envvar, unset = NA) + if (!is.na(envval)) + return(tolower(envval) %in% c("true", "t", "1")) + } + + # enable by default + TRUE + + }) + + if (!enabled) + return(FALSE) + + # avoid recursion + if (identical(getOption("renv.autoloader.running"), TRUE)) { + warning("ignoring recursive attempt to run renv autoloader") + return(invisible(TRUE)) + } + + # signal that we're loading renv during R startup + options(renv.autoloader.running = TRUE) + on.exit(options(renv.autoloader.running = NULL), add = TRUE) + + # signal that we've consented to use renv + options(renv.consent = TRUE) + + # load the 'utils' package eagerly -- this ensures that renv shims, which + # mask 'utils' packages, will come first on the search path + library(utils, lib.loc = .Library) + + # unload renv if it's already been loaded + if ("renv" %in% loadedNamespaces()) + unloadNamespace("renv") + + # load bootstrap tools + `%||%` <- function(x, y) { + if (is.null(x)) y else x + } + + catf <- function(fmt, ..., appendLF = TRUE) { + + quiet <- getOption("renv.bootstrap.quiet", default = FALSE) + if (quiet) + return(invisible()) + + msg <- sprintf(fmt, ...) + cat(msg, file = stdout(), sep = if (appendLF) "\n" else "") + + invisible(msg) + + } + + header <- function(label, + ..., + prefix = "#", + suffix = "-", + n = min(getOption("width"), 78)) + { + label <- sprintf(label, ...) + n <- max(n - nchar(label) - nchar(prefix) - 2L, 8L) + if (n <= 0) + return(paste(prefix, label)) + + tail <- paste(rep.int(suffix, n), collapse = "") + paste0(prefix, " ", label, " ", tail) + + } + + startswith <- function(string, prefix) { + substring(string, 1, nchar(prefix)) == prefix + } + + bootstrap <- function(version, library) { + + friendly <- renv_bootstrap_version_friendly(version) + section <- header(sprintf("Bootstrapping renv %s", friendly)) + catf(section) + + # attempt to download renv + catf("- Downloading renv ... ", appendLF = FALSE) + withCallingHandlers( + tarball <- renv_bootstrap_download(version), + error = function(err) { + catf("FAILED") + stop("failed to download:\n", conditionMessage(err)) + } + ) + catf("OK") + on.exit(unlink(tarball), add = TRUE) + + # now attempt to install + catf("- Installing renv ... ", appendLF = FALSE) + withCallingHandlers( + status <- renv_bootstrap_install(version, tarball, library), + error = function(err) { + catf("FAILED") + stop("failed to install:\n", conditionMessage(err)) + } + ) + catf("OK") + + # add empty line to break up bootstrapping from normal output + catf("") + + return(invisible()) + } + + renv_bootstrap_tests_running <- function() { + getOption("renv.tests.running", default = FALSE) + } + + renv_bootstrap_repos <- function() { + + # get CRAN repository + cran <- getOption("renv.repos.cran", "https://cloud.r-project.org") + + # check for repos override + repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) + if (!is.na(repos)) { + + # check for RSPM; if set, use a fallback repository for renv + rspm <- Sys.getenv("RSPM", unset = NA) + if (identical(rspm, repos)) + repos <- c(RSPM = rspm, CRAN = cran) + + return(repos) + + } + + # check for lockfile repositories + repos <- tryCatch(renv_bootstrap_repos_lockfile(), error = identity) + if (!inherits(repos, "error") && length(repos)) + return(repos) + + # retrieve current repos + repos <- getOption("repos") + + # ensure @CRAN@ entries are resolved + repos[repos == "@CRAN@"] <- cran + + # add in renv.bootstrap.repos if set + default <- c(FALLBACK = "https://cloud.r-project.org") + extra <- getOption("renv.bootstrap.repos", default = default) + repos <- c(repos, extra) + + # remove duplicates that might've snuck in + dupes <- duplicated(repos) | duplicated(names(repos)) + repos[!dupes] + + } + + renv_bootstrap_repos_lockfile <- function() { + + lockpath <- Sys.getenv("RENV_PATHS_LOCKFILE", unset = "renv.lock") + if (!file.exists(lockpath)) + return(NULL) + + lockfile <- tryCatch(renv_json_read(lockpath), error = identity) + if (inherits(lockfile, "error")) { + warning(lockfile) + return(NULL) + } + + repos <- lockfile$R$Repositories + if (length(repos) == 0) + return(NULL) + + keys <- vapply(repos, `[[`, "Name", FUN.VALUE = character(1)) + vals <- vapply(repos, `[[`, "URL", FUN.VALUE = character(1)) + names(vals) <- keys + + return(vals) + + } + + renv_bootstrap_download <- function(version) { + + sha <- attr(version, "sha", exact = TRUE) + + methods <- if (!is.null(sha)) { + + # attempting to bootstrap a development version of renv + c( + function() renv_bootstrap_download_tarball(sha), + function() renv_bootstrap_download_github(sha) + ) + + } else { + + # attempting to bootstrap a release version of renv + c( + function() renv_bootstrap_download_tarball(version), + function() renv_bootstrap_download_cran_latest(version), + function() renv_bootstrap_download_cran_archive(version) + ) + + } + + for (method in methods) { + path <- tryCatch(method(), error = identity) + if (is.character(path) && file.exists(path)) + return(path) + } + + stop("All download methods failed") + + } + + renv_bootstrap_download_impl <- function(url, destfile) { + + mode <- "wb" + + # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 + fixup <- + Sys.info()[["sysname"]] == "Windows" && + substring(url, 1L, 5L) == "file:" + + if (fixup) + mode <- "w+b" + + args <- list( + url = url, + destfile = destfile, + mode = mode, + quiet = TRUE + ) + + if ("headers" %in% names(formals(utils::download.file))) + args$headers <- renv_bootstrap_download_custom_headers(url) + + do.call(utils::download.file, args) + + } + + renv_bootstrap_download_custom_headers <- function(url) { + + headers <- getOption("renv.download.headers") + if (is.null(headers)) + return(character()) + + if (!is.function(headers)) + stopf("'renv.download.headers' is not a function") + + headers <- headers(url) + if (length(headers) == 0L) + return(character()) + + if (is.list(headers)) + headers <- unlist(headers, recursive = FALSE, use.names = TRUE) + + ok <- + is.character(headers) && + is.character(names(headers)) && + all(nzchar(names(headers))) + + if (!ok) + stop("invocation of 'renv.download.headers' did not return a named character vector") + + headers + + } + + renv_bootstrap_download_cran_latest <- function(version) { + + spec <- renv_bootstrap_download_cran_latest_find(version) + type <- spec$type + repos <- spec$repos + + baseurl <- utils::contrib.url(repos = repos, type = type) + ext <- if (identical(type, "source")) + ".tar.gz" + else if (Sys.info()[["sysname"]] == "Windows") + ".zip" + else + ".tgz" + name <- sprintf("renv_%s%s", version, ext) + url <- paste(baseurl, name, sep = "/") + + destfile <- file.path(tempdir(), name) + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (inherits(status, "condition")) + return(FALSE) + + # report success and return + destfile + + } + + renv_bootstrap_download_cran_latest_find <- function(version) { + + # check whether binaries are supported on this system + binary <- + getOption("renv.bootstrap.binary", default = TRUE) && + !identical(.Platform$pkgType, "source") && + !identical(getOption("pkgType"), "source") && + Sys.info()[["sysname"]] %in% c("Darwin", "Windows") + + types <- c(if (binary) "binary", "source") + + # iterate over types + repositories + for (type in types) { + for (repos in renv_bootstrap_repos()) { + + # retrieve package database + db <- tryCatch( + as.data.frame( + utils::available.packages(type = type, repos = repos), + stringsAsFactors = FALSE + ), + error = identity + ) + + if (inherits(db, "error")) + next + + # check for compatible entry + entry <- db[db$Package %in% "renv" & db$Version %in% version, ] + if (nrow(entry) == 0) + next + + # found it; return spec to caller + spec <- list(entry = entry, type = type, repos = repos) + return(spec) + + } + } + + # if we got here, we failed to find renv + fmt <- "renv %s is not available from your declared package repositories" + stop(sprintf(fmt, version)) + + } + + renv_bootstrap_download_cran_archive <- function(version) { + + name <- sprintf("renv_%s.tar.gz", version) + repos <- renv_bootstrap_repos() + urls <- file.path(repos, "src/contrib/Archive/renv", name) + destfile <- file.path(tempdir(), name) + + for (url in urls) { + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (identical(status, 0L)) + return(destfile) + + } + + return(FALSE) + + } + + renv_bootstrap_download_tarball <- function(version) { + + # if the user has provided the path to a tarball via + # an environment variable, then use it + tarball <- Sys.getenv("RENV_BOOTSTRAP_TARBALL", unset = NA) + if (is.na(tarball)) + return() + + # allow directories + if (dir.exists(tarball)) { + name <- sprintf("renv_%s.tar.gz", version) + tarball <- file.path(tarball, name) + } + + # bail if it doesn't exist + if (!file.exists(tarball)) { + + # let the user know we weren't able to honour their request + fmt <- "- RENV_BOOTSTRAP_TARBALL is set (%s) but does not exist." + msg <- sprintf(fmt, tarball) + warning(msg) + + # bail + return() + + } + + catf("- Using local tarball '%s'.", tarball) + tarball + + } + + renv_bootstrap_download_github <- function(version) { + + enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") + if (!identical(enabled, "TRUE")) + return(FALSE) + + # prepare download options + pat <- Sys.getenv("GITHUB_PAT") + if (nzchar(Sys.which("curl")) && nzchar(pat)) { + fmt <- "--location --fail --header \"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "curl", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } else if (nzchar(Sys.which("wget")) && nzchar(pat)) { + fmt <- "--header=\"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "wget", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } + + url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) + name <- sprintf("renv_%s.tar.gz", version) + destfile <- file.path(tempdir(), name) + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (!identical(status, 0L)) + return(FALSE) + + renv_bootstrap_download_augment(destfile) + + return(destfile) + + } + + # Add Sha to DESCRIPTION. This is stop gap until #890, after which we + # can use renv::install() to fully capture metadata. + renv_bootstrap_download_augment <- function(destfile) { + sha <- renv_bootstrap_git_extract_sha1_tar(destfile) + if (is.null(sha)) { + return() + } + + # Untar + tempdir <- tempfile("renv-github-") + on.exit(unlink(tempdir, recursive = TRUE), add = TRUE) + untar(destfile, exdir = tempdir) + pkgdir <- dir(tempdir, full.names = TRUE)[[1]] + + # Modify description + desc_path <- file.path(pkgdir, "DESCRIPTION") + desc_lines <- readLines(desc_path) + remotes_fields <- c( + "RemoteType: github", + "RemoteHost: api.github.com", + "RemoteRepo: renv", + "RemoteUsername: rstudio", + "RemotePkgRef: rstudio/renv", + paste("RemoteRef: ", sha), + paste("RemoteSha: ", sha) + ) + writeLines(c(desc_lines[desc_lines != ""], remotes_fields), con = desc_path) + + # Re-tar + local({ + old <- setwd(tempdir) + on.exit(setwd(old), add = TRUE) + + tar(destfile, compression = "gzip") + }) + invisible() + } + + # Extract the commit hash from a git archive. Git archives include the SHA1 + # hash as the comment field of the tarball pax extended header + # (see https://www.kernel.org/pub/software/scm/git/docs/git-archive.html) + # For GitHub archives this should be the first header after the default one + # (512 byte) header. + renv_bootstrap_git_extract_sha1_tar <- function(bundle) { + + # open the bundle for reading + # We use gzcon for everything because (from ?gzcon) + # > Reading from a connection which does not supply a 'gzip' magic + # > header is equivalent to reading from the original connection + conn <- gzcon(file(bundle, open = "rb", raw = TRUE)) + on.exit(close(conn)) + + # The default pax header is 512 bytes long and the first pax extended header + # with the comment should be 51 bytes long + # `52 comment=` (11 chars) + 40 byte SHA1 hash + len <- 0x200 + 0x33 + res <- rawToChar(readBin(conn, "raw", n = len)[0x201:len]) + + if (grepl("^52 comment=", res)) { + sub("52 comment=", "", res) + } else { + NULL + } + } + + renv_bootstrap_install <- function(version, tarball, library) { + + # attempt to install it into project library + dir.create(library, showWarnings = FALSE, recursive = TRUE) + output <- renv_bootstrap_install_impl(library, tarball) + + # check for successful install + status <- attr(output, "status") + if (is.null(status) || identical(status, 0L)) + return(status) + + # an error occurred; report it + header <- "installation of renv failed" + lines <- paste(rep.int("=", nchar(header)), collapse = "") + text <- paste(c(header, lines, output), collapse = "\n") + stop(text) + + } + + renv_bootstrap_install_impl <- function(library, tarball) { + + # invoke using system2 so we can capture and report output + bin <- R.home("bin") + exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" + R <- file.path(bin, exe) + + args <- c( + "--vanilla", "CMD", "INSTALL", "--no-multiarch", + "-l", shQuote(path.expand(library)), + shQuote(path.expand(tarball)) + ) + + system2(R, args, stdout = TRUE, stderr = TRUE) + + } + + renv_bootstrap_platform_prefix <- function() { + + # construct version prefix + version <- paste(R.version$major, R.version$minor, sep = ".") + prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") + + # include SVN revision for development versions of R + # (to avoid sharing platform-specific artefacts with released versions of R) + devel <- + identical(R.version[["status"]], "Under development (unstable)") || + identical(R.version[["nickname"]], "Unsuffered Consequences") + + if (devel) + prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") + + # build list of path components + components <- c(prefix, R.version$platform) + + # include prefix if provided by user + prefix <- renv_bootstrap_platform_prefix_impl() + if (!is.na(prefix) && nzchar(prefix)) + components <- c(prefix, components) + + # build prefix + paste(components, collapse = "/") + + } + + renv_bootstrap_platform_prefix_impl <- function() { + + # if an explicit prefix has been supplied, use it + prefix <- Sys.getenv("RENV_PATHS_PREFIX", unset = NA) + if (!is.na(prefix)) + return(prefix) + + # if the user has requested an automatic prefix, generate it + auto <- Sys.getenv("RENV_PATHS_PREFIX_AUTO", unset = NA) + if (auto %in% c("TRUE", "True", "true", "1")) + return(renv_bootstrap_platform_prefix_auto()) + + # empty string on failure + "" + + } + + renv_bootstrap_platform_prefix_auto <- function() { + + prefix <- tryCatch(renv_bootstrap_platform_os(), error = identity) + if (inherits(prefix, "error") || prefix %in% "unknown") { + + msg <- paste( + "failed to infer current operating system", + "please file a bug report at https://github.com/rstudio/renv/issues", + sep = "; " + ) + + warning(msg) + + } + + prefix + + } + + renv_bootstrap_platform_os <- function() { + + sysinfo <- Sys.info() + sysname <- sysinfo[["sysname"]] + + # handle Windows + macOS up front + if (sysname == "Windows") + return("windows") + else if (sysname == "Darwin") + return("macos") + + # check for os-release files + for (file in c("/etc/os-release", "/usr/lib/os-release")) + if (file.exists(file)) + return(renv_bootstrap_platform_os_via_os_release(file, sysinfo)) + + # check for redhat-release files + if (file.exists("/etc/redhat-release")) + return(renv_bootstrap_platform_os_via_redhat_release()) + + "unknown" + + } + + renv_bootstrap_platform_os_via_os_release <- function(file, sysinfo) { + + # read /etc/os-release + release <- utils::read.table( + file = file, + sep = "=", + quote = c("\"", "'"), + col.names = c("Key", "Value"), + comment.char = "#", + stringsAsFactors = FALSE + ) + + vars <- as.list(release$Value) + names(vars) <- release$Key + + # get os name + os <- tolower(sysinfo[["sysname"]]) + + # read id + id <- "unknown" + for (field in c("ID", "ID_LIKE")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + id <- vars[[field]] + break + } + } + + # read version + version <- "unknown" + for (field in c("UBUNTU_CODENAME", "VERSION_CODENAME", "VERSION_ID", "BUILD_ID")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + version <- vars[[field]] + break + } + } + + # join together + paste(c(os, id, version), collapse = "-") + + } + + renv_bootstrap_platform_os_via_redhat_release <- function() { + + # read /etc/redhat-release + contents <- readLines("/etc/redhat-release", warn = FALSE) + + # infer id + id <- if (grepl("centos", contents, ignore.case = TRUE)) + "centos" + else if (grepl("redhat", contents, ignore.case = TRUE)) + "redhat" + else + "unknown" + + # try to find a version component (very hacky) + version <- "unknown" + + parts <- strsplit(contents, "[[:space:]]")[[1L]] + for (part in parts) { + + nv <- tryCatch(numeric_version(part), error = identity) + if (inherits(nv, "error")) + next + + version <- nv[1, 1] + break + + } + + paste(c("linux", id, version), collapse = "-") + + } + + renv_bootstrap_library_root_name <- function(project) { + + # use project name as-is if requested + asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") + if (asis) + return(basename(project)) + + # otherwise, disambiguate based on project's path + id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) + paste(basename(project), id, sep = "-") + + } + + renv_bootstrap_library_root <- function(project) { + + prefix <- renv_bootstrap_profile_prefix() + + path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) + if (!is.na(path)) + return(paste(c(path, prefix), collapse = "/")) + + path <- renv_bootstrap_library_root_impl(project) + if (!is.null(path)) { + name <- renv_bootstrap_library_root_name(project) + return(paste(c(path, prefix, name), collapse = "/")) + } + + renv_bootstrap_paths_renv("library", project = project) + + } + + renv_bootstrap_library_root_impl <- function(project) { + + root <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) + if (!is.na(root)) + return(root) + + type <- renv_bootstrap_project_type(project) + if (identical(type, "package")) { + userdir <- renv_bootstrap_user_dir() + return(file.path(userdir, "library")) + } + + } + + renv_bootstrap_validate_version <- function(version, description = NULL) { + + # resolve description file + # + # avoid passing lib.loc to `packageDescription()` below, since R will + # use the loaded version of the package by default anyhow. note that + # this function should only be called after 'renv' is loaded + # https://github.com/rstudio/renv/issues/1625 + description <- description %||% packageDescription("renv") + + # check whether requested version 'version' matches loaded version of renv + sha <- attr(version, "sha", exact = TRUE) + valid <- if (!is.null(sha)) + renv_bootstrap_validate_version_dev(sha, description) + else + renv_bootstrap_validate_version_release(version, description) + + if (valid) + return(TRUE) + + # the loaded version of renv doesn't match the requested version; + # give the user instructions on how to proceed + remote <- if (!is.null(description[["RemoteSha"]])) { + paste("rstudio/renv", description[["RemoteSha"]], sep = "@") + } else { + paste("renv", description[["Version"]], sep = "@") + } + + # display both loaded version + sha if available + friendly <- renv_bootstrap_version_friendly( + version = description[["Version"]], + sha = description[["RemoteSha"]] + ) + + fmt <- paste( + "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", + "- Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", + "- Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", + sep = "\n" + ) + catf(fmt, friendly, renv_bootstrap_version_friendly(version), remote) + + FALSE + + } + + renv_bootstrap_validate_version_dev <- function(version, description) { + expected <- description[["RemoteSha"]] + is.character(expected) && startswith(expected, version) + } + + renv_bootstrap_validate_version_release <- function(version, description) { + expected <- description[["Version"]] + is.character(expected) && identical(expected, version) + } + + renv_bootstrap_hash_text <- function(text) { + + hashfile <- tempfile("renv-hash-") + on.exit(unlink(hashfile), add = TRUE) + + writeLines(text, con = hashfile) + tools::md5sum(hashfile) + + } + + renv_bootstrap_load <- function(project, libpath, version) { + + # try to load renv from the project library + if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) + return(FALSE) + + # warn if the version of renv loaded does not match + renv_bootstrap_validate_version(version) + + # execute renv load hooks, if any + hooks <- getHook("renv::autoload") + for (hook in hooks) + if (is.function(hook)) + tryCatch(hook(), error = warnify) + + # load the project + renv::load(project) + + TRUE + + } + + renv_bootstrap_profile_load <- function(project) { + + # if RENV_PROFILE is already set, just use that + profile <- Sys.getenv("RENV_PROFILE", unset = NA) + if (!is.na(profile) && nzchar(profile)) + return(profile) + + # check for a profile file (nothing to do if it doesn't exist) + path <- renv_bootstrap_paths_renv("profile", profile = FALSE, project = project) + if (!file.exists(path)) + return(NULL) + + # read the profile, and set it if it exists + contents <- readLines(path, warn = FALSE) + if (length(contents) == 0L) + return(NULL) + + # set RENV_PROFILE + profile <- contents[[1L]] + if (!profile %in% c("", "default")) + Sys.setenv(RENV_PROFILE = profile) + + profile + + } + + renv_bootstrap_profile_prefix <- function() { + profile <- renv_bootstrap_profile_get() + if (!is.null(profile)) + return(file.path("profiles", profile, "renv")) + } + + renv_bootstrap_profile_get <- function() { + profile <- Sys.getenv("RENV_PROFILE", unset = "") + renv_bootstrap_profile_normalize(profile) + } + + renv_bootstrap_profile_set <- function(profile) { + profile <- renv_bootstrap_profile_normalize(profile) + if (is.null(profile)) + Sys.unsetenv("RENV_PROFILE") + else + Sys.setenv(RENV_PROFILE = profile) + } + + renv_bootstrap_profile_normalize <- function(profile) { + + if (is.null(profile) || profile %in% c("", "default")) + return(NULL) + + profile + + } + + renv_bootstrap_path_absolute <- function(path) { + + substr(path, 1L, 1L) %in% c("~", "/", "\\") || ( + substr(path, 1L, 1L) %in% c(letters, LETTERS) && + substr(path, 2L, 3L) %in% c(":/", ":\\") + ) + + } + + renv_bootstrap_paths_renv <- function(..., profile = TRUE, project = NULL) { + renv <- Sys.getenv("RENV_PATHS_RENV", unset = "renv") + root <- if (renv_bootstrap_path_absolute(renv)) NULL else project + prefix <- if (profile) renv_bootstrap_profile_prefix() + components <- c(root, renv, prefix, ...) + paste(components, collapse = "/") + } + + renv_bootstrap_project_type <- function(path) { + + descpath <- file.path(path, "DESCRIPTION") + if (!file.exists(descpath)) + return("unknown") + + desc <- tryCatch( + read.dcf(descpath, all = TRUE), + error = identity + ) + + if (inherits(desc, "error")) + return("unknown") + + type <- desc$Type + if (!is.null(type)) + return(tolower(type)) + + package <- desc$Package + if (!is.null(package)) + return("package") + + "unknown" + + } + + renv_bootstrap_user_dir <- function() { + dir <- renv_bootstrap_user_dir_impl() + path.expand(chartr("\\", "/", dir)) + } + + renv_bootstrap_user_dir_impl <- function() { + + # use local override if set + override <- getOption("renv.userdir.override") + if (!is.null(override)) + return(override) + + # use R_user_dir if available + tools <- asNamespace("tools") + if (is.function(tools$R_user_dir)) + return(tools$R_user_dir("renv", "cache")) + + # try using our own backfill for older versions of R + envvars <- c("R_USER_CACHE_DIR", "XDG_CACHE_HOME") + for (envvar in envvars) { + root <- Sys.getenv(envvar, unset = NA) + if (!is.na(root)) + return(file.path(root, "R/renv")) + } + + # use platform-specific default fallbacks + if (Sys.info()[["sysname"]] == "Windows") + file.path(Sys.getenv("LOCALAPPDATA"), "R/cache/R/renv") + else if (Sys.info()[["sysname"]] == "Darwin") + "~/Library/Caches/org.R-project.R/R/renv" + else + "~/.cache/R/renv" + + } + + renv_bootstrap_version_friendly <- function(version, shafmt = NULL, sha = NULL) { + sha <- sha %||% attr(version, "sha", exact = TRUE) + parts <- c(version, sprintf(shafmt %||% " [sha: %s]", substring(sha, 1L, 7L))) + paste(parts, collapse = "") + } + + renv_bootstrap_exec <- function(project, libpath, version) { + if (!renv_bootstrap_load(project, libpath, version)) + renv_bootstrap_run(version, libpath) + } + + renv_bootstrap_run <- function(version, libpath) { + + # perform bootstrap + bootstrap(version, libpath) + + # exit early if we're just testing bootstrap + if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) + return(TRUE) + + # try again to load + if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { + return(renv::load(project = getwd())) + } + + # failed to download or load renv; warn the user + msg <- c( + "Failed to find an renv installation: the project will not be loaded.", + "Use `renv::activate()` to re-initialize the project." + ) + + warning(paste(msg, collapse = "\n"), call. = FALSE) + + } + + renv_json_read <- function(file = NULL, text = NULL) { + + jlerr <- NULL + + # if jsonlite is loaded, use that instead + if ("jsonlite" %in% loadedNamespaces()) { + + json <- catch(renv_json_read_jsonlite(file, text)) + if (!inherits(json, "error")) + return(json) + + jlerr <- json + + } + + # otherwise, fall back to the default JSON reader + json <- catch(renv_json_read_default(file, text)) + if (!inherits(json, "error")) + return(json) + + # report an error + if (!is.null(jlerr)) + stop(jlerr) + else + stop(json) + + } + + renv_json_read_jsonlite <- function(file = NULL, text = NULL) { + text <- paste(text %||% read(file), collapse = "\n") + jsonlite::fromJSON(txt = text, simplifyVector = FALSE) + } + + renv_json_read_default <- function(file = NULL, text = NULL) { + + # find strings in the JSON + text <- paste(text %||% read(file), collapse = "\n") + pattern <- '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]' + locs <- gregexpr(pattern, text, perl = TRUE)[[1]] + + # if any are found, replace them with placeholders + replaced <- text + strings <- character() + replacements <- character() + + if (!identical(c(locs), -1L)) { + + # get the string values + starts <- locs + ends <- locs + attr(locs, "match.length") - 1L + strings <- substring(text, starts, ends) + + # only keep those requiring escaping + strings <- grep("[[\\]{}:]", strings, perl = TRUE, value = TRUE) + + # compute replacements + replacements <- sprintf('"\032%i\032"', seq_along(strings)) + + # replace the strings + mapply(function(string, replacement) { + replaced <<- sub(string, replacement, replaced, fixed = TRUE) + }, strings, replacements) + + } + + # transform the JSON into something the R parser understands + transformed <- replaced + transformed <- gsub("{}", "`names<-`(list(), character())", transformed, fixed = TRUE) + transformed <- gsub("[[{]", "list(", transformed, perl = TRUE) + transformed <- gsub("[]}]", ")", transformed, perl = TRUE) + transformed <- gsub(":", "=", transformed, fixed = TRUE) + text <- paste(transformed, collapse = "\n") + + # parse it + json <- parse(text = text, keep.source = FALSE, srcfile = NULL)[[1L]] + + # construct map between source strings, replaced strings + map <- as.character(parse(text = strings)) + names(map) <- as.character(parse(text = replacements)) + + # convert to list + map <- as.list(map) + + # remap strings in object + remapped <- renv_json_remap(json, map) + + # evaluate + eval(remapped, envir = baseenv()) + + } + + renv_json_remap <- function(json, map) { + + # fix names + if (!is.null(names(json))) { + lhs <- match(names(json), names(map), nomatch = 0L) + rhs <- match(names(map), names(json), nomatch = 0L) + names(json)[rhs] <- map[lhs] + } + + # fix values + if (is.character(json)) + return(map[[json]] %||% json) + + # handle true, false, null + if (is.name(json)) { + text <- as.character(json) + if (text == "true") + return(TRUE) + else if (text == "false") + return(FALSE) + else if (text == "null") + return(NULL) + } + + # recurse + if (is.recursive(json)) { + for (i in seq_along(json)) { + json[i] <- list(renv_json_remap(json[[i]], map)) + } + } + + json + + } + + # load the renv profile, if any + renv_bootstrap_profile_load(project) + + # construct path to library root + root <- renv_bootstrap_library_root(project) + + # construct library prefix for platform + prefix <- renv_bootstrap_platform_prefix() + + # construct full libpath + libpath <- file.path(root, prefix) + + # run bootstrap code + renv_bootstrap_exec(project, libpath, version) + + invisible() + +}) diff --git a/renv/settings.json b/renv/settings.json new file mode 100644 index 0000000..a1ae228 --- /dev/null +++ b/renv/settings.json @@ -0,0 +1,19 @@ +{ + "bioconductor.version": null, + "external.libraries": [], + "ignored.packages": [], + "package.dependency.fields": [ + "Imports", + "Depends", + "LinkingTo" + ], + "ppm.enabled": null, + "ppm.ignored.urls": [], + "r.version": null, + "snapshot.type": "all", + "use.cache": true, + "vcs.ignore.cellar": true, + "vcs.ignore.library": true, + "vcs.ignore.local": true, + "vcs.manage.ignores": true +} diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..348f5c1 --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,12 @@ +# This file is part of the standard setup for testthat. +# It is recommended that you do not modify it. +# +# Where should you do additional test configuration? +# Learn more about the roles of various files in: +# * https://r-pkgs.org/testing-design.html#sec-tests-files-overview +# * https://testthat.r-lib.org/articles/special-files.html + +library(testthat) +library(EFSATools) + +test_check("EFSATools") diff --git a/tests/testthat/test-SCD2.R b/tests/testthat/test-SCD2.R new file mode 100644 index 0000000..47f66a2 --- /dev/null +++ b/tests/testthat/test-SCD2.R @@ -0,0 +1,105 @@ +test_that("The new data must be a data frame", { + expect_error( + SCD2( + newData = 1, + currentData = tibble()) + ) +}) + +test_that("The current data must be a data frame", { + expect_error( + SCD2( + newData = tibble(), + currentData = 1) + ) +}) + +test_that("The key must be a vector", { + expect_error( + SCD2( + newData = tibble(), + currentData = tibble(), + key = 1) + ) +}) + +test_that("A data frame must be returned", { + newData_ <- .activate(dataframe = iris) + currentData_ <- .deactivate(dataframe = newData_) + + checkmate::expect_data_frame( + SCD2(newData = newData_, currentData = currentData_)) +}) + +test_that("The START_DATE column is present", { + newData_ <- .activate(dataframe = iris) + currentData_ <- .deactivate(dataframe = newData_) + + expect_true(any(names(SCD2( + newData = newData_, + currentData = currentData_ + )) == "START_DATE")) +}) + +test_that("The END_DATE column is present", { + newData_ <- .activate(dataframe = iris) + currentData_ <- .deactivate(dataframe = newData_) + + expect_true(any(names(SCD2( + newData = newData_, + currentData = currentData_ + )) == "END_DATE")) +}) + +test_that("The IS_ACTIVE column is present", { + newData_ <- .activate(dataframe = iris) + currentData_ <- .deactivate(dataframe = newData_) + + expect_true(any(names(SCD2( + newData = newData_, + currentData = currentData_ + )) == "IS_ACTIVE")) +}) + +test_that("SCD2 gets activated", { + newData_ <- .activate(dataframe = iris) + currentData_ <- .deactivate(dataframe = newData_) + + mergedData_ <- SCD2(newData = newData_, currentData = currentData_) + + expect_true(all(unique(mergedData_$IS_ACTIVE) %in% c(TRUE, FALSE))) +}) + +test_that("The function must output as expected", { + currentData_ <- tibble::tribble( + ~id, ~colA, ~colB, ~colC, + 1, "a1", "b1", "c1", + 2, "a2", "b2", "c2", + 3, "a3", "b3", "c3" + ) + currentData_ <- .activate(dataframe = currentData_) + + newData_ <- tibble::tribble( + ~id, ~colA, ~colB, ~colC, + 1, "a1", "b1", "c1", # Identical row. + 2, "a2", "b2", "c20", # Almost identical row. + 3, "a4", "b4", "c4" # Different row. + ) + + mergedData_ <- SCD2(newData = newData_, currentData = currentData_) + + expectedResult_ <- tibble::tribble( + ~id, ~colA, ~colB, ~colC, ~IS_ACTIVE, + 1, "a1", "b1", "c1", TRUE, + 2, "a2", "b2", "c20", TRUE, + 3, "a4", "b4", "c4", TRUE, + 2, "a2", "b2", "c2", FALSE, + 3, "a3", "b3", "c3", FALSE) |> + dplyr::arrange(id) + + arrangedMergedData_ <- mergedData_ |> + dplyr::select(-ends_with("DATE")) |> + dplyr::arrange(id) + + expect_equal(arrangedMergedData_, expectedResult_) +}) diff --git a/tests/testthat/test-SSCD2.R b/tests/testthat/test-SSCD2.R new file mode 100644 index 0000000..3a4f61d --- /dev/null +++ b/tests/testthat/test-SSCD2.R @@ -0,0 +1,50 @@ +test_that("The new data must be a data frame", { + expect_error( + SSCD2( + newData = 1, + currentData = tibble()) + ) +}) + +test_that("The current data must be a data frame", { + expect_error( + SSCD2( + newData = tibble(), + currentData = 1) + ) +}) + +test_that("The function must output as expected", { + currentData_ <- tibble::tribble( + ~id, ~colA, ~colB, ~colC, + 1, "a1", "b1", "c1", + 2, "a2", "b2", "c2", + 3, "a3", "b3", "c3" + ) + currentData_ <- .activate(dataframe = currentData_) + + newData_ <- tibble::tribble( + ~id, ~colA, ~colB, ~colC, + 1, "a1", "b1", "c1", # Identical row. + 2, "a2", "b2", "c20", # Almost identical row. + 3, "a4", "b4", "c4" # Different row. + ) + + mergedData_ <- SSCD2(newData = newData_, currentData = currentData_) + + expectedResult_ <- tibble::tribble( + ~id, ~colA, ~colB, ~colC, ~IS_ACTIVE, + 1, "a1", "b1", "c1", FALSE, + 2, "a2", "b2", "c2", FALSE, + 3, "a3", "b3", "c3", FALSE, + 1, "a1", "b1", "c1", TRUE, + 2, "a2", "b2", "c20", TRUE, + 3, "a4", "b4", "c4", TRUE) |> + dplyr::arrange(id) + + arrangedMergedData_ <- mergedData_ |> + dplyr::select(-ends_with("DATE")) |> + dplyr::arrange(id) + + expect_equal(expectedResult_, arrangedMergedData_) +}) diff --git a/tests/testthat/test-activate.R b/tests/testthat/test-activate.R new file mode 100644 index 0000000..3c2a17d --- /dev/null +++ b/tests/testthat/test-activate.R @@ -0,0 +1,34 @@ +test_that("The parameter must be a data frame", { + expect_error( + .activate(dataframe = 1) + ) +}) + +test_that("Iris gets activated", { + checkmate::expect_data_frame( + .activate(dataframe = iris)) +}) + +test_that("The START_DATE column is present", { + dataframe_ <- .activate(dataframe = iris) + expect_true( + any(names(dataframe_) == "START_DATE")) + expect_true( + all(!is.null(dataframe_$START_DATE))) +}) + +test_that("The END_DATE column is present", { + dataframe_ <- .activate(dataframe = iris) + expect_true( + any(names(dataframe_) == "END_DATE")) + expect_true( + all(is.na(dataframe_$END_DATE))) +}) + +test_that("The IS_ACTIVE column is present", { + dataframe_ <- .activate(dataframe = iris) + expect_true( + any(names(dataframe_) == "IS_ACTIVE")) + expect_true( + all(dataframe_$IS_ACTIVE == TRUE)) +}) diff --git a/tests/testthat/test-deactivate.R b/tests/testthat/test-deactivate.R new file mode 100644 index 0000000..e6f138e --- /dev/null +++ b/tests/testthat/test-deactivate.R @@ -0,0 +1,32 @@ +test_that("The parameter must be a data frame", { + expect_error( + .deactivate(dataframe = 1) + ) +}) + +test_that("Iris gets deactivated", { + dataframe_ <- .activate(dataframe = iris) + + checkmate::expect_data_frame( + .deactivate(dataframe = dataframe_)) +}) + +test_that("The END_DATE column is present", { + dataframe_ <- .activate(dataframe = iris) + dataframe_ <- .deactivate(dataframe = dataframe_) + + expect_true( + any(names(.deactivate(dataframe = dataframe_)) == "END_DATE")) + expect_true( + all(!is.null(dataframe_["END_DATE"]))) +}) + +test_that("The IS_ACTIVE column is present", { + dataframe_ <- .activate(dataframe = iris) + dataframe_ <- .deactivate(dataframe = dataframe_) + + expect_true( + any(names(.deactivate(dataframe = dataframe_)) == "IS_ACTIVE")) + expect_true( + all(dataframe_$IS_ACTIVE == FALSE)) +}) diff --git a/tests/testthat/test-dropEmpty.R b/tests/testthat/test-dropEmpty.R new file mode 100644 index 0000000..be1efad --- /dev/null +++ b/tests/testthat/test-dropEmpty.R @@ -0,0 +1,42 @@ +test_that("The parameter must be a data frame", { + expect_error( + dropEmpty(dataframe = 1) + ) +}) + +test_that("A dataframe must be returned", { + dataframe_ <- head(iris, 10) + + # The Species column is going to be dropped. + dataframe_$Species <- NA + dataframeWithoutSpecies_ <- dropEmpty(dataframe = dataframe_) + + # The first row is going to be dropped. + dataframe_[1, ] <- NA + dataframeWithoutFirstRow_ <- dropEmpty(dataframe = dataframe_) + + expect_true(is.data.frame(dataframeWithoutSpecies_)) + expect_true(is.data.frame(dataframeWithoutFirstRow_)) +}) + +test_that("Drops a column", { + dataframe_ <- head(iris, 10) + + # The Species column is going to be dropped. + dataframe_$Species <- NA + dataframeWithoutSpecies_ <- dropEmpty(dataframe_) + numberOfColumns_ <- ncol(dataframeWithoutSpecies_) + + expect_equal(numberOfColumns_, 4) +}) + +test_that("Drops a row", { + dataframe_ <- head(iris, 10) + + # The first row is going to be dropped. + dataframe_[1, ] <- NA + dataframeWithoutFirstRow_ <- dropEmpty(dataframe = dataframe_) + numberOfRows_ <- nrow(dataframeWithoutFirstRow_) + + expect_equal(numberOfRows_, 9) +}) diff --git a/tests/testthat/test-enrich.R b/tests/testthat/test-enrich.R new file mode 100644 index 0000000..fd29e31 --- /dev/null +++ b/tests/testthat/test-enrich.R @@ -0,0 +1,67 @@ +test_that("The data frame to enrich must be a data frame", { + expect_error( + enrich( + dataframe = 1, + catalogue = tibble(), + joinBy = "", + enrichedColumnName = "") + ) +}) + +test_that("The catalogue must be a data frame", { + expect_error( + enrich( + dataframe = tibble(), + catalogue = 1, + joinBy = "", + enrichedColumnName = "") + ) +}) + +test_that("The catalogue must contain the NAME and CODE columns", { + expect_error( + enrich( + dataframe = tibble(), + catalogue = tibble(), + joinBy = "", + enrichedColumnName = "") + ) +}) + +test_that("The join key must be a string", { + expect_error( + enrich( + dataframe = tibble(), + catalogue = tibble(), + joinBy = 1, + enrichedColumnName = "") + ) +}) + +test_that("The enriched column name must be a string", { + expect_error( + enrich( + dataframe = tibble(), + catalogue = tibble(), + joinBy = "", + enrichedColumnName = 1) + ) +}) + +test_that("A data frame must be returned", { + dataframe_ <- iris |> dplyr::rename(CODE = Species) + + catalogue_ <- iris |> + dplyr::rename(CODE = Species) |> + dplyr::mutate(NAME = "test") |> + dplyr::select(CODE, NAME) |> + unique() + + enriched_ <- enrich( + dataframe = dataframe_, + catalogue = catalogue_, + joinBy = "CODE", + enrichedColumnName = "enrichedColumn") + + expect_true(is.data.frame(enriched_)) +}) diff --git a/tests/testthat/test-removeReplicatedColumns.R b/tests/testthat/test-removeReplicatedColumns.R new file mode 100644 index 0000000..a1669fe --- /dev/null +++ b/tests/testthat/test-removeReplicatedColumns.R @@ -0,0 +1,45 @@ +test_that("The data frame to manipulate must be a data frame", { + expect_error( + removeReplicatedColumns( + dataframe = 1, + prefix = "") + ) +}) + +test_that("The prefix must be a string", { + expect_error( + removeReplicatedColumns( + dataframe = tibble(), + prefix = 1) + ) +}) + +test_that("A data frame must be returned", { + dataframe_ <- data.frame( + prefix_1 = c(1, NA, NA), + prefix_2 = c(NA, 2, NA), + prefix_3 = c(NA, NA, 3), + another_col = c(NA, NA, 3)) + + dataframeDeduplicated_ <- suppressWarnings( + removeReplicatedColumns( + dataframe = dataframe_, + prefix = "prefix")) + + expect_true(is.data.frame(dataframeDeduplicated_)) +}) + +test_that("Only the deduplicated column gets returned", { + dataframe_ <- data.frame( + prefix_1 = c(1, NA, NA), + prefix_2 = c(NA, 2, NA), + prefix_3 = c(NA, NA, 3), + another_col = c(NA, NA, 3)) + + dataframeDeduplicated_ <- suppressWarnings( + removeReplicatedColumns( + dataframe = dataframe_, + prefix = "prefix")) + + expect_equal(ncol(dataframeDeduplicated_), 2) +}) diff --git a/vignettes/.gitignore b/vignettes/.gitignore new file mode 100644 index 0000000..097b241 --- /dev/null +++ b/vignettes/.gitignore @@ -0,0 +1,2 @@ +*.html +*.R diff --git a/vignettes/EFSATools.Rmd b/vignettes/EFSATools.Rmd new file mode 100644 index 0000000..6726dac --- /dev/null +++ b/vignettes/EFSATools.Rmd @@ -0,0 +1,134 @@ +--- +title: "EFSATools" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{EFSATools} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include=FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +## Overview + +The **EFSATools** package brings together all the functions developed for EFSA's ad hoc data collections, providing tools for dataset operations as well as utilities designed to preserve data history. + +The package is intended for researchers, analysts, and practitioners who require convenient programmatic access to data collection utilities. + +## Installation + +### From CRAN + +```{r eval=FALSE} +install.packages("EFSATools") +``` + +### Development version (from GitHub) + +To install the latest development version: + +```{r eval=FALSE} +# install.packages("devtools") +devtools::install_github("openefsa/EFSATools") +``` + +## Basic usage + +The main purpose of *EFSATools* is to provide tools for managing datasets and tracking data history within the context of data collections. + +Below are examples demonstrating how to use the functions in this package. First, load the *EFSATools* package: + +```{r loadLibrary, eval=FALSE} +library(EFSATools) +``` + +To explore the arguments and usage of a specific function, you can run: + +```{r eval=FALSE} +help("") +``` + +This will show the full documentation for the function, including its arguments, return values, and usage examples. + +For example, if you are working with the `SCD2()` function, you can check its documentation with: + +```{r eval=FALSE} +help("SCD2") +``` + +## Dropping empty rows and columns from a data frame + +If a data frame contain empty rows or columns, you can remove them using the `dropEmpty()` function, as follows: + +```{r dropEmpty, eval=FALSE} +irisDropped <- dropEmpty(dataframe = iris) + +print(head(irisDropped)) +``` + +## Enriching a data frame with an EFSA's catalogue + +The `enrich()` function enables the augmentation of a data frame using information stored in an EFSA's catalogue. It requires specifying the column used to join the two datasets, as well as the name of the column that will contain the enriched information (namely, the 'NAME' field of EFSA's catalogues). + +```{r enrich, eval=FALSE} +enrichedDataFrame <- enrich( + dataframe = dataframe_, + catalogue = CV_MTX_, + joinBy = "CODE", + enrichedColumnName = "enrichedColumn" +) + +print(head(enrichedDataFrame)) +``` + +## Removing replicated columns from a data frame + +The `removeReplicatedColumns()` function merges all the replicated columns in a data frame into a single column whose name includes the "_deduplicated" suffix. After the merge, the original replicated columns are removed from the data frame. + +In the following example, we present a data frame containing the columns *region_1*, *region_2*, ..., *region_n* with *n* > 100. Using the `removeReplicatedColumns()` function, these columns can be efficiently consolidated into a single *region_deduplicated* column, assuming that for each row only one of the *n* columns contains a meaningful (non-NA) value. + +```{r removeReplicatedColumns, eval=FALSE} +iris$Species_1 <- iris$Species +iris$Species_2 <- iris$Species +iris$Species <- NULL + +irisDeduplicated <- removeReplicatedColumns( + dataframe = iris, + prefix = "Species_" +) + +print(head(irisDeduplicated)) +``` + +## Implementing a "Simple" Slowly Changing Dimension Type 2 (SSCD2) + +The `SSCD2()` function makes it possible to preserve data history when new data becomes available by implementing a simplified version of Slowly Changing Dimension Type 2. It marks all records in the current data frame as inactive and appends the new data, flagging each newly added record as active. + +Unlike the `SCD2()` function, `SSCD2()` does not check which records have actually changed. Instead, it marks all existing records as inactive and treats all incoming records as new, setting the previous ones to inactive status even if they are still included in the updated dataset. + +An example of how to use the function is provided below: + +```{r SSCD2, eval=FALSE} +sscd2Dataframe <- SSCD2(newData = newDataframe, currentData = oldDataframe) + +print(head(sscd2Dataframe)) +``` + +## Implementing a Slowly Changing Dimension Type 2 (SCD2) + +The `SCD2()` function makes it possible to preserve data history when new data becomes available by implementing a Slowly Changing Dimension Type 2. It compares the current records with the new ones, marking as inactive any existing records that no longer appear in the updated dataset. Then, it flags as active any new records that are not present among the currently active data. + +Unlike the `SSCD2()` function, `SCD2()` checks which records have actually changed. It marks as inactive any existing records that no longer appear in the updated dataset, and flags as active any new records that are not present among the currently active data. + +An example of how to use the function is provided below: + +```{r SCD2, eval=FALSE} +scd2Dataframe <- SCD2(newData = newDataframe, currentData = oldDataframe) + +print(head(scd2Dataframe)) +```