Module:Size

From Wicri pool V1.31 Démo.Istex
Revision as of 10:32, 2 November 2018 by Jacques Ducloy (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Documentation for this module may be created at Module:Size/doc

--[[
  __  __           _       _        ____  _         
 |  \/  | ___   __| |_   _| | ___ _/ ___|(_)_______ 
 | |\/| |/ _ \ / _` | | | | |/ _ (_)___ \| |_  / _ \
 | |  | | (_) | (_| | |_| | |  __/_ ___) | |/ /  __/
 |_|  |_|\___/ \__,_|\__,_|_|\___(_)____/|_/___\___|
                                                    
Authors and maintainers:
* User:Zolo   - original draft
* User:Jarekt - original version
]]

local formatnum = require "Module:Formatnum".formatNum
local wdLabel   = require "Module:Wikidata label"._getLabel

-- ==================================================
-- === global parameters  ===========================
-- ==================================================

--  arrays for unit conversion   3     4     5     6     7       8       9        10     11  12    13   14       15
local unitMult   = {1e-9, 1e-6, 1e-3, 1e-2,  1  , 1e3 , 0.0254, 0.3048, 0.9144, 1609.344, 1, 1e3, 1e6, 28.3495, 453.592, 1}  -- conversion to meters
local unitList   = {'nm', 'um', 'mm', "cm", "m" , "km", "in"  , "ft"  , "yd"  , "mi",    "g", "kg", "t", "oz", "lb", "ct" }   -- units handled by this module
local unitType   = {'m' , 'm' , 'm' , 'm' , 'm' , 'm' , 'i'   , 'i'   , 'i'   , 'i' ,    'm', 'm' , 'm', 'i' , 'i', ''    }   -- m for metric and i for imperial
local unitItem   = {nm='Q178674', um='Q175821', mm='Q174789', cm='Q174728', m='Q11573', km='Q828224',     -- used for unit abbreviation translation
                    ['in']='Q218593', ft='Q3710', yd='Q482798', mi='Q253276', kg='Q11570', g='Q41803', t='Q191118', oz='Q48013', lb="Q100995", ct="Q261247" }
-- properties used for harvesting the wikidata 	and item IDs are used for translating dimension's name									
local dimProp = { length='P2043' , height='P2048'  , width='P2049' , depthH='P5524',    depthV='P4511'  , thickness='P2610'   , diameter='P2386' , radius='P2120'  , perimeter='P2547' , weight='P2067'}
local dimName = { length='Q36253', height='Q208826', width='Q35059', depthH='Q3250078', depthV='Q930412', thickness='Q3589038', diameter='Q37221', radius='Q173817', perimeter='Q28474', weight='Q11423'} 

local	aliases    = { -- aliases for units used to unify
	Q178674="nm", nm="nm", nanometer="nm",	nanometers= "nm", nanometre="nm", nanometres="nm",
	Q175821="um",	um="um", ["µm"]="um", micrometer="um", micrometers="um", micrometre="um", micrometres="um",
	Q200323="dm", dm="dm", decimeter="dm", decimeters="dm", decimetre="dm", decimetres="dm",
	Q174789="mm", mm="mm", millimeter="mm", millimeters="mm", millimetre="mm", millimetres="mm",
	Q174728="cm", cm="cm", centimeter="cm", centimeters="cm", centimetre="cm", centimetres="cm",
	Q11573="m", m="m", meter="m", meters="m", metre="m", metres="m", 
	Q828224="km", km="km", kilometer="km", kilometers="km", kilometre="km", kilometres="km",
	Q218593="in", ["in"]="in", inch="in", inches="in",
	Q3710="ft", ft="ft", foot="ft", feet="ft", 
	Q482798="yd", yd="yd", yard="yd", yards="yd",
	Q253276="mi", mi="mi", mile="mi", miles="mi",
	Q93318="nmi", nmi="nmi", ["nautic mile"]="nmi", ["nautic miles"]="nmi",
	Q11570="kg", kilogram="kg", kilograms="kg", kg="kg",
	Q41803="g", gram="g", grams="g", g="g",
	Q191118="t", tonne="t", tonnes="t", ton="t", tons="t", ["metric ton"]="t", t="t", 
	Q48013="oz", ounce="oz", oz="oz", 
	Q100995="lb", pound="lb", pounds="lb", lb="lb",
	Q261247="ct", carat="ct", ct="ct"
}

-- ==================================================
-- === Internal functions ===========================
-- ==================================================
local function langSwitch(list,lang)
	local langList = mw.language.getFallbacksFor(lang)
	table.insert(langList,1,lang)
	for i,language in ipairs(langList) do
		if list[language] then
			return list[language]
		end
	end
	return nil
end

local function getProperty(itemID, prop, lang)
	local n, title = 0, {}
	entity = mw.wikibase.getEntity(itemID)
	for _, statement in pairs( entity:getBestStatements(prop)) do 
		if (statement.mainsnak.snaktype == "value") then 
			local val = statement.mainsnak.datavalue.value
			title[val.language] = val.text -- look for multiple values each with a language code
			n = n+1;
		end
	end
	if n>0 then
		return langSwitch(title, lang)
	end
	--return ''
end

local function normalize_input_args(input_args, output_args)
	for name, value in pairs( input_args ) do 
		if value ~= '' then -- nuke empty strings
			if type(name)=='string' then 
				name = string.lower(name)
			end
			output_args[name] = string.gsub(value, "^%s*(.-)%s*$", "%1") -- trim whitespaces from the beggining and the end of the string
		end
	end
	return output_args
end

local function findInArray(str, list)
	for k, v in ipairs(list) do
		if v==str then                -- match units with the list
			return k;
		end
	end	
	return nil
end

local function formatNum( value, lang, precision )
	assert(value, "Input value is nil")
	assert(precision, "Input precision is nil")
	local str = formatnum( value, lang, precision )
	str = mw.ustring.gsub(str, "%.0+$", "") -- remove trailing zeros
	return str
end

--[[
INPUTS:
* v - size in meters
* unitMult - array used to convert meters to other units
* iMin, iMax - min and max index of unitMult array to use
]]
local function niceNumber(v, iMin, iMax)
	s = 10; -- scaling parameter. Means that "nice" numbers are in units that give the smallest number bigger than s
	if v<s*unitMult[iMin] then
			return iMin  -- will show as fractions of the smallest unit
	end
	for k = iMin,iMax-1 do
		if v>=s*unitMult[k] and v<s*unitMult[k+1] then
			return k
		end
	end
	return iMax -- will use the largest unit
end

local function pickUnit(val, unit, lang)
	local k1, k2, unit1, unit2
	unit1 = aliases[unit]                                         -- convert unit item ID to standard units
	assert(unit1, "Unit name is not recognized: " .. unit)
	k1    = findInArray(unit1, unitList)	
	unit1 = getProperty(unitItem[unit1], 'P5061', lang) -- gets the abbreviated form of the name of the unit
	
	local valInM = val*unitMult[k1]            -- find value in metres
	if (k1<=6) then                         -- input units are metric length
		k2 = niceNumber(valInM, 7, 10) -- find best imperial units
	elseif (k1<=10) then                    -- input units are imperial length
		k2 = niceNumber(valInM, 1, 6)  -- find best metric units
	elseif (k1<=13) then                    -- input units are metric weight
		k2 = niceNumber(valInM, 14, 15)-- find best imperial units
	else                                    -- input units are imperial weight
		k2 = niceNumber(valInM, 11, 13)-- find best metric units
	end
	factor = unitMult[k1]/unitMult[k2]
	unit2 = getProperty(unitItem[unitList[k2]], 'P5061', lang) -- gets the abbreviated form of the name of the unit
	return unit1 or unit, unit2 or unitItem[unitList[k2]], factor, unitType[k1]
end

local function unit_conversion(val, unit, prec, lang, wordsep)
	local factor, unit1, unit2, numStr1, numStr2, system

	unit1, unit2, factor, system = pickUnit(val, unit, lang) -- based on val magnitude and unit, translate unit and provide coversion factor to convert to other type of units

	numStr1 = formatNum( val, lang, prec)  .. wordsep .. unit1
	if (lang~='en' and lang~='en-US' and system=='m') or (system=='') then -- if input is in metric units and output language is not English then show only metric output
		return numStr1 --  just show metric values
	end
	
	-- final string in imperial and metric units
	numStr2 = formatNum( val*factor, lang, prec)
	return mw.ustring.format("%s%s(%s%s%s)", numStr1, wordsep, numStr2, wordsep, unit2)
end

local function disambiguate_dimensions(args)
	-- compare painting dimensions to image dimensions
	if args[2] and args[3] and not args[4] then
		local title = mw.title.getCurrentTitle()
		if title.namespace==6 then  -- this is a file
			local width, height, ratio, R, dr1, dr2, dr
			width  = title.file.width
			height = title.file.height
			ratio  = 1.0*height/width           -- file size ratio
			R      = 1.0*args[2]/args[3]        -- painting size ratio
			dr1    = math.abs(  R-ratio)/ratio  -- compare ratios
			dr2    = math.abs(1/R-ratio)/ratio
			dr     = math.min(dr1, dr2)
			args.debug = string.format('width=%f; height=%f; ratio=%f; R=%f; dr=%f', width, height, ratio, R, dr)
			if dr<0.1 and (ratio>1.15 or ratio<0.85) then -- ratios are within 10% from each other and image is not square
				if dr1<dr2 then
					args.height, args.width  = args[2], args[3]
				else
					args.height, args.width = args[3], args[2]
				end
				args[2], args[3] = nil, nil
			end
		end
	end
	return args
end

-- ==================================================
-- === External functions ===========================
-- ==================================================
local p = {}

function p._size_old(args, unit, prec, lang)
--This function mimics the functionality of the original {{Size|unit|dim1|dim2|dim3}} template

	if not prec then
		prec = 1;
		if unit == 'mm' then prec=2; end
	end
	
	-- process values
	local val, mean = {}, 0
	for i = 2,4 do
		v = args[i]
		if v then
			v = string.gsub(v, ',', '.')
			v = tonumber(v)
			if type(v)=='number' and v>0 then
				table.insert(val, v)
				mean = mean + v
			end
		end
	end
	mean = mean / #val -- find mean of 3 dimensions
	assert(#val>0, "No numeric dimensions found.")
	--if n==0 then return '' end
	
	-- pick metric and imperial units
	local factor, unit1, unit2, system 
	unit1, unit2, factor, system = pickUnit(mean, unit, lang)

	-- convert numbers to localized strings
	local numStr1, numStr2 = {}, {}
	for _, v in ipairs(val) do
		table.insert(numStr1, formatNum( v       , lang, prec))
		table.insert(numStr2, formatNum( v*factor, lang, prec))
	end
	
	-- final string in the same units as input
	local wordsep = mw.message.new( "Word-separator" ):inLanguage(lang):plain()
	local x = wordsep .. '×'.. wordsep
	numStr1 = table.concat( numStr1, x) .. wordsep .. unit1
	if (lang~='en' and lang~='en-US' and system=='m') or (system=='') then -- if input is in metric units and output language is not English then show only metric output
		return numStr1 --  just show metric values
	end

	-- final string in imperial and metric units
	numStr2 = table.concat( numStr2, x) .. wordsep .. unit2
	return mw.ustring.format("%s%s(%s)", numStr1, wordsep, numStr2)
end

-- ==================================================
function p._size(args, unit, prec, lang)
--This function mimics the functionality of the latter {{Size|unit|width=...|height=...|...}} template
	unit1 = aliases[unit] -- disambiguate units
	assert(unit1 or args.wikidata or args.entity, "Unit name is not recognized")
	if not prec then
		prec = 1;
		if unit == 'mm' then prec=2; end
	end
	args.depthH = args.depth  -- assume that "depth" defined by {{Size}} meant "horizontal dimension away from the observer"
	
	-- harvest wikidata
	-- each property stores a single dimension. Notice that P4511 is for vertical depth only, while Size template parameter "depth" was mostly used for horizontal depth
	local entity, units = nil, {}
	if args.wikidata then
		entity = mw.wikibase.getEntity(args.wikidata)
	elseif args.entity then
		entity = args.entity
	end
	if entity then
		for field, prop in pairs(dimProp) do
			if entity.claims and entity.claims[prop] then -- if we have wikidata item and item has the property
				for _, statement in pairs( entity:getBestStatements( prop )) do
					if (statement.mainsnak.snaktype == "value") then 
						local v = statement.mainsnak.datavalue.value
						args [field] = v.amount
						units[field] = string.gsub(v.unit, "http:%/%/www%.wikidata%.org%/entity%/", "") -- strip URL and keep the item ID
					end
				end
			end
		end
	end
	
	-- create non-visible encoding with untranslated dimensions
	local mata_str = ''
	local fields = { 'length', 'height', 'width', 'depthH', 'depthV', 'thickness', 'diameter'}
	local meta = {}
	for _, field in ipairs( fields ) do
		if args[field] then
			local uStr = units[field] or unitItem[unit] -- get item ID of the unit
			table.insert(meta, dimProp[field] .. ',' .. args[field] .. "U" .. string.sub(uStr, 2, -1) )-- replace Q with U on the beginning of the string
		end
	end
	if #meta>0 then
		mata_str = ' <div style="display: none;">dimensions QS:' .. table.concat(meta, ";") .. '</div>'
	end
	
	-- create the final string
	local colon     = mw.message.new( "Colon-separator" ):inLanguage(lang):plain()
	local semicolon = mw.message.new( "Semicolon-separator" ):inLanguage(lang):plain()
	local wordsep   = mw.message.new( "Word-separator" ):inLanguage(lang):plain()
	local dimOrder = { 'length', 'height', 'width', 'depthH', 'depthV', 'thickness', 'diameter', 'radius', 'perimeter', 'weight'} -- array with order of fields to display
	local results = {}
	for _, field in ipairs(dimOrder) do -- values with named dimensions like "depth: 2 cm"
		local val = args[field]
		if val then
			val = string.gsub(val, ',', '.')
			val = tonumber(val)
			if type(val)=='number' then 
				local dimStr = '???' -- wdLabel(dimName[field], lang, '-', "ucfirst") 
				local valStr = unit_conversion(val, units[field] or unit, prec, lang, wordsep)
				table.insert(results, dimStr .. colon .. valStr) 
			end
		end
	end
	return table.concat(results, semicolon) .. mata_str
end
 
-- ==================================================
function p.size(frame)
	local args = {}
	args = normalize_input_args(frame:getParent().args, args)
	args = normalize_input_args(frame.args, args)
	if not args.lang or not mw.language.isSupportedLanguage(args.lang) then 
		args.lang = frame:callParserFunction( "int", "lang" ) -- get user's chosen language
	end
	
	local unit = args[1] or args.unit or args.units
	unit = aliases[unit]
	if not unit and not args.wikidata then
		return ''
	end
	
	-- see if we can deduce which dimension is which
	local cat = ''
	if args[2] and args[3] then
		args = disambiguate_dimensions(args)
		if not args[2] then
			cat = '\n[[Category:Size templates with unnamed dimensions]]'
		end
		--cat = cat .. args.debug
	end
	
	-- call either a function for named and for unnamed dimensions
	if args[2] or args[3] or args[4] then
		return p._size_old(args, unit, args.prec, args.lang) .. cat -- old style of display for unnamed dimensions
	else
		return p._size(args, unit, args.prec, args.lang) .. cat -- dimensions are named
	end
end

return p