Модуль:SummaryII/format

Материал из свободной русской энциклопедии «Традиция»
Перейти к навигации Перейти к поиску

Для документации этого модуля может быть создана страница Модуль:SummaryII/format/doc

--[[
	Dependencies:
--]]
local dep = require 'Module:SummaryII/dependencies'
local wrap, yield = dep.wrap, dep.yield
local lc, sub, gsub, find, gmatch, len = dep.lc, dep.sub, dep.gsub, dep.find, dep.gmatch, dep.len
local trim, split = dep.trim, dep.split
local lpeg, pcre, re = dep.lpeg, dep.regex, dep.re
local sort, merge, merge_to_first, join, clone = dep.sort, dep.merge, dep.merge_to_first, dep.join, dep.clone
local ask = dep.ask

-- Default format syntax:
local default_syntax  = {
	-- Iterators
	-- Iterators over strings:
	quote1		= '"'
  , quote2		= "'"
  , re			= {'`', '`'}
  , pcre		= {'/', '/'}
  , inject_str	= {'{', '}'}
  , value		= '='
	-- Iterator 'arithmetics':
  , union		= '+'
  , union_all	= '&'
  , choice		= ','
  , index		= '.'
  , parentheses	= {'(', ')'}
  , open		= '('
  , close		= ')'
  , group		= '@'
  , order		= '%'
  , field_sep	= ','  
	-- Iterators over tables:
  , inject_tbl	= '^'
	-- Formatting:
  , tag			= {'<', '>'}
  , slash		= '/'
  , optional	= '?'
  , necessary	= '!'
  , default		= '~'
  , separator	= ';'
  , assign		= '*'
  , key_label	= '#'
  , value_label	= '_'
}	-- local default_syntax

-- Field usage:
local function unused (table, usage)
	local unused_table = {}
	for key, value in pairs (table) do
		if type (value) ~= 'table' then
			if not (usage [table] and usage [table] [key]) then
				unused_table [key] = value
			end
		else
			unused_table [key] = unused (value, usage)
		end
	end
	return unused_table
end	-- local function unused (table, usage)

-- Return a grammar for parsing format strings:
local function grammar (syntax, inject)
	local P, V, S, C, Cg, Cb, Cc, Ct, Cf, Cmt
		= lpeg.P, lpeg.V, lpeg.S, lpeg.C, lpeg.Cg, lpeg.Cb, lpeg.Cc, lpeg.Ct, lpeg.Cf, lpeg.Cmt
	local escape = P'\\'
	local space = lpeg.locale ().space
	local spaces, any, never = space ^ 0, P (1), P (false)
	
	local function s (string)
		return spaces * string * spaces
	end	-- local function s (str)
	
	local syntax, inject = syntax or default_syntax, inject or {}
	
	local open, close			= s (syntax.parentheses [1]), s (syntax.parentheses [2])
	local quote1, quote2		= syntax.quote1, syntax.quote2
	local open_re, close_re		= syntax.re [1], syntax.re [2]
	local open_pcre, close_pcre	= syntax.pcre [1], syntax.pcre [2]
	local open_inj, close_inj	= syntax.inject_str [1], syntax.inject_str [2]
	
	local union, union_all, choice, index
		= s (syntax.union), s (syntax.union_all), s (syntax.choice), s (syntax.index)
	local inject_tbl = syntax.inject_tbl
	local equals, group, order, comma = s (syntax.value), s (syntax.group), s (syntax.order), s (syntax.field_sep)
	
	local open_tag, close_tag, slash = syntax.tag [1], syntax.tag [2], syntax.slash

	local optional, necessary, separator, default, assign
		= syntax.optional, syntax.necessary, syntax.separator, syntax.default, syntax.assign
		
	local key_label, value_label = syntax.key_label, syntax.value_label

	local specials = equals + choice + union + union_all + index + group + order
				   + open_re + open_pcre + open_inj + inject_tbl
				   + open + close
				   + s (close_tag)

	-- A row of characters, except ...:
	local function sans (...)
		local forbidden = never
		for _, char in ipairs {...} do
			forbidden = forbidden + char
		end
		return (any - forbidden + escape * forbidden) ^ 1
	end	-- local function sans (...)

	-- A list of injectable functions:
	local func_name = P (false)
	if inject and type (inject) == 'table' then
		for key, func in pairs (inject) do
			func_name = func_name + key
		end
	end

	-- Unused fields:
	local usage = {}
	local function log_usage (table, key)
		usage [table] = usage [table] or {}
		usage [table] [key] = true
	end	-- local function log_usage (table, key)

	-- Iterator arithmetics operators' priorities:
	local operator_priorities = {
		restrict = 1, enter = 2, choice = 3, union = 3, union_all = 3
	}
	local function prioritised_operators (priority, priority0)
		if priority == 0 then
			return V (priority0)
		else
			local operators = never
			for operator, operator_priority in pairs (operator_priorities) do
				if operator_priority == priority then
					operators = operators + V (operator)
				end
			end
			local higher_priority = prioritised_operators (priority - 1, priority0)
			return Cf ((V'parentheses' + higher_priority)
				 * Cg (operators * (V'parentheses' + higher_priority)) ^ 0
				 , function (iterator1, combiner, iterator2)
				 		-- Combine iterators with combiner:
						return iterator2 and combiner (iterator1, iterator2) or iterator1
				   end)	-- function (iterator1, combiner, iterator2)
		end
	end	-- local function prioritised_operators (priority, priority0)
	
	-- A trivial format to be used in self-closing or empty tags:
	local function trivial_format (table)
		return tostring (table [value_label]) -- TODO: may need to check if type (table) == 'table'
	end	-- local function trivial_format (table)

	-- Tags differing in the ways they attach to the enclosing format:
	local connectors = {
		-- Optional macro (<?iterator>format</?><? iterator />):
		[optional] =  function (table, chunks, separator, chunk)
	  		chunks [#chunks + 1] = chunk
	  		return false, chunks, separator
	  	end	-- [optional] =  function (table, chunks, separator, chunk)
	  	-- Necessary macro (<!iterator>format</!><! iterator />):
	  , [necessary] = function (table, chunks, separator, chunk)
	  		if (chunk or '') == '' then
	  			return true
	  		else
	  			chunks [#chunks + 1] = chunk
	  			return false, chunks, separator
	  		end
	  	end	-- [necessary] = function (table, chunks, separator, chunk)
	  	-- Separator (<;iterator>format</;><; iterator />):
	  , [separator] = function (table, chunks, separator, chunk)	  	
	  		return false, chunks, chunk
	  	end	-- [separator] = function (table, chunks, separator, chunk)
	  	-- Default. Only used if the previous chunks are empty
	  	--  (<~iterator>format</~><~ iterator />):
	  , [default] = function (table, chunks, separator, chunk)
	  		if join (chunks) == '' then
	  			chunks [#chunks + 1] = chunk
	  		end
	  		return false, chunks, separator
	  	end	-- [default] = function (table, chunks, separator, chunk)
	  , [assign] = function (table, chunks, separator, chunk)
	  		table [chunk] = join (chunks)
	  		return false, chunks, separator
	  	end	-- [assign] = function (table, chunks, separator, chunk)
	}	-- local connectors
	-- A grammar for full (<?iterator>format</?>) or self-closing (<?iterator/>)tag:
	function make_tag (tag_name, connector, attribute)
		local pointer = not plaintext and (V'iterator' + Cc (false)) or V'plaintext'
		return P(open_tag) * tag_name * Cc (connector) * spaces
			 * (attribute * spaces * close_tag
			 * (V'format' + Cc (trivial_format)) * open_tag * slash * tag_name
			   -- Self-closing, e.g. <?... />
			 + attribute * spaces * slash * Cc (trivial_format))
			 * close_tag
	end	-- make_tag (tag_name, func, plaintext)
	local tags, tags_and_connectors = never, never
	for tag, _ in pairs (connectors) do
		tags = tags + tag
		tags_and_connectors = tags_and_connectors
							+ make_tag (tag, connectors [tag], V'iterator' + Cc (false))
	end
	
	-- Group and sort (if order == true) by fields values return by iterator:
	local function group_and_sort (table, iterator, fields, order)
		-- Distinct:
		local distinct, no, numbered = {}, 0, {}
		for key, value, captures, original_table in iterator (table) do
			log_usage (original_table or table, key)
			local new_key = ''
			for _, field in ipairs (fields)	do
				new_key = new_key .. field .. '=' .. tostring (captures [field])
			end
			local target = distinct [new_key]
			if target then
				distinct [new_key].captures = merge (target.captures, captures)
			else
				distinct [new_key] = {key = key, value = value, captures = captures}
				no = no + 1
				numbered [no] = distinct [new_key] -- reference.
			end
		end	-- for key, value, captures in iterator (table)
		-- Sort:
		if order then
			local function compare (a, b)
				for _, field in ipairs (fields)	do
					if (a.captures [field] or '') < (b.captures [field] or '') then
						return true
					end
				end
				return false
			end	--  local function compare (a, b)
			sort (numbered, compare)
		end
		return numbered
	end	-- local function group_and_sort (table, iterator, fields, order)
	
	-- Finally, a grammar for table formatter:
	local g = P {'full_format'
	  , full_format = V'format' / function (formatter)
	  		return function (table)
	  			return formatter (table), usage
	  		end
	  	end * -1	-- full_format = V'format' / function (formatter)
	  , format = (V'macro' + V'text') ^ 1 / function (...)
			local chunks = {...}
			return function (table)
				local fail, formatted, separator = false, {}, ''
				for _, chunk in ipairs (chunks) do
					local expanded, connector
					if type (chunk) == 'string' then
						expanded, connector = chunk, connectors [optional]
					else
						expanded, connector = chunk (table)
					end
					fail, formatted, separator = connector (table, formatted, separator, expanded)
					if fail then return nil end
				end
				return #formatted > 0 and join (formatted) or nil, separator
			end	-- return function (table)
		end	-- (V'macro' + V'text') ^ 1 / function (...)
	  , macro = tags_and_connectors	/ function (connector, iterator, format)
	  		return function (table)
	  			if iterator then
	  				local results, separator = {}, ''
	  				for key, value, captures in iterator (table) do
	  					local new_context = merge (table, captures
	  									  , {[key_label] = tostring (key), [value_label] = tostring (value)})
	  					results [#results + 1], separator = format (new_context)
	  				end
					return join (results, separator), connector
				else
					return format (table), connector
				end
			end	-- return function (table)
  		end	-- / function (connector, iterator, format)
	  , text = C (sans (open_tag * (tags + assign + slash)) ^ 1)
	  
	  , iterator = V'grouped_iterator' + V'ungrouped_iterator'
		-- A groupable/sortable iterator like /regex1/ = /regex2/ @ capture1, capture2:
	  , grouped_iterator = prioritised_operators (3, 'simple_table_iterator')
				  * (order * Cc (true) + group * Cc (false))
				  * V'plaintext' * (comma * V'plaintext') ^ 0 * spaces
				  / function (iterator, order, ...) -- distinct and sort.
			local fields = {...}
			return function (table)
				return wrap (function ()
					-- Return distinct and sorted:
					for _, match in ipairs (group_and_sort(table, iterator, fields, order)) do
						yield (match.key, match.value, match.captures)
					end
				end)	-- return wrap (function ()
			end	-- return function (table)
		end	-- / function (iterator, order, ...)
		-- A simple iterator without grouping or sorting:
	  , ungrouped_iterator = prioritised_operators (3, 'simple_table_iterator')	/ function (iterator)
	  		return function (table)
				return wrap (function ()
					for key, value, captures, original_table in iterator (table) do
						log_usage (original_table or table, key)							
						yield (key, value, captures)
					end
				end)	-- return wrap (function ()
			end	-- return function (table)
		end	-- ungrouped_iterator = (prioritised_operators (3, 'simple_table_iterator')	/ function (iterator)
		-- () to order operator priorities:
	  , parentheses = open * V'iterator' * close
		-- iterator1 + iterator2, distinct values:
	  , union = union * Cc (function (iterator1, iterator2)
	  		return function (table)
	  			return wrap (function ()
	  				local set = {}
	  				for key1, value1, captures1 in iterator1 (table) do
	  					set [value1] = true
	  					yield (key1, value1, captures1)
	  				end
	  				for key2, value2, captures2 in iterator2 (table) do
	  					if not set [value2] then
	  						yield (key2, value2, captures2)
	  					end
	  				end
	  			end)	-- return wrap (function ()
	  		end	-- return function (table)
	  	end)	-- union = union * Cc (function (iterator1, iterator2)
	  	-- iterator1 & iterator2, repeating values:
	  , union_all = union_all * Cc (function (iterator1, iterator2)
	  		return function (table)
	  			return wrap (function ()
	  				for key1, value1, captures1 in iterator1 (table) do
	  					yield (key1, value1, captures1)
	  				end
	  				for key2, value2, captures2 in iterator2 (table) do
	  					yield (key2, value2, captures2)
	  				end
	  			end)	-- return wrap (function ()
	  		end	-- return function (table)
	  	end)	-- union_all = union_all * Cc (function (iterator1, iterator2)
		-- iterator1, iterator2; works first non-empty:
		-- TODO: check associativity:
	  , choice = choice * Cc (function (iterator1, iterator2)
	  		return function (table)
	  			return wrap (function ()
	  				local found = false
	  				for key1, value1, captures1 in iterator1 (table) do
	  					found = true
	  					yield (key1, value1, captures1)
	  				end
	  				if not found then
		  				for key2, value2, captures2 in iterator2 (table) do
		  					yield (key2, value2, captures2)
		  				end
	  				end
	  			end)	-- return wrap (function ()
	  		end	-- return function (table)
	  	end)	-- choice = choice * Cc (function (iterator1, iterator2)
		-- iterator1.iterator2 to enter subtables or captures:
	  , enter = index * Cc (function (iterator1, iterator2)
	  		return function (table)
	  			return wrap (function ()
		  			for key1, value1, captures1 in iterator1 (table) do
		  				local table2 = type (value1) == 'table' and value1 or captures1
		  				for key2, value2, captures2 in iterator2 (table2) do
		  					yield (key2, value2, merge (captures1, captures2), table2)
		  				end
		  			end
		  		end)	-- return wrap ()
		  	end	-- return function (table)
	  	end)	-- enter = index * Cc (function (iterator1, iterator2)
		-- iterator1 iterator2 to filter what iterator1 yields with iterator2;
		-- espacially useful as iterator1 = iterator2 to restrict both keys and values:
	  , restrict = Cc (function (iterator1, iterator2)
	  		return function (table)
	  			return wrap (function ()
		  			for key1, value1, captures1 in iterator1 (table) do
		  				for key2, value2, captures2 in iterator2 {[key1] = value1} do
		  					yield (key2, value2, merge (captures1, captures2))
		  				end
		  			end
	  			end)	-- return wrap (function ()
	  		end	-- return function (table)
	  	end)	-- restrict = Cc (function (iterator1, iterator2)
	
		-- This rule transforms a string iterator into a table iterator:
	  , simple_table_iterator = V'inject' + V'accelerated' + V'bruteforce'
	  , bruteforce = (equals * Cc (true) + Cc (false)) * V'string_iterator'
			/ function (filter_value, string_iterator)
				return function (table)
					return wrap (function ()
						for key, value in pairs (table) do
							for captures in string_iterator (filter_value and value or key, table) do
								yield (key, value, captures, table)
							end
						end
					end)	-- return wrap (function (t)
				end	-- return function (table)
			end	-- / function (value_filter, string_iterator)
		-- Injects a user-defined iterator:
	  , inject = Cmt (inject_tbl * spaces * C (func_name) * spaces, function (_, pos, func)
	  		local iterator = inject [func]
	  		if not iterator then
	  			return false
	  		end
	  		return pos, iterator
	  	end)	-- inject = Cmt (inject_tbl * space * C (func_name), function (_, pos, func)
		-- This is just to avoid iterating, when a simple <?key/> is parsed:			
	  , accelerated = V'plaintext' / function (key)
	  		return function (table)
	  			return wrap (function ()
	  				if table [key] then
	  					yield (key, table [key], {}, table)
	  				end
	  			end)	-- return wrap (function ()
	  		end	-- return function (table)
	  	end	-- accelerated = V'plaintext' / function (key)
			
	  , string_iterator	= V're' + V'pcre' + V'inject_string' + V'plain'
		-- `lpeg re` (see Module:re):
	  , re = Cmt (spaces * open_re * C(sans (close_re, close_tag)) * close_re * spaces
	  	   , function (_, pos, pattern)
	  		local success, compiled = pcall (re, '{|' .. pattern .. '|} {}')
	  		if not success then
	  			return false	-- an invalid LPEG re renders the whole grammar invalid.
	  		end
  			return pos, function (string)
  				local string = tostring (string)
		  		local start, finish, length, captures = 1, nil, len (string), {}
		  		return wrap (function ()
		  			while start and start <= length do
		  				captures, start = compiled:match (string, start)
		  				if start then
		  					yield (captures)
		  				end
		  			end
		  		end)	-- return function (string)
		  	end	-- return function (string)
	  	end)	-- function (_, pos, pattern)
		-- /perl-compatible regular expression/:
	  , pcre = Cmt (spaces * open_pcre * C(sans (close_pcre, close_tag)) * close_pcre * C (S'mxi' ^ 0) * spaces
			 , function (_, pos, pattern, flags)
	  		local success, compiled = pcall (pcre, pattern, flags)
	  		if not success then
	  			return false	-- an invalid PCRE renders the whole grammar invalid.
	  		end
	  		return pos, function (string)
	  			local string = tostring (string)
		  		local start, finish, length, captures = 1, nil, len (string), {}
		  		return wrap (function ()
		  			while start and start <= length do
		  				start, finish, captures = compiled:tfind (string, start)
		  				if start then
		  					start = finish + 1
		  					yield (captures)
		  				end
		  			end
		  		end)	-- return wrap (function ()
		  	end	-- return function (string)
	  	end)	-- , function (_, pos, pattern, flags)
	  , inject_string = Cmt (spaces * open_inj * C(sans (close_inj, close_tag)) * close_inj * spaces
			 , function (_, pos, func_name)
			local func = inject [func_name]
			if not func or type (func) ~= 'function' then
	  			return false	-- not a function.
	  		end
	  		return pos, func
	  	end)	-- , function (_, pos, func_name)	  	
		-- Simply field or 'field' or "field":
	  , plaintext = quote1 * C (sans (quote1)) * quote1
				  + quote2 * C (sans (quote2)) * quote2
				  + C(sans (specials, space))
	  , plain = V'plaintext' / function (str)
	  		return function (string)
	  			local string = tostring (string)	  			
				return wrap (function ()
					if string == str then
						yield {string}
					end
				end)	-- return wrap (function ()
			end	-- return function (string)
		end	-- plain = V'plain_text' / function (str)
	}	-- local g = P {'groups' ...}
	return g
end	-- local function grammar (inject)

-- Generate a table formatting function from tring/table format:
local function compile (format, inject, syntax)
	local pattern_grammar = grammar (syntax, inject)
	local rules = type (format) == 'table' and format or {format}
	local compileds = {}
	for _, rule in ipairs (rules) do
		local compiled = type (rule) ~= 'function' and pattern_grammar:match (rule) or rule
		if not compiled then
			return 'Pattern grammar does not match: ' .. (rule)
		end
		compileds [#compileds + 1] = compiled
	end
	return function (table)
		local formatted, final_usage = {}, {}
		for _, format in ipairs (compileds) do
			local usage
			formatted [#formatted + 1], usage = format (table)
			merge_to_first (final_usage, usage)
		end
		return join (formatted), unused (table, final_usage)
	end	-- return function (table)
end	-- local function compile (format, inject, syntax)

-- Exported table:
local p = {
	syntax	= default_syntax
  , grammar = grammar
  , compile = compile
}	-- local p

function p.test (table, format, inject, syntax)
	local test_table = {
		key			= 'The value',
		not_the_key	= 'The irrelevant value',
		key1		= 'Value one',
		key2		= 'Value two',
		key3		= 'Value three',
		default_key = 'Value one',
		never_used	= 'This parametre has never been used',
		subtable	= {
			field1	= 'A correct value in subtable',
			field2	= 'A wrong value in subtable',
			unused  = 'An unused value in subtable'
		},
		field1	= 'A correct value in table',
		field2	= 'A wrong value in table',
		side1   = 'Russia',
		commander1 = 'Skobelev',
		side2 = 'Turkey',
		commander2 = 'Osman-Pasha',
		losses2 = 'Very many'
	}
	local test_format = [[
What I found:
	plain = <?key/>
	pcre = <?/^key(?'no'\d+)$/ @ no><?no /> = <?_/><;>, </;></?>
	re = <? `'key' {:no: %d+ :}` @ no><?no /> = <?_/><;>, </;></?>	
	complex = <?subtable./^field(?'no'\d+)$/=/correct/ />
	choice1 = <?/^field(?'no'\d+)$/, (subtable./^field(?'no'\d+)$/) />
	choice2 = <?/^arg(?'no'\d+)$/, (subtable./^field(?'no'\d+)$/) />
	
Group and sort:
	union_all = <?/^key(?'no'\d+)$/ & default_key @ no><?_/><;>, </;></?>
	union = <?/^key(?'no'\d+)$/ + default_key @ no><?_/><;>, </;></?>		
	sorted union_all = <?/^key(?'no'\d+)$/ & default_key % no><?_/><;>, </;></?>
	sorted union = <?/^key(?'no'\d+)$/ + default_key % no><?_/><;>, </;></?>

Key and value:
	key and value = <?/^key(?'no'\d+)$/ @ no><?#/> = <?_/><;>, </;></?>

Fields from parent table:
	parent = <?subtable./^field(?'no'\d+)$/ = /^(?'value'.+)$/><!no /> = <!value /> (<?key />)<;>, </;></?>

Defaults:
	default not needed = <?><?key/><~>Default for key</~></?>
	default needed = <?><?clue/><~>Default for clue</~></?>
	two defaults = <?><?clue/><~><?clavis/></~><~>Default for clue/clavis</~></?>	
	
Collecting records:
	optional losses = <?>Sides: <?/^side(?'no'\d+)$/ = /^(?'side'.+)$/
					+ /^commander(?'no'\d+)$/ = /^(?'commander'.+)$/
					+ /^losses(?'no'\d+)$/ = /^(?'losses'.+)$/ @ no>Side: <?side />, commander: <?commander />, losses: <?losses /><;>; </;></?></?>
	necessary losses = <?>Sides: <?/^side(?'no'\d+)$/ = /^(?'side'.+)$/
					 + /^commander(?'no'\d+)$/ = /^(?'commander'.+)$/
					 + /^losses(?'no'\d+)$/ = /^(?'losses'.+)$/ @ no>Side: <!side />, commander: <!commander />, losses: <!losses /><;>; </;></?></?>

Formatted output:
	<?/^key(?'no'\d+)$/>no = <?no /><;>, </;></?>
	Head and tail: <?>Head: <!/^(?'stem'key)(?'no'\d+)$/ @ no>no = <?no /><;>, </;></!> -- tail!</?>
	Head and tail with default: <?><?>Head: <!/^(?'stem'clue)(?'no'\d+)$/ @ no>no = <?no /><;>, </;></!> -- tail!</?><~>Default</~></?>

No iterator:
	<?>Key = <?key />.</?>
	
Inject:
	simple ask: <?><?>Works: <! ^ask >title = <?title />, year = <?year /><;>; </;></!></?><~>No works</~></?>
	inject string iterator: <?>Keys: <! {lua} % no ><? no /> = <? _ /><;>, </;></!></?>	
	
Remember:
	<?>Processed <! key /><*>remembered</*></?>
	remembered (processed key) = <? remembered />
]]

		--[====[test_format = [==[<?><?/^side(?'no'\d+)$/ = /^(?'side'.+)$/
					+ /^commander(?'no'\d+)$/ = /^(?'commander'.+)$/
					+ /^losses(?'no'\d+)$/ = /^(?'losses'.+)$/ @ no>Side: <?side />, commander: <?commander />, losses: <?losses /><;>; </;></?></?>
]==]
		]====]
		--test_format = [=[<* remembered ><?/^key(?'no'\d+)$/ + default_key % no><?_/><;>, </;></?></*>]=]
	
	local function query (table)
		local result = ask {'[[Написан автором::Александр Сергеевич Пушкин]]', '?#-=title', '?Год публикации#-=year', link='none'}
		return wrap (function ()
			for no, line in ipairs (result) do
				line.year = type (line.year) == 'table' and line.year [1] or line.year
				yield (no, line, merge (table, line), table)
			end
		end)	-- return wrap (function ()
	end	-- local function query (table)
	
	local function lua_regex (string)
		local pattern = 'key(%d+)'
		return wrap (function ()
			for capture in gmatch (string, pattern) do
				yield {no = capture}
			end
		end)	-- return wrap (function ()
	end	-- local function lua_regex (string)
	
	local injects = inject or {ask = query, lua = lua_regex}

	local times, time, diff, start = 100, os.time, os.difftime
	local average = {}
	
	start = time ()
	local formatter
	for i = 1, times do
		formatter = compile (format or test_format, injects, syntax)
	end
	average.compile = diff (time (), start) * 1000000 / times
	
	--times = 1
	start = time ()
	local text, unused_fields
	for i = 1, times do
		text, unused_fields = formatter (table or test_table)
	end
	average.run = diff (time (), start) * 1000000 / times
	
	return 'Compiling took ' .. tostring (average.compile) .. ' mks'
		.. ', running took ' .. tostring (average.run) .. ' mks'
		.. '\nFormatted =\n' .. text .. '\nUnused fields: ' .. mw.dumpObject (unused_fields)

end	-- function p.test (table, format, inject, syntax)

return p