Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

- fixed issue GeoBosh/Rdpack#40.

- bibtex readers now parse the biblatex fields `date`, `journaltitle` and
`location`.


# rbibutils 2.4.1

Expand Down
6 changes: 6 additions & 0 deletions inst/bib/00README.org
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 2026-09-16

=biblatex_aliases.bib= tests biblatex fields 'date', 'journaltitle' and 'location'
in the bibtex readers.


# 2025-11-04

add ~@string{j-GUTENBERG = Gutenberg journal}~ at the beginning of Cousquer*
Expand Down
25 changes: 25 additions & 0 deletions inst/bib/biblatex_aliases.bib
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@article{full,
author = {Smith, John}, title = {Full date},
JournalTitle = {Some Journal}, Date = {2020-02-02}, volume = {3}
}
@article{yearmonth,
author = {Jones, Ann}, title = {Year and month},
journaltitle = {J}, date = "2019-07"
}
@article{precedence,
author = {Doe, Jane}, title = {Biblatex fields take precedence},
journal = {Old Journal}, journaltitle = {New Journal},
year = {1999}, month = {dec}, day = {31}, date = {2001~/2002-03}
}
@book{location,
author = {Brown, Bob}, title = {Location as address}, publisher = {P},
location = {Durham}, date = {2021}
}
@book{address,
author = {Brown, Bob}, title = {Address wins, unparsable date ignored}, publisher = {P},
address = {London}, location = {Durham}, year = 2005, date = {circa 2004}
}
@patent{patent,
author = {Green, Gill}, title = {A patent}, number = {1},
location = {countryfr}, date = {2010-05}
}
6 changes: 6 additions & 0 deletions man/readBib.Rd
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ charToBib(text, informat, \dots)
non-standard fields in the bib entries are preserved in the bibentry
object.

Following pandoc and biber, \code{date} takes precedence over \code{year},
\code{month} and \code{day}, and \code{journaltitle} overrides
\code{journal}. \code{location} is used for \code{address} if the
latter is missing (except for patents). These biblatex fields are kept
when \code{direct = TRUE}.

Argument \code{texChars}, currently implemented only for the case
\code{direct = TRUE}, gives some control over the processing of TeX
sequences representing characters (such as accented Latin characters):
Expand Down
127 changes: 127 additions & 0 deletions src/common_bt_btd.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
*
*/

#include <ctype.h>
#include <string.h>

#include "str.h"
#include "fields.h"
#include "slist.h"
Expand All @@ -21,6 +24,128 @@
extern slist find;
extern slist replace;

/* 2026-09-16: biblatex aliases
*
* Biblatex (and hence e.g. Zotero's Better BibTeX export) uses fields 'date' and
* 'journaltitle' instead of bibtex's 'year'/'month'/'day' and 'journal', and 'location'
* as an alias of 'address'. Without 'year' and 'journal', R's bibentry() refuses to
* create entries of type 'Article', etc.
*
* Mirror pandoc (and biber): if present, 'date' and 'journaltitle' take precedence over
* 'year'/'month'/'day' and 'journal'; 'location' is used only if 'address' is missing
* (and not for patents).
* The biblatex fields themselves are kept.
*/

static const char *biblatex_month_names[12] = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};

/* Parse the start of a biblatex/EDTF date: [-]YYYY[-MM[-DD]], optionally followed by
* an end of range ('/...'), a time ('T...') or qualifiers ('?', '~', '%').
* Months 21-24 (seasons) are ignored.
* Returns 1 on success, 0 if 'date' is not recognised (then it is ignored).
*/
static int
parse_biblatex_date( const char *p, str *year, str *month, str *day )
{
int m = 0;
const char *q;

str_empty( year ); str_empty( month ); str_empty( day );

while ( isspace( (unsigned char) *p ) ) p++;

q = p;
if ( *q == '-' ) q++;
if ( !( isdigit( (unsigned char) q[0] ) && isdigit( (unsigned char) q[1] ) &&
isdigit( (unsigned char) q[2] ) && isdigit( (unsigned char) q[3] ) ) )
return 0;
q += 4;
str_segcpy( year, (char *) p, (char *) q );

if ( q[0] == '-' && isdigit( (unsigned char) q[1] ) && isdigit( (unsigned char) q[2] ) ) {
m = 10 * ( q[1] - '0' ) + ( q[2] - '0' );
q += 3;
if ( m >= 1 && m <= 12 ) {
str_strcpyc( month, biblatex_month_names[m - 1] );
if ( q[0] == '-' && isdigit( (unsigned char) q[1] ) &&
isdigit( (unsigned char) q[2] ) ) {
str_segcpy( day, (char *) q + 1, (char *) q + 3 );
q += 3;
}
} else if ( m < 21 || m > 24 ) {
return 0;
}
}

while ( *q == '?' || *q == '~' || *q == '%' ) q++;
while ( isspace( (unsigned char) *q ) ) q++;
if ( *q != '\0' && *q != '/' && *q != 'T' ) return 0;

return !str_memerr( year ) && !str_memerr( month ) && !str_memerr( day );
}

/* set field 'tag' to 'value', or remove all instances of 'tag' if 'value' is empty */
static int
biblatex_set_field( fields *bibin, const char *tag, str *value )
{
int i;

for ( i = fields_num( bibin ) - 1; i >= 0; i-- ) {
if ( !fields_match_casetag( bibin, i, tag ) ) continue;
if ( fields_remove( bibin, i ) != FIELDS_OK ) return BIBL_ERR_MEMERR;
}
if ( str_has_value( value ) &&
fields_add( bibin, tag, str_cstr( value ), LEVEL_MAIN ) != FIELDS_OK )
return BIBL_ERR_MEMERR;

return BIBL_OK;
}

static int
process_biblatex_aliases( fields *bibin )
{
int n, status = BIBL_OK;
str year, month, day, value;

strs_init( &year, &month, &day, &value, NULL );

n = fields_find( bibin, "date", LEVEL_MAIN );
if ( n != FIELDS_NOTFOUND &&
parse_biblatex_date( fields_value( bibin, n, FIELDS_CHRP ), &year, &month, &day ) ) {
status = biblatex_set_field( bibin, "year", &year );
if ( status == BIBL_OK ) status = biblatex_set_field( bibin, "month", &month );
if ( status == BIBL_OK ) status = biblatex_set_field( bibin, "day", &day );
if ( status != BIBL_OK ) goto out;
}

n = fields_find( bibin, "journaltitle", LEVEL_MAIN );
if ( n != FIELDS_NOTFOUND ) {
str_strcpyc( &value, fields_value( bibin, n, FIELDS_CHRP ) );
status = biblatex_set_field( bibin, "journal", &value );
if ( status != BIBL_OK ) goto out;
}

/* for patents biblatex's 'location' is the jurisdiction, not an address */
n = fields_find( bibin, "INTERNAL_TYPE", LEVEL_MAIN );
if ( n != FIELDS_NOTFOUND && !strcasecmp( fields_value( bibin, n, FIELDS_CHRP ), "patent" ) )
goto out;

if ( fields_find( bibin, "address", LEVEL_MAIN ) == FIELDS_NOTFOUND ) {
n = fields_find( bibin, "location", LEVEL_MAIN );
if ( n != FIELDS_NOTFOUND &&
fields_add( bibin, "address", fields_value( bibin, n, FIELDS_CHRP ),
LEVEL_MAIN ) != FIELDS_OK )
status = BIBL_ERR_MEMERR;
}

out:
strs_free( &year, &month, &day, &value, NULL );
return status;
}

/* process_ref()
*
*/
Expand Down Expand Up @@ -54,6 +179,8 @@ process_ref( fields *bibin, const char *p, loc *currloc )
if ( fstatus!=FIELDS_OK ) { status = BIBL_ERR_MEMERR; goto out; }

}

status = process_biblatex_aliases( bibin ); // 2026-09-16
out:
strs_free( &type, &id, &tag, &data, NULL );
return status;
Expand Down
34 changes: 34 additions & 0 deletions tests/testthat/test-bib.R
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,37 @@ test_that("bibRead works ok", {
if(is.numeric(svnrev <- R.Version()$'svn rev') && svnrev >= 84986)
expect_known_value(accfn, "acc_fn.rds", FALSE)
})

test_that("biblatex fields date, journaltitle and location are understood", {
fn <- system.file("bib", "biblatex_aliases.bib", package = "rbibutils")
## with direct = FALSE, the (pre-existing) conversion of type 'patent' to 'Misc'
## prints "Cannot identify TYPE in reference 6 patent"
for(direct in c(TRUE, FALSE)){
bib <- readBib(fn, direct = direct)
expect_setequal(names(bib), c("full", "yearmonth", "precedence",
"location", "address", "patent"))

expect_equal(bib[["full"]]$year, "2020")
expect_equal(bib[["full"]]$month, "February")
expect_equal(bib[["full"]]$day, "02")
expect_equal(bib[["full"]]$journal, "Some Journal")

expect_equal(bib[["yearmonth"]]$year, "2019")
expect_equal(bib[["yearmonth"]]$month, "July")
expect_null(bib[["yearmonth"]]$day)

## date and journaltitle take precedence (as in pandoc and biber)
expect_equal(bib[["precedence"]]$year, "2001")
expect_null(bib[["precedence"]]$month)
expect_null(bib[["precedence"]]$day)
expect_equal(bib[["precedence"]]$journal, "New Journal")

expect_equal(bib[["location"]]$address, "Durham")
## address takes precedence over location; unparsable date is ignored
expect_equal(bib[["address"]]$address, "London")
expect_equal(bib[["address"]]$year, "2005")

expect_null(bib[["patent"]]$address)
expect_equal(bib[["patent"]]$year, "2010")
}
})