Модуль:Wikidata/Population

Модуль для шаблонов {{Wikidata/Population}} и {{Wikidata/Population/Table}}.


local WDS = require('Module:WikidataSelectors');

local p = {};

local DEFAULT_COLUMNS = 4;
local DEFAULT_WIDTH = 700;
local DEFAULT_HEIGHT = 300;
local COLLAPSE_IF_ROWS_MORE_THAN = 11;

local TABLE_COLLAPSIBLE_HEADER = "Статистика численности населения с %s по %s";
local TABLE_COLUMN_HEADER_YEAR = "Год";
local TABLE_COLUMN_HEADER_POPULATION = "Численность";

local function deepcopy(orig)
    local orig_type = type(orig)
    local copy
    if orig_type == 'table' then
        copy = {}
        for orig_key, orig_value in next, orig, nil do
            copy[deepcopy(orig_key)] = deepcopy(orig_value)
        end
        setmetatable(copy, deepcopy(getmetatable(orig)))
    else -- number, string, boolean, etc
        copy = orig
    end
    return copy
end

local function formatPopulationPropertyImpl( context, options )
	if ( not context ) then error( 'context not specified' ); end;
	if ( not options ) then error( 'options not specified' ); end;
	if ( not options.entity ) then error( 'options.entity missing' ); end;

    local claims = context.selectClaims( options, options.property );
    if (claims == nil) then
        return nil --TODO error?
    end
    
    for i, j in ipairs(claims) do
    	if ( not j.qualifiers.P585[1]) then return nil end --проверка на наличие момента времени
    end

	local comparator = function(o1, o2)
		local t1 = context.parseTimeFromSnak( o1.qualifiers.P585[1] );
		local t2 = context.parseTimeFromSnak( o2.qualifiers.P585[1] );
		return t1 < t2;
	end
	table.sort( claims, comparator )

	return claims;
end

-- тестирование не работает
-- =p.formatProperty(mw.getCurrentFrame():newChild{title="Модуль:Wikidata",args={["property-module"]="Wikidata/Population",["property-function"]="formatPopulationPropertyForGraph",["claim-module"]="Wikidata/Population",["claim-function"]="formatPopulationClaimForGraph",["property"]="p1082[p585][rank:preferred,rank:normal]",["datatype"]="quantity",}}:newChild{title="Модуль:Wikidata/Population"}:newChild{title="Сереседа-де-ла-Сьерра"}) 
function p.formatPopulationPropertyForGraph( context, options )
	local claims = formatPopulationPropertyImpl( context, options );
    -- Обход всех заявлений утверждения и с накоплением оформленых предпочтительных 
    -- заявлений в таблице
    local formattedClaims = {}
    local years = {}

	local count = 0;
	local csv = 'year,month,day,population,formatted';
	
	if ( not claims ) then
		return '';
	end

    for i, claim in ipairs(claims) do
    	-- уточняем даты: для года до середины, для месяца до 15-го числа

    	local p585Value = claim.qualifiers.P585[1].datavalue.value;
    	local p585Precision = p585Value.precision;
    	local p585Time = p585Value.time;
    	if ( p585Precision == 10 ) then
    		-- Set 15-th day of month
    		p585Time = mw.ustring.gsub(p585Time, "\-[0-9]+T", "-15T");
    	elseif ( p585Precision == 9 ) then
    		-- Set to 1-st of July
    		p585Time = mw.ustring.gsub(p585Time, "\-[0-9]+\-[0-9]+T", "-07-01T");
    	end
    	local year, month, day = mw.ustring.gmatch( p585Time, "(\-?[0-9]+)\-([0-9]+)\-([0-9]+)T" )(1);
    	
		if not years[ year ] then
			years[ year ] = true;

			local value 
			if not claim.mainsnak.datavalue then value = ""
			else value = string.gsub( claim.mainsnak.datavalue.value.amount, '^%+', '' ) end
			
			local formatted = mw.language.getContentLanguage():formatNum( tonumber( value ) );
			
			local line = year .. ',' .. month .. ',' .. day .. ',' .. value .. ',' .. formatted;
	    	csv = csv .. '\\n' .. line;
	    	count = count + 1;
    	end
    end

	if ( count == 0 ) then
		return '';
	end

	local graphData = '{ "version": 2, "width": ' .. DEFAULT_WIDTH .. ', "height": ' .. DEFAULT_HEIGHT .. ', "data": [ { "name": "table", "values": "';
	graphData = graphData .. csv;
    graphData = graphData .. '","format": { "parse": {"year": "integer", "month": "integer", "day": "integer", "population": "integer", "formatted": "string"}, "type": "csv" },'
    graphData = graphData .. '"transform": [{ "type": "formula", "field": "date", "expr": "datetime(datum.year,datum.month-1,datum.day)" }] } ],';
	graphData = graphData .. '"scales": [{ "name": "x", "type": "time", "range": "width", "nice": "year", "domain": {"data": "table", "field": "date"} },';
	graphData = graphData .. '{ "name": "y", "type": "linear", "range": "height", "domain": {"data": "table", "field": "population"} } ],';
	graphData = graphData .. '"axes": [ {"type": "x", "scale": "x", "ticks": 10}, {"type": "y", "scale": "y", "ticks": 5, "grid": true, "orient": "right", "format": "d"} ],';

	graphData = graphData .. '"marks": [{ "type": "area", "from": {"data": "table"}, "properties": { "enter": {';
    graphData = graphData .. '"x": {"scale": "x", "field": "date"}, "y": {"scale": "y", "value": 0}, "y2": {"scale": "y", "field": "population"}, ';
    graphData = graphData .. '"fill": {"value": "#99B2CC"}, "fillOpacity": {"value": 0.35}, "interpolate": {"value": "linear"}}}},';
    graphData = graphData .. '{ "type": "line", "from": {"data": "table"}, "properties": { "enter": {';
    graphData = graphData .. '"x": {"scale": "x", "field": "date"}, "y": {"scale": "y", "field": "population"},';
    graphData = graphData .. '"stroke": {"value": "#99B2CC"}, "strokeWidth": {"value": 3}, "interpolate": {"value": "linear"}}}},';
    graphData = graphData .. '{"type": "symbol","from": {"data": "table"},"properties": {"enter": {';
    graphData = graphData .. '"x": {"scale": "x", "field": "date"},"y": {"scale": "y", "field": "population"},"stroke": {"value": "#99B2CC"},"fill": {"value": "#fff"},"size": {"value": 10}}}},'
    graphData = graphData .. '{"type": "text", "from": {"data": "table"}, "properties": { "enter": { "x": {"scale": "x", "field": "date"}, "y": {"scale": "y", "field": "population", "offset": -1},';
    graphData = graphData .. '"align": {"value": "center"}, "opacity": {"value": "0"}, "fill": {"value": "#000000"}, "size": {"value": 4}, "text": {"template": "{{datum.formatted}}  ({{datum.year}})"}},';
    graphData = graphData .. '"hover": {"opacity": {"value": "1"}}, "update": {"opacity": {"value": "0"}}}},';
    graphData = graphData .. ']}';

	local result = options.frame:callParserFunction{ name = '#tag:graph', args = { graphData, mode = 'interactive' } };

	local columns = options.columns or DEFAULT_COLUMNS;
	local perColumn = math.ceil( count / columns );
	if ( perColumn > COLLAPSE_IF_ROWS_MORE_THAN ) then
		return result;
	else
		-- side-by-side display
		return '<div style="display: inline-block; vertical-align: bottom;">' .. result .. '</div>';
	end
end

function p.formatPopulationClaimForGraph( context, options, statement )
	local time = context.parseTimeFromSnak( statement.qualifiers.P585[1] );
    local value = string.gsub( statement.mainsnak.datavalue.value.amount, '^%+', '' );

	return os.date("*t", time / 1000).year .. ',' .. value;
end

function p.formatPopulationPropertyForTable( context, options )
	local claims = formatPopulationPropertyImpl( context, options );
    -- Обход всех заявлений утверждения и с накоплением оформленых предпочтительных 
    -- заявлений в таблице
    local formattedClaims = {}
    local years = {}

	local firstTime = false;
	local lastTime = '';

	local count = 0;

	if ( not claims ) then
		return '';
	end

    for i, claim in ipairs( claims ) do
    	
    	-- обрезаем выводимую дату до года
    	local timeQualifier = claim.qualifiers.P585[1];
    	if ( timeQualifier.datavalue.value.precision > 9 ) then
    		timeQualifier = deepcopy( timeQualifier );
    		timeQualifier.datavalue.value.precision = 9;
    	end
		local year = string.sub( timeQualifier.datavalue.value.time, 2, 5 );
		if not years[ year ] then
			years[ year ] = true;

			local time = context.formatSnak( options, timeQualifier );
		    local value = context.formatSnak( options, claim.mainsnak );
	
			if ( not firstTime ) then firstTime = time end;
			lastTime = time;
	
			local line = '\n|-\n! scope="row" | ' .. time
			line = line .. '\n| style="text-align: right; border-right: none; padding-right: 0;" | ' .. ( value or "" );
			line = line .. '\n| style="text-align: left; border-left: none; padding-left: 0;" | '
			if options.references then
				line = line .. context.formatRefs( options, claim );
			end
	        table.insert( formattedClaims, line )
	        count = count + 1;
        end
    end

	if ( count == 0 ) then
		return '';
	end

	local columns = options.columns or DEFAULT_COLUMNS;
	if ( count <= columns ) then
		local out = '{| class="wikitable" \n|-\n! scope="col" | ' .. TABLE_COLUMN_HEADER_YEAR .. ' !! scope="colgroup" colspan="2" | ' .. TABLE_COLUMN_HEADER_POPULATION .. '\n|-';
	    for i, formattedClaim in ipairs(formattedClaims) do
			out = out .. '\n' .. formattedClaim;
		end
		out = out .. '\n|}';
		return out;
	end

	local out = '';

	local perColumn = math.ceil( count / columns );
	if ( perColumn > COLLAPSE_IF_ROWS_MORE_THAN ) then
		local caption = mw.ustring.format( TABLE_COLLAPSIBLE_HEADER, firstTime, lastTime );

		out = out .. '{| class="collapsible collapsed" |\n';
		out = out .. '! ' .. caption .. '\n';
		out = out .. '|-\n';
		out = out .. '| class="ts-wikidata-population-table" |';
	else
		out = out .. '<div class="ts-wikidata-population-table">';
	end

    for i, formattedClaim in ipairs(formattedClaims) do
    	if ( i % perColumn == 1 ) then
			out = out .. '\n{| class="wikitable" \n|-\n! scope="col" | ' .. TABLE_COLUMN_HEADER_YEAR .. ' !! scope="colgroup" colspan=2 | ' .. TABLE_COLUMN_HEADER_POPULATION .. '\n|-';
		end
		out = out .. '\n' .. formattedClaim;
		if ( i % perColumn == 0 ) then
			out = out .. '\n|}';
		end
	end
	if ( count % perColumn ~= 0 ) then
		out = out .. '\n|}';
	end
	if ( perColumn > COLLAPSE_IF_ROWS_MORE_THAN ) then
		out = out .. '\n|}';
	else
		out = out .. '\n</div>';
	end
	return out
end

return p;