diff --git a/R/as_bibentry.R b/R/as_bibentry.R index 88dfc462..3274ad20 100644 --- a/R/as_bibentry.R +++ b/R/as_bibentry.R @@ -118,18 +118,10 @@ as_bibentry <- function(x, } - # Three cases: - # A) Full cff reference object or - # B) Individual reference list - # C) List of references + # Guess case + cff_type <- guess_cff_part(obj) - - # Detect case A - is_full_cff <- "cff-version" %in% names(obj) - # Detect case B - is_single_ref <- "type" %in% names(obj) - - if (is_full_cff) { + if (cff_type == "cff_full") { # Try to generate preferred if not present if (!("preferred-citation" %in% names(obj))) { prefcit <- obj @@ -146,7 +138,7 @@ as_bibentry <- function(x, "references" = obj$references, c(list(obj$`preferred-citation`), obj$references) ) - } else if (is_single_ref) { + } else if (cff_type == "cff_ref") { obj_extract <- list(obj) } else { obj_extract <- obj @@ -158,20 +150,18 @@ as_bibentry <- function(x, return(NULL) } - ref <- lapply(obj_extract, cff_bibtex_parser) + ref <- lapply(obj_extract, make_bibentry) ref <- do.call(c, ref) return(ref) } -cff_bibtex_parser <- function(x) { +make_bibentry <- function(x) { if (is.null(x)) { return(NULL) } - stopifnotcff(x) - # Partially based on ruby parser # https://github.com/citation-file-format/ruby-cff/blob/main/lib/cff/ >> # (cont) formatter/bibtex_formatter.rb @@ -184,79 +174,13 @@ cff_bibtex_parser <- function(x) { # edition institution journal month number pages publisher title volume year # Guess type of entry---- - tobibentry$bibtype <- switch(tolower(x$type), - "article" = "article", - "book" = "book", - "manual" = "manual", - "unpublished" = "unpublished", - "conference" = "inproceedings", - "conference-paper" = "inproceedings", - "proceedings" = "proceedings", - "magazine-article" = "article", - "newspaper-article" = "article", - "pamphlet" = "booklet", - "report" = "techreport", - "thesis" = "mastersthesis", - # We would need to guess - "misc" - ) - # Try guess thesis - ttype <- clean_str(gsub("[[:punct:]]", "", - x$`thesis-type`, - perl = TRUE - )) - - if (!is.null(ttype) && x$type == "thesis") { - if (grepl("Phd", ttype, ignore.case = TRUE)) { - tobibentry$bibtype <- "phdthesis" - } - } - - # Check if it may be an incollection - # Hint: is misc with collection-title and publisher - - if (all( - tobibentry$bibtype == "misc", !is.null(x$`collection-title`), - !is.null(x$publisher), !is.null(x$year) - )) { - tobibentry$bibtype <- "incollection" - } - + tobibentry$bibtype <- guess_bibtype(x) # address---- - # BibTeX 'address' is taken from the publisher (book, others) or the - # conference (inproceedings). - # Set logic: conference > institution > publisher - if (!is.null(x$conference)) { - addr_search <- x$conference - } else if (!is.null(x$institution)) { - addr_search <- x$institution - } else { - addr_search <- x$publisher - } - - - tobibentry$address <- clean_str(paste( - c( - addr_search$address, - addr_search$city, - addr_search$region, - addr_search$country - ), - collapse = ", " - )) - - # As a fallback, use also location - if (is.null(tobibentry$address) && !is.null(x$location)) { - tobibentry$address <- x$location$name - } - - + tobibentry$address <- guess_address(x) # author---- - aut <- x$authors - author <- as.person(aut) - tobibentry$author <- author + tobibentry$author <- as.person(x$authors) # booktitle ---- @@ -268,7 +192,10 @@ cff_bibtex_parser <- function(x) { # Fallback to conference name - if (tobibentry$bibtype == "inproceedings" && is.null(tobibentry$booktitle)) { + if (all( + tobibentry$bibtype == "inproceedings", + is.null(tobibentry$booktitle) + )) { tobibentry$booktitle <- x$conference$name } @@ -280,21 +207,10 @@ cff_bibtex_parser <- function(x) { # editor---- # Same case than authors - editors <- as.person(x$editors) - tobibentry$editor <- editors + tobibentry$editor <- as.person(x$editors) # howpublished---- - - howpublished <- x$medium - - if (!is.null(howpublished)) { - # Capitalize first letter - letts <- unlist(strsplit(howpublished, "|")) - howpublished <- - clean_str(paste0(c(toupper(letts[1]), letts[-1]), collapse = "")) - - tobibentry$howpublished <- howpublished - } + tobibentry$howpublished <- make_howpublised(x) # institution/organization ---- @@ -320,41 +236,6 @@ cff_bibtex_parser <- function(x) { # journal---- tobibentry$journal <- x$journal - # key: First two given of author and year---- - # Bear in mind institutions has only given - # Use the first two authors - aut_sur <- lapply(tobibentry$author[1:2], function(z) { - unz <- unlist(z) - if ("family" %in% names(unz)) { - r <- unz["family"] - return(clean_str(r)) - } - - r <- unz["given"] - return(clean_str(r)) - }) - - - aut_sur <- tolower(paste0(unlist(aut_sur), collapse = "")) - aut_sur <- gsub("\\s*", "", aut_sur) - - # Try hard to remove accents - # First with iconv - aut_sur <- iconv(aut_sur, - from = "UTF-8", to = "ASCII//TRANSLIT", - sub = "?" - ) - - # Next to latex - aut_sur <- encoded_utf_to_latex(aut_sur) - - # Finally keep only a-z letters for key - aut_sur <- gsub("[^_a-z]", "", aut_sur) - - y <- x$year - - tobibentry$key <- paste(c(aut_sur, y), collapse = ":") - # month---- m <- x$month @@ -446,6 +327,10 @@ cff_bibtex_parser <- function(x) { tobibentry$bibtype <- "inbook" } + # key: First two given of author and year---- + tobibentry$key <- make_bibkey(tobibentry) + + # Handle anonymous author---- # If anonymous and not needed, then not use it @@ -525,3 +410,130 @@ cff_bibtex_parser <- function(x) { return(bib) } + + +guess_bibtype <- function(x) { + init_guess <- switch(tolower(x$type), + "article" = "article", + "book" = "book", + "manual" = "manual", + "unpublished" = "unpublished", + "conference" = "inproceedings", + "conference-paper" = "inproceedings", + "proceedings" = "proceedings", + "magazine-article" = "article", + "newspaper-article" = "article", + "pamphlet" = "booklet", + "report" = "techreport", + "thesis" = "mastersthesis", + # We would need to guess + "misc" + ) + + + # Try guess thesis + ttype <- clean_str(gsub("[[:punct:]]", "", + x$`thesis-type`, + perl = TRUE + )) + + if (!is.null(ttype) && x$type == "thesis") { + if (grepl("Phd", ttype, ignore.case = TRUE)) { + init_guess <- "phdthesis" + } + } + + # Check if it may be an incollection + # Hint: is misc with collection-title and publisher + + if (all( + init_guess == "misc", !is.null(x$`collection-title`), + !is.null(x$publisher), !is.null(x$year) + )) { + init_guess <- "incollection" + } + + init_guess +} + +guess_address <- function(x) { + # BibTeX 'address' is taken from the publisher (book, others) or the + # conference (inproceedings). + # Set logic: conference > institution > publisher + if (!is.null(x$conference)) { + addr_search <- x$conference + } else if (!is.null(x$institution)) { + addr_search <- x$institution + } else { + addr_search <- x$publisher + } + + + address <- clean_str(paste( + c( + addr_search$address, + addr_search$city, + addr_search$region, + addr_search$country + ), + collapse = ", " + )) + + # As a fallback, use also location + if (is.null(address) && !is.null(x$location)) { + address <- clean_str(x$location$name) + } + + address +} + + +make_bibkey <- function(tobibentry) { + # Bear in mind institutions has only given + # Use the first two authors + aut_sur <- lapply(tobibentry$author[1:2], function(z) { + unz <- unlist(z) + if ("family" %in% names(unz)) { + r <- unz["family"] + return(clean_str(r)) + } + + r <- unz["given"] + return(clean_str(r)) + }) + + + aut_sur <- tolower(paste0(unlist(aut_sur), collapse = "")) + aut_sur <- gsub("\\s*", "", aut_sur) + + # Try hard to remove accents + # First with iconv + aut_sur <- iconv(aut_sur, + from = "UTF-8", to = "ASCII//TRANSLIT", + sub = "?" + ) + + # Next to latex + aut_sur <- encoded_utf_to_latex(aut_sur) + + # Finally keep only a-z letters for key + aut_sur <- gsub("[^_a-z]", "", aut_sur) + + y <- tobibentry$year + + key <- paste(c(aut_sur, y), collapse = ":") + key +} + +make_howpublised <- function(x) { + howpublished <- x$medium + + if (!is.null(howpublished)) { + # Capitalize first letter + letts <- unlist(strsplit(howpublished, "|")) + howpublished <- + clean_str(paste0(c(toupper(letts[1]), letts[-1]), collapse = "")) + } + + clean_str(howpublished) +} diff --git a/R/assertions.R b/R/assertions.R index ddf705f7..245dc9e1 100644 --- a/R/assertions.R +++ b/R/assertions.R @@ -44,15 +44,11 @@ is_substring <- function(x, sub) { } } -#' Check if a object is cff +#' Check if a object is `cff` #' @param x object to be evaluated #' @noRd is_cff <- function(x) { - if (inherits(x, "cff")) { - return(TRUE) - } else { - return(FALSE) - } + inherits(x, "cff") } #' Check if a object is cff file @@ -71,7 +67,7 @@ is_cff_file <- function(x) { return(TRUE) } -#' Check if a object is cff +#' Check if an url is from GitHub #' @param x object to be evaluated #' @noRd is_github <- function(x) { @@ -83,7 +79,7 @@ is_github <- function(x) { return(res) } -#' Error if it is not a cff file +#' Error if it is not a `cff` file or object #' @param x file to be evaluated #' @noRd stopifnotcff <- function(x) { @@ -114,3 +110,10 @@ stopifnotexists <- function(x) { } return(invisible(NULL)) } + +#' Check if `x` has names +#' @param x object to be evaluated +#' @noRd +is_named <- function(x) { + !is.null(names(x)) +} diff --git a/R/cff-methods.R b/R/cff-methods.R index a3cd772c..d2c80e05 100644 --- a/R/cff-methods.R +++ b/R/cff-methods.R @@ -24,6 +24,7 @@ c.cff <- function(..., recursive = FALSE) { } +# nolint start #' Coerce to a Data Frame #' #' @noRd @@ -47,6 +48,7 @@ as.data.frame.cff <- function(x, row.names = NULL, optional = FALSE, ...) { return(the_df) } +# nolint end #' @rdname as_cff_person #' @name as.person.cff @@ -134,7 +136,7 @@ toBibtex.cff <- function(object, ..., class(object) <- c("cff", "list") } - bib_list <- lapply(object, cff_bibtex_parser) + bib_list <- lapply(object, make_bibentry) biblist_cff <- do.call(c, bib_list) } toBibtex(biblist_cff, ...) diff --git a/R/utils.R b/R/utils.R index 5689e8e7..1125fcf4 100644 --- a/R/utils.R +++ b/R/utils.R @@ -167,3 +167,46 @@ fuzzy_keys <- function(keys) { return(new_keys) } + +guess_cff_named_part <- function(x) { + nms <- names(x) + # Search for names + is_person <- any(grepl("^name$|family|given|particle", nms)) + if (is_person) { + return("cff_pers") + } + + # VALID full cff file + is_full <- any(grepl("cff-version|message", nms)) + if (is_full) { + return("cff_full") + } + + # Reference + is_ref <- any(grepl("title|type", nms)) + if (is_ref) { + return("cff_ref") + } + + # Else + return("unclear") +} + + +guess_cff_part <- function(x) { + named <- is_named(x) + if (named) { + return(guess_cff_named_part(x)) + } + + # Look to first element + guess <- guess_cff_named_part(x[[1]]) + + fin <- switch(guess, + "cff_pers" = "cff_pers_list", + "cff_ref" = "cff_ref_list", + "unclear" + ) + + fin +} diff --git a/README.md b/README.md index 74cf4226..2b438aed 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ file and the `CITATION` file (if present) of your package. Note that **cffr** works best if your package pass `R CMD check/devtools::check()`. -As per 2024-03-02 there are at least 306 repos on GitHub using **cffr**. +As per 2024-03-03 there are at least 306 repos on GitHub using **cffr**. [Check them out here](https://github.com/search?q=cffr%20path%3A**%2FCITATION.cff&type=code). diff --git a/codemeta.json b/codemeta.json index 974131e4..8ba3d173 100644 --- a/codemeta.json +++ b/codemeta.json @@ -200,7 +200,7 @@ }, "isPartOf": "https://ropensci.org", "keywords": ["attribution", "citation", "credit", "citation-files", "cff", "metadata", "r", "r-package", "citation-file-format", "rstats", "ropensci", "cran"], - "fileSize": "954.018KB", + "fileSize": "961.96KB", "citation": [ { "@type": "ScholarlyArticle", diff --git a/inst/schemaorg.json b/inst/schemaorg.json index 8056af78..dec7a9c6 100644 --- a/inst/schemaorg.json +++ b/inst/schemaorg.json @@ -26,6 +26,6 @@ "name": "Comprehensive R Archive Network (CRAN)", "url": "https://cran.r-project.org" }, - "runtimePlatform": "R version 4.3.2 (2023-10-31)", + "runtimePlatform": "R version 4.3.2 (2023-10-31 ucrt)", "version": "0.5.0.9000" } diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index 8d2d778f..ca293aec 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -14,48 +14,48 @@ home: description: >- Utilities to generate and validate CFF files with R. -# reference: -# - title: The whole game -# - subtitle: The function -# desc: >- -# The main function of the package and likely to be the only one you would -# need. -# contents: cff_write -# - subtitle: Other core functions -# desc: 'Create, modify and write and `cff` objects.' -# contents: has_concept("core") -# - subtitle: The `cff` class -# desc: Brief introduction to the `cff` class and S3 Methods available. -# contents: -# - cff-class -# - has_concept("s3method") -# - subtitle: Citation File Format schema -# desc: >- -# These functions provides lists of valid keys and fields as defined by the -# CFF schema.json **(v1.2.0)**. -# contents: has_concept("schemas") -# - title: Continuous Integration -# contents: has_concept("git") -# - title: Additional features -# - subtitle: Read and write files -# desc: >- -# Read and write local files on different formats (CFF files, BibTeX, -# etc.). -# contents: -# - has_concept("reading") -# - has_concept("writing") -# - subtitle: Coercing objects -# desc: >- -# Coerce **R** objects into different classes (`cff`, `person`, `bibentry`) -# and more: -# contents: has_concept("coercing") -# - subtitle: BibTeX helpers -# desc: Functions that works with BibTeX markup language. -# contents: has_concept("bibtex") -# - title: Datasets -# contents: has_concept("datasets") -# - title: About the package -# contents: cffr-package +reference: + - title: The whole game + - subtitle: The function + desc: >- + The main function of the package and likely to be the only one you would + need. + contents: cff_write + - subtitle: Other core functions + desc: 'Create, modify and write and `cff` objects.' + contents: has_concept("core") + - subtitle: The `cff` class + desc: Brief introduction to the `cff` class and S3 Methods available. + contents: + - cff-class + - has_concept("s3method") + - subtitle: Citation File Format schema + desc: >- + These functions provides lists of valid keys and fields as defined by the + CFF schema.json **(v1.2.0)**. + contents: has_concept("schemas") + - title: Continuous Integration + contents: has_concept("git") + - title: Additional features + - subtitle: Read and write files + desc: >- + Read and write local files on different formats (CFF files, BibTeX, + etc.). + contents: + - has_concept("reading") + - has_concept("writing") + - subtitle: Coercing objects + desc: >- + Coerce **R** objects into different classes (`cff`, `person`, `bibentry`) + and more: + contents: has_concept("coercing") + - subtitle: BibTeX helpers + desc: Functions that works with BibTeX markup language. + contents: has_concept("bibtex") + - title: Datasets + contents: has_concept("datasets") + - title: About the package + contents: cffr-package navbar: structure: diff --git a/tests/testthat/_snaps/as_bibentry.md b/tests/testthat/_snaps/as_bibentry.md index 063a6a62..6bb63ce4 100644 --- a/tests/testthat/_snaps/as_bibentry.md +++ b/tests/testthat/_snaps/as_bibentry.md @@ -224,6 +224,23 @@ organization = {IJCAI}, } +--- + + Code + toBibtex(bib) + Output + @InProceedings{aberdeenbayer:1999, + title = {Implementing Practical Dialogue Systems with the DARPA Communicator Architecture}, + author = {John Aberdeen and Samuel Bayer and Sasha Caskey and Laurie Damianos and Alan Goldschen and Lynette Hirschman and Dan Loehr and Hugo Trapper}, + year = {1999}, + booktitle = {I Am a conference}, + publisher = {International Joint Conference on Artificial Intelligence}, + address = {Murray Hill, New Jersey}, + editor = {Jan Alexandersson}, + pages = {81--86}, + organization = {IJCAI}, + } + # Manual to bibtex Code @@ -392,6 +409,18 @@ note = {Unpublished MS, Computer Science Department, University of Pittsburgh.}, } +--- + + Code + toBibtex(bib) + Output + @Unpublished{aronisprovost:1959, + title = {Efficiently Constructing Relational Features from Background}, + author = {John M. Aronis and Foster J. Provost}, + year = {1959}, + note = {Extracted with cffr R package}, + } + # particle names Code @@ -616,7 +645,7 @@ Code toBibtex(parsed) Output - @Misc{doe, + @Misc{doe:2020, title = {My Research Software}, author = {John Doe}, year = {2020}, diff --git a/tests/testthat/_snaps/cff_write_misc/CITAT_ION b/tests/testthat/_snaps/cff_write_misc/CITAT_ION index 7473bbd2..e54d5050 100644 --- a/tests/testthat/_snaps/cff_write_misc/CITAT_ION +++ b/tests/testthat/_snaps/cff_write_misc/CITAT_ION @@ -5,7 +5,7 @@ bibentry(bibtype = "Misc", family = "PĂ©rez")) bibentry(bibtype = "Misc", - key = "basic", + key = "basic:1999", title = "basicdescdate: A Basic Description with Date", author = person(given = "Marc", family = "Basic", diff --git a/tests/testthat/_snaps/xtra-check-bibtex-ruby.md b/tests/testthat/_snaps/xtra-check-bibtex-ruby.md index c80cd5ed..4e11e51f 100644 --- a/tests/testthat/_snaps/xtra-check-bibtex-ruby.md +++ b/tests/testthat/_snaps/xtra-check-bibtex-ruby.md @@ -102,7 +102,7 @@ Code toBibtex(bib) Output - @Article{hartmannwong, + @Article{hartmannwong:2020, title = {An image-based data-driven analysis of cellular architecture in a developing tissue}, author = {Jonas Hartmann and Mie Wong and Elisa Gallo and Darren Gilmour}, year = {2020}, diff --git a/tests/testthat/test-as_bibentry.R b/tests/testthat/test-as_bibentry.R index 1d7efcd5..0baf3dc6 100644 --- a/tests/testthat/test-as_bibentry.R +++ b/tests/testthat/test-as_bibentry.R @@ -1,3 +1,4 @@ +# Test Bibtex ---- test_that("Article to bibtex", { bib <- bibentry("Article", key = "knuth:1984", @@ -137,6 +138,12 @@ test_that("InProceedings to bibtex", { bibparsed <- as_cff(bib) bib <- as_bibentry(bibparsed) expect_snapshot(toBibtex(bib)) + + # If we remove collection title use conference + bibparsed[[1]]$`collection-title` <- NULL + bibparsed[[1]]$conference$name <- "I Am a conference" + bib <- as_bibentry(bibparsed) + expect_snapshot(toBibtex(bib)) }) @@ -244,8 +251,14 @@ test_that("Unpublished to bibtex", { bibparsed <- as_cff(bib) bib <- as_bibentry(bibparsed) expect_snapshot(toBibtex(bib)) + + # With custom note + bibparsed[[1]]$notes <- NULL + bib <- as_bibentry(bibparsed) + expect_snapshot(toBibtex(bib)) }) +# Other testers ---- test_that("particle names", { bib <- bibentry("Book", @@ -486,5 +499,7 @@ test_that("Corrupt entry", { }) test_that("Parser return nulls", { - expect_null(cff_bibtex_parser(NULL)) + expect_null(make_bibentry(NULL)) }) + +# Classes ----