6
\$\begingroup\$

Intro

I've been working to implement a modified version of the achemso.bst file from the American Chemical Society'sachemso package that properly implements patents as defined by the ACS Style Guide.

This is the first time that I've worked in a meaningful way with BibTeX Style files and while I am presently happy with the result - I really want to have someone take a look over the final code and jsut check for glaring or stupid mistakes.

Main Control Function

In general, the changes that I would like to have reviewed are best shown by the following definition.

FUNCTION { patent } {
  begin.bibitem
  format.authors
  format.assignees
  format.title
  format.type.number.patent  
  format.date
  format.CAS  
  format.note
  end.bibitem
}

That is, the BST file adds a definition for handling patents. To do this, I have added the functions format.assignees, format.date, format.type.number.patent and format.CAS. All other format. functions are as defined in the orignal achemso.bst file.

Additionally, the begin.bibitem and end.bibitem have been modified to wrap the entire entry in an \href{}{} wrapper if a url is present.

Added/Modified Functions

All of the added/modified functions, in order of my uncertainty in the implementation of them.

format.CAS function

This function constructs the Chemcial Abstract reference line at the end of the bibtex entry. It checks if both the CAS_CAN and CAS_AN exists. It then constructs the output string by adding the appropriate elements from left to right.

FUNCTION { format.CAS } {
  CAS_CAN duplicate$ empty$ not
    { 
      CAS_AN  duplicate$ empty$ not
        {
          "Chem. Abstr. " emph swap$ 
          "AN " "" find.replace 
            #1 #4 substring$
            add.comma bold
            *
          swap$
          "CAN " "" find.replace 
            ":" ",} " find.replace 
            "\emph{" swap$ *
            *
          output
        }
        { pop$ }
      if$
    }
    { pop$ }
  if$
}

Modifed begin.bibitem and end.bibitem functions

This section modifies the begin.bibitem and end.bibitem definitions to include a \href{}{} wrapper.

INTEGERS { href.present.bool }

FUNCTION { begin.bibitem } {
  newline$
  "\bibitem" write$
  label
  calculate.full.names
  duplicate$
  short.names =
    { pop$ }
    { * }
  if$ 
  "[" swap$ * "]" * write$
  "{" cite$ * "}" * write$
  newline$
  url empty$ not
  'href.present.bool := 
  href.present.bool
    {"\href{" url * "}{" * write$}
    { }
  if$
  ""
  next.punct.comma 'next.punct.int :=
}

FUNCTION { end.bibitem } {
  would.add.period
    {
      href.present.bool
        { "}" * }
        { }
      if$
      "\relax" * write$
      newline$
      "\mciteBstWouldAddEndPuncttrue" write$
      newline$
      "\mciteSetBstMidEndSepPunct{\mcitedefaultmidpunct}" write$
      newline$
      "{\mcitedefaultendpunct}{\mcitedefaultseppunct}\relax"
    }
    {
      href.present.bool
        { "}" * }
        { }
      if$
      "\relax" * write$
      newline$
      "\mciteBstWouldAddEndPunctfalse" write$
      newline$
      "\mciteSetBstMidEndSepPunct{\mcitedefaultmidpunct}" write$
      newline$
      "{}{\mcitedefaultseppunct}\relax"
    }
  if$
  write$
  newline$
  "\EndOfBibitem" write$
}

format.assignees function

This function pushes the assignee value to the stack, checks if is empty, then if not, formats it and outputs. Formatting includes changing AND to ;, and ensureing that the value is wrapped in parens.

FUNCTION { format.assignees } {
  assignee duplicate$ empty$
    { pop$ }
    { " AND" ";" find.replace.upper
      duplicate$
      duplicate$
      #-1 #1 substring$
      ")" =
      #1 #1 substring$
      "(" =
      and
        { paren }
        {  }
      if$
      output
      next.punct.period 'next.punct.int :=    
    }
  if$   
}

format.type.number.patent function

This function, and its helper function format the patent type and number, and output them.

FUNCTION { format.type.patent } {
  type empty$
    { "Patent" }{ type }
  if$ output
}

FUNCTION { format.type.number.patent } {
   number empty$
    { }
    {
      format.type.patent " " number * * output
      next.punct.comma 'next.punct.int :=
    }
  if$
}

Helper Functions

These are helper functions that are necesary for some of the above function to work correctly. I think that they are sufficent for this use case as is, but any improvements are appreciated.

% From           : https://tex.stackexchange.com/a/28104
% Originally from: http://ctan.org/pkg/tamethebeast
INTEGERS{ l }
FUNCTION{ string.length }
{ #1 'l :=
    { duplicate$ duplicate$ #1 l substring$ = not }
    { l #1 + 'l := }
  while$
  pop$ l
}

STRINGS{replace find text}
INTEGERS{find_length}
FUNCTION{find.replace}
{ 'replace :=
  'find :=
  'text :=
  find string.length 'find_length :=
  ""
    { text empty$ not }
    { text #1 find_length substring$ find =
        { replace *
          text #1 find_length + global.max$ substring$ 'text :=
        }
        { text #1 #1 substring$ *
          text #2 global.max$ substring$ 'text :=
        }
      if$
    }
  while$
}

% new code
FUNCTION{find.replace.upper}
{ 'replace :=
  'find :=
  'text :=
  find string.length 'find_length :=
  ""
    { text empty$ not }
    { text #1 find_length substring$ "u" change.case$ find =
        { replace *
          text #1 find_length + global.max$ substring$ 'text :=
        }
        { text #1 #1 substring$ *
          text #2 global.max$ substring$ 'text :=
        }
      if$
    }
  while$
}

FUNCTION{find.replace.ignorecase}
{ swap$
  "u" change.case$
  swap$
  find.replace.upper
}

Full BibTeX Style Definition

All of the above code are held in a single BST file, as

% Notes: this is an editied version of the ``achemso.bst'' file, used to develop the patent entry type based off of the specification of the ACS Style Guide (3rd ed. pp 310-311)

ENTRY
{
    abstract
    address
    assignee
    author
    booktitle
    CODEN
    CAS_AN
    CAS_CAN
    chapter
    ctrl-article-title
    ctrl-chapter-title
    ctrl-doi
    ctrl-etal-firstonly
    ctrl-etal-number
    ctrl-use-title
    date
    day
    doi
    edition
    editor
    howpublished
    institution
    journal
    key
    maintitle
    month
    note
    number
    organization
    pages
    publisher
    school
    series
    title
    type
    url
    version
    volume
    year
}
{ }
{
    label
    short.names
}

% Generic logic functions, from the core BibTeX styles

FUNCTION { and } {
    { }
    {
    pop$
    #0
    }
if$ 
}

FUNCTION { not } {
    { #0 }
    { #1 }
if$ 
}

FUNCTION { or } {
    {
    pop$
    #1
    }
    { }
if$ 
}

FUNCTION { xor } {
    { not }
    { }
if$ 
}

% Generic functions for basic manipulations: many of these
% come from the core BibTeX styles or 'Tame the BeaST'

FUNCTION { chr.to.value.error } {
#48 +
int.to.chr$
"'" swap$ *
"'" *
" is not a number: treated as zero." *
warning$
#0
}

FUNCTION { chr.to.value } {
chr.to.int$ #48 -
duplicate$
#0 <
    { chr.to.value.error }
    {
    duplicate$
    #9 >
        { chr.to.value.error }
        { }
    if$ 
    }
if$ 
}

STRINGS {
extract.input.str
extract.output.str
}

FUNCTION { is.a.digit } {
duplicate$
"" =
    {
    pop$
    #0
    }
    {
    chr.to.int$
    #48 -
    duplicate$
    #0 < swap$ #9 > or not
}
if$
}

FUNCTION{ is.a.number } {
{
    duplicate$
    #1 #1 substring$
    is.a.digit
}
    { #2 global.max$ substring$ }
while$
"" =
}

FUNCTION { extract.number } {
duplicate$
'extract.input.str :=
"" 'extract.output.str :=
{ extract.input.str empty$ not }
    {
    extract.input.str #1 #1 substring$
    extract.input.str #2 global.max$ substring$ 'extract.input.str :=
    duplicate$
    is.a.number
        { extract.output.str swap$ * 'extract.output.str := }
        {
        pop$
        "" 'extract.input.str :=
        }
    if$
    }
while$
extract.output.str empty$
    { }
    {
    pop$
    extract.output.str
    }
if$
}

FUNCTION { field.or.null } {
duplicate$
empty$
    {
    pop$
    ""
    }
    { }
if$
}

INTEGERS {
multiply.a.int
multiply.b.int
}

FUNCTION { multiply } {
'multiply.a.int :=
'multiply.b.int :=
multiply.b.int #0 <
    {
    #-1
    #0 multiply.b.int - 'multiply.b.int :=
    }
    { #1 }
if$
#0
{ multiply.b.int #0 > } 
    {
    multiply.a.int +
    multiply.b.int #1 - 'multiply.b.int :=
    }
while$
swap$
    { }
    { #0 swap$ - }
if$ 
}

INTEGERS { str.conversion.int }

FUNCTION { str.to.int.aux.ii } {
{
    duplicate$
    empty$ not
}
    {
    swap$
    #10 multiply 'str.conversion.int :=
    duplicate$
    #1 #1 substring$
    chr.to.value
    str.conversion.int +
    swap$
    #2 global.max$ substring$
    }
while$
pop$
}

FUNCTION { str.to.int.aux.i } {
duplicate$
#1 #1 substring$
"-" =
{
    #1 swap$
    #2 global.max$ substring$
    }
    {
    #0 swap$
    }
if$ 
#0
swap$
str.to.int.aux.ii
swap$
    { #0 swap$ - }
    { }
if$
}

FUNCTION { str.to.int } {
duplicate$
empty$
    {
    pop$
    #0
    }
    { str.to.int.aux.i }
if$ 
}

FUNCTION { tie.or.space.connect } {
duplicate$
text.length$ #3 <
    { "~" }
    { " " }
if$
swap$ * *
}

FUNCTION { yes.no.to.bool } {
duplicate$
empty$
    {
    pop$
    #0
    }
    {
    "l" change.case$
    "yes" =
        { #1 }
        { #0 }
    if$ 
    }
if$ 
}

% Functions of formatting

FUNCTION { bold } {
duplicate$
empty$
    {
    pop$
    ""
    }
    { "\textbf{" swap$ * "}" * }
if$
}

FUNCTION { emph } {
duplicate$
empty$
    {
    pop$
    ""
    }
    { "\emph{" swap$ * "}" * }
if$
}


FUNCTION { paren } {
duplicate$
empty$
    {
    pop$
    ""
    }
    { "(" swap$ * ")" * }
if$
}

% Functions for punctuation

FUNCTION { add.comma } { ", " * }
FUNCTION { add.colon } {  ": " * }
FUNCTION { add.period } { add.period$ " " * }
FUNCTION { add.semicolon } { "; " * }
FUNCTION { add.space } { " " * }

% Bibliography strings: fixed values collected into functions

FUNCTION { bbl.and }     { "and" }
FUNCTION { bbl.chapter } { "Chapter" }
FUNCTION { bbl.doi }     { "DOI:" }
FUNCTION { bbl.editor }  { "Ed." }
FUNCTION { bbl.editors } { "Eds." }
FUNCTION { bbl.edition } { "ed." }
FUNCTION { bbl.etal }    { "\latin{et~al.}" }
FUNCTION { bbl.in }      { "In" }
FUNCTION { bbl.inpress } { "in press" }
FUNCTION { bbl.msc }     { "M.Sc.\ thesis" }
FUNCTION { bbl.page }    { "p" }
FUNCTION { bbl.pages }   { "pp" }
FUNCTION { bbl.phd }     { "Ph.D.\ thesis" }
FUNCTION { bbl.version } { "version" }
FUNCTION { bbl.volume }  { "Vol." }

% Functions for number formatting

STRINGS { pages.str }

FUNCTION { hyphen.to.dash } {
'pages.str :=
""
{ pages.str empty$ not }
    {
    pages.str #1 #1 substring$
    "-" =
        {
        "--" *
        {
            pages.str #1 #1 substring$
            "-" =
        }
            { pages.str #2 global.max$ substring$ 'pages.str := }
        while$
        }
        {
        pages.str #1 #1 substring$
        *
        pages.str #2 global.max$ substring$ 'pages.str :=
        }
    if$
    }
while$
}

INTEGERS { multiresult.bool }

FUNCTION { multi.page.check } {
'pages.str :=
#0 'multiresult.bool :=
{
    multiresult.bool not
    pages.str empty$ not
    and
}
    {
    pages.str #1 #1 substring$
    duplicate$
    "-" = swap$ duplicate$
    "," = swap$
    "+" =
    or or
        { #1 'multiresult.bool := }
        { pages.str #2 global.max$ substring$ 'pages.str := }
    if$
    }
while$
multiresult.bool
}

% Functions for calculating the label data needed by natbib


INTEGERS {
current.name.int
names.separate.comma
names.separate.semicolon
names.separate.comma.bool
remaining.names.int
total.names.int
}

STRINGS {
current.name.str
names.str
}

FUNCTION { full.format.names } {
'names.str :=
#1 'current.name.int :=
names.str num.names$ 'remaining.names.int :=
{ remaining.names.int #0 > }
    {
    names.str current.name.int "{vv~}{ll}" format.name$
    current.name.int #1 >
        {
        swap$ add.comma swap$
        remaining.names.int #1 >
            { }
            {
            duplicate$
            "others" =
                { bbl.etal }
                { bbl.and }
            if$
            add.space swap$ *
            }
        if$
        *
        }
        { }
    if$   
    remaining.names.int #1 - 'remaining.names.int :=
    current.name.int #1 + 'current.name.int :=
    }
while$
}

FUNCTION { full.author } {
author empty$
    { "" }
    { author full.format.names }
if$ 
}

FUNCTION { full.author.editor } {
author empty$
    {
    editor empty$
        { "" }
        { editor full.format.names }
    if$
    }
    { author full.format.names }
if$ 
}

FUNCTION { full.editor } {
editor empty$
    { "" }
    { editor full.format.names }
if$ 
}

FUNCTION { short.format.names } {
'names.str :=
names.str #1 "{vv~}{ll}" format.name$
names.str num.names$
duplicate$
#2 >
    {
    pop$
    add.space bbl.etal *
    }
    {
    #2 <
        { }
        {
        names.str #2 "{ff }{vv }{ll}{ jj}" format.name$
        "others" =
            { add.space bbl.etal * }
            {
            add.space
            bbl.and add.space
            *
            names.str #2 "{vv~}{ll}" format.name$
            *
            }
        if$
        }
    if$ 
    }
if$ 
}

FUNCTION { short.author.key } {
author empty$
    {
    key empty$
        { cite$ #1 #3 substring$ }
        { key }
    if$ 
    }
    { author short.format.names }
if$ 
}

FUNCTION { short.author.editor.key } {
author empty$
    {
    editor empty$
        {
        key empty$
            { cite$ #1 #3 substring$ }
            { key }
        if$ 
        }
        { editor short.format.names }
    if$
    }
    { author short.format.names }
if$
}

FUNCTION { short.author.key.organization } {
author empty$
    {
    key empty$
        {
        organization empty$
            { cite$ #1 #3 substring$ }
            {
            organization #1 #4 substring$
            "The " =
                { organization }
                { organization #5 global.max$ substring$ }
            if$
            #3 text.prefix$
            }
        if$ 
        }
        { key }
    if$ 
    }
    { author short.format.names }
if$ 
}

FUNCTION { short.editor.key.organization } {
editor empty$
    {
    key empty$
        {
        organization empty$
            { cite$ #1 #3 substring$ }
            {
            organization #1 #4 substring$
            "The " =
                { organization }
                { organization #5 global.max$ substring$ }
            if$
            #3 text.prefix$
            }
        if$ 
        }
        { key }
    if$ 
    }
    { editor short.format.names }
if$ 
}

FUNCTION { calculate.full.names } {
type$ "book" =
type$ "inbook" =
or
    { full.author.editor }
    {
    type$ "proceedings" =
        { full.editor }
        { full.author }
    if$ 
    }
if$
}

FUNCTION { calculate.short.names } {
type$ "book" =
type$ "inbook" =
or
    { short.author.editor.key }
    {
    type$ "proceedings" =
        { short.editor.key.organization }
        {
        type$ "manual" =
            { short.author.key.organization }
            { short.author.key }
        if$ 
        }
    if$ 
    }
if$
'short.names :=
}

FUNCTION { calculate.names } {
calculate.short.names
short.names
year empty$
    { "()" }
    { "(" year * ")" * }
if$
*
'label :=
}

% Counting up the number of entries

INTEGERS { entries.int }

FUNCTION { initialize.count.entries } {
#0 'entries.int  :=
}

FUNCTION { count.entries } {
entries.int #1 + 'entries.int  :=
}

% Start and end of bibliography functions

FUNCTION { begin.bib } {
"achemso 2019-02-14 v3.12a" top$
preamble$ empty$
    { }
    {
    preamble$ write$
    newline$
    }
if$
"\providecommand{\latin}[1]{#1}" write$
newline$
"\makeatletter" write$
newline$
"\providecommand{\doi}" write$
newline$
"  {\begingroup\let\do\@makeother\dospecials" write$
newline$
"  \catcode`\{=1 \catcode`\}=2 \doi@aux}" write$
newline$
"\providecommand{\doi@aux}[1]{\endgroup\texttt{#1}}" write$
newline$
"\makeatother" write$
newline$
"\providecommand*\mcitethebibliography{\thebibliography}" write$
newline$
"\csname @ifundefined\endcsname{endmcitethebibliography}" write$
"  {\let\endmcitethebibliography\endthebibliography}{}" write$
newline$
"\begin{mcitethebibliography}{"
    entries.int int.to.str$  * "}" * write$
newline$
"\providecommand*\natexlab[1]{#1}" write$
newline$
"\providecommand*\mciteSetBstSublistMode[1]{}" write$
newline$
"\providecommand*\mciteSetBstMaxWidthForm[2]{}" write$
newline$
"\providecommand*\mciteBstWouldAddEndPuncttrue" write$
newline$
"  {\def\EndOfBibitem{\unskip.}}" write$
newline$
"\providecommand*\mciteBstWouldAddEndPunctfalse" write$
newline$
"  {\let\EndOfBibitem\relax}" write$
newline$
"\providecommand*\mciteSetBstMidEndSepPunct[3]{}" write$
newline$
"\providecommand*\mciteSetBstSublistLabelBeginEnd[3]{}" write$
newline$
"\providecommand*\EndOfBibitem{}" write$
newline$
"\mciteSetBstSublistMode{f}" write$
newline$
"\mciteSetBstMaxWidthForm{subitem}{(\alph{mcitesubitemcount})}" write$
newline$
"\mciteSetBstSublistLabelBeginEnd" write$
newline$
"  {\mcitemaxwidthsubitemform\space}" write$
newline$
"  {\relax}" write$
newline$
"  {\relax}" write$
newline$    
}

FUNCTION { end.bib } {
newline$
"\end{mcitethebibliography}" write$
newline$
}

% Functions used for the special "control" entry type, to pass data
% from LaTeX to BibTeX

INTEGERS {
ctrl.article.title.bool
ctrl.chapter.title.bool
ctrl.doi.bool
ctrl.etal.firstonly.bool
ctrl.etal.number.int
}

FUNCTION { initialize.control.values } {
#1 'ctrl.article.title.bool :=
#0 'ctrl.chapter.title.bool :=
#0 'ctrl.doi.bool :=
#1 'ctrl.etal.firstonly.bool :=
#15 'ctrl.etal.number.int :=
}

FUNCTION { control } {
ctrl-article-title  yes.no.to.bool 'ctrl.article.title.bool  :=
ctrl-chapter-title  yes.no.to.bool 'ctrl.chapter.title.bool  :=
ctrl-doi            yes.no.to.bool 'ctrl.doi.bool            :=
ctrl-etal-firstonly yes.no.to.bool 'ctrl.etal.firstonly.bool :=
ctrl-etal-number    str.to.int     'ctrl.etal.number.int     :=
ctrl-use-title empty$
    'skip$
    { ctrl-use-title yes.no.to.bool 'ctrl.article.title.bool  := }
if$
}

% Functions of each bibitem: tracking punctuation and transferring
% items to the .bbl file

INTEGERS {
next.punct.comma
next.punct.period
next.punct.semicolon
next.punct.space
}

FUNCTION { initialize.tracker } {
#0 'next.punct.comma     :=
#1 'next.punct.period    :=
#2 'next.punct.semicolon :=
#3 'next.punct.space     :=
}

INTEGERS { next.punct.int }

FUNCTION { output } {
swap$
duplicate$ empty$
    { pop$ }
    {
    next.punct.int next.punct.space =
        { add.space }
        {
        next.punct.int next.punct.comma =
            { add.comma }
            {
            next.punct.int next.punct.semicolon =
                { add.semicolon }
                { add.period }
            if$ 
            }
        if$ 
        }
    if$
    write$
    }
if$  
next.punct.comma 'next.punct.int :=
}


%<Edited>
INTEGERS { href.present.bool }

% Functions for each bibliography entry: start and finish
FUNCTION { begin.bibitem } {
    newline$
    "\bibitem" write$
    label
    calculate.full.names
    duplicate$
    short.names =
    { pop$ }
    { * }
    if$ 
    "[" swap$ * "]" * write$
    "{" cite$ * "}" * write$
    newline$
    url empty$ not
    'href.present.bool := 
    href.present.bool
    {"\href{" url * "}{" * write$}
    { }
    if$
    ""
    next.punct.comma 'next.punct.int :=
}

INTEGERS { add.period.length.int }

FUNCTION { would.add.period } {
duplicate$
add.period$
text.length$ 'add.period.length.int :=
duplicate$
text.length$
add.period.length.int =
    { #0 }
    { #1 }
if$
}

FUNCTION { end.bibitem } {
would.add.period
    {
    href.present.bool
        { "}" * }
        { }
    if$
    "\relax" * write$
    newline$
    "\mciteBstWouldAddEndPuncttrue" write$
    newline$
    "\mciteSetBstMidEndSepPunct{\mcitedefaultmidpunct}" write$
    newline$
    "{\mcitedefaultendpunct}{\mcitedefaultseppunct}\relax"
    }
    {
    href.present.bool
    { "}" * }
    { }
    if$
    "\relax" * write$
    newline$
    "\mciteBstWouldAddEndPunctfalse" write$
    newline$
    "\mciteSetBstMidEndSepPunct{\mcitedefaultmidpunct}" write$
    newline$
    "{}{\mcitedefaultseppunct}\relax"
    }
if$
write$
newline$
"\EndOfBibitem" write$
}
%<!Edited>

% Formatting names: authors and editors are not quite the same,
% and there is the question of how to handle 'et al.'

FUNCTION { initialize.name.separator } {
#1 'names.separate.comma     :=
#0 'names.separate.semicolon :=
}

FUNCTION { format.names.loop } {
{ remaining.names.int #0 > }
    {
    names.str current.name.int "{vv~}{ll,}{~f.}{,~jj}" format.name$
    duplicate$
    'current.name.str :=
    current.name.int #1 >
        {
        duplicate$
        "others," =
            {
            pop$
            *
            bbl.etal
            add.space
            remaining.names.int #1 - 'remaining.names.int :=
            }
            { 
            swap$
            names.separate.comma.bool
                { add.comma }
                { add.semicolon } 
            if$
%<*bio>
%              remaining.names.int #1 >
%                { }
%                { bbl.and add.space * }
%              if$ 
%</bio>s
            swap$
            *
            }
        if$  
        }
        { }
    if$   
    remaining.names.int #1 - 'remaining.names.int :=
    current.name.int #1 + 'current.name.int :=
    }
while$
}

FUNCTION { format.names.all } {
total.names.int 'remaining.names.int :=
format.names.loop
}

FUNCTION { format.names.etal } {
ctrl.etal.firstonly.bool
    { #1 'remaining.names.int := }
    { ctrl.etal.number.int 'remaining.names.int := }
if$ 
format.names.loop
current.name.str "others," =
    { }
    {
    add.space
    bbl.etal
    add.space
    *
    }
if$ 
}

FUNCTION { format.names } {
'names.separate.comma.bool :=
'names.str :=
#1 'current.name.int :=
names.str num.names$ 'total.names.int :=
total.names.int ctrl.etal.number.int >
    {
    ctrl.etal.number.int #0 =
        { format.names.all }
        { format.names.etal }
    if$ 
    }
    { format.names.all }
if$ 
}

%<Added>

% From           : https://tex.stackexchange.com/a/28104
% Originally from: http://ctan.org/pkg/tamethebeast
INTEGERS{ l }
FUNCTION{ string.length }
{
#1 'l :=
{duplicate$ duplicate$ #1 l substring$ = not}
    {l #1 + 'l :=}
while$
pop$ l
}

STRINGS{replace find text}
INTEGERS{find_length}
FUNCTION{find.replace}
{ 'replace :=
'find :=
'text :=
find string.length 'find_length :=
""
    { text empty$ not }
    { text #1 find_length substring$ find =
        {
        replace *
        text #1 find_length + global.max$ substring$ 'text :=
        }
        { text #1 #1 substring$ *
        text #2 global.max$ substring$ 'text :=
        }
    if$
    }
while$
}
% new code
FUNCTION{find.replace.upper}
{ 'replace :=
'find :=
'text :=
find string.length 'find_length :=
""
    { text empty$ not }
    { text #1 find_length substring$ "u" change.case$ find =
        {
        replace *
        text #1 find_length + global.max$ substring$ 'text :=
        }
        { text #1 #1 substring$ *
        text #2 global.max$ substring$ 'text :=
        }
    if$
    }
while$
}
FUNCTION{find.replace.ignorecase}
{ swap$
"u" change.case$
swap$
find.replace.upper
}
% if assignee is not empty then check replace and w/ `;`. then check if it is parened. if not paren
FUNCTION { format.assignees } {
    assignee duplicate$ empty$
    { pop$ }
    { " AND" ";" find.replace.upper
    duplicate$
    duplicate$
    #-1 #1 substring$
    ")" =
    #1 #1 substring$
    "(" =
    and
        { paren }
        {  }
    if$
    output
    next.punct.period 'next.punct.int :=    
    }
if$   
}

FUNCTION { format.CAS } {
CAS_CAN duplicate$ empty$ not
    { 
    CAS_AN  duplicate$ empty$ not
        {
            "Chem. Abstr. " emph swap$ 
            "AN " "" find.replace 
                #1 #4 substring$
                add.comma bold
                *
            swap$
            "CAN " "" find.replace 
                ":" ",} " find.replace 
                "\emph{" swap$ *
                *
            output
        }
        { pop$ }
    if$
    }
    { pop$ }
if$
}

%</Added>

% Converting editions into their fixed representations

FUNCTION { bbl.first }  { "1st" }
FUNCTION { bbl.second } { "2nd" }
FUNCTION { bbl.third }  { "3rd" }
FUNCTION { bbl.fourth } { "4th" }
FUNCTION { bbl.fifth }  { "5th" }
FUNCTION { bbl.st }     { "st" }
FUNCTION { bbl.nd }     { "nd" }
FUNCTION { bbl.rd }     { "rd" }
FUNCTION { bbl.th }     { "th" }

STRINGS {
ord.input.str
ord.output.str
}

FUNCTION { make.ordinal } {
duplicate$
"1" swap$
*
#-2 #1 substring$
"1" =
    {
    bbl.th *
    }
    {
    duplicate$
    #-1 #1 substring$
    duplicate$
    "1" =
        {
        pop$
        bbl.st *
        }
        {
        duplicate$
        "2" =
            {
            pop$
            bbl.nd *
            }
            {
            "3" =
                { bbl.rd * }
                { bbl.th * }
            if$ 
            }
        if$
        }
    if$
    }
if$ 
}

FUNCTION { convert.to.ordinal } {
extract.number
"l" change.case$ 'ord.input.str :=
ord.input.str "first" = ord.input.str "1" = or
    { bbl.first 'ord.output.str := }
    {
    ord.input.str "second" = ord.input.str "2" = or
        { bbl.second 'ord.output.str := }
        {
        ord.input.str "third" = ord.input.str "3" = or
            { bbl.third 'ord.output.str := }
            {
            ord.input.str "fourth" = ord.input.str "4" = or
                { bbl.fourth 'ord.output.str := }
                {
                ord.input.str "fifth" = ord.input.str "5" = or
                    { bbl.fifth 'ord.output.str := }
                    {
                    ord.input.str #1 #1 substring$
                    is.a.number
                        { ord.input.str make.ordinal }
                        { ord.input.str }
                    if$
                    'ord.output.str :=
                    }
                if$
                }
            if$ 
            }
        if$ 
        }
    if$ 
    }
if$
ord.output.str
}

% Functions for each type of entry

FUNCTION { format.address } {
address empty$
    { }
    {
    address
    output
    }
if$
}

FUNCTION { format.authors } {
author empty$
    { }
    {
%<*bio>
%      author names.separate.comma format.names
%</bio>
%<*!bio>
    author names.separate.semicolon format.names
%</!bio>
    output
    next.punct.space 'next.punct.int :=
    }
if$   
}

FUNCTION { format.editors } {
editor empty$
    { }
    {
    editor names.separate.comma format.names
    add.comma
    editor num.names$ #1 >
        { bbl.editors }
        { bbl.editor }
    if$ 
    *
    output
    next.punct.semicolon 'next.punct.int :=
    } 
if$ 
}

FUNCTION { format.authors.or.editors } {
author empty$
    { format.editors }
    { format.authors }
if$ 
next.punct.space 'next.punct.int :=
}

FUNCTION { format.chapter } {
chapter empty$
    { }
    {
    bbl.chapter add.space
    chapter
    *
    output
    }
if$
}

FUNCTION { format.doi } {
doi empty$
    'skip$
    {
    bbl.doi add.space
    "\doi{" * doi * "}" *
    output
    }
if$
}

FUNCTION { format.edition } {
edition empty$
    { }
    {
    edition convert.to.ordinal
    add.space bbl.edition *
    output
    }
if$ 
next.punct.semicolon 'next.punct.int :=
}

FUNCTION { format.group.address } {
duplicate$
empty$
    { pop$ }
    {
    address empty$
        { }
        {
        add.colon
        address
        *
        }
    if$ 
    output
    }
if$ 
}

FUNCTION { format.howpublished } {
howpublished empty$
    { }
    {
    howpublished
    output
    }
if$
}

FUNCTION { format.journal } {
journal emph
output
next.punct.space 'next.punct.int :=
}

FUNCTION { format.journal.unpub } { journal emph output }
FUNCTION { format.note } { note empty$ { }{ note output } if$ }

FUNCTION { format.number.series } {
series empty$
    { }
    {
    series
    number empty$
        { }
        {
        add.space
        number *
        }
    if$ 
    output
    next.punct.semicolon 'next.punct.int :=
    }
if$ 
}

FUNCTION { format.organization } {
organization empty$
    { }
    {
    organization paren
    output
    next.punct.period 'next.punct.int :=
    }
if$ 
}

FUNCTION { format.organization.address } { organization format.group.address }

FUNCTION { format.pages } {
pages empty$
    { }
    {
    pages multi.page.check
        {
        bbl.pages
        pages hyphen.to.dash
        }
        { bbl.page pages }
    if$
    tie.or.space.connect
    output
    }
if$
ctrl.doi.bool
    { format.doi }
    'skip$
if$
}

FUNCTION { format.pages.article } {
pages empty$
    { }
    {
    pages hyphen.to.dash
    output
    }
if$
ctrl.doi.bool
    { format.doi }
    'skip$
if$
}

FUNCTION { format.publisher.address } {
publisher format.group.address
}

FUNCTION { format.school.address } { 
school
duplicate$
empty$
    { pop$ }
    {
    address empty$
        { }
        {
        add.comma
        address
        *
        }
    if$ 
    output
    }
if$
}

FUNCTION { format.title } {
title empty$
    { }
    {
    title
    output
    next.punct.period 'next.punct.int :=
    }
if$
}

FUNCTION { format.title.article } {
ctrl.article.title.bool
    {
    title empty$
        { }
        {
        title
        output
        next.punct.period 'next.punct.int :=
        }
    if$ 
    }
    { }
if$
}

FUNCTION { format.title.techreport } {
title empty$
    { }
    {
    title emph
    output
    next.punct.semicolon 'next.punct.int :=
    }
if$
}

FUNCTION { format.title.booktitle } {
title empty$
    { }
    {
    title
    output
    next.punct.period 'next.punct.int :=
    }
if$ 
booktitle empty$
    { }
    {
    booktitle
    output
    next.punct.period 'next.punct.int :=
    }
if$   
}

STRINGS {
book.title
chapter.title
}

FUNCTION { format.title.booktitle.book } {
"" 'chapter.title :=
booktitle empty$
    {
    "" 'chapter.title :=
    title 'book.title :=
    }
    {
    ctrl.chapter.title.bool
        {
        title empty$
            'skip$
            { title 'chapter.title := }
        if$
        }
        'skip$
    if$
    maintitle empty$
        { booktitle 'book.title := }
        { maintitle add.period booktitle * 'book.title := }
    if$ 
    }
if$
chapter.title empty$
    { }
    {
    chapter.title
    output
    next.punct.period 'next.punct.int :=
    }
if$ 
book.title emph
chapter.title empty$
    {
    author empty$
        { }
        {
        editor empty$
            { }
            { bbl.in add.space swap$ * }
        if$  
        }
    if$ 
    }
    { bbl.in add.space swap$ * }
if$ 
output
}

FUNCTION { format.type } {
type empty$
    { }
    {
    pop$
    type
    }
if$
output
}

FUNCTION { format.type.number } {
type empty$
    { }
    {
    number empty$
        { }
        { 
        type 
        number tie.or.space.connect 
        *
        output        
        }
    if$ 
    }
if$
}
FUNCTION { format.type.patent } {
type empty$
    { "Patent" }{ type }
if$ output
}

FUNCTION { format.type.number.patent } {
number empty$
    { }{
    format.type.patent " " number * * output
    next.punct.comma 'next.punct.int :=
    }
if$
}

FUNCTION { format.url } {
url empty$
    { }
    {
    "\url{" url * "}" *
    output
    }
if$ 
}

FUNCTION { format.year } {
year empty$
    { }
    {
    year
    output
    next.punct.semicolon 'next.punct.int :=
    }
if$ 
}

STRINGS  { sYear sMonth sDay d}
INTEGERS { iYear iMonth iDay } 

FUNCTION { format.date } {

"" 'sYear  := 
"" 'sMonth := 
"" 'sDay   := 

date empty$ not 
    { % if the date is note empty, then 
    
    % split the date into year month and day
    date #1 #4 substring$ 'sYear  :=
    date #6 #2 substring$ 'sMonth :=
    date #9 #2 substring$ 'sDay   := 

    % remove leading zero from sMonth and sDay iff leading zeros exist
    sMonth #1 #1 substring$ "0" = { sMonth #2 #1 substring$ 'sMonth := }{ } if$
    sDay   #1 #1 substring$ "0" = { sDay   #2 #1 substring$ 'sDay   := }{ } if$

    }{ % else, gather values from appropriate fields

    year  empty$ not {year }{""}if$ 'sYear  := 
    month empty$ not {month}{""}if$ 'sMonth := 
    day   empty$ not {day  }{""}if$ 'sDay   := 
    
    } if$    

    % Convert sMonth from number string to abbreviated month
    sMonth "1" = sMonth "01" = or { "Jan"   'sMonth := }{ } if$
    sMonth "2" = sMonth "02" = or { "Feb"   'sMonth := }{ } if$
    sMonth "3" = sMonth "03" = or { "March" 'sMonth := }{ } if$
    sMonth "4" = sMonth "04" = or { "April" 'sMonth := }{ } if$
    sMonth "5" = sMonth "05" = or { "May"   'sMonth := }{ } if$
    sMonth "6" = sMonth "06" = or { "June"  'sMonth := }{ } if$
    sMonth "7" = sMonth "07" = or { "July"  'sMonth := }{ } if$
    sMonth "8" = sMonth "08" = or { "Aug"   'sMonth := }{ } if$
    sMonth "9" = sMonth "09" = or { "Sept"  'sMonth := }{ } if$
    sMonth "10" =                 { "Oct"   'sMonth := }{ } if$
    sMonth "11" =                 { "Nov"   'sMonth := }{ } if$
    sMonth "12" =                 { "Dec"   'sMonth := }{ } if$


    sYear empty$ not {
    sMonth empty$ not {
        sMonth sDay empty$ not {" " sDay *}{ "" }if$ ", " * *
        }{ "" }if$
    sYear * output
    }{}if$ 

    next.punct.semicolon 'next.punct.int :=
}

FUNCTION { format.year.article } {
year empty$
    { }
    {
%<*bio>
%      year paren
%      output
%      next.punct.space 'next.punct.int :=
%</bio>
%<*!bio>
    year bold
    output
%</!bio>
    }
if$
}

FUNCTION { format.version } {
version empty$
    { }
    {
    bbl.version add.space
    version
    *
    output
    } 
if$ 
}

FUNCTION { format.volume.article } {
volume emph
output
}

FUNCTION { format.volume } {
volume empty$
    { }
    {
    bbl.volume
    volume
    tie.or.space.connect
    output
    next.punct.semicolon 'next.punct.int :=
    }
if$   
}

% The functions to deal with each entry type

FUNCTION { article } {
begin.bibitem
format.authors
%<*bio>
%  format.year.article
%</bio>
format.title.article
format.journal
%<*!bio>
format.year.article
%</!bio>
format.volume.article
format.pages.article
format.note
end.bibitem
}

FUNCTION { book } {
begin.bibitem
format.authors.or.editors
format.title.booktitle.book
format.edition
author empty$
    { }
    { format.editors }
if$
format.number.series
format.publisher.address
format.year
format.volume
format.chapter
format.pages
format.note
end.bibitem
}

FUNCTION { inbook } { book }

FUNCTION { booklet } {
begin.bibitem
format.authors
format.title
format.howpublished
format.address
format.year
format.note
end.bibitem
}

FUNCTION { collection } { book }

FUNCTION { incollection } { book }

FUNCTION { inpress } {
begin.bibitem
format.authors
%<*bio>
%  format.year.article
%</bio>
format.journal.unpub
doi empty$
    {
    bbl.inpress
    output
    }
    {
%<*!bio>
    format.year.article
%</!bio>
    next.punct.comma 'next.punct.int :=
    format.doi
    }
if$ 
format.note
end.bibitem
}

FUNCTION { inproceedings } {
begin.bibitem
format.authors
format.title.booktitle
format.address
format.year
format.pages
format.note
end.bibitem
}

FUNCTION { manual } {
begin.bibitem
format.authors
format.title
format.version
format.organization.address
format.year
format.note
end.bibitem
}

FUNCTION { mastersthesis } {
begin.bibitem
format.authors
format.title
bbl.msc format.type
format.school.address
format.year
format.note
end.bibitem
}

FUNCTION { misc } {
begin.bibitem
format.authors
format.title
format.howpublished
format.year
format.url
format.note
end.bibitem
}

FUNCTION { patent } {
begin.bibitem
format.authors
format.assignees
format.title
format.type.number.patent  
format.date
format.CAS  
format.note
end.bibitem
}

FUNCTION { phdthesis } {
begin.bibitem
format.authors
format.title
bbl.phd format.type
format.school.address
format.year
format.note
end.bibitem
}

FUNCTION { proceeding } {
begin.bibitem
format.title
format.address
format.year
format.pages
format.note
end.bibitem
}

FUNCTION { techreport } {
begin.bibitem
format.authors.or.editors
format.title.techreport
format.type.number
format.organization.address
format.year
format.volume
format.pages
format.note
end.bibitem
}

FUNCTION { unpublished } {
begin.bibitem
format.authors
format.journal.unpub
doi empty$
    { format.howpublished }
    {
    format.year
    next.punct.comma 'next.punct.int :=
    format.doi
    }
if$ 
format.note
end.bibitem
}

FUNCTION { default.type } { misc }

% Macros containing pre-defined short cuts

% patent macros per the biblatex documentaion
% See page 283 of http://mirrors.ibiblio.org/CTAN/macros/latex/contrib/biblatex/doc/biblatex.pdf
MACRO { patent   } { "Patent" }
MACRO { patentau } { "Au. Patent" }
MACRO { patentuk } { "Br. Patent" }
MACRO { patenteu } { "Eur. Patent" }
MACRO { patentfr } { "Fr. Patent" }
MACRO { patentde } { "Ger. Offen." }    % seen in acs reference - maybe short for "offensichtlich"; a possible translation of patent
MACRO { patentus } { "U.S. Patent" }

% patent requests
MACRO { patapp   } { "Patent Appl." }
MACRO { patappau } { "Au. Pat. Appl." }
MACRO { patappuk } { "Br. Pat. Appl." }
MACRO { patappeu } { "Eur. Pat. Appl." }
MACRO { patappfr } { "Fr. Pat. Appl." }
MACRO { patappde } { "Ger. Pat. Appl." }
MACRO { patappjp } { "kōkai tokkyo kōhō" }
MACRO { patappus } { "U.S. Pat. Appl." }

% alias using biblatex style abbreviations
MACRO { patreq   } { "Patent Appl." }
MACRO { patreqde } { "Ger. Pat. Appl." }
MACRO { patreqeu } { "Eur. Pat. Appl." }
MACRO { patreqfr } { "Fr. Pat. Appl." }
MACRO { patrequk } { "Br. Pat. Appl." }
MACRO { patrequs } { "U.S. Pat. Appl." }

% Corrected in accordance with ACS Style guide, 3rd ed, pp. 161
MACRO { jan } { "Jan" }
MACRO { feb } { "Feb" }
MACRO { mar } { "March" }
MACRO { apr } { "April" }
MACRO { may } { "May" }
MACRO { jun } { "June" }
MACRO { jul } { "July" }
MACRO { aug } { "Aug" }
MACRO { sep } { "Sept" }
MACRO { oct } { "Oct" }
MACRO { nov } { "Nov" }
MACRO { dec } { "Dec" }

MACRO { acbcct } { "ACS Chem.\ Biol." }
MACRO { achre4 } { "Acc.\ Chem.\ Res." }
MACRO { acncdm } { "ACS Chem.\ Neurosci." }
MACRO { ancac3 } { "ACS Nano" }
MACRO { ancham } { "Anal.\ Chem." }
MACRO { bichaw } { "Biochemistry" }
MACRO { bcches } { "Bioconjugate Chem." }
MACRO { bomaf6 } { "Biomacromolecules" }
MACRO { bipret } { "Biotechnol.\ Prog." }
MACRO { crtoec } { "Chem.\ Res.\ Toxicol." }
MACRO { chreay } { "Chem.\ Rev." }
MACRO { cmatex } { "Chem.\ Mater." }
MACRO { cgdefu } { "Cryst.\ Growth Des." }
MACRO { enfuem } { "Energy Fuels" }
MACRO { esthag } { "Environ.\ Sci.\ Technol." }
MACRO { iechad } { "Ind.\ Eng.\ Chem.\ Res." }
MACRO { inoraj } { "Inorg.\ Chem." }
MACRO { jafcau } { "J.~Agric.\ Food Chem." }
MACRO { jceaax } { "J.~Chem.\ Eng.\ Data" }
MACRO { jceda8 } { "J.~Chem.\ Ed." }
MACRO { jcisd8 } { "J.~Chem.\ Inf.\ Model." }
MACRO { jctcce } { "J.~Chem.\ Theory Comput." }
MACRO { jcchff } { "J. Comb. Chem." }
MACRO { jmcmar } { "J. Med. Chem." }
MACRO { jnprdf } { "J. Nat. Prod." }
MACRO { joceah } { "J.~Org.\ Chem." }
MACRO { jpcafh } { "J.~Phys.\ Chem.~A" }
MACRO { jpcbfk } { "J.~Phys.\ Chem.~B" }
MACRO { jpccck } { "J.~Phys.\ Chem.~C" }
MACRO { jpclcd } { "J.~Phys.\ Chem.\ Lett." }
MACRO { jprobs } { "J.~Proteome Res." }
MACRO { jacsat } { "J.~Am.\ Chem.\ Soc." }
MACRO { langd5 } { "Langmuir" }
MACRO { mamobx } { "Macromolecules" }
MACRO { mpohbp } { "Mol.\ Pharm." }
MACRO { nalefd } { "Nano Lett." }
MACRO { orlef7 } { "Org.\ Lett." }
MACRO { oprdfk } { "Org.\ Proc.\ Res.\ Dev." }
MACRO { orgnd7 } { "Organometallics" }

% Construction of bibliography: reading of data, construction of
% labels, output of formatted bibliography

READ

EXECUTE { initialize.control.values }
EXECUTE { initialize.count.entries }
EXECUTE { initialize.name.separator }
EXECUTE { initialize.tracker }

ITERATE { calculate.names }
ITERATE { count.entries }

EXECUTE { begin.bib }

ITERATE { call.type$ }

EXECUTE { end.bib }
%</bst>

Python Conversion Tool

This short program takes in a file containing one or more SciFinder References in the Tagged Format (.txt) format, and converts it into a BibTeX entry compatable with the bibtex style above.

r"""Simple sctipt to convert Scifinder refernces for patents into bibtex entries for my customized acspat.bst bibtex style. 

    note: acspat.bst complies with the ACS format as outlined by The ACS Style Guide, 3rd Edition

    usage 
        python "c:\...\cas2bibtex.py" "c:\...\ref.txt" > "c:\...\ref.bib"
"""

import sys
import re
import inspect
import httplib2
import pycountry 
from datetime import datetime
from tkinter import Tk
from tkinter.filedialog import askopenfilename
from pylatexenc import latexencode

# method to convert patents to bibtex
def convert_Patent(fields):
    """Convert the passed dictionary from SciFinder `Tagged Format (*.txt)` to Custom @Patent bibtex entry

    Args:
        fields (dict): dictionary verions of Scifinder CAS reference sheet
    """

    assert isinstance(fields, dict)
    assert fields["Document Type"]=="Patent"

    # ACS spec uses the application date, not publishing date
    # pub_date = datetime.strptime(fields["Publication Date"],"%Y%m%d")
    app_date = datetime.strptime(fields["Patent Application Date"],"%Y%m%d")
    print()
    # print(pycountry.countries.get(name=str(fields["Patent Assignee"]).replace(r"\(.*\)","").title()))

    # construct a google patent url from the text file data
    google_pat_url =("https://patents.google.com/patent/" + 
            fields["Patent Country"].strip() + 
            fields["Patent Number"].strip() + 
            fields["Patent Kind Code"].strip())

    # check if the url is valid, if not, ditch it. store result as url
    url = google_pat_url if int(httplib2.Http().request(google_pat_url, "HEAD")[0]["status"])<400 else ''

    

    bib_str = inspect.cleandoc(
            # Key, abstract and keywords - stuf to make finding the right doc easier when writing
        r"""@Patent{{{citation_key},
            abstract    = {{{abstract}}},
            keywords    = {{{keywords}}},
            """
            # Basic Publication Info  
        r"""author      = {{{author}}},
            assignee    = {{{assignee}}},
            title       = {{{title}}},
            date        = {{{date}}},
            year        = {{{year}}},
            month       = {{{month}}},
            day         = {{{day}}},
            """
            # Document info
        r"""pages       = {{{pages}}},
            language    = {{{language}}},
            """
            # patent-specific info
        r"""type        = {{{patent_type}}},
            number      = {{{patent_number}}}, 
            """
            # Search-related info
        r"""CODEN       = {{{CODEN}}},
            CAS_AN      = {{{accession_number}}},
            CAS_CAN     = {{{chemical_abstracts_number}}}, 
            url         = {{{url}}}
        }}""".format(
            citation_key                = fields["Inventor Name"].split(maxsplit=1)[0].replace(',','') +
                                          app_date.strftime("%Y"), 
            abstract                    = latexencode.unicode_to_latex(fields["Abstract"].strip()),
            # abstract                    = fields["Abstract"],
            keywords                    = fields["Index Terms"], 

            author                      = fields["Inventor Name"].replace('; '," and "),
            assignee                    = fields["Patent Assignee"] if 
                                            pycountry.countries.get(name=fields["Patent Assignee"].strip('()'))==None
                                            else '',
            title                       = latexencode.unicode_to_latex(fields["Title"]).title(),
            date                        = app_date.strftime("%Y-%m-%d"),
            year                        = app_date.strftime("%Y"),
            month                       = app_date.strftime("%m"),
            day                         = app_date.strftime("%d"), 
            
            pages                       = fields["Page"],
            language                    = fields["Language"],
 
            patent_type                 = fields["Journal Title"]+'.',  # this holds the abbreviation for patent type
                                                                        # period added as this an abbreviation; this is an exception
            patent_number               = fields["Patent Number"],
            
            CODEN                       = fields["CODEN"],
            accession_number            = fields["Accession Number"],
            chemical_abstracts_number   = fields["Chemical Abstracts Number(CAN)"],

            url                         = fields["URL"] if fields["URL"] != '' else url # use the cas specified url, if it exists, else use the url selected above
            ))


    return(bib_str)

# Main method - runs at execution-time
def main():
    """ Check if any command-line arguments for what files to look at are passed, 
        if not, prompt user for what files to use

        Assert that the file is of the appropriate scifinder format

        extract records, and one by one, convert those records to the bibtex entries 
        using `convert_Patent()`
    """

    if len(sys.argv)<2:
        tk = Tk()
        tk.withdraw()
        tk.call('wm', 'attributes', '.', '-topmost', True)
        files = askopenfilename(title="Choose File(s) to Convert",multiple=True)
    else:
        files = sys.argv[1:]

    # Define patterns to seek out the records
    rec_pattern = r"""START_RECORD\n    # Begins with start record
                    (?P<Fields>.+?)     # Capture as few lines as possible between 
                    END_RECORD"""       # Ends with End Record
    fld_pattern = r"""FIELD\s           # Begins with 
                    (?P<Key>.+?)\:      # Capture the key (everything before the `:` - newlines excluded)
                    (?P<Def>.+?)?\.?    # Capture the definition (everything after the `:` - trailing periods excluded)
                    \n"""               # Ends with a non-optional newline

    # Compile the patterns into regex objects
    rec_regex = re.compile(rec_pattern, re.VERBOSE | re.MULTILINE | re.DOTALL)
    fld_regex = re.compile(fld_pattern, re.VERBOSE | re.MULTILINE)

    # iter over all passed files
    for filePath in files:

        # open and read the file into memory
        file = open(filePath)
        fileTxt = file.read()
        file.close()
        
        # find records using regexp and iter over them 
        for record in rec_regex.findall(fileTxt):
            # convert the records into dicts
            fields = dict(fld_regex.findall(record))
            
            # decision tree for converting based off of doc type
            # print result with intention that this can be used 
            # at the shell and piped into a file
            if fields["Document Type"]=="Patent":
                print(convert_Patent(fields))
            else:
                print("Attempted to covert file: {}\nHowever, document type <{}> is yet not supported".format(
                    filePath, fields["Document Type"]))

# Force auto-run of main
if __name__ == "__main__": main()

Sample Input

Sample Output

Output

Expected Output

From The ACS Style Guide 3rd Ed., 310-311, patents are expected to presented as

Recommended Format:

Patent Owner 1; Patent Owner 2; etc. Title of Patent. Patent Number, Date.

Examples:

  1. Sheem, S. K. Low-Cost Fiber Optic Pressure Sensor. U.S. Patent 9,738,537, May 18, 2004.

  2. Lenssen, K. C.; Jantscheff, P.; Kiedrowski, G.; Massing, U. Cationic Lipids With Serine Backbone for Transfecting Biological Molecules.Eu. Pat. Appl. 1457483, 2004.

  3. Langhals, H.; Wetzel, F.; Perylene Pigments with Metallic Effects. Ger. Offen. DE 10357978.8, Dec 11, 2013; Chem. Abstr. 2005, 143, 134834.

  4. Shimizu, Y.; Kajiyama, H. (Kanebo, Ltd., Japan; Kanebo Synthetic Fibers, Ltd.). Jpn. Kokai Tokkyo Koho JP 2004176197 A2 20040624, 2004.

Actual Output

Using the code above in latex, and data from Scifinder converted to BibTeX using the Python Script, below, the same patents (excluding the 3rd, which was not present in the SciFinder Database), produce the following citations

Actual Output

Take a look at the MWE that generated this on Overleaf


Discussion

It would seem that there are a fair number of inconsistencies with the data available from the SciFinder database, when compared to the expeced values.

Most Concerningly - the date which is cited according to the style guide is inconsistent. For Examples 2 and 5 In the guide, the publication date is cited, for 3 and 4 the application date is used. Example 1 appears to use a completely unrelated date.

As a result of this I decided to use the publication date for all of my citations, but if anyone can provide me with guidance as to why the dates are varying and how I might programmatically address this, I would be very appreciative of this.

Other Concerns

In the BibTeX Style File, the Definitions for format.assignees and format.CAS both use the find.replace function or a variant of this. This function ends up adding quite a few steps onto each citation generation, and I am concerned that this my significantly negatively affect proformence with large bibliographies. How might this be improved?

In the Python script below, the latexencode.unicode_to_latex(...) function sometimes incorrectly converts inputs - for instance the character gamma (\$\gamma\$) is not properly handled to the expected \gamma - is there a better alternative for converting unicode to latex?

Are there any resources that you woudl suggest for working with BST files (asside from Tame the BeaST)? Documentation on the format is quite scarce.

Are there any other general improvements that you might suggest?

\$\endgroup\$
  • \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers. \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Jun 12 at 15:57
5
\$\begingroup\$

A good first step is always to look at the BibTeX output and trying to resolve all the errors that appear. In your case, I get

This is BibTeX, Version 0.99d (TeX Live 2020)
The top-level auxiliary file: main.aux
The style file: acspat.bst
Database file #1: ref.bib
achemso 2019-02-14 v3.12a
1 is an integer literal, not a string, for entry Sheem1997
while executing---line 1932 of file acspat.bst
You can't pop an empty literal stack for entry Sheem1997
while executing---line 1932 of file acspat.bst
1 is an integer literal, not a string, for entry Lenssen2003
while executing---line 1932 of file acspat.bst
You can't pop an empty literal stack for entry Lenssen2003
while executing---line 1932 of file acspat.bst
You can't pop an empty literal stack for entry Langhals2003
while executing---line 1932 of file acspat.bst
1 is an integer literal, not a string, for entry Shimizu2002
while executing---line 1932 of file acspat.bst
You can't pop an empty literal stack for entry Shimizu2002
while executing---line 1932 of file acspat.bst
(There were 7 error messages)

That's a lot of repetition, but basically BibTeX reports two issues for every entry:

1 is an integer literal, not a string, for entry Sheem1997
while executing---line 1932 of file acspat.bst
You can't pop an empty literal stack for entry Sheem1997
while executing---line 1932 of file acspat.bst

Where are these coming from? The 1 is an integer literal is caused by

FUNCTION { format.assignees } {
  assignee duplicate$ empty$
    { pop$ }
    { " AND" ";" find.replace.upper
      duplicate$
      duplicate$
      #-1 #1 substring$
      ")" = % <--------- After this comparison, a `1` (which represents "true" is at the top of the stack
            % not the string you duplicated earlier
      #1 #1 substring$
      "(" =
      and
        { paren }
        {  }
      if$
      output
      next.punct.period 'next.punct.int :=    
    }
  if$   
}

To fix this, add a swap$ between ")" = and #1 #1 substring$ to get the string above the number (aka boolean) again.

This leaves the "You can't pop an empty literal stack". Look at

FUNCTION { format.type.patent } {
  type empty$
    { "Patent" }{ type }
  if$ output
}

FUNCTION { format.type.number.patent } {
   number empty$
    { }
    {
      format.type.patent " " number * * output
      next.punct.comma 'next.punct.int :=
    }
  if$
}

Especially the line

format.type.patent " " number * * output

This tries to concatenate " " and number to the string left on the stack by format.type.patent, but because format.type.patent ends with output, the string is no longer on the stack but already printed.

To fix this, change format.type.patent to

FUNCTION { format.type.patent } {
  type empty$
    { "Patent" }{ type }
  if$
}

to avoid the premature output. (You might want to rename the function name because it no longer behaves like the other format. functions)

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Absolutely fantastic - I had been developing this on Overleaf and was completely unaware that there even was a BibTeX output file. I've implemented both of the changes you have suggested. Thank you :) \$\endgroup\$ – Taylor Scott Jun 12 at 15:31
  • \$\begingroup\$ Just as a note on this answer, in implenting the change to format.assignees the code segment and { paren } { } if$ should be changed to and { } { paren } if$ because I had my logic backwards \$\endgroup\$ – Taylor Scott Jun 12 at 16:33
0
\$\begingroup\$

Fix for Latex Encoding

Thank you to David Carlisle for helping me figure out this one.

The problems encoding \$\gamma\$ and similar unicode characters arrives not from the latexencode.unicode_to_latex(...) function, but rather from reading in the files incorrectly. Rather than using open(filePath), open(filePath,encoding="utf-8") should be used to ensure that the unicode characters are read in properly.

Additionally, this unicode conversion should be applied to the keywords field as this may also contain unicode characters, as shown by the Langhals Example.

When implemented, this renders a python file, cas2bibtex.py of the form

r"""Simple sctipt to convert Scifinder refernces for patents into bibtex entries for my customized acspat.bst bibtex style. 

    note: acspat.bst complies with the ACS format as outlined by The ACS Style Guide, 3rd Edition

    usage 
        python "c:\...\cas2bibtex.py" "c:\...\ref.txt" > "c:\...\ref.bib"
"""

import sys
import re
import inspect
import httplib2
import pycountry 
from datetime import datetime
from tkinter import Tk
from tkinter.filedialog import askopenfilename
from pylatexenc import latexencode

# method to convert patents to bibtex
def convert_Patent(fields):
    """Convert the passed dictionary from SciFinder `Tagged Format (*.txt)` to Custom @Patent bibtex entry

    Args:
        fields (dict): dictionary verions of Scifinder CAS reference sheet
    """

    assert isinstance(fields, dict)
    assert fields["Document Type"]=="Patent"

    # ACS spec uses application and publication date interchangably. I am not sure which to use, so the code for both is provided below.
    pub_date = datetime.strptime(fields["Publication Date"],"%Y%m%d")
    app_date = datetime.strptime(fields["Patent Application Date"],"%Y%m%d")

    # construct a google patent url from the text file data
    google_pat_url =("https://patents.google.com/patent/" + 
            fields["Patent Country"].strip() + 
            fields["Patent Number"].strip() + 
            fields["Patent Kind Code"].strip())

    # check if the url is valid, if not, ditch it. store result as url
    url = google_pat_url if int(httplib2.Http().request(google_pat_url, "HEAD")[0]["status"])<400 else ''

    bib_str = inspect.cleandoc(
            # Key, abstract and keywords - stuf to make finding the right doc easier when writing
        r"""@Patent{{{citation_key},
            abstract    = {{{abstract}}},
            keywords    = {{{keywords}}},
            """
            # Basic Publication Info  
        r"""author      = {{{author}}},
            assignee    = {{{assignee}}},
            title       = {{{title}}},
            date        = {{{date}}},
            year        = {{{year}}},
            month       = {{{month}}},
            day         = {{{day}}},
            """
            # Document info
        r"""pages       = {{{pages}}},
            language    = {{{language}}},
            """
            # patent-specific info
        r"""type        = {{{patent_type}}},
            number      = {{{patent_number}}}, 
            """
            # Search-related info
        r"""CODEN       = {{{CODEN}}},
            CAS_AN      = {{{accession_number}}},
            CAS_CAN     = {{{chemical_abstracts_number}}}, 
            url         = {{{url}}}
        }}""".format(
            citation_key                = fields["Inventor Name"].split(maxsplit=1)[0].replace(',','') +
                                        app_date.strftime("%Y"), 
            abstract                    = latexencode.unicode_to_latex(fields["Abstract"].strip()),
            keywords                    = latexencode.unicode_to_latex(fields["Index Terms"]),
            author                      = fields["Inventor Name"].replace('; '," and "),
            assignee                    = fields["Patent Assignee"] if 
                                            pycountry.countries.get(name=fields["Patent Assignee"].strip('()'))==None
                                            else '',
            title                       = latexencode.unicode_to_latex(fields["Title"]).title(),

            date                        = app_date.strftime("%Y-%m-%d"),
            year                        = app_date.strftime("%Y"),
            month                       = app_date.strftime("%m"),
            day                         = app_date.strftime("%d"), 
            
            pages                       = fields["Page"],
            language                    = fields["Language"],

            patent_type                 = fields["Journal Title"]   # this holds the abbreviation for patent type
                                            + '.' if str(fields["Journal Title"]).find('.') > 0 else '',
                                                                    # period added if the entry is a period - subject to change
            patent_number               = fields["Patent Number"],
            
            CODEN                       = fields["CODEN"],
            accession_number            = fields["Accession Number"],
            chemical_abstracts_number   = fields["Chemical Abstracts Number(CAN)"],

            url                         = fields["URL"] if fields["URL"] != '' else url # use the cas specified url, if it exists, else use the url selected above
            ))

    return(bib_str)

# Main method - runs at execution-time
def main():
    """ Check if any command-line arguments for what files to look at are passed, 
        if not, prompt user for what files to use

        Assert that the file is of the appropriate scifinder format

        extract records, and one by one, convert those records to the bibtex entries 
        using `convert_Patent()`
    """

    if len(sys.argv)<2:
        tk = Tk()
        tk.withdraw()
        tk.call('wm', 'attributes', '.', '-topmost', True)
        files = askopenfilename(title="Choose File(s) to Convert",multiple=True)
    else:
        files = sys.argv[1:]

    # Define patterns to seek out the records
    rec_pattern = r"""START_RECORD\n    # Begins with start record
                    (?P<Fields>.+?)     # Capture as few lines as possible between 
                    END_RECORD"""       # Ends with End Record
    fld_pattern = r"""FIELD\s           # Begins with 
                    (?P<Key>.+?)\:      # Capture the key (everything before the `:` - newlines excluded)
                    (?P<Def>.+?)?\.?    # Capture the definition (everything after the `:` - trailing periods excluded)
                    \n"""               # Ends with a non-optional newline

    # Compile the patterns into regex objects
    rec_regex = re.compile(rec_pattern, re.VERBOSE | re.MULTILINE | re.DOTALL)
    fld_regex = re.compile(fld_pattern, re.VERBOSE | re.MULTILINE)

    # iter over all passed files
    for filePath in files:

        # open and read the file into memory
        file = open(filePath,encoding="utf-8")
        fileTxt = file.read()
        file.close()
        
        # find records using regexp and iter over them 
        for record in rec_regex.findall(fileTxt):
            # convert the records into dicts
            fields = dict(fld_regex.findall(record))
            
            # decision tree for converting based off of doc type
            # print result with intention that this can be used 
            # at the shell and piped into a file
            if fields["Document Type"]=="Patent":
                print(convert_Patent(fields))
            else:
                print("Attempted to covert file: {}\nHowever, document type <{}> is yet not supported".format(
                    filePath, fields["Document Type"]))

# Force auto-run of main
if __name__ == "__main__": main()

Updated Sample Output

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.