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,
	}