モジュール: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()
--------------------------------------------------------------------------------
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_tx values are numbers
	if (index_t1 >= target_isbn) or (index_tidx_count )
	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_tidx_mid >= target_isbn then									-- when mid-point index value is greater than or equal to the target isbn
			if index_tidx_mid-1  value is less than the target isbn
				return index_tidx_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
------------------------------------------------------------------------
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
------------------------------------------------------------------------
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
--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, error='error', 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_tindex;									-- 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
--out= – takes either of 10 or 13 to specify the output format if different from the default
	
local function format_plain (frame)
	local args_t = require ('Module:Arguments').getArgs (frame);				-- get template and invoke parameters
	local isbn_str = args_t1;
	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
--out= – takes either of 10 or 13 to specify the output format if different from the default
	
local function format_linked (frame)
	local args_t = require ('Module:Arguments').getArgs (frame);				-- get template and invoke parameters
	local isbn_str = args_t1;
	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
--------------------------------------------------------------------------------------------
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
	}