Module:Format ISBN
From Zoophilia Wiki
Jump to navigationJump to search
This module implements {{Format ISBN}}
.
require ('strict');
-- Fetch separator positioning data.
local data = mw.loadData ('Module:Format ISBN/data');
-- The hyphen positioning data key/value table.
local hyphen_pos_t = data.hyphen_pos_t;
-- An index sequence into the hyphen positioning data table; used by
-- binary_search().
local index_t = data.index_t;
-- From count = #index_t; in ~/data; used by binary_search().
local idx_count = data.count;
-- BINARY SEARCH
-- Do a binary search for the hyphen positioning data for <target_isbn> in
-- <hyphen_pos_t> using its index sequence <index_t>. Accepts one input
-- <target_isbn> (a string) which it converts to a number and returns index into
-- <hyphen_pos_t> as a number when proper formatting is found, else nil.
local function binary_search (target_isbn)
-- Convert to number because index_t[x] values are numbers.
target_isbn = tonumber (target_isbn);
-- Invalid/out of range; 9780000000000 to whatever the last value is.
if (index_t[1] >= target_isbn) or (index_t[idx_count] < target_isbn) then
-- TODO: Return something meaningful?
return;
end
-- Initialize to index 1 (first element in <index_t>).
local idx_bot = 1;
-- Initialize to index of last element in <index_t>.
local idx_top = idx_count;
while idx_bot ~= idx_top do
-- Get the mid-point in the index sequence.
local idx_mid = math.ceil ((idx_bot + idx_top) / 2);
-- When mid-point index value ≥ the target ISBN…
if index_t[idx_mid] >= target_isbn then
-- …and the preceding <index_t> < target ISBN.
if index_t[idx_mid-1] < target_isbn then
-- Found correct mapping for <target> ISBN; return index into
-- <hyphen_pos_t>.
return index_t[idx_mid];
end
-- Adjust <idx_top>.
idx_top = idx_mid - 1;
else
-- Adjust <idx_bot>.
idx_bot = idx_mid;
end
end
-- Just in case, for the nonce.
mw.logObject ('didn\'t find formatting for isbn: ' .. target_isbn);
end
-- CONVERT TO ISBN-10
-- Convert a 13-digit ISBN to a 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 as there is no
-- mapping between ISBN-10 and 979-prefixed ISBN-13s. Calling functions are
-- required to ensure that <isbn13> is a properly formed string of 13 digits
-- with no separators that begins with 978.
local function convert_to_isbn10 (isbn13)
-- Get nine digits of <isbn13> following '978' GS1 prefix, drop check digit.
local isbn9 = isbn13:sub (4, 12);
-- Initialize the check digit calculation.
local check = 0;
-- Index.
local i = 1;
-- <j> is weighting for each of the nine digits, from left to right.
for j=10, 2, -1 do
-- Accumulate the sum of the weighted-digit-products.
check = check + tonumber (isbn9:sub (i, i)) * j;
-- Bump the index.
i = i + 1;
end
-- Remainder of the weighted-digit-products, divided by 11.
check = check % 11;
if 0 == check then
-- Special case.
return isbn9 .. '0';
else
-- Calculate the check digit.
check = 11 - check;
-- When <check> is ten, use 'X'; <check> else.
return isbn9 .. ((10 == check) and 'X' or check);
end
end
-- CONVERT TO ISBN13
-- Convert the ISBN-10 to ISBN13, adding the 978 GS1 prefix, and recalculate the
-- check digit. Takes single input: 10-digit ISBN with no separators.
local function convert_to_isbn13 (isbn10)
-- Concatenate 978 with first nine digits of <isbn10>, dropping check digit.
local isbn12 = '978'.. isbn10:sub(1, 9);
-- Initialize the check digit calculation.
local check = 0;
-- For the first 12 digits ('978' and nine others).
for i=1, 12 do
-- Accumulate checksum.
check = check + tonumber (isbn12:sub (i, i)) * (3 - (i % 2) * 2);
end
-- Extract check digit from checksum, append and done.
return isbn12 .. ((10 - (check % 10)) %10);
end
-- _FORMAT_ISBN
-- Module entry point when require()'d into another module; takes five inputs:
-- <isbn_str> (int): ISBN; <show_err_msg> (boolean): when true show error
-- message returned from check_isbn() else nil; <separator> (boolean): when true
-- use space character as separator, else hyphen; <template_name> (string):
-- supplied by template for error messaging; <output_format> (int): a value of
-- 10 or 13 dictates the format of the output ignoring other values. Returns
-- formatted SBN, ISBN-10 or ISBN-13 on success, whichever was the input or per
-- 'out' parameter, else initial <isbn_str>.
local function _format_isbn (isbn_str, show_err_msg, separator, output_format, template_name)
if (not isbn_str) or ('' == isbn_str) then
-- Empty or nil input? Empty output.
return '';
end
-- This will be the return value, if unable to format.
local isbn_str_raw = isbn_str;
-- Strip all formatting (spaces and hyphens) from the ISBN/SBN.
isbn_str = isbn_str:gsub ('[^%dX]', '');
-- A convenient place for flag stuff.
local flags = {};
-- Set a flag for output format; ignored when <isbn_str> is an SBN.
if '13' == output_format then
flags.out13 = true;
elseif '10' == output_format then
flags.out10 = true;
end
-- Looks like an SBN?
if 9 == #isbn_str then
-- Convert to ISBN-10.
isbn_str = '0' .. isbn_str;
-- Set a flag.
flags.sbn = true;
end
-- Does <isbn_str> 'look' like a valid ISBN? Does not check ranging.
local err_msg = require ("Module:Check isxn").check_isbn ({args={isbn_str, template_name=template_name}});
-- When there is an error message…
if '' ~= err_msg then
-- …and we are showing error messages…
if show_err_msg then
-- Return our input and the message.
return isbn_str_raw, err_msg;
else
-- Not showing error messages; return our input without the message.
return isbn_str_raw;
end
end
-- If ISBN-13, but we want an ISBN-10 output.
if 13 == #isbn_str and flags.out10 then
-- Calculate and extract the ISBN-10 check digit for later.
flags.isbn10_check_digit = (convert_to_isbn10 (isbn_str)):sub (-1);
end
-- If ISBN-10 or SBN.
if 10 == #isbn_str then
-- Extract the check digit for later.
flags.isbn10_check_digit = isbn_str:sub (-1);
-- Convert ISBN-10 to ISBN-13 for formatting.
isbn_str = convert_to_isbn13 (isbn_str);
end
-- Look for the formatting that applies to <isbn_str>.
local index = binary_search (isbn_str);
-- If found…
if index then
-- Get the formatting sequence.
local format_t = hyphen_pos_t[index];
-- Init <result_t> with prefix: the GS1 prefix element ('978' or '979').
local result_t = {isbn_str:sub (1, 3)};
-- Initialize to point at registration group element.
local digit_ptr = 4;
-- Loop through formatting sequence to build ISBN-13 elements.
for _, n in ipairs (format_t) do
-- Add digits from <isbn_str>[<digit_ptr>] to
-- <isbn_str>[<digit_ptr+n-1>] to <result_t> sequence.
table.insert (result_t, isbn_str:sub (digit_ptr, digit_ptr+n-1));
-- Advance the digit pointer.
digit_ptr = digit_ptr + n;
end
-- Add the check digit element to <result_t>.
table.insert (result_t, isbn_str:sub (13));
-- Assemble formatted <isbn_str> with space/hyphen (default) separators.
isbn_str = table.concat (result_t, separator and ' ' or '-');
-- If we saved the check digit from an SBN or ISBN-10.
if flags.isbn10_check_digit then
-- When input is an SBN.
if flags.sbn then
-- Remove GS1 prefix and registration group elements, then
-- restore check digit.
isbn_str = isbn_str:gsub ('^978%-0%-', ''):gsub ('%d$', flags.isbn10_check_digit);
-- When input is an ISBN-10.
else
if not flags.out13 then
-- Remove GS1 prefix element and restore check digit.
isbn_str = isbn_str:gsub ('^978%-', ''):gsub ('%d$', flags.isbn10_check_digit);
end
end
end
-- Return formatted <isbn_str>.
return isbn_str;
end
-- Should never be reached, but if so, return original input string.
return isbn_str_raw;
end
-- FORMAT_PLAIN {{#invoke:format isbn|format_plain}}
-- Plain text output: No linking to Special:BookSources, and no error message
-- output. On error, return input; for use in CS1 & CS2 templates' 'isbn'
-- parameter, don't confuse with multiple error messages. 'separator=space'
-- renders formatted ISBN with spaces not hyphens, out=<10|13> specifies the
-- output format, if different from the default.
local function format_plain (frame)
-- Get template and invoke parameters.
local args_t = require ('Module:Arguments').getArgs (frame);
local isbn_str = args_t[1];
-- Boolean: When true, use space separator, else hyphen.
local separator = 'space' == args_t.separator;
-- 10 or 13, to convert input format to the other for output.
local output_format = args_t.out;
-- No error messaging.
return _format_isbn (isbn_str, nil, separator, output_format);
end
-- FORMAT_LINK {{#invoke:format isbn|format_linked|template=Format ISBN link}}
-- Linked text output: links to Special:BookSources. 'suppress-errors=yes':
-- suppress error messages; 'separator=space': render formatted ISBN with spaces
-- instead of hyphens; 'out=': 10 or 13 digit output, if different from default.
local function format_linked (frame)
-- Get template and invoke parameters.
local args_t = require ('Module:Arguments').getArgs (frame);
local isbn_str = args_t[1];
-- Always show errors unless 'suppress-errors=yes'.
local show_err_msg = 'yes' ~= args_t['suppress-errors'];
-- Boolean: When true use space separator, else hyphen.
local separator = 'space' == args_t.separator;
-- 10 or 13: Convert input format to the other for output.
local output_format = args_t.out;
-- Show error messages unless suppressed.
local formatted_isbn_str, err_msg = _format_isbn (isbn_str, show_err_msg, separator, output_format, args_t.template_name);
if err_msg then
-- Return unformatted, unlinked ISBN and error message.
return formatted_isbn_str .. ' ' .. err_msg;
else
-- Return formatted and linked ISBN.
return '[[Special:BookSources/' ..isbn_str .. '|' .. formatted_isbn_str ..']]';
end
end
-- EXPORTS
return {
-- Template entry points.
format_plain = format_plain,
format_linked = format_linked,
-- Entry point when this module require()'d into another module.
_format_isbn = _format_isbn,
}