Hopp til innhold

Modul:Inference

Fra Wikipedia, den frie encyklopedi

--- Class for inferences about entities

local mt = {}
local Inference = setmetatable( {}, mt )

--- Fast lookup of all entities known to the lib
local entitiesById = {}

--- Fast lookup of all claims known to the lib
local claimsById = {}

--- Lookup of missing class members
function Inference:__index( key ) -- luacheck: no self
	return Inference[key]
end

local validEntityTypes = {}
validEntityTypes['item'] = true
validEntityTypes['property'] = true
validEntityTypes['lexeme'] = true

local validClaimTypes = {}
validClaimTypes['statement'] = true

local validSchemaVersion = {}
validSchemaVersion[2] = true

function assertEntity( entity )
	assert( entity )
	assert( entity.id)
	assert( entity.type)
	assert( entity.schemaVersion )
	assert( validSchemaVersion[entity.schemaVersion] )
	assert( validEntityTypes[entity.type] )
end

function assertEntityId( id )
	assert( string.match( id, '^[lLpPqQ]%d+$') )
end

function assertClaim( claim )
	assert( claim )
	assert( claim.id)
	assert( claim.mainsnak)
	assert( validClaimTypes[claim.type] )
end

function assertClaimId( id )
	assert( string.match( id, '^[lLpPqQ]%d+%$[-a-fA-F0-9]+$') )
end

--- Create a new instance
function Inference.create( ... )
	local self = setmetatable( {}, Inference )
	self:_init( ... )
	return self
end

--- Initialize a new instance
function Inference:_init( ... )
	self._selected = {}
	for _,arg in ipairs( {...} ) do
		local tpe = type( arg )
		local value = arg
		if tpe == 'function' then
			value = arg()
			tpe = type( value )
		end
		if tpe == 'string' then
			self:path( arg )
			--assertEntityId( value )
			--local ucEntityId = string.upper( value )
			--local entity = Inference.loadEntity( ucEntityId )
			--if entity then
			--	self:selectEntity( entity )
			--end
		elseif tpe == 'table' then
			assertEntity( value )
			local entity = value
			Inference.registerEntity( entity )
			Inference.registerClaims( entity )
		elseif tpe == 'nil' then
			-- never mind
		else
			error()
		end
	end
	return self
end

function mt.__call( tbl, ... )
	return tbl.create( ... )
end

local function split( str )
   local ret={}
   local idx=1
   for s in str:gmatch("([^%s]*)") do
      ret[idx] = ret[idx] or s
      if s == "" then
         idx = idx + 1
      end
   end
   return unpack( ret )
end

local selectors = {}
selectors['eq'] = function( obj, str )
	end
selectors['snaktype'] = function( obj, str )
	obj:snaktype( split( str ) )
end
selectors['valuetype'] = function( obj, str )
	obj:valuetype( split( str ) )
end
selectors['type'] = function( obj, str )
	obj:type( split( str ) )
end
selectors['rank'] = function( obj, str )
	obj:rank( split( str ) )
end

function Inference:path( path )
	local str = path
	local len = string.len( str )
	local index = 1
	local first = nil
	local last = nil
	local step = 10
		local keep = nil
	repeat
		keep = index
		step = step - 1
		first, last = string.find( str, '^%s+', index)
		if first then
			index = last+1
		end
		first, last = string.find( str, '^%.', index)
		if first then
			local entity = mw.wikibase.getEntity()
			if entity then
				Inference.registerEntity( entity, true )
				Inference.registerClaims( entity, true )
				self:selectEntity( entity )
			end
			index = last+1
		end
		first, last = string.find( str, '^[pP]%d+', index)
		if first then
			self:property( string.upper( string.sub( str, first, last ) ) )
			index = last+1
		end
		first, last = string.find( str, '^[lLqQpP]%d+', index)
		if first then
			local ucEntityId = string.upper( string.sub( str, first, last ) )
			local entity = Inference.loadEntity( ucEntityId )
			if entity then
				self:selectEntity( entity )
			end
			index = last+1
		end
		first, last = string.find( str, '^%b[]', index)
		if first then
			local substr = string.sub( str, first+1, last-1 )
			local id = string.match( substr, '^%s*([pP]%d+)%s*$')
			if id then
				self:property( string.upper( id ) )
			else
				local id, oper, arg = string.match( substr, '^%s*([pP]%d+)%s+(%a+)%s+(.*)$')
				if id and selectors[oper] then
					selectors[oper]()
				else
					local name, arg = string.match( substr, '^%s*(%a+)%s+(.*)$')
					if name and selectors[name] then
						selectors[name]( self, arg )
					end
				end
			end
			index = last+1
		end
		first, last, name = string.find( str, '^(%a*)(%b())', index)
		if first then
			--self:path( string.sub( str, first, last ) )
			index = last+1
		end
		first, last = string.find( str, '^/', index)
		if first then
			self:fetch()
			index = last+1
		end
	until (index == keep or index >= len)
	return self
end

function Inference.registerEntity( entity, noAssert )
	assertEntity( entity, noAssert )
	local ucEntityId = string.upper( entity.id )
	entitiesById[ucEntityId] = entity
end

function Inference.registerClaims( entity, noAssert )
	assertEntity( entity, noAssert )
	local ucEntityId = string.upper( entity.id )
	for prop,list in pairs( entity.claims or {} ) do
		for _,claim in ipairs( list or {} ) do
			assertClaimId( claim.id, noAssert )
			local ucClaimId = string.upper( claim.id )
			claimsById[ucClaimId] = claim
		end
	end
end

function Inference:selectEntity( entity, noAssert )
	assertEntity( entity, noAssert )
	for _,list in pairs( entity.claims or {} ) do
		for _,claim in ipairs( list or {} ) do
			self:selectClaim( claim, noAssert )
		end
	end
end

function Inference:selectClaim( claim, noAssert )
	assertClaim( claim, noAssert )
	local ucClaimId = string.upper( claim.id )
	table.insert( self._selected, ucClaimId )
end

function Inference.loadEntity( entityId, noAssert )
	assertEntityId( entityId, noAssert )
	local ucEntityId = string.upper( entityId )
	local entity = entitiesById[ucEntityId]
	if not entity then
		entity = mw.wikibase.getEntity( ucEntityId )
		if not entity then
			return nil
		end
		Inference.registerEntity( entity, true )
	end
	if entity then
		Inference.registerClaims( entity, true )
	end
	return entity
end

function Inference:fetch()
	self:valuetype( 'wikibase-entityid' )
	local values = self:getValues()
	self:empty()
	for _,value in ipairs( values ) do
		local ucEntityId = string.upper( value.id )
		local entity = Inference.loadEntity( ucEntityId )
		if entity then
			self:selectEntity( entity )
		end
	end
	return self
end

--- Is the current selection empty
-- Note that the selection is non-empty even if a nil
-- is stored internally.
-- @return boolean saying whether the selection is empty
function Inference:isEmpty()
	return #self._selected == 0
end

function Inference:size()
	return #self._selected
end

function Inference:empty()
	self._selected = {}
end

--- Is this an identifier for a claim
-- @param id string form of an identifier
-- @return boolean saying whether the selection is empty
function Inference:isClaimId()
	return #self._selected == 0
end

function Inference.filterRank( ... )
	local filter = {}
	for _,str in ipairs( {...} ) do
		if string.match( str, '^[nN]ormal$') then
			filter['normal'] = true
		elseif string.match( str, '^[pP]referred$') then
			filter['preferred'] = true
		elseif string.match( str, '^[dD]eprecated$') then
			filter['deprecated'] = true
		end
	end
	return filter
end

function Inference.filterType( ... )
	local filter = {}
	for _,str in ipairs( {...} ) do
		if string.match( str, '^[sS]tatements?$') then
			filter['statement'] = true
		elseif string.match( str, '^[pP]roperty$') or string.match( str, '^[pP]roperties$') then
			filter['property'] = true
		elseif string.match( str, '^[lL]exemes?$') then
			filter['lexeme'] = true
		end
	end
	return filter
end

function Inference.filterProperty( ... )
	local filter = {}
	for _,str in ipairs( {...} ) do
		if string.match( str, '^[pP]%d+$') then
			filter[string.upper(str)] = true
		end
	end
	return filter
end

function Inference.filterSnaktype( ... )
	local filter = {}
	for _,str in ipairs( {...} ) do
		if string.match( str, '^[vV]alues?$') then
			filter['value'] = true
		elseif string.match( str, '^[sS]ome%-?[vV]alues?$') then
			filter['somevalue'] = true
		elseif string.match( str, '^[nN]o%-?[vV]alues?$') then
			filter['novalue'] = true
		end
	end
	return filter
end

function Inference.filterDatatype( ... )
	local filter = {}
	for _,str in ipairs( {...} ) do
		if string.match( str, '^[wW]ikibase%-?[iI]tems?$') then
			filter['wikibase-item'] = true
		elseif string.match( str, '^[wW]ikibase%-?[pP]ropertys?$') then
			filter['wikibase-property'] = true
		elseif string.match( str, '^[qQ]uantity$') or string.match( str, '^[qQ]uantities$') then
			filter['quantity'] = true
		elseif string.match( str, '^[tT]imes?$') then
			filter['time'] = true
		elseif string.match( str, '^[uU][rR][lL]s?$') then
			filter['url'] = true
		elseif string.match( str, '^[sS]trings?$') then
			filter['string'] = true
		elseif string.match( str, '^[mM]ono%-?[lL]ingual%-[tT]exts?$') then
			filter['monolingualtext'] = true
		elseif string.match( str, '^[gG]eo%-?[sS]hapes?$') then
			filter['geo-shape'] = true
		elseif string.match( str, '^[gG]lobe%-?[cC]oordinates?$') then
			filter['globe-coordinate'] = true
		elseif string.match( str, '^[tT]abular%-?[dD]atas?$') then
			filter['tabular-data'] = true
		elseif string.match( str, '^[mM]aths?$') then
			filter['math'] = true
		elseif string.match( str, '^[eE]xternal%-?[iI]ds?$') then
			filter['external-id'] = true
		elseif string.match( str, '^[cC]ommons%-?[mM]edias?$') then
			filter['commonsMedia'] = true
		end
	end
	return filter
end

function Inference.filterValuetype( ... )
	local filter = {}
	for _,str in ipairs( {...} ) do
		
		if string.match( str, '^[wW]ikibase%-?[eE]ntityids?') then
			filter['wikibase-entityid'] = true
		elseif string.match( str, '^[qQ]uantity$') or string.match( str, '^[qQ]uantities$') then
			filter['quantity'] = true
		elseif string.match( str, '^[sS]trings?$') then
			filter['string'] = true
		elseif string.match( str, '^[tT]imes?$') then
			filter['string'] = true
		elseif string.match( str, '^[mM]ono%-?[lL]ingual%-?[tT]exts?$') then
			filter['monolingualtext'] = true
		elseif string.match( str, '^[gG]lobe%-?[cC]oordinates?$') then
			filter['globecoordinate'] = true
		end
	end
	return filter
end

function Inference.extractRank( claim )
	return claim.rank
end

function Inference.extractType( claim )
	return claim.type
end

function Inference.extractProperty( claim )
	return claim and claim.mainsnak and claim.mainsnak.property or nil
end

function Inference.extractSnaktype( claim )
	return claim and claim.mainsnak and claim.mainsnak.snaktype or nil
end

function Inference.extractMainsnak( claim )
	return claim and claim.mainsnak or nil
end

function Inference.extractDatatype( claim )
	return claim and claim.mainsnak and claim.mainsnak.datatype or nil
end

function Inference.extractDatavalue( claim )
	return claim and claim.mainsnak and claim.mainsnak.datavalue or nil
end

function Inference.extractValuetype( claim )
	return claim and claim.mainsnak and claim.mainsnak.datavalue and claim.mainsnak.datavalue.type or nil
end

function Inference.extractValue( claim )
	return claim and claim.mainsnak and claim.mainsnak.datavalue and claim.mainsnak.datavalue.value or nil
end

function Inference.extractIdentity( claim )
	return claim
end

function Inference:filter( extractFunc, filterFunc, ... )
	local funs = {}
	local strs = {}
	for _,arg in ipairs( {...} ) do
		local tpe = type( arg )
		if tpe == 'string' then
			table.insert( strs, arg )
		elseif tpe == 'function' then
			table.insert( funs, arg )
		end
	end
	local filter = filterFunc( unpack( strs ) )
	local selected = {}
	for i,claimId in ipairs( self._selected ) do
		if claimId then
			local ucClaimId = string.upper( claimId )
			local claim = claimsById[ucClaimId]
			local extract = extractFunc( claim )
			local keep = filter[extract]
			for _,fun in ipairs(funs) do
				keep = keep or fun( extract )
			end
			if keep then
				table.insert( selected, ucClaimId )
			end
		end
	end
	self._selected = selected
	return self
end

function Inference:rank( ... )
	self:filter( Inference.extractRank, Inference.filterRank, ... )
	return self
end

function Inference:type( ... )
	self:filter( Inference.extractType, Inference.filterType, ... )
	return self
end

--- Filter out these claims
-- @param ... string forms of identifiers
-- @return self for chaining
function Inference:property( ... )
	self:filter( Inference.extractProperty, Inference.filterProperty, ... )
	return self
end

function Inference:snaktype( ... )
	self:filter( Inference.extractSnaktype, Inference.filterSnaktype, ... )
	return self
end

function Inference:datatype( ... )
	self:filter( Inference.extractDatatype, Inference.filterDatatype, ... )
	return self
end

function Inference:valuetype( ... )
	self:filter( Inference.extractValuetype, Inference.filterValuetype, ... )
	return self
end

function Inference:getSelectedIds()
	return self._selected
end

function Inference:getEntities()
	local entities = {}
	local entityIds = {}
	local seenIds = {}
	for _,claimId in ipairs( self._selected ) do
		local ucEntityId = string.upper( string.match( claimId, '^([lLpPqQ]%d+)' ) )
		if not seenIds[ucEntityId] then
			seenIds[ucEntityId] = true
			table.insert( entityIds, ucEntityId )
		end
	end
	for _,entityId in ipairs( entityIds ) do
		local entity = entitiesById[entityId]
		if entity then
			table.insert( entities, entity )
		end
	end
	return entities
end

function Inference:get( extractFunc )
	local extracts = {}
	for i,claimId in ipairs( self._selected ) do
		extracts[i] = extractFunc( claimsById[claimId] )
	end
	return extracts
end

function Inference:getClaims()
	return self:get( Inference.extractIdentity )
end

function Inference:getProperties()
	return self:get( Inference.extractProperty )
end

function Inference:getMainsnaks()
	return self:get( Inference.extractMainsnak )
end

function Inference:getDatavalues()
	return self:get( Inference.extractDatavalue )
end

function Inference:getValues()
	return self:get( Inference.extractValue )
end

function Inference:render( ... )
	local flags = {}
	for _,v in ipairs( {...} ) do
		if v == 'string' then
			flags[v] = true
		end
	end
	local singularFormatter = (flags['rich'] and mw.wikibase.formatValue) or mw.wikibase.renderSnak
	local pluralFormatter = (flags['rich'] and mw.wikibase.formatValues) or mw.wikibase.renderSnaks
	local rendered = {}
	for i,claimId in ipairs( self._selected ) do
		local items = {}
		local claim = claimsById[claimId]
		if claim.mainsnak then
			local main = tostring( singularFormatter( claim.mainsnak ) )
			table.insert( items, main )
		end
		if flags['qualifiers'] and claim.qualifiers then
			local qualifiers = tostring( pluralFormatter( claim.qualifiers ) )
			table.insert( items, qualifiers and string.format( "(%s)", main ) )
		end
		if flags['references'] and claim.references then
			for _,refs in ipairs( claim.references ) do
				local reference = tostring( pluralFormatter( refs ) )
				table.insert( items, reference and string.format( "[%s]", reference ) )
			end
		end
		table.insert( rendered, table.concat() )
	end
	
end

function Inference:dump( args )
	local rendered = {}
	for i,claimId in ipairs( self._selected ) do
		local items = {}
		local claim = claimsById[claimId]
		local main = mw.wikibase.renderSnak( claim.mainsnak )
		table.insert( items, main )
		if claim.qualifiers then
			local qual = args['qualifiers'] and mw.wikibase.renderSnaks( claim.qualifiers )
			if qual and #qual ~= 0 then
				table.insert( items, string.format( "(%s)", qual ) )
			end
		end
		for _,refs in ipairs( claim.references or {} ) do
			local ref = args['references'] and mw.wikibase.renderSnaks( refs.snaks )
			if ref and #ref ~= 0 then
				table.insert( items, string.format( "[%s]", ref ) )
			end
		end
		table.insert( rendered, table.concat( items, args['space'] or ' ' ) )
	end
	return table.concat( rendered, args['separator'] or ",\n" )
end

function Inference:rich( frame, args )
	local rendered = {}
	local outer = mw.html.create( 'div' )
	for i,claimId in ipairs( self._selected ) do
		local items = {}
		local claim = claimsById[claimId]
		local main = mw.wikibase.formatValue( claim.mainsnak )
		table.insert( items, main )
		if claim.qualifiers then
			local qual = args['qualifiers'] and mw.wikibase.formatValues( claim.qualifiers )
			if qual and #qual ~= 0 then
				table.insert( items, string.format( "(%s)", tostring( qual ) ) )
			end
		end
		for _,refs in ipairs( claim.references or {} ) do
			local ref = args['references'] and mw.wikibase.formatValues( refs.snaks )
			if ref and #ref ~= 0 then
				table.insert( items, string.format( "[%s]", tostring( ref ) ) )
			end
		end
		local inner = mw.html.create( 'span' )
		inner:wikitext( table.concat( items, args['li'] or ' ' ) )
		outer:node( inner )
	end
	return tostring( outer )
end

function Inference.query( ... )
	local list = {...}
	local frame = nil
	local args = {}

	if #list == 1 and type( list[1] ) and list[1].args then
		frame = list[1]
		list = frame.args
	end

	for k,v in pairs( list ) do
		if not v.args then
			if tonumber(k) and type( v ) == 'string' then
				args[v] = true
			end
			args[k] = v
		end
	end

	args['qualifiers'] = args['qual'] or args['qualifier'] or args['qualifiers']
	args['references'] = args['ref'] or args['reference'] or args['references']
	--args['format'] = args['format'] or 'rich'
	
	local result = Inference( args[1] or '' )

	if args['format'] and string.match( args['format'], '^[dD]ump$' ) then
		return result:dump( args )
	end

	if args['format'] and string.match( args['format'], '^[rR]ich$' ) then
		if not frame then
			frame = mw.getCurrentFrame()
		end
		return result:rich( frame, args )
	end

	return result:size()
end

-- Return the final class
return Inference