<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://mywikibiz.com/index.php?action=history&amp;feed=atom&amp;title=Module%3AIPblock</id>
	<title>Module:IPblock - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://mywikibiz.com/index.php?action=history&amp;feed=atom&amp;title=Module%3AIPblock"/>
	<link rel="alternate" type="text/html" href="https://mywikibiz.com/index.php?title=Module:IPblock&amp;action=history"/>
	<updated>2026-07-27T03:59:17Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.35.3</generator>
	<entry>
		<id>https://mywikibiz.com/index.php?title=Module:IPblock&amp;diff=471963&amp;oldid=prev</id>
		<title>Zoran: Pywikibot 6.4.0</title>
		<link rel="alternate" type="text/html" href="https://mywikibiz.com/index.php?title=Module:IPblock&amp;diff=471963&amp;oldid=prev"/>
		<updated>2021-07-15T22:07:34Z</updated>

		<summary type="html">&lt;p&gt;Pywikibot 6.4.0&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;-- Calculate the minimum-sized blocks of IP addresses that cover each&lt;br /&gt;
-- IPv4 or IPv6 address entered in the arguments.&lt;br /&gt;
&lt;br /&gt;
local bit32 = require('bit32')&lt;br /&gt;
&lt;br /&gt;
local Collection  -- a table to hold items&lt;br /&gt;
Collection = {&lt;br /&gt;
	add = function (self, item)&lt;br /&gt;
		if item ~= nil then&lt;br /&gt;
			self.n = self.n + 1&lt;br /&gt;
			self[self.n] = item&lt;br /&gt;
		end&lt;br /&gt;
	end,&lt;br /&gt;
	join = function (self, sep)&lt;br /&gt;
		return table.concat(self, sep)&lt;br /&gt;
	end,&lt;br /&gt;
	remove = function (self, pos)&lt;br /&gt;
		if self.n &amp;gt; 0 and (pos == nil or (0 &amp;lt; pos and pos &amp;lt;= self.n)) then&lt;br /&gt;
			self.n = self.n - 1&lt;br /&gt;
			return table.remove(self, pos)&lt;br /&gt;
		end&lt;br /&gt;
	end,&lt;br /&gt;
	sort = function (self, comp)&lt;br /&gt;
		table.sort(self, comp)&lt;br /&gt;
	end,&lt;br /&gt;
	new = function ()&lt;br /&gt;
		return setmetatable({n = 0}, Collection)&lt;br /&gt;
	end&lt;br /&gt;
}&lt;br /&gt;
Collection.__index = Collection&lt;br /&gt;
&lt;br /&gt;
local function empty(text)&lt;br /&gt;
	-- Return true if text is nil or empty (assuming a string).&lt;br /&gt;
	return text == nil or text == ''&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local timestamps = {}  -- cache&lt;br /&gt;
local function start_date(code, months)&lt;br /&gt;
	-- Return a timestamp string for a URL to list user contributions&lt;br /&gt;
	-- on and after the returned date.&lt;br /&gt;
	-- The code specifies the wanted format.&lt;br /&gt;
	-- For this module, only recent contributions are wanted, so the&lt;br /&gt;
	-- timestamp is today's date less the given number of months (1 to 12).&lt;br /&gt;
	local key = code .. months&lt;br /&gt;
	if not timestamps[key] then&lt;br /&gt;
		local date = os.date('!*t')  -- today's UTC date&lt;br /&gt;
		local y, m, d = date.year, date.month, date.day  -- full year, month (1-12), day (1-31)&lt;br /&gt;
		m = m - months&lt;br /&gt;
		if m &amp;lt;= 0 then&lt;br /&gt;
			m = m + 12&lt;br /&gt;
			y = y - 1&lt;br /&gt;
		end&lt;br /&gt;
		local limit = m == 2 and 28 or 30&lt;br /&gt;
		if d &amp;gt; limit then&lt;br /&gt;
			d = limit  -- good enough to ensure date is valid&lt;br /&gt;
		end&lt;br /&gt;
		timestamps['y-m-d' .. months] = string.format('%d-%02d-%02d', y, m, d)&lt;br /&gt;
		timestamps['ymdHMS' .. months] = string.format('%d%02d%02d000000', y, m, d)&lt;br /&gt;
	end&lt;br /&gt;
	return timestamps[key] or ''&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local note_text = {&lt;br /&gt;
	range = '*Links for ranges show the contributions in the previous %s.',&lt;br /&gt;
	gadget = [=[&lt;br /&gt;
*&amp;lt;span id=&amp;quot;need-gadget&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;Contributions links for IPv6 ranges need the &amp;quot;&amp;lt;span style=&amp;quot;color:green;&amp;quot;&amp;gt;Allow /16, /24 and /27 – /32 CIDR ranges on Special:Contributions forms&amp;lt;/span&amp;gt;&amp;quot; gadget enabled in [[Special:Preferences#mw-prefsection-gadgets|Special:Preferences]], and scripting enabled in the browser.]=],&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
local function make_note(strings, key)&lt;br /&gt;
	-- Record the fact that a particular note is needed, and return&lt;br /&gt;
	-- wikitext for a link to the note or '' if no link needed.&lt;br /&gt;
	if not strings.nonote then&lt;br /&gt;
		strings.notes = strings.notes or {}&lt;br /&gt;
		if not strings.notes[key] then&lt;br /&gt;
			if key == 'gadget' then&lt;br /&gt;
				strings.notes[key] = note_text[key]&lt;br /&gt;
			elseif key == 'range' then&lt;br /&gt;
				local when = 'month'&lt;br /&gt;
				if strings.months &amp;gt; 1 then&lt;br /&gt;
					when = strings.months .. ' months'&lt;br /&gt;
				end&lt;br /&gt;
				strings.notes[key] = string.format(note_text.range, when)&lt;br /&gt;
			else&lt;br /&gt;
				error('make_note: unexpected key')&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
		if key == 'gadget' then&lt;br /&gt;
			return ' [[#need-gadget|&amp;lt;sup&amp;gt;[note]&amp;lt;/sup&amp;gt;]]'&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return ''&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function describe_total(total, isalloc)&lt;br /&gt;
	-- Return text describing given number of addresses or /64 allocations.&lt;br /&gt;
	if total &amp;lt;= 9999 then&lt;br /&gt;
		-- Can have fractions if total is the number of /64 allocations.&lt;br /&gt;
		if total &amp;lt; 9 then&lt;br /&gt;
			return (string.format('%.1f', total):gsub('%.0$', ''))&lt;br /&gt;
		end&lt;br /&gt;
		return string.format('%.0f', total)&lt;br /&gt;
	end&lt;br /&gt;
	if not isalloc then&lt;br /&gt;
		local alloc = 2^64&lt;br /&gt;
		if total &amp;gt;= alloc then&lt;br /&gt;
			return describe_total(total / alloc, true) .. ' /64'&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	total = total/1024&lt;br /&gt;
	local suffix = 'K'&lt;br /&gt;
	if total &amp;gt;= 1024 then&lt;br /&gt;
		total = total/1024&lt;br /&gt;
		suffix = 'M'&lt;br /&gt;
		if total &amp;gt;= 1024 then&lt;br /&gt;
			total = total/1024&lt;br /&gt;
			suffix = 'G'&lt;br /&gt;
			if total &amp;gt; 64 then&lt;br /&gt;
				return '&amp;gt;64G'&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return string.format('%.0f', total) .. suffix&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function describe_size(ipsize, size)&lt;br /&gt;
	-- Return text describing how many IPs are in a range with size = prefix length.&lt;br /&gt;
	local function numtext(n)&lt;br /&gt;
		if n &amp;lt;= 16 then&lt;br /&gt;
			return tostring(2^n)&lt;br /&gt;
		end&lt;br /&gt;
		if n &amp;lt;= 19 then&lt;br /&gt;
			return tostring(2^(n - 10)) .. 'K'&lt;br /&gt;
		end&lt;br /&gt;
		if n &amp;lt;= 29 then&lt;br /&gt;
			return tostring(2^(n - 20)) .. 'M'&lt;br /&gt;
		end&lt;br /&gt;
		if n &amp;lt;= 36 then&lt;br /&gt;
			return tostring(2^(n - 30)) .. 'G'&lt;br /&gt;
		end&lt;br /&gt;
		return '&amp;gt;64G'&lt;br /&gt;
	end&lt;br /&gt;
	local host = ipsize - size&lt;br /&gt;
	if host &amp;lt;= 32 then&lt;br /&gt;
		-- IPv4 or IPv6.&lt;br /&gt;
		return numtext(host)&lt;br /&gt;
	end&lt;br /&gt;
	-- Must be IPv6.&lt;br /&gt;
	if host &amp;lt;= 64 then&lt;br /&gt;
		local s = ({&lt;br /&gt;
			[64] = '1',  [63] = '50%', [62] = '25%', [61] = '12%',&lt;br /&gt;
			[60] = '6%', [59] = '3%',  [58] = '2%'&lt;br /&gt;
		})[host] or '&amp;lt;1%'&lt;br /&gt;
		return s .. ' /64'&lt;br /&gt;
	end&lt;br /&gt;
	-- IPv6 with size &amp;lt; 64.&lt;br /&gt;
	return numtext(host - 64) .. ' /64'&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function ipv6_string(ip)&lt;br /&gt;
	-- Return a string equivalent to the given IPv6 address.&lt;br /&gt;
	local z1, z2  -- indices of run of zeros to be displayed as &amp;quot;::&amp;quot;&lt;br /&gt;
	local zstart, zcount&lt;br /&gt;
	for i = 1, 9 do&lt;br /&gt;
		-- Find left-most occurrence of longest run of two or more zeros.&lt;br /&gt;
		if i &amp;lt; 9 and ip[i] == 0 then&lt;br /&gt;
			if zstart then&lt;br /&gt;
				zcount = zcount + 1&lt;br /&gt;
			else&lt;br /&gt;
				zstart = i&lt;br /&gt;
				zcount = 1&lt;br /&gt;
			end&lt;br /&gt;
		else&lt;br /&gt;
			if zcount and zcount &amp;gt; 1 then&lt;br /&gt;
				if not z1 or zcount &amp;gt; z2 - z1 + 1 then&lt;br /&gt;
					z1 = zstart&lt;br /&gt;
					z2 = zstart + zcount - 1&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
			zstart = nil&lt;br /&gt;
			zcount = nil&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	local parts = Collection.new()&lt;br /&gt;
	for i = 1, 8 do&lt;br /&gt;
		if z1 and z1 &amp;lt;= i and i &amp;lt;= z2 then&lt;br /&gt;
			if i == z1 then&lt;br /&gt;
				if z1 == 1 or z2 == 8 then&lt;br /&gt;
					if z1 == 1 and z2 == 8 then&lt;br /&gt;
						return '::'&lt;br /&gt;
					end&lt;br /&gt;
					parts:add(':')&lt;br /&gt;
				else&lt;br /&gt;
					parts:add('')&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
		else&lt;br /&gt;
			parts:add(string.format('%x', ip[i]))&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return parts:join(':')&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function ip_string(ip)&lt;br /&gt;
	-- Return a string equivalent to given IP address (IPv4 or IPv6).&lt;br /&gt;
	if ip.n == 2 then&lt;br /&gt;
		-- IPv4.&lt;br /&gt;
		local parts = {}&lt;br /&gt;
		for i = 1, 2 do&lt;br /&gt;
			local w = ip[i]&lt;br /&gt;
			local q = i == 1 and 1 or 3&lt;br /&gt;
			parts[q] = math.floor(w / 256)&lt;br /&gt;
			parts[q+1] = w % 256&lt;br /&gt;
		end&lt;br /&gt;
		return table.concat(parts, '.')&lt;br /&gt;
	end&lt;br /&gt;
	return ipv6_string(ip)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Metatable for some operations on IP addresses.&lt;br /&gt;
local ipmt = {&lt;br /&gt;
	__eq = function (lhs, rhs)&lt;br /&gt;
		-- Return true if values in numbered tables match.&lt;br /&gt;
		if lhs.n == rhs.n then&lt;br /&gt;
			for i = 1, lhs.n do&lt;br /&gt;
				if lhs[i] ~= rhs[i] then&lt;br /&gt;
					return false&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
			return true&lt;br /&gt;
		end&lt;br /&gt;
		return false&lt;br /&gt;
	end,&lt;br /&gt;
	__lt = function (lhs, rhs)&lt;br /&gt;
		-- Return true if lhs &amp;lt; rhs; for sort.&lt;br /&gt;
		if lhs.n == rhs.n then&lt;br /&gt;
			for i = 1, lhs.n do&lt;br /&gt;
				if lhs[i] ~= rhs[i] then&lt;br /&gt;
					return lhs[i] &amp;lt; rhs[i]&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
			return false&lt;br /&gt;
		end&lt;br /&gt;
		return lhs.n &amp;lt; rhs.n  -- sort IPv4 before IPv6, although not needed&lt;br /&gt;
	end,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
local function ipv4_address(ip_str)&lt;br /&gt;
	-- Return a collection of two 16-bit words (numbers) equivalent&lt;br /&gt;
	-- to the IPv4 address given as a quad-dotted string, or&lt;br /&gt;
	-- return nil if invalid.&lt;br /&gt;
	-- This representation is for compatibility with IPv6 addresses.&lt;br /&gt;
	local parts = Collection.new()&lt;br /&gt;
	local s = ip_str:match('^%s*(.-)%s*$') .. '.'&lt;br /&gt;
	for item in s:gmatch('(.-)%.') do&lt;br /&gt;
		parts:add(item)&lt;br /&gt;
	end&lt;br /&gt;
	if parts.n == 4 then&lt;br /&gt;
		for i, s in ipairs(parts) do&lt;br /&gt;
			if s:match('^%d+$') then&lt;br /&gt;
				local num = tonumber(s)&lt;br /&gt;
				if 0 &amp;lt;= num and num &amp;lt;= 255 then&lt;br /&gt;
					if num &amp;gt; 0 and s:match('^0') then&lt;br /&gt;
						-- A redundant leading zero is an error because it is for an IP in octal.&lt;br /&gt;
						return nil&lt;br /&gt;
					end&lt;br /&gt;
					parts[i] = num&lt;br /&gt;
				else&lt;br /&gt;
					return nil&lt;br /&gt;
				end&lt;br /&gt;
			else&lt;br /&gt;
				return nil&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
		local result = Collection.new()&lt;br /&gt;
		for i = 1, 3, 2 do&lt;br /&gt;
			result:add(parts[i] * 256 + parts[i+1])&lt;br /&gt;
		end&lt;br /&gt;
		return setmetatable(result, ipmt)&lt;br /&gt;
	end&lt;br /&gt;
	return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function ipv6_address(ip_str)&lt;br /&gt;
	-- Return a collection of eight 16-bit words (numbers) equivalent&lt;br /&gt;
	-- to the IPv6 address given as a colon-delimited string, or&lt;br /&gt;
	-- return nil if invalid.&lt;br /&gt;
	local _, n = ip_str:gsub(':', ':')&lt;br /&gt;
	if n &amp;lt; 7 then&lt;br /&gt;
		ip_str, n = ip_str:gsub('::', string.rep(':', 9 - n))&lt;br /&gt;
	end&lt;br /&gt;
	local parts = Collection.new()&lt;br /&gt;
	for item in (ip_str .. ':'):gmatch('(.-):') do&lt;br /&gt;
		parts:add(item)&lt;br /&gt;
	end&lt;br /&gt;
	if parts.n == 8 then&lt;br /&gt;
		for i, s in ipairs(parts) do&lt;br /&gt;
			if s == '' then&lt;br /&gt;
				parts[i] = 0&lt;br /&gt;
			else&lt;br /&gt;
				local num = tonumber('0x' .. s)&lt;br /&gt;
				if num and 0 &amp;lt;= num and num &amp;lt;= 65535 then&lt;br /&gt;
					parts[i] = num&lt;br /&gt;
				else&lt;br /&gt;
					return nil&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
		return setmetatable(parts, ipmt)&lt;br /&gt;
	end&lt;br /&gt;
	return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function common_length(num1, num2, nr_bits)&lt;br /&gt;
	-- Return number of prefix bits that two integers have in common.&lt;br /&gt;
	-- Number of bits in each number is nr_bits = 16, 8, 4, 2 or 1.&lt;br /&gt;
	if nr_bits &amp;lt;= 1 then&lt;br /&gt;
		return num1 == num2 and 1 or 0&lt;br /&gt;
	end&lt;br /&gt;
	local half = nr_bits / 2&lt;br /&gt;
	local splitter = 2^half&lt;br /&gt;
	local upper1, lower1 = math.modf(num1 / splitter)&lt;br /&gt;
	local upper2, lower2 = math.modf(num2 / splitter)&lt;br /&gt;
	if upper1 == upper2 then&lt;br /&gt;
		lower1 = math.floor(lower1 * splitter + 0.5)&lt;br /&gt;
		lower2 = math.floor(lower2 * splitter + 0.5)&lt;br /&gt;
		return half + common_length(lower1, lower2, half)&lt;br /&gt;
	end&lt;br /&gt;
	return common_length(upper1, upper2, half)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function common_prefix_length(ip1, ip2)&lt;br /&gt;
	-- Return number of prefix bits that two IPs have in common.&lt;br /&gt;
	-- Caller ensures that both IPs are IPv4 or both are IPv6.&lt;br /&gt;
	local size = 0&lt;br /&gt;
	for i = 1, ip1.n do&lt;br /&gt;
		local w1, w2 = ip1[i], ip2[i]&lt;br /&gt;
		if w1 == w2 then&lt;br /&gt;
			size = size + 16&lt;br /&gt;
		else&lt;br /&gt;
			return size + common_length(w1, w2, 16)&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return size&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function ip_prefix(ip, length)&lt;br /&gt;
	-- Return a copy of ip masked to contain only the prefix of given length.&lt;br /&gt;
	local result = { n = ip.n }&lt;br /&gt;
	for i = 1, ip.n do&lt;br /&gt;
		if length &amp;gt; 0 then&lt;br /&gt;
			if length &amp;gt;= 16 then&lt;br /&gt;
				result[i] = ip[i]&lt;br /&gt;
				length = length - 16&lt;br /&gt;
			else&lt;br /&gt;
				result[i] = bit32.band(ip[i], bit32.arshift(0xffff8000, length - 1))&lt;br /&gt;
				length = 0&lt;br /&gt;
			end&lt;br /&gt;
		else&lt;br /&gt;
			result[i] = 0&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return setmetatable(result, ipmt)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function ip_incremented(ip)&lt;br /&gt;
	-- Return a new IP equal to ip + 1.&lt;br /&gt;
	-- Will wraparound (255.255.255.255 + 1 = 0.0.0.0)!&lt;br /&gt;
	local result = { n = ip.n }&lt;br /&gt;
	local carry = 1&lt;br /&gt;
	for i = ip.n, 1, -1 do&lt;br /&gt;
		local sum = ip[i] + carry&lt;br /&gt;
		if sum &amp;gt;= 0x10000 then&lt;br /&gt;
			carry = 1&lt;br /&gt;
			sum = sum - 0x10000&lt;br /&gt;
		else&lt;br /&gt;
			carry = 0&lt;br /&gt;
		end&lt;br /&gt;
		result[i] = sum&lt;br /&gt;
	end&lt;br /&gt;
	return setmetatable(result, ipmt)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function is_next_ip(ip1, ip2)&lt;br /&gt;
	-- Return true if ip2 is the next IP after ip1 (ip2 == ip1 + 1).&lt;br /&gt;
	-- IPs are sorted and unique so ip1 &amp;lt; ip2 and can ignore wrapping to zero.&lt;br /&gt;
	-- This is lower overhead than making a new incremented IP then comparing.&lt;br /&gt;
	if ip1 and ip2 then&lt;br /&gt;
		local carry = 1&lt;br /&gt;
		for i = ip1.n, 1, -1 do&lt;br /&gt;
			local sum = ip1[i] + carry&lt;br /&gt;
			if sum &amp;gt;= 0x10000 then&lt;br /&gt;
				carry = 1&lt;br /&gt;
				sum = sum - 0x10000&lt;br /&gt;
			else&lt;br /&gt;
				carry = 0&lt;br /&gt;
			end&lt;br /&gt;
			if sum ~= ip2[i] then&lt;br /&gt;
				return false&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
		return true&lt;br /&gt;
	end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Each IP in a range except for the last IP has a 'common' field which is&lt;br /&gt;
-- a number specifying how many bits are common between the prefixes of this&lt;br /&gt;
-- IP and the next IP (0 if this IP starts with 0 and the next starts with 1).&lt;br /&gt;
-- Each non-empty range has exactly one &amp;quot;minimum common&amp;quot;, that is, its value&lt;br /&gt;
-- of common is smaller than all others. That there is only one minimum common&lt;br /&gt;
-- follows from the fact that the IPs are unique and sorted.&lt;br /&gt;
local function make_range(iplist, ifirst, ilast)&lt;br /&gt;
	-- Return a table for the range of IPs from iplist[ifirst] to iplist[ilast] inclusive.&lt;br /&gt;
	local imin, vmin, done&lt;br /&gt;
	if ifirst &amp;lt; ilast then&lt;br /&gt;
		for i = ifirst, ilast - 1 do&lt;br /&gt;
			-- Find the (unique) minimum of common lengths.&lt;br /&gt;
			local common = iplist[i].common&lt;br /&gt;
			if vmin then&lt;br /&gt;
				if vmin &amp;gt; common then&lt;br /&gt;
					vmin = common&lt;br /&gt;
					imin = i&lt;br /&gt;
				end&lt;br /&gt;
			else&lt;br /&gt;
				vmin = common&lt;br /&gt;
				imin = i&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	else&lt;br /&gt;
		vmin = iplist.ipsize&lt;br /&gt;
		imin = ifirst&lt;br /&gt;
		done = true&lt;br /&gt;
	end&lt;br /&gt;
	if vmin &amp;gt; iplist.allocation then&lt;br /&gt;
		-- For IPv6, the default allocation is /64 and there is no point having&lt;br /&gt;
		-- more precise ranges as they add unnecessary complexity.&lt;br /&gt;
		-- However, using results=all sets allocation = 128 so vmin is not changed.&lt;br /&gt;
		vmin = iplist.allocation&lt;br /&gt;
	end&lt;br /&gt;
	return {&lt;br /&gt;
		ifirst = ifirst,  -- index of first IP&lt;br /&gt;
		ilast = ilast,    -- index of last IP&lt;br /&gt;
		imin = imin,      -- index of IP with minimum common&lt;br /&gt;
		size = vmin,      -- number of common bits in prefix (the minimum)&lt;br /&gt;
		prefix = ip_prefix(iplist[imin], vmin),  -- IP table of the base IP&lt;br /&gt;
		done = done,      -- true if know that this range cannot be improved&lt;br /&gt;
	}&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function split_range(iplist, range, depth)&lt;br /&gt;
	-- Return a table of two or more ranges that more precisely target&lt;br /&gt;
	-- the IPs in range, or return nothing if unable to improve range.&lt;br /&gt;
	depth = depth and depth + 1 or 0&lt;br /&gt;
	if depth &amp;lt;= 20 and  -- 20 examines 1M contiguous addresses down to individual IPs&lt;br /&gt;
			not range.done and&lt;br /&gt;
			range.size &amp;lt; iplist.allocation and&lt;br /&gt;
			range.ifirst &amp;lt; range.ilast then&lt;br /&gt;
		local imin = range.imin&lt;br /&gt;
		assert(imin and range.ifirst &amp;lt;= imin and imin &amp;lt; range.ilast)&lt;br /&gt;
		local r1 = make_range(iplist, range.ifirst, range.imin)&lt;br /&gt;
		local r2 = make_range(iplist, range.imin + 1, range.ilast)&lt;br /&gt;
		local pointless = range.size + 1&lt;br /&gt;
		if r1.size &amp;gt; pointless or r2.size &amp;gt; pointless then&lt;br /&gt;
			return { r1, r2 }&lt;br /&gt;
		end&lt;br /&gt;
		local result = Collection.new()&lt;br /&gt;
		local function store_split(range)&lt;br /&gt;
			local split = split_range(iplist, range, depth)&lt;br /&gt;
			if split then&lt;br /&gt;
				for _, r in ipairs(split) do&lt;br /&gt;
					result:add(r)&lt;br /&gt;
				end&lt;br /&gt;
				return true&lt;br /&gt;
			else&lt;br /&gt;
				result:add(range)&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
		local improved1 = store_split(r1)&lt;br /&gt;
		local improved2 = store_split(r2)&lt;br /&gt;
		if improved1 or improved2 then&lt;br /&gt;
			return result&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	range.done = true&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function better_summary(iplist, summary)&lt;br /&gt;
	-- Return a better summary that more precisely targets the specified IPs,&lt;br /&gt;
	-- or return nil if unable to improve the summary.&lt;br /&gt;
	local better = Collection.new()&lt;br /&gt;
	local improved&lt;br /&gt;
	for _, range in ipairs(summary) do&lt;br /&gt;
		local split = split_range(iplist, range)&lt;br /&gt;
		if split then&lt;br /&gt;
			improved = true&lt;br /&gt;
			for _, r in ipairs(split) do&lt;br /&gt;
				better:add(r)&lt;br /&gt;
			end&lt;br /&gt;
		else&lt;br /&gt;
			better:add(range)&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return improved and better&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function make_summaries(iplist)&lt;br /&gt;
	-- Return a collection where each item is a summary.&lt;br /&gt;
	-- A summary is a table of one or more ranges.&lt;br /&gt;
	-- A summary covers all the given IPs and probably more.&lt;br /&gt;
	-- A range is a table representing a CIDR block such as 1.2.248.0/21.&lt;br /&gt;
	-- The first summary found is a single range; each subsequent summary&lt;br /&gt;
	-- (if any) uses more ranges to better target the given IPs.&lt;br /&gt;
	-- The result omits any summary with a range size that is too small (too many IPs).&lt;br /&gt;
	local function good_size(summary)&lt;br /&gt;
		for _, range in ipairs(summary) do&lt;br /&gt;
			if range.size &amp;lt; iplist.minsize then&lt;br /&gt;
				return false&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
		return true&lt;br /&gt;
	end&lt;br /&gt;
	local summaries = Collection.new()&lt;br /&gt;
	if iplist.n &amp;gt; 0 then&lt;br /&gt;
		for i = 1, iplist.n - 1 do&lt;br /&gt;
			-- Set length of prefixes common between each pair of IPs.&lt;br /&gt;
			iplist[i].common = common_prefix_length(iplist[i], iplist[i+1])&lt;br /&gt;
		end&lt;br /&gt;
		local summary = { make_range(iplist, 1, iplist.n) }&lt;br /&gt;
		while summary and summaries.n &amp;lt; iplist.maxresults do&lt;br /&gt;
			if good_size(summary) then&lt;br /&gt;
				summaries:add(summary)&lt;br /&gt;
			end&lt;br /&gt;
			summary = better_summary(iplist, summary)&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return summaries&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function extract_ipv4(result, omitted, line)&lt;br /&gt;
	-- Extract any IPv4 addresses from given line or throw error.&lt;br /&gt;
	-- Accept CIDR /n to specify a range (only accept 16 to 32).&lt;br /&gt;
	-- Addresses must be delimited with whitespace to reduce false positives.&lt;br /&gt;
	local function store(hit)&lt;br /&gt;
		local n = 32&lt;br /&gt;
		local lhs, rhs = hit:match('^(.-)/(%d+)$')&lt;br /&gt;
		if lhs then&lt;br /&gt;
			hit = lhs&lt;br /&gt;
			n = tonumber(rhs)&lt;br /&gt;
			if not (n and 16 &amp;lt;= n and n &amp;lt;= 32) then&lt;br /&gt;
				error('CIDR /n only accepts n = 16 to 32, invalid: ' .. lhs .. '/' .. rhs, 0)&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
		local ip = ipv4_address(hit)&lt;br /&gt;
		if ip then&lt;br /&gt;
			if n == 32 then&lt;br /&gt;
				result:add(ip)&lt;br /&gt;
			else&lt;br /&gt;
				if ip ~= ip_prefix(ip, n) then&lt;br /&gt;
					error('Invalid base address (host bits should be zero): ' .. hit, 0)&lt;br /&gt;
				end&lt;br /&gt;
				for _ = 1, 2^(32 - n) do&lt;br /&gt;
					result:add(ip)&lt;br /&gt;
					ip = ip_incremented(ip)&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
		else&lt;br /&gt;
			omitted:add(hit)&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	line = line:gsub(':', ' ')  -- so wikitext indents or other colons don't obscure an IVp4 address&lt;br /&gt;
	for hit in line:gmatch('%S+') do&lt;br /&gt;
		if hit:match('^%d+%.%d+[%.%d/]+$') then&lt;br /&gt;
			local _, n = hit:gsub('%.', '.')&lt;br /&gt;
			if n &amp;gt;= 3 then&lt;br /&gt;
				store(hit)&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function extract_ipv6(result, omitted, line)&lt;br /&gt;
	-- Extract any IPv6 addresses from given line or throw error.&lt;br /&gt;
	-- Addresses must be delimited with whitespace to reduce false positives.&lt;br /&gt;
	-- Want to accept all valid IPv6 despite the fact that contributors will&lt;br /&gt;
	-- not have an address starting with ':'.&lt;br /&gt;
	-- Also want to be able to parse arbitrary wikitext which might use colons&lt;br /&gt;
	-- for indenting. To achieve that, if an address at the start of a line&lt;br /&gt;
	-- is valid, use it; otherwise strip any leading colons and try again.&lt;br /&gt;
	for pos, hit in line:gmatch('()(%S+)') do&lt;br /&gt;
		local ipstr, length = hit:match('^([:%x]+)(/?%d*)$')&lt;br /&gt;
		if ipstr then&lt;br /&gt;
			local _, n = ipstr:gsub(':', ':')&lt;br /&gt;
			if n &amp;gt;= 2 then&lt;br /&gt;
				local ip = ipv6_address(ipstr)&lt;br /&gt;
				if not ip and pos == 1 then&lt;br /&gt;
					ipstr, n = ipstr:gsub('^:+', '')&lt;br /&gt;
					if n &amp;gt; 0 then&lt;br /&gt;
						ip = ipv6_address(ipstr)&lt;br /&gt;
					end&lt;br /&gt;
				end&lt;br /&gt;
				if ip then&lt;br /&gt;
					if length and #length &amp;gt; 0 then&lt;br /&gt;
						error('CIDR /n not accepted for IPv6: ' .. hit, 0)&lt;br /&gt;
					end&lt;br /&gt;
					result:add(ip)&lt;br /&gt;
				else&lt;br /&gt;
					omitted:add(hit)&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function contribs(address, strings, ipbase, size)&lt;br /&gt;
	-- Return a URL or wikilink to list the contributions for an IP or IP range,&lt;br /&gt;
	-- or return an empty string if cannot do anything useful.&lt;br /&gt;
	-- The given address is a string of either a single IP or a CIDR range.&lt;br /&gt;
	-- If using old system:&lt;br /&gt;
	--   For IPv6 CIDR, return a Special:Contributions link using an asterisk&lt;br /&gt;
	--   wildcard which should work if the user has enabled the gadget&lt;br /&gt;
	--   &amp;quot;Allow /16, /24 and /27 – /32 CIDR ranges on Special:Contributions&amp;quot;.&lt;br /&gt;
	local encoded, count = address:gsub('/', '%%2F')&lt;br /&gt;
	if strings.want_old and count &amp;gt; 0 then&lt;br /&gt;
		make_note(strings, 'range')&lt;br /&gt;
		if address:find(':', 1, true) then&lt;br /&gt;
			if ipbase and size then&lt;br /&gt;
				local digits = math.floor(size / 4)&lt;br /&gt;
				if digits &amp;lt; 3 then&lt;br /&gt;
					digits = 3&lt;br /&gt;
				end&lt;br /&gt;
				local wildcard = digits % 4 == 0 and ':*' or '*'&lt;br /&gt;
				local parts = {}&lt;br /&gt;
				for i = 1, 8 do&lt;br /&gt;
					local hex = string.format('%X', ipbase[i])  -- must be uppercase&lt;br /&gt;
					if digits &amp;gt;= 4 then&lt;br /&gt;
						parts[i] = hex&lt;br /&gt;
						digits = digits - 4&lt;br /&gt;
						if digits &amp;lt;= 0 then&lt;br /&gt;
							break&lt;br /&gt;
						end&lt;br /&gt;
					else&lt;br /&gt;
						local nz  -- number of leading zeros in this group of four digits&lt;br /&gt;
						if hex == '0' then&lt;br /&gt;
							nz = 4&lt;br /&gt;
						else&lt;br /&gt;
							nz = 4 - #hex&lt;br /&gt;
						end&lt;br /&gt;
						if digits &amp;lt;= nz then&lt;br /&gt;
							-- Cannot properly handle this case; have to omit group&lt;br /&gt;
							-- because &amp;quot;0&amp;quot; never occurs as the first digit.&lt;br /&gt;
							wildcard = ':*'&lt;br /&gt;
						else&lt;br /&gt;
							hex = string.rep('0', nz) .. hex  -- four digits&lt;br /&gt;
							parts[i] = hex:sub(1, digits)&lt;br /&gt;
						end&lt;br /&gt;
						break&lt;br /&gt;
					end&lt;br /&gt;
				end&lt;br /&gt;
				address = table.concat(parts, ':') .. wildcard&lt;br /&gt;
				local url = '[https://en.wikipedia.org/wiki/Special:Contributions/%s?ucstart=%s contribs]'&lt;br /&gt;
				-- %s = IPv6 prefix address in uppercase with '*' wildcard at end&lt;br /&gt;
				-- %s = Start date formatted 'yyyymmdd000000'&lt;br /&gt;
				return string.format(url, address, start_date('ymdHMS', strings.months)) .. make_note(strings, 'gadget')&lt;br /&gt;
			end&lt;br /&gt;
			return ''  -- no contributions link available&lt;br /&gt;
		end&lt;br /&gt;
		local url = '[https://tools.wmflabs.org/xtools/rangecontribs/?project=en.wikipedia.org&amp;amp;namespace=all&amp;amp;limit=50&amp;amp;text=%s&amp;amp;begin=%s contribs]'&lt;br /&gt;
		-- %s = IPv4 CIDR range with '/' changed to '%2F'&lt;br /&gt;
		-- %s = Start date formatted 'yyyy-mm-dd'&lt;br /&gt;
		return string.format(url, encoded, start_date('y-m-d', strings.months))&lt;br /&gt;
	end&lt;br /&gt;
	return '[[Special:Contributions/' .. address .. '|contribs]]'&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Strings for results using plain text.&lt;br /&gt;
-- The pre tags used are html which do not provide &amp;quot;nowiki&amp;quot;,&lt;br /&gt;
-- but that is not required by the text used.&lt;br /&gt;
local plaintext = {&lt;br /&gt;
&lt;br /&gt;
header = [=[&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Total       Affected     Given        Range]=],&lt;br /&gt;
&lt;br /&gt;
footer = '&amp;lt;/pre&amp;gt;',&lt;br /&gt;
&lt;br /&gt;
sumfirst = [=[&lt;br /&gt;
----------------------------------------------------------&lt;br /&gt;
%s%-12s %-12s %-11d %s%s]=],&lt;br /&gt;
-- %s = empty string (dummy for compatibility)&lt;br /&gt;
-- %s = total affected&lt;br /&gt;
-- %s = affected&lt;br /&gt;
-- %d = given (number of addresses given in input covered by this range)&lt;br /&gt;
-- %s = IP address range&lt;br /&gt;
-- %s = empty string&lt;br /&gt;
&lt;br /&gt;
sumnext = [=[&lt;br /&gt;
             %-12s %-11d %s%s]=],&lt;br /&gt;
-- %s = affected&lt;br /&gt;
-- %d = given&lt;br /&gt;
-- %s = IP address range&lt;br /&gt;
-- %s = empty string&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
-- Strings for results using a table in wikitext.&lt;br /&gt;
local wikitable = {&lt;br /&gt;
&lt;br /&gt;
header = [=[&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Total&amp;lt;br /&amp;gt;affected !! Affected&amp;lt;br /&amp;gt;addresses !! Given&amp;lt;br /&amp;gt;addresses !! Range !! Contribs]=],&lt;br /&gt;
&lt;br /&gt;
footer = '|}',&lt;br /&gt;
&lt;br /&gt;
sumfirst = [=[&lt;br /&gt;
|- style=&amp;quot;background: darkgray; height: 6px;&amp;quot;&lt;br /&gt;
|colspan=&amp;quot;5&amp;quot; |&lt;br /&gt;
|- style=&amp;quot;vertical-align: top;&amp;quot;&lt;br /&gt;
|rowspan=&amp;quot;%s&amp;quot; |%s ||%s ||%d ||%s ||%s]=],&lt;br /&gt;
-- %s = string of number of ranges in summary (number of rows)&lt;br /&gt;
-- %s = total affected&lt;br /&gt;
-- %s = affected&lt;br /&gt;
-- %d = given&lt;br /&gt;
-- %s = IP address range&lt;br /&gt;
-- %s = contributions link&lt;br /&gt;
&lt;br /&gt;
sumnext = [=[&lt;br /&gt;
|-&lt;br /&gt;
|%s ||%d ||%s ||%s]=],&lt;br /&gt;
-- %s = affected&lt;br /&gt;
-- %d = given&lt;br /&gt;
-- %s = IP address range&lt;br /&gt;
-- %s = contributions link&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
local function show_summary(lines, strings, iplist, summary)&lt;br /&gt;
	-- Show the summary by adding table wikitext or plain text to lines.&lt;br /&gt;
	local want_plain = iplist.want_plain&lt;br /&gt;
	local total = 0&lt;br /&gt;
	for _, range in ipairs(summary) do&lt;br /&gt;
		-- A number is a double which easily handles 2^128 = 3.4e38.&lt;br /&gt;
		total = total + 2^(iplist.ipsize - range.size)&lt;br /&gt;
	end&lt;br /&gt;
	for i, range in ipairs(summary) do&lt;br /&gt;
		local prefix = ip_string(range.prefix)&lt;br /&gt;
		local size = range.size&lt;br /&gt;
		local affected = describe_size(iplist.ipsize, size)&lt;br /&gt;
		local given = range.ilast - range.ifirst + 1&lt;br /&gt;
		local address&lt;br /&gt;
		local link = ''&lt;br /&gt;
		if size == iplist.ipsize then&lt;br /&gt;
			address = prefix&lt;br /&gt;
			if not want_plain then&lt;br /&gt;
				link = contribs(address, strings)&lt;br /&gt;
			end&lt;br /&gt;
		else&lt;br /&gt;
			address = prefix .. '/' .. size&lt;br /&gt;
			if not want_plain then&lt;br /&gt;
				link = contribs(address, strings, range.prefix, size)&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
		local s&lt;br /&gt;
		if i == 1 then&lt;br /&gt;
			s = string.format(strings.sumfirst,&lt;br /&gt;
					want_plain and '' or tostring(#summary),&lt;br /&gt;
					describe_total(total),&lt;br /&gt;
					affected, given, address, link)&lt;br /&gt;
		else&lt;br /&gt;
			s = string.format(strings.sumnext,&lt;br /&gt;
					affected, given, address, link)&lt;br /&gt;
		end&lt;br /&gt;
		-- Pre tags returned by a module are html tags, not like wikitext &amp;lt;pre&amp;gt;...&amp;lt;/pre&amp;gt;.&lt;br /&gt;
		lines:add(want_plain and mw.text.nowiki(s) or s)&lt;br /&gt;
	end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function process_ips(lines, iplist, omitted)&lt;br /&gt;
	-- Process a list of IP addresses, adding text of results to lines.&lt;br /&gt;
	-- The list should contain either all IPv4 addresses, or all IPv6 (not a mixture).&lt;br /&gt;
	local seq1, seq2, seqmany&lt;br /&gt;
	local function show_sequence()&lt;br /&gt;
		if seq1 and seq2 then&lt;br /&gt;
			local text = ip_string(seq1)&lt;br /&gt;
			if seqmany then&lt;br /&gt;
				seqmany = false&lt;br /&gt;
				text = text .. ' – ' .. ip_string(seq2)&lt;br /&gt;
			end&lt;br /&gt;
			seq1 = nil&lt;br /&gt;
			seq2 = nil&lt;br /&gt;
			local markup = text:sub(1, 1) == ':' and ':&amp;lt;nowiki/&amp;gt;' or ':'&lt;br /&gt;
			lines:add(markup .. text)&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	local function show_ip(ip)&lt;br /&gt;
		-- Show IP or record it to be included in a &amp;quot;from to&amp;quot; sequence of IPs.&lt;br /&gt;
		if is_next_ip(seq2, ip) then&lt;br /&gt;
			seq2 = ip&lt;br /&gt;
			seqmany = true&lt;br /&gt;
		else&lt;br /&gt;
			show_sequence()&lt;br /&gt;
			seq1 = ip&lt;br /&gt;
			seq2 = ip&lt;br /&gt;
			seqmany = false&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	if iplist.n &amp;lt; 1 then&lt;br /&gt;
		return&lt;br /&gt;
	end&lt;br /&gt;
	if lines.n &amp;gt; 0 then&lt;br /&gt;
		lines:add('')&lt;br /&gt;
	end&lt;br /&gt;
	if omitted.n &amp;gt; 0 then&lt;br /&gt;
		lines:add('Warning, omitted as invalid: ' .. omitted:join(' '))&lt;br /&gt;
		lines:add('')&lt;br /&gt;
	end&lt;br /&gt;
	local heading_line&lt;br /&gt;
	if not iplist.nolist then&lt;br /&gt;
		lines:add('')  -- this blank line is replaced with a heading&lt;br /&gt;
		heading_line = lines.n&lt;br /&gt;
	end&lt;br /&gt;
	local duplicates = Collection.new()&lt;br /&gt;
	local previous&lt;br /&gt;
	iplist:sort()&lt;br /&gt;
	-- Check for duplicates which can interfere with method to get ranges.&lt;br /&gt;
	for i, ip in ipairs(iplist) do&lt;br /&gt;
		if previous == ip then&lt;br /&gt;
			duplicates:add(i)  -- index to omit duplicate later&lt;br /&gt;
		elseif not iplist.nolist then&lt;br /&gt;
			show_ip(ip)&lt;br /&gt;
		end&lt;br /&gt;
		previous = ip&lt;br /&gt;
	end&lt;br /&gt;
	show_sequence()&lt;br /&gt;
	local duplicate_text = ''&lt;br /&gt;
	if duplicates.n &amp;gt; 0 then&lt;br /&gt;
		duplicate_text = ' (after omitting some duplicates)'&lt;br /&gt;
		for i = duplicates.n, 1, -1 do&lt;br /&gt;
			iplist:remove(duplicates[i])&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	local heading_text = string.format('Sorted %d %s address%s',&lt;br /&gt;
		iplist.n,&lt;br /&gt;
		iplist.ipname,&lt;br /&gt;
		iplist.n == 1 and '' or 'es'&lt;br /&gt;
		)&lt;br /&gt;
	if heading_line then&lt;br /&gt;
		lines[heading_line] = heading_text .. duplicate_text .. ':'&lt;br /&gt;
	end&lt;br /&gt;
	local strings = iplist.want_plain and plaintext or wikitable&lt;br /&gt;
	strings.notes = nil  -- needed when module is kept loaded for multiple tests&lt;br /&gt;
	strings.want_old = iplist.want_old&lt;br /&gt;
	strings.nonote = iplist.nonote&lt;br /&gt;
	strings.months = iplist.months&lt;br /&gt;
	lines:add(strings.header)&lt;br /&gt;
	local upto = lines.n&lt;br /&gt;
	for _, summary in ipairs(make_summaries(iplist)) do&lt;br /&gt;
		show_summary(lines, strings, iplist, summary)&lt;br /&gt;
	end&lt;br /&gt;
	lines:add(strings.footer)&lt;br /&gt;
	if upto + 1 == lines.n then&lt;br /&gt;
		-- Show message in the very unlikely event that no results are found.&lt;br /&gt;
		lines:add('----')&lt;br /&gt;
		lines:add('No suitable ranges found; use &amp;lt;code&amp;gt;|results=all&amp;lt;/code&amp;gt; to see all ranges.')&lt;br /&gt;
	end&lt;br /&gt;
	if strings.notes then&lt;br /&gt;
		lines:add('')&lt;br /&gt;
		lines:add(&amp;quot;'''Notes'''&amp;quot;)&lt;br /&gt;
		for _, key in ipairs({'range', 'gadget'}) do&lt;br /&gt;
			if strings.notes[key] then&lt;br /&gt;
				lines:add(strings.notes[key])&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function make_options(args)&lt;br /&gt;
	-- Return table of options from validated args or throw error.&lt;br /&gt;
	local options = {}&lt;br /&gt;
	if not empty(args.comment) then&lt;br /&gt;
		options.comment = args.comment&lt;br /&gt;
	end&lt;br /&gt;
	-- Parameter 'months' is only used if 'old' is also used.&lt;br /&gt;
	local months = math.floor(tonumber(args.months) or tonumber(args.month) or 1)&lt;br /&gt;
	if months &amp;lt; 1 then&lt;br /&gt;
		months = 1&lt;br /&gt;
	elseif months &amp;gt; 12 then&lt;br /&gt;
		months = 12&lt;br /&gt;
	end&lt;br /&gt;
	options.months = months  -- silently ignore invalid input&lt;br /&gt;
	local allocation&lt;br /&gt;
	if not empty(args.allocation) then&lt;br /&gt;
		allocation = tonumber(args.allocation)&lt;br /&gt;
		if not (allocation and 48 &amp;lt;= allocation and allocation &amp;lt;= 128) then&lt;br /&gt;
			error('Invalid allocation &amp;quot;' .. args.allocation .. '&amp;quot; (should be 48 to 128; default is 64)', 0)&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	local maxresults&lt;br /&gt;
	if not empty(args.results) then&lt;br /&gt;
		if args.results == 'all' then&lt;br /&gt;
			options.all = true&lt;br /&gt;
			allocation = allocation or 128&lt;br /&gt;
			maxresults = 1000&lt;br /&gt;
		else&lt;br /&gt;
			maxresults = tonumber(args.results)&lt;br /&gt;
			if not (maxresults and 1 &amp;lt;= maxresults and maxresults &amp;lt;= 100) then&lt;br /&gt;
				error('Invalid results &amp;quot;' .. args.results .. '&amp;quot; (should be 1 to 100)', 0)&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	options.allocation = allocation or 64&lt;br /&gt;
	options.maxresults = maxresults or 10&lt;br /&gt;
	local keywords = {&lt;br /&gt;
		-- Table of k, v strings.&lt;br /&gt;
		-- If an argument matches k, an option named v is set to true.&lt;br /&gt;
		ok = 'noannounce',&lt;br /&gt;
		old = 'want_old',&lt;br /&gt;
		nolist = 'nolist',&lt;br /&gt;
		nonote = 'nonote',&lt;br /&gt;
		text = 'text',&lt;br /&gt;
	}&lt;br /&gt;
	local want_old&lt;br /&gt;
	for i, arg in ipairs(args) do&lt;br /&gt;
		local flag = keywords[arg:match('^%s*(%w+)%s*$')]&lt;br /&gt;
		if flag then&lt;br /&gt;
			options[i] = 'skip'&lt;br /&gt;
			options[flag] = true&lt;br /&gt;
			if flag == 'want_old' then&lt;br /&gt;
				want_old = true&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	if not want_old then&lt;br /&gt;
		options.nonote = true&lt;br /&gt;
	end&lt;br /&gt;
	return options&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function _IPblock(args)&lt;br /&gt;
	-- Process given args; can be called from another module.&lt;br /&gt;
	-- Throw an error if need to report a problem.&lt;br /&gt;
	local options = make_options(args)&lt;br /&gt;
	local v4list, v4omitted = Collection.new(), Collection.new()&lt;br /&gt;
	local v6list, v6omitted = Collection.new(), Collection.new()&lt;br /&gt;
	v4list.ipsize = 32&lt;br /&gt;
	v4list.ipname = 'IPv4'&lt;br /&gt;
	v6list.ipsize = 128&lt;br /&gt;
	v6list.ipname = 'IPv6'&lt;br /&gt;
	v4list.allocation = 32&lt;br /&gt;
	v6list.allocation = options.allocation&lt;br /&gt;
	if options.all then&lt;br /&gt;
		v4list.minsize = 0&lt;br /&gt;
		v6list.minsize = 0&lt;br /&gt;
	else&lt;br /&gt;
		v4list.minsize = 16  -- cannot block more IPs than /16 for IPv4&lt;br /&gt;
		v6list.minsize = 19  -- or /19 for IPv6 ($wgBlockCIDRLimit)&lt;br /&gt;
	end&lt;br /&gt;
	for _, k in ipairs({'maxresults', 'months', 'want_old', 'nolist', 'nonote'}) do&lt;br /&gt;
		v4list[k] = options[k]&lt;br /&gt;
		v6list[k] = options[k]&lt;br /&gt;
	end&lt;br /&gt;
	if options.text then&lt;br /&gt;
		v4list.want_plain = true&lt;br /&gt;
		v6list.want_plain = true&lt;br /&gt;
	end&lt;br /&gt;
	for i, arg in ipairs(args) do&lt;br /&gt;
		if options[i] ~= 'skip' then&lt;br /&gt;
			for line in string.gmatch(arg .. '\n', '[\t ]*(.-)[\t\r ]*\n') do&lt;br /&gt;
				-- Skip line if is empty or a comment.&lt;br /&gt;
				if line ~= '' then&lt;br /&gt;
					local comment = options.comment&lt;br /&gt;
					if not (comment and line:sub(1, #comment) == comment) then&lt;br /&gt;
						line = line&lt;br /&gt;
							:gsub('[Ss]pecial:[Cc]ontrib%w*/', ' ')  -- so input &amp;quot;Special:Contributions/1.2.3.4&amp;quot; works&lt;br /&gt;
							:gsub('[Tt]alk:', ' ')&lt;br /&gt;
							:gsub('[Uu]ser:', ' ')&lt;br /&gt;
							:gsub('[!&amp;quot;#&amp;amp;\'()+,%-;&amp;lt;=&amp;gt;?[%]_{|}]', ' ')  -- replace accepted delimiters with a space&lt;br /&gt;
							:gsub('\226\128\142', ' ')  -- replace LTR marks (U+200E)&lt;br /&gt;
						extract_ipv4(v4list, v4omitted, line)&lt;br /&gt;
						extract_ipv6(v6list, v6omitted, line)&lt;br /&gt;
					end&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	if v4list.n &amp;lt; 1 and v6list.n &amp;lt; 1 then&lt;br /&gt;
		error('No valid IPv4 or IPv6 address in arguments', 0)&lt;br /&gt;
	end&lt;br /&gt;
	local lines = Collection.new()&lt;br /&gt;
	if not options.noannounce then&lt;br /&gt;
		-- 1: Commented out April 2016 as expired.&lt;br /&gt;
		-- 1: lines:add(&amp;quot;'''Please see [[Template talk:Blockcalc#Version February 2016|this announcement]].'''&amp;quot;)&lt;br /&gt;
		-- 2: Commented out December 2017 as expired.&lt;br /&gt;
		-- 2: lines:add(&amp;quot;'''By default, links now use [[Special:Contributions]] per [[Template talk:IP range calculator#Version November 2017|this announcement]].'''&amp;quot;)&lt;br /&gt;
	end&lt;br /&gt;
	process_ips(lines, v4list, v4omitted)&lt;br /&gt;
	process_ips(lines, v6list, v6omitted)&lt;br /&gt;
	return lines:join('\n')&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function IPblock(frame)&lt;br /&gt;
	-- Return wikitext to display the smallest IPv4 or IPv6 CIDR range that&lt;br /&gt;
	-- covers each address given in the arguments, or return error text.&lt;br /&gt;
	-- Input can have any mixture of IPs; IPv4 and IPv6 are processed separately.&lt;br /&gt;
	local ok, msg = pcall(_IPblock, frame:getParent().args)&lt;br /&gt;
	if ok then&lt;br /&gt;
		return msg&lt;br /&gt;
	end&lt;br /&gt;
	return '&amp;lt;strong class=&amp;quot;error&amp;quot;&amp;gt;Error: ' .. msg .. '&amp;lt;/strong&amp;gt;'&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function sha1(frame)&lt;br /&gt;
	-- Return SHA-1 hash of first parameter.&lt;br /&gt;
	-- This is for use at [[User:Johnuniq/Security]] to generate hash of a password.&lt;br /&gt;
	local text = (frame.args[1] or ''):match(&amp;quot;^%s*(.-)%s*$&amp;quot;)&lt;br /&gt;
	if text ~= '' then&lt;br /&gt;
		return 'SHA-1 hash after removing any leading or trailing whitespace is &amp;lt;code&amp;gt;' .. mw.hash.hashValue('sha1', text) .. '&amp;lt;/code&amp;gt;'&lt;br /&gt;
	end&lt;br /&gt;
	return 'Usage: &amp;lt;code&amp;gt;&amp;amp;#123;{#invoke:IPblock|sha1|text}&amp;amp;#125;&amp;lt;/code&amp;gt; to display the SHA-1 hash of &amp;lt;code&amp;gt;text&amp;lt;/code&amp;gt;.'&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return {&lt;br /&gt;
	IPblock = IPblock,&lt;br /&gt;
	_IPblock = _IPblock,&lt;br /&gt;
	sha1 = sha1,&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Zoran</name></author>
	</entry>
</feed>