err_flag = set_message ('err_bad_lccn'); -- set an error message
end
elseif 10 == len then -- LCCN should be aadddddddd or dddddddddd
if id:match("[^%d]") then -- if LCCN has anything but digits (nil if only digits) ...
if nil == id:match("^%l%l%d%d%d%d%d%d%d%d") then -- ... see if it matches our pattern
err_flag = set_message ('err_bad_lccn'); -- no match, set an error message
end
end
elseif 11 == len then -- LCCN should be aaadddddddd or adddddddddd
if not (id:match("^%l%l%l%d%d%d%d%d%d%d%d") or id:match("^%l%d%d%d%d%d%d%d%d%d%d")) then -- see if it matches one of our patterns
err_flag = set_message ('err_bad_lccn'); -- no match, set an error message
end
elseif 12 == len then -- LCCN should be aadddddddddd
if not id:match("^%l%l%d%d%d%d%d%d%d%d%d%d") then -- see if it matches our pattern
err_flag = set_message ('err_bad_lccn'); -- no match, set an error message
end
else
err_flag = set_message ('err_bad_lccn'); -- wrong length, set an error message
end  
if not is_set (err_flag) and nil ~= lccn:find ('%s') then
err_flag = set_message ('err_bad_lccn'); -- lccn contains a space, set an error message
end  
if is_set (err_flag) then
options.coins_list_t['LCCN'] = nil; -- when error, unset so not included in COinS
end  
return external_link_id ({link = handler.link, label = handler.label, q = handler.q, redirect = handler.redirect,
prefix = handler.prefix, id = lccn, separator = handler.separator, encode = handler.encode});
end  
----------------------------< M R >--------------------------------------------------------------------------  
A seven digit number; if not seven digits, zero-fill leading digits to make seven digits.  
local function mr (options)
local id = options.id;
local handler = options.handler;
local id_num;
local id_len;  
id_num = id:match ('^[Mm][Rr](%d+)$'); -- identifier with mr prefix  
if is_set (id_num) then
set_message ('maint_mr_format'); -- add maint cat
else -- plain number without mr prefix
id_num = id:match ('^%d+$'); -- if here id is all digits
end  
id_len = id_num and id_num:len or 0;
if (7 >= id_len) and (0 ~= id_len) then
id = string.rep ('0', 7-id_len) .. id_num; -- zero-fill leading digits
else
set_message ('err_bad_mr'); -- set an error message
options.coins_list_t['MR'] = nil; -- when error, unset so not included in COinS
end  
return external_link_id ({link = handler.link, label = handler.label, q = handler.q, redirect = handler.redirect,
prefix = handler.prefix, id = id, separator = handler.separator, encode = handler.encode});
end  
----------------------------< O C L C >----------------------------------------------------------------------  
Validate and format an OCLC ID. https://www.oclc.org/batchload/controlnumber.en.html
archived at: https://web.archive.org/web/20161228233804/https://www.oclc.org/batchload/controlnumber.en.html  
local function oclc (options)
local id = options.id;
local handler = options.handler;
local number;  
if id:match('^ocm%d%d%d%d%d%d%d%d$') then -- ocm prefix and 8 digits; 001 field (12 characters)
number = id:match('ocm(%d+)'); -- get the number
elseif id:match('^ocn%d%d%d%d%d%d%d%d%d$') then -- ocn prefix and 9 digits; 001 field (12 characters)
number = id:match('ocn(%d+)'); -- get the number
elseif id:match('^on%d%d%d%d%d%d%d%d%d%d+$') then -- on prefix and 10 or more digits; 001 field (12 characters)
number = id:match('^on(%d%d%d%d%d%d%d%d%d%d+)$'); -- get the number
elseif id:match('^%(OCoLC%)[1-9]%d*$') then -- (OCoLC) prefix and variable number digits; no leading zeros; 035 field
number = id:match('%(OCoLC%)([1-9]%d*)'); -- get the number
if 9 < number:len then
number = nil; -- constrain to 1 to 9 digits; change this when OCLC issues 10-digit numbers
end
elseif id:match('^%d+$') then -- no prefix
number = id; -- get the number
if 10 < number:len then
number = nil; -- constrain to 1 to 10 digits; change this when OCLC issues 11-digit numbers
end
end  
if number then -- proper format
id = number; -- exclude prefix, if any, from external link
else
set_message ('err_bad_oclc') -- add an error message if the id is malformed
options.coins_list_t['OCLC'] = nil; -- when error, unset so not included in COinS
end  
return external_link_id ({link = handler.link, label = handler.label, q = handler.q, redirect = handler.redirect,
prefix = handler.prefix, id = id, separator = handler.separator, encode = handler.encode});
end  
----------------------------< O P E N L I B R A R Y >--------------------------------------------------------  
Formats an OpenLibrary link, and checks for associated errors.  
local function openlibrary (options)
local id = options.id;
local access = options.access;
local handler = options.handler;
local ident, code = id:gsub('^OL', ''):match("^(%d+([AMW]))$"); -- strip optional OL prefix followed immediately by digits followed by 'A', 'M', or 'W';
local err_flag;
local prefix = { -- these are appended to the handler.prefix according to code
['A']='authors/OL',
['M']='books/OL',
['W']='works/OL',
['X']='OL' -- not a code; spoof when 'code' in id is invalid
};  
if not ident then
code = 'X'; -- no code or id completely invalid
ident = id; -- copy id to ident so that we display the flawed identifier
err_flag = set_message ('err_bad_ol');
end  
if not is_set (err_flag) then
options.coins_list_t['OL'] = handler.prefix .. prefix[code] .. ident; -- experiment for ol coins
else
options.coins_list_t['OL'] = nil; -- when error, unset so not included in COinS
end  
return external_link_id ({link = handler.link, label = handler.label, q = handler.q, redirect = handler.redirect,
prefix = handler.prefix .. prefix[code],
id = ident, separator = handler.separator, encode = handler.encode,
access = access});
end  
----------------------------< O S T I >----------------------------------------------------------------------  
Format OSTI and do simple error checking. OSTIs are sequential numbers beginning at 1 and counting up. This
code checks the OSTI to see that it contains only digits and is less than test_limit specified in the configuration;
the value in test_limit will need to be updated periodically as more OSTIs are issued.  
NB. 1018 is the lowest OSTI number found in the wild (so far) and resolving OK on the OSTI site  
local function osti (options)
local id = options.id;
local access = options.access;
local handler = options.handler;  
if id:match("[^%d]") then -- if OSTI has anything but digits
set_message ('err_bad_osti'); -- set an error message
options.coins_list_t['OSTI'] = nil; -- when error, unset so not included in COinS
else -- OSTI is only digits
local id_num = tonumber (id); -- convert id to a number for range testing
if 1018 > id_num or handler.id_limit < id_num then -- if OSTI is outside test limit boundaries
set_message ('err_bad_osti'); -- set an error message
options.coins_list_t['OSTI'] = nil; -- when error, unset so not included in COinS
end
end  
return external_link_id ({link = handler.link, label = handler.label, q = handler.q, redirect = handler.redirect,
prefix = handler.prefix, id = id, separator = handler.separator, encode = handler.encode, access = access});
end  
--[[--------------------------< P M C >------------------------------------------------------------------------  
Format a PMC, do simple error checking, and check for embargoed articles.  
The embargo parameter takes a date for a value. If the embargo date is in the future the PMC identifier will not
be linked to the article. If the embargo date is today or in the past, or if it is empty or omitted, then the
PMC identifier is linked to the article through the link at cfg.id_handlers['PMC'].prefix.  
PMC embargo date testing is done in function is_embargoed which is called earlier because when the citation
has |pmc=<value> but does not have a |url= then |title= is linked with the PMC link. Function is_embargoed
returns the embargo date if the PMC article is still embargoed, otherwise it returns an empty string.