Module:Size
From WWII Archives
Documentation for this module may be created at Module:Size/doc
--[[ __ __ _ _ ____ _ | \/ | ___ __| |_ _| | ___ _/ ___|(_)_______ | |\/| |/ _ \ / _` | | | | |/ _ (_)___ \| |_ / _ \ | | | | (_) | (_| | |_| | | __/_ ___) | |/ / __/ |_| |_|\___/ \__,_|\__,_|_|\___(_)____/|_/___\___| Authors and maintainers: * User:Zolo - original draft * User:Jarekt - original version ]] require('strict') local core = require('Module:Core') local formatnum = require "Module:Formatnum".formatNum local createTag = require('Module:TagQS').createTag -- ================================================== -- === 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", Q61771670="in", Q3710="ft", ft="ft", foot="ft", feet="ft", Q482798="yd", yd="yd", yard="yd", yards="yd", Q61756607="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", Q469356="t", Q667419="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 getBareLabel(id1, id2, userLang) -- code equivalent to require("Module:Wikidata label")._getLabel with Wikidata=- option local label, link -- build language fallback list local langList = mw.language.getFallbacksFor(userLang) table.insert(langList, 1, userLang) for _, lang in ipairs(langList) do -- loop over language fallback list looking for label in the specific language label = mw.wikibase.getLabelByLang(id1, lang) if not label and id2 then label = mw.wikibase.getLabelByLang(id2, lang) end if label then break end -- label found and we are done end return label or id end ------------------------------------------------------------------------------- local function translateProperty(item, prop, lang) local n, title = 0, {} for _, statement in pairs( mw.wikibase.getBestStatements( item, 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 core.langSwitch(title, lang) end 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) local 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 = translateProperty(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 local factor = unitMult[k1]/unitMult[k2] unit2 = translateProperty(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, str 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) str = mw.ustring.format("%s%s(%s%s%s)", numStr1, wordsep, numStr2, wordsep, unit2) str = mw.ustring.gsub(str, '(%d)%s(%p)', '%1%2') return str end ------------------------------------------------------------------------------- local function disambiguate_dimensions(args) -- compare painting dimensions to image dimensions if args[1] and args[2] and not args[3] 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[1]/args[2] -- 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[1], args[2] else args.height, args.width = args[2], args[1] end args[1], args[2] = nil, nil end end end return args end ------------------------------------------------------------------------------- local function create_QScode(args, unitIDs) -- create non-visible encoding with untranslated dimensions local meta_str = '' local fields = { 'length', 'height', 'width', 'depthH', 'depthV', 'thickness', 'diameter'} local meta = {} for _, field in ipairs( fields ) do if args[field] then local uStr = unitIDs[field] -- get item ID of the unit uStr = "U" .. string.sub(uStr, 2, -1) -- replace Q with U on the beginning of the string table.insert(meta, createTag('dimensions', dimProp[field], args[field] .. uStr) ) end end if #meta>0 then meta_str = table.concat(meta, '') end return meta_str end ------------------------------------------------------------------------------- local function harvest_wikidata(args, lang) -- 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, wdIcon = 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 if v.unit and #v.unit>20 then args [field] = v.amount units [field] = string.gsub(v.unit, "http:%/%/www%.wikidata%.org%/entity%/", "") -- strip URL and keep the item ID wdIcon[field] = core.editAtWikidata(entity.id, prop, lang) end end end end end end return args, units, wdIcon end -- ================================================== -- === External functions =========================== -- ================================================== local p = {} -- =========================================================================== -- === Version of the function to be called from other LUA codes -- =========================================================================== 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 = 1,3 do local 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 local 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 local unitIDs, wdIcon args, unitIDs, wdIcon = harvest_wikidata(args, lang) if core.yesno(args.noicon, false) then wdIcon = {} 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 unitIDs[field] = unitIDs[field] or unitItem[unit] val = string.gsub(val, ',', '.') val = tonumber(val) if (type(val)=='number' and val==val) then -- val==val check will reject nans as in "{{Size|unit=cm|width=nan|height=6,3}}" local dimStr = getBareLabel(dimName[field], dimProp[field], lang) local valStr = unit_conversion(val, unitIDs[field], prec, lang, wordsep) table.insert(results, dimStr .. colon .. valStr .. (wdIcon[field] or '')) end end end local qs = '' if core.yesno(args.tagqs, true) then qs = create_QScode(args, unitIDs) end return table.concat(results, semicolon) .. qs end -- =========================================================================== -- === Versions of the function to be called from template namespace -- =========================================================================== -- ================================================== function p.size(frame) local args = core.getArgs(frame) local unit = args[1] or args.unit or args.units table.remove(args,1) 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[1] and args[2] 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[1] or args[2] or args[3] then -- args = table.remove(args,1) 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