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

returns index into <hyphen_pos_t> 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 <index_t>)
	local idx_top = idx_count;													-- initialize to index of last element in <index_t>
	
	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 <index_t> value is less than the target isbn
				return index_t[idx_mid];										-- we found the correct mapping for <target> isbn; return index into <hyphen_pos_t>
			end
			idx_top = idx_mid - 1;												-- adjust <idx_top>
		else
			idx_bot = idx_mid;													-- adjust <idx_bot>
		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 <isbn13> 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 <isbn13> 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															-- <j> 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 <check> is ten, use 'X'; <check> 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 <isbn10> (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_str> – isbn as a string
	<show_err_msg>: boolean: when true, shows error message returned from check_isbn(); no message else
	<separator>: boolean: when true, use space character as separator; hyphen else
	<template_name>: supplied by the template for use in error messaging
	<output_format>: 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 <isbn_str> 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 <isbn_str> 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 <isbn_str> '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 <isbn_str>
	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 <result_t> 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 <isbn_str>[<digit_ptr>] to <isbn_str>[<digit_ptr+n-1>] to <result_t> 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 <result_t>

		isbn_str = table.concat (result_t, separator and ' ' or '-');			-- assemble formatted <isbn_str> 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 <isbn_str>
	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 '[[Special:BookSources/' ..isbn_str .. '|' .. 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
	}

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.