Module:Format ISBN

require ('strict');

local data = mw.loadData ('Module:Format ISBN/data'); -- fetch separator positioning data

local hyphen_pos_t = data.hyphen_pos_t; -- the hyphen positioning data k/v table

local index_t = data.index_t; -- an index sequence into the hyphen positioning data table; used by binary_search()

local idx_count = data.count; -- from count = #index_t; in ~/data; used by binary_search()

--[[--------------------------< B I N A R Y _ S E A R C H >----------------------------------------------------

do a binary search for the hyphen positioning data for in using its index sequence

.

accepts one input (a string) which it converts to a number

returns index into as a number when proper formatting is found; nil else

]]

local function binary_search (target_isbn)

target_isbn = tonumber (target_isbn); -- convert to number because index_t[x] values are numbers

if (index_t[1] >= target_isbn) or (index_t[idx_count] < target_isbn) then -- invalid; out of range; 9780000000000 to whatever the last value is

return; -- TODO: return something meaningful?

end

local idx_bot = 1; -- initialize to index 1 (first element in )

local idx_top = idx_count; -- initialize to index of last element in

while idx_bot ~= idx_top do

local idx_mid = math.ceil ((idx_bot + idx_top) / 2); -- get the mid-point in the index sequence

if index_t[idx_mid] >= target_isbn then -- when mid-point index value is greater than or equal to the target isbn

if index_t[idx_mid-1] < target_isbn then -- and when the preceding value is less than the target isbn

return index_t[idx_mid]; -- we found the correct mapping for isbn; return index into

end

idx_top = idx_mid - 1; -- adjust

else

idx_bot = idx_mid; -- adjust

end

end

mw.logObject ('didn\'t find formatting for isbn: ' .. target_isbn); -- just in case for the nonce

end

--[[--------------------------< C O N V E R T _ T O _ I S B N 1 0 >--------------------------------------------

convert 13-digit isbn to 10-digit isbn; removes 978 GS1 prefix and recalculates the check digit

takes a single input; the 13-digit isbn as a string without separators

assumes that the GS1 prefix is 978; there is no mapping between isbn10 and 979-prefixed isbn13. calling functions

are required to ensure that is a properly formed string of 13 digits (no separators) that begins with 978.

]]

local function convert_to_isbn10 (isbn13)

local isbn9 = isbn13:sub (4, 12); -- get the 9 digits of that follow the '978' GS1 prefix (drop the check digit)

local check = 0; -- initialize the check digit calculation

local i = 1; -- index

for j=10, 2, -1 do -- is weighting for each of the 9 digits; counting down, left to right

check = check + tonumber (isbn9:sub (i, i)) * j; -- accumulate the sum the weighted-digit-products

i = i + 1; -- bump the index

end

check = check % 11; -- remainder of the weighted-digit-products divided by 11

if 0 == check then

return isbn9 .. '0'; -- special case

else

check = 11 - check; -- calculate the check digit

return isbn9 .. ((10 == check) and 'X' or check); -- when is ten, use 'X'; else

end

end

--[[--------------------------< C O N V E R T _ T O _ I S B N 1 3 >--------------------------------------------

convert 10-digit isbn to 13-digit isbn; adds 978 GS1 prefix and recalculates the check digit

takes a single input; the 10-digit isbn as a string (no separators)

]]

local function convert_to_isbn13 (isbn10)

local isbn12 = '978'.. isbn10:sub(1, 9); -- concatenate '978' with first 9 digits of (drop the check digit)

local check = 0; -- initialize the check digit calculation

for i=1, 12 do -- for the first 12 digits ('978' and 9 others)

check = check + tonumber (isbn12:sub (i, i)) * (3 - (i % 2) * 2); -- accumulate checksum

end

return isbn12 .. ((10 - (check % 10)) %10); -- extract check digit from checksum; append and done

end

--[[--------------------------< _ F O R M A T _ I S B N >------------------------------------------------------

Module entry point when require()'d into another module

takes five inputs:

– isbn as a string

: boolean: when true, shows error message returned from check_isbn(); no message else

: boolean: when true, use space character as separator; hyphen else

: supplied by the template for use in error messaging

: a value of 10 or 13 dictates the format of the output; other values ignored

returns formatted sbn, isbn10, or isbn13 (whichever was the input or per |out=) on success; initial else

]]

local function _format_isbn (isbn_str, show_err_msg, separator, output_format, template_name)

if (not isbn_str) or ('' == isbn_str) then

return ''; -- empty or nil input? empty output

end

local isbn_str_raw = isbn_str; -- this will be the return value if unable to format

isbn_str = isbn_str:gsub ('[^%dX]', ''); -- strip all formatting (spaces and hyphens) from the isbn/sbn

local flags = {}; -- a convenient place for flag stuff

if '13' == output_format then -- set a flag for output format; ignored when is an sbn

flags.out13 = true;

elseif '10' == output_format then

flags.out10 = true;

end

if 9 == #isbn_str then -- looks like an sbn?

isbn_str = '0' .. isbn_str; -- convert to isbn10

flags.sbn = true; -- set a flag

end

local err_msg = require ("Module:Check isxn").check_isbn ({args={isbn_str, template_name=template_name}}); -- does 'look' like a valid isbn? does not check ranging

if '' ~= err_msg then -- when there is an error message

if show_err_msg then -- and we are showing error messages

return isbn_str_raw, err_msg; -- return our input and the message

else

return isbn_str_raw; -- not showing error messages; return our input without the message

end

end

if 13 == #isbn_str and flags.out10 and isbn_str:match ('^978') then -- if isbn13 but we want an isbn10 output; only for GS1 prefix of 978

flags.isbn10_check_digit = (convert_to_isbn10 (isbn_str)):sub (-1); -- calculate and extract the isbn10 check digit for later

end

if 10 == #isbn_str then -- if isbn10 or sbn

flags.isbn10_check_digit = isbn_str:sub (-1); -- extract the check digit for later

isbn_str = convert_to_isbn13 (isbn_str); -- convert isbn10 to isbn13 for formatting

end

local index = binary_search (isbn_str); -- look for the formatting that applies to

if index then -- if found

local format_t = hyphen_pos_t[index]; -- get the formatting sequence

local result_t = {isbn_str:sub (1, 3)}; -- init with prefix; the GS1 prefix element ('978' or '979')

local digit_ptr = 4; -- initialize to point at registration group element

for _, n in ipairs (format_t) do -- loop through the formatting sequence to build a sequence of isbn13 elements

table.insert (result_t, isbn_str:sub (digit_ptr, digit_ptr+n-1)); -- add the digits from [] to [] to sequence

digit_ptr = digit_ptr + n; -- advance the digit pointer

end

table.insert (result_t, isbn_str:sub (13)); -- and add the check digit element to

isbn_str = table.concat (result_t, separator and ' ' or '-'); -- assemble formatted with space or hyphen (default) separators

if flags.isbn10_check_digit then -- if we saved the check digit from an sbn or isbn10

if flags.sbn then -- when input is an sbn

isbn_str = isbn_str:gsub ('^978%-0%-', ''):gsub ('%d$', flags.isbn10_check_digit); -- remove GS1 prefix element and registration group element; restore check digit

else -- when input is an isbn10

if not flags.out13 then

isbn_str = isbn_str:gsub ('^978%-', ''):gsub ('%d$', flags.isbn10_check_digit); -- remove GS1 prefix element; restore check digit

end

end

end

return isbn_str; -- return formatted

end

return isbn_str_raw; -- should never actually be reached; but, if we do, return original input string

end

--[[--------------------------< F O R M A T _ P L A I N >------------------------------------------------------

plain text output:

no linking to Special:BookSources

no error message output – on error, return input; for use in cs1|2 template |isbn= params, no point in causing confusion due to multiple error messages

|separator=space – render formatted ISBN with spaces instead of hyphens

|out= – takes either of 10 or 13 to specify the output format if different from the default

{{#invoke:format isbn|format_plain}}

]]

local function format_plain (frame)

local args_t = require ('Module:Arguments').getArgs (frame); -- get template and invoke parameters

local isbn_str = args_t[1];

local separator = 'space' == args_t.separator; -- boolean: when true use space separator; hyphen else

local output_format = args_t.out; -- 10 or 13 to convert input format to the other for output

return _format_isbn (isbn_str, nil, separator, output_format); -- no error messaging

end

--[[--------------------------< F O R M A T _ L I N K >--------------------------------------------------------

linked text output:

links to Special:BookSources

|suppress-errors=yes – suppress error messages

|separator=space – render formatted ISBN with spaces instead of hyphens

|out= – takes either of 10 or 13 to specify the output format if different from the default

{{#invoke:format isbn|format_linked|template=Format ISBN link}}

]]

local function format_linked (frame)

local args_t = require ('Module:Arguments').getArgs (frame); -- get template and invoke parameters

local isbn_str = args_t[1];

local show_err_msg = 'yes' ~= args_t['suppress-errors']; -- always show errors unless |suppress-errors=yes

local separator = 'space' == args_t.separator; -- boolean: when true use space separator; hyphen else

local output_format = args_t.out; -- 10 or 13 to convert input format to the other for output

local formatted_isbn_str, err_msg = _format_isbn (isbn_str, show_err_msg, separator, output_format, args_t.template_name); -- show error messages unless suppressed

if err_msg then

return formatted_isbn_str .. ' ' .. err_msg; -- return unformatted, unlinked isbn and error message

else

return '' .. formatted_isbn_str ..''; -- return formatted and linked isbn

end

end

--[[--------------------------< E X P O R T S >----------------------------------------------------------------

]]

return {

format_plain = format_plain, -- template entry points

format_linked = format_linked,

_format_isbn = _format_isbn, -- entry point when this module require()'d into another module

}