Module:WikidataIB/lite
Documentation for this module may be created at Module:WikidataIB/lite/doc
-- Version: 2021-01-03 -- Module to implement getValue from Module:WikidataIB attempting to minimise resource use local p = {} local cdate -- initialise as nil and only load _complex_date function if needed -- [[Module:Complex date]] is loaded lazily and has the following dependencies: -- Module:I18n/complex date, Module:ISOdate, Module:DateI18n (alternative for Module:Date), -- Module:Formatnum, Module:I18n/date, Module:Yesno, Module:Linguistic, Module:Calendar -- The following, taken from https://www.mediawiki.org/wiki/Wikibase/DataModel#Dates_and_times, -- is needed to use Module:Complex date which seemingly requires date precision as a string. -- It would work better if only the authors of the mediawiki page could spell 'millennium'. local dp = { [6] = "millennium", [7] = "century", [8] = "decade", [9] = "year", [10] = "month", [11] = "day", } local i18n = { ["errors"] = { ["property-not-found"] = "Property not found.", ["No property supplied"] = "No property supplied", ["entity-not-found"] = "Wikidata entity not found.", ["unknown-claim-type"] = "Unknown claim type.", ["unknown-entity-type"] = "Unknown entity type.", ["qualifier-not-found"] = "Qualifier not found.", ["site-not-found"] = "Wikimedia project not found.", ["labels-not-found"] = "No labels found.", ["descriptions-not-found"] = "No descriptions found.", ["aliases-not-found"] = "No aliases found.", ["unknown-datetime-format"] = "Unknown datetime format.", ["local-article-not-found"] = "Article is available on Wikidata, but not on Wikipedia", ["dab-page"] = " (dab)", }, ["months"] = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }, ["century"] = "century", ["BC"] = "BC", ["BCE"] = "BCE", ["ordinal"] = { [1] = "st", [2] = "nd", [3] = "rd", ["default"] = "th" }, ["filespace"] = "File", ["Unknown"] = "Unknown", ["NaN"] = "Not a number", -- set the following to the name of a tracking category, -- e.g. "[[Category:Articles with missing Wikidata information]]", or "" to disable: ["missinginfocat"] = "[[Category:Articles with missing Wikidata information]]", ["editonwikidata"] = "Edit this on Wikidata", ["latestdatequalifier"] = function (date) return "before " .. date end, -- some languages, e.g. Bosnian use a period as a suffix after each number in a date ["datenumbersuffix"] = "", ["list separator"] = ", ", ["multipliers"] = { [0] = "", [3] = " thousand", [6] = " million", [9] = " billion", [12] = " trillion", } } -- This allows an internationisation module to override the above table if 'en' ~= mw.getContentLanguage():getCode() then require("Module:i18n").loadI18n("Module:WikidataIB/i18n", i18n) end -- This piece of html implements a collapsible container. Check the classes exist on your wiki. local collapsediv = '<div class="mw-collapsible mw-collapsed" style="width:100%; overflow:auto;" data-expandtext="{{int:show}}" data-collapsetext="{{int:hide}}">' -- Some items should not be linked. -- Each wiki can create a list of those in Module:WikidataIB/nolinks -- It should return a table called itemsindex, containing true for each item not to be linked local donotlink = {} local nolinks_exists, nolinks = pcall(mw.loadData, "Module:WikidataIB/nolinks") if nolinks_exists then donotlink = nolinks.itemsindex end ------------------------------------------------------------------------------- -- Private functions ------------------------------------------------------------------------------- -- ------------------------------------------------------------------------------- -- findLang takes a "langcode" parameter if supplied and valid -- otherwise it tries to create it from the user's set language ({{int:lang}}) -- failing that it uses the wiki's content language. -- It returns a language object ------------------------------------------------------------------------------- -- Dependencies: none ------------------------------------------------------------------------------- local findLang = function(langcode) local langobj langcode = mw.text.trim(langcode or "") if mw.language.isKnownLanguageTag(langcode) then langobj = mw.language.new( langcode ) else langcode = mw.getCurrentFrame():preprocess( '{{int:lang}}' ) if mw.language.isKnownLanguageTag(langcode) then langobj = mw.language.new( langcode ) else langobj = mw.language.getContentLanguage() end end return langobj end ------------------------------------------------------------------------------- -- roundto takes a number (x) -- and returns it rounded to (sf) significant figures ------------------------------------------------------------------------------- -- Dependencies: none ------------------------------------------------------------------------------- local roundto = function(x, sf) if x == 0 then return 0 end local s = 1 if x < 0 then x = -x s = -1 end if sf < 1 then sf = 1 end local p = 10 ^ (math.floor(math.log10(x)) - sf + 1) x = math.floor(x / p + 0.5) * p * s -- if it's integral, cast to an integer: if x == math.floor(x) then x = math.floor(x) end return x end ------------------------------------------------------------------------------- -- decimalToDMS takes a decimal degrees (x) with precision (p) -- and returns degrees/minutes/seconds according to the precision ------------------------------------------------------------------------------- -- Dependencies: none ------------------------------------------------------------------------------- local decimalToDMS = function(x, p) -- if p is not supplied, use a precision around 0.1 seconds if not tonumber(p) then p = 1e-4 end local d = math.floor(x) local ms = (x - d) * 60 if p > 0.5 then -- precision is > 1/2 a degree if ms > 30 then d = d + 1 end ms = 0 end local m = math.floor(ms) local s = (ms - m) * 60 if p > 0.008 then -- precision is > 1/2 a minute if s > 30 then m = m +1 end s = 0 elseif p > 0.00014 then -- precision is > 1/2 a second s = math.floor(s + 0.5) elseif p > 0.000014 then -- precision is > 1/20 second s = math.floor(10 * s + 0.5) / 10 elseif p > 0.0000014 then -- precision is > 1/200 second s = math.floor(100 * s + 0.5) / 100 else -- cap it at 3 dec places for now s = math.floor(1000 * s + 0.5) / 1000 end return d, m, s end ------------------------------------------------------------------------------- -- decimalPrecision takes a decimal (x) with precision (p) -- and returns x rounded approximately to the given precision -- precision should be between 1 and 1e-6, preferably a power of 10. ------------------------------------------------------------------------------- -- Dependencies: none ------------------------------------------------------------------------------- local decimalPrecision = function(x, p) local s = 1 if x < 0 then x = -x s = -1 end -- if p is not supplied, pick an arbitrary precision if not tonumber(p) then p = 1e-4 elseif p > 1 then p = 1 elseif p < 1e-6 then p = 1e-6 else p = 10 ^ math.floor(math.log10(p)) end x = math.floor(x / p + 0.5) * p * s -- if it's integral, cast to an integer: if x == math.floor(x) then x = math.floor(x) end -- if it's less than 1e-4, it will be in exponent form, so return a string with 6dp -- 9e-5 becomes 0.000090 if math.abs(x) < 1e-4 then x = string.format("%f", x) end return x end ------------------------------------------------------------------------------- -- dateFormat is the handler for properties that are of type "time" -- It takes timestamp, precision (6 to 11 per mediawiki), dateformat (y/dmy/mdy), BC format (BC/BCE), -- a plaindate switch (yes/no/adj) to en/disable "sourcing circumstances"/use adjectival form, -- any qualifiers for the property, the language, and any adjective to use like 'before'. -- It passes the date through the "complex date" function -- and returns a string with the internatonalised date formatted according to preferences. ------------------------------------------------------------------------------- -- Dependencies: findLang(); cdate(); dp[] ------------------------------------------------------------------------------- local dateFormat = function(timestamp, dprec, df, bcf, pd, qualifiers, lang, adj, model) -- output formatting according to preferences (y/dmy/mdy/ymd) df = (df or ""):lower() -- if ymd is required, return the part of the timestamp in YYYY-MM-DD form -- but apply Year zero#Astronomers fix: 1 BC = 0000; 2 BC = -0001; etc. if df == "ymd" then if timestamp:sub(1,1) == "+" then return timestamp:sub(2,11) else local yr = tonumber(timestamp:sub(2,5)) - 1 yr = ("000" .. yr):sub(-4) if yr ~= "0000" then yr = "-" .. yr end return yr .. timestamp:sub(6,11) end end -- A year can be stored like this: "+1872-00-00T00:00:00Z", -- which is processed here as if it were the day before "+1872-01-01T00:00:00Z", -- and that's the last day of 1871, so the year is wrong. -- So fix the month 0, day 0 timestamp to become 1 January instead: timestamp = timestamp:gsub("%-00%-00T", "-01-01T") -- just in case date precision is missing dprec = dprec or 11 -- override more precise dates if required dateformat is year alone: if df == "y" and dprec > 9 then dprec = 9 end -- complex date only deals with precisions from 6 to 11, so clip range dprec = dprec>11 and 11 or dprec dprec = dprec<6 and 6 or dprec -- BC format is "BC" or "BCE" bcf = (bcf or ""):upper() -- plaindate only needs the first letter (y/n/a) pd = (pd or ""):sub(1,1):lower() if pd == "" or pd == "n" or pd == "f" or pd == "0" then pd = false end -- in case language isn't passed lang = lang or findLang().code -- set adj as empty if nil adj = adj or "" -- extract the day, month, year from the timestamp local bc = timestamp:sub(1, 1)=="-" and "BC" or "" local year, month, day = timestamp:match("[+-](%d*)-(%d*)-(%d*)T") local iso = tonumber(year) -- if year is missing, let it throw an error -- this will adjust the date format to be compatible with cdate -- possible formats are Y, YY, YYY0, YYYY, YYYY-MM, YYYY-MM-DD if dprec == 6 then iso = math.floor( (iso - 1) / 1000 ) + 1 end if dprec == 7 then iso = math.floor( (iso - 1) / 100 ) + 1 end if dprec == 8 then iso = math.floor( iso / 10 ) .. "0" end if dprec == 10 then iso = year .. "-" .. month end if dprec == 11 then iso = year .. "-" .. month .. "-" .. day end -- add "circa" (Q5727902) from "sourcing circumstances" (P1480) local sc = not pd and qualifiers and qualifiers.P1480 if sc then for k1, v1 in pairs(sc) do if v1.datavalue and v1.datavalue.value.id == "Q5727902" then adj = "circa" break end end end -- deal with Julian dates: -- no point in saying that dates before 1582 are Julian - they are by default -- doesn't make sense for dates less precise than year -- we can suppress it by setting |plaindate, e.g. for use in constructing categories. local calendarmodel = "" if tonumber(year) > 1582 and dprec > 8 and not pd and model == "http://www.wikidata.org/entity/Q1985786" then calendarmodel = "julian" end if not cdate then cdate = require("Module:Complex date")._complex_date end local fdate = cdate(calendarmodel, adj, tostring(iso), dp[dprec], bc, "", "", "", "", lang, 1) -- this may have QuickStatements info appended to it in a div, so remove that fdate = fdate:gsub(' <div style="display: none;">[^<]*</div>', '') -- it may also be returned wrapped in a microformat, so remove that fdate = fdate:gsub("<[^>]*>", "") -- there may be leading zeros that we should remove fdate = fdate:gsub("^0*", "") -- if a plain date is required, then remove any links (like BC linked) if pd then fdate = fdate:gsub("%[%[.*|", ""):gsub("]]", "") end -- if 'circa', use the abbreviated form *** internationalise later *** fdate = fdate:gsub('circa ', '<abbr title="circa">c.</abbr> ') -- deal with BC/BCE if bcf == "BCE" then fdate = fdate:gsub('BC', 'BCE') end -- deal with mdy format if df == "mdy" then fdate = fdate:gsub("(%d+) (%w+) (%d+)", "%2 %1, %3") end -- deal with adjectival form *** internationalise later *** if pd == "a" then fdate = fdate:gsub(' century', '-century') end return fdate end ------------------------------------------------------------------------------- -- parseParam takes a (string) parameter, e.g. from the list of frame arguments, -- and makes "false", "no", and "0" into the (boolean) false -- it makes the empty string and nil into the (boolean) value passed as default -- allowing the parameter to be true or false by default. -- It returns a boolean. ------------------------------------------------------------------------------- -- Dependencies: none ------------------------------------------------------------------------------- local parseParam = function(param, default) if type(param) == "boolean" then param = tostring(param) end if param and param ~= "" then param = param:lower() if (param == "false") or (param:sub(1,1) == "n") or (param == "0") then return false else return true end else return default end end ------------------------------------------------------------------------------- -- The label in a Wikidata item is subject to vulnerabilities -- that an attacker might try to exploit. -- It needs to be 'sanitised' by removing any wikitext before use. -- If it doesn't exist, return the id for the item -- a second (boolean) value is also returned, value is true when the label exists ------------------------------------------------------------------------------- -- Dependencies: none ------------------------------------------------------------------------------- local labelOrId = function(id, lang) if lang == "default" then lang = findLang().code end local label if lang then label = mw.wikibase.getLabelByLang(id, lang) else label = mw.wikibase.getLabel(id) end if label then return mw.text.nowiki(label), true else return id, false end end ------------------------------------------------------------------------------- -- linkedItem takes an entity-id and returns a string, linked if possible. -- This is the handler for "wikibase-item". Preferences: -- 1. Display linked disambiguated sitelink if it exists -- 2. Display linked label if it is a redirect -- 3. TBA: Display an inter-language link for the label if it exists other than in default language -- 4. Display unlinked label if it exists -- 5. Display entity-id for now to indicate a label could be provided -- dtxt is text to be used instead of label, or nil. -- lang is the current language code. -- uselbl is boolean switch to force display of the label instead of the sitelink (default: false) -- linkredir is boolean switch to allow linking to a redirect (default: false) -- formatvalue is boolean switch to allow formatting as italics or quoted (default: false) ------------------------------------------------------------------------------- -- Dependencies: labelOrId(); donotlink[] ------------------------------------------------------------------------------- local linkedItem = function(id, args) local lprefix = args.lprefix or "" -- toughen against nil values passed local lpostfix = args.lpostfix or "" local prefix = args.prefix or "" local postfix = args.postfix or "" local dtxt = args.dtxt local lang = args.lang or "en" -- fallback to default if missing local uselbl = args.uselabel or args.uselbl uselbl = parseParam(uselbl, false) local linkredir = args.linkredir linkredir = parseParam(linkredir, false) local disp local sitelink = mw.wikibase.getSitelink(id) local label, islabel if dtxt then label, islabel = dtxt, true else label, islabel = labelOrId(id) end if mw.site.siteName ~= "Wikimedia Commons" then if sitelink then if not dtxt then -- if sitelink and label are the same except for case, no need to process further if sitelink:lower() ~= label:lower() then -- strip any namespace or dab from the sitelink local pos = sitelink:find(":") or 0 local slink = sitelink if pos > 0 then local pfx = sitelink:sub(1,pos-1) if mw.site.namespaces[pfx] then -- that prefix is a valid namespace, so remove it slink = sitelink:sub(pos+1) end end -- remove stuff after commas or inside parentheses - ie. dabs slink = slink:gsub("%s%(.+%)$", ""):gsub(",.+$", "") -- if uselbl is false, use sitelink instead of label if not uselbl then -- use slink as display, preserving label case - find("^%u") is true for 1st char uppercase if label:find("^%u") then label = slink:gsub("^(%l)", string.upper) else label = slink:gsub("^(%u)", string.lower) end end end end if donotlink[label] then disp = prefix .. label .. postfix else disp = "[[" .. lprefix .. sitelink .. lpostfix .. "|" .. prefix .. label .. postfix .. "]]" end elseif islabel then -- no sitelink, label exists, so check if a redirect with that title exists, if linkredir is true -- display plain label by default disp = prefix .. label .. postfix if linkredir then local artitle = mw.title.new(label, 0) -- only nil if label has invalid chars if not donotlink[label] and artitle and artitle.redirectTarget then -- there's a redirect with the same title as the label, so let's link to that disp = "[[".. lprefix .. label .. lpostfix .. "|" .. prefix .. label .. postfix .. "]]" end end -- test if article title exists as redirect on current Wiki else -- no sitelink and no label, so return whatever was returned from labelOrId for now -- add tracking category [[Category:Articles with missing Wikidata information]] -- for enwiki, just return the tracking category if mw.wikibase.getGlobalSiteId() == "enwiki" then disp = i18n.missinginfocat else disp = prefix .. label .. postfix .. i18n.missinginfocat end end else local ccat = mw.wikibase.getBestStatements(id, "P373")[1] if ccat and ccat.mainsnak.datavalue then ccat = ccat.mainsnak.datavalue.value disp = "[[" .. lprefix .. "Category:" .. ccat .. lpostfix .. "|" .. prefix .. label .. postfix .. "]]" elseif sitelink then -- this asumes that if a sitelink exists, then a label also exists disp = "[[" .. lprefix .. sitelink .. lpostfix .. "|" .. prefix .. label .. postfix .. "]]" else -- no sitelink and no Commons cat, so return label from labelOrId for now disp = prefix .. label .. postfix end end return disp end ------------------------------------------------------------------------------- -- sourced takes a table representing a statement that may or may not have references -- it looks for a reference sourced to something not containing the word "wikipedia" -- it returns a boolean = true if it finds a sourced reference. ------------------------------------------------------------------------------- -- Dependencies: none ------------------------------------------------------------------------------- local sourced = function(claim) if claim.references then for kr, vr in pairs(claim.references) do local ref = mw.wikibase.renderSnaks(vr.snaks) if not ref:find("Wiki") then return true end end end end ------------------------------------------------------------------------------- -- setRanks takes a flag (parameter passed) that requests the values to return -- "b[est]" returns preferred if available, otherwise normal -- "p[referred]" returns preferred -- "n[ormal]" returns normal -- "d[eprecated]" returns deprecated -- multiple values are allowed, e.g. "preferred normal" (which is the default) -- "best" will override the other flags, and set p and n ------------------------------------------------------------------------------- -- Dependencies: none ------------------------------------------------------------------------------- local setRanks = function(rank) rank = (rank or ""):lower() -- if nothing passed, return preferred and normal -- if rank == "" then rank = "p n" end local ranks = {} for w in string.gmatch(rank, "%a+") do w = w:sub(1,1) if w == "b" or w == "p" or w == "n" or w == "d" then ranks[w] = true end end -- check if "best" is requested or no ranks requested; and if so, set preferred and normal if ranks.b or not next(ranks) then ranks.p = true ranks.n = true end return ranks end ------------------------------------------------------------------------------- -- parseInput processes the Q-id , the blacklist and the whitelist -- if an input parameter is supplied, it returns that and ends the call. -- it returns (1) either the qid or nil indicating whether or not the call should continue -- and (2) a table containing all of the statements for the propertyID and relevant Qid -- if "best" ranks are requested, it returns those instead of all non-deprecated ranks ------------------------------------------------------------------------------- -- Dependencies: none ------------------------------------------------------------------------------- local parseInput = function(frame, input_parm, property_id) -- There may be a local parameter supplied, if it's blank, set it to nil input_parm = mw.text.trim(input_parm or "") if input_parm == "" then input_parm = nil end -- return nil if Wikidata is not available if not mw.wikibase then return false, input_parm end local args = frame.args -- can take a named parameter |qid which is the Wikidata ID for the article. -- if it's not supplied, use the id for the current page local qid = args.qid or "" if qid == "" then qid = mw.wikibase.getEntityIdForCurrentPage() end -- if there's no Wikidata item for the current page return nil if not qid then return false, input_parm end -- The blacklist is passed in named parameter |suppressfields local blacklist = args.suppressfields or args.spf or "" -- The whitelist is passed in named parameter |fetchwikidata local whitelist = args.fetchwikidata or args.fwd or "" if whitelist == "" then whitelist = "NONE" end -- The name of the field that this function is called from is passed in named parameter |name local fieldname = args.name or "" if blacklist ~= "" then -- The name is compulsory when blacklist is used, so return nil if it is not supplied if fieldname == "" then return false, nil end -- If this field is on the blacklist, then return nil if blacklist:find(fieldname) then return false, nil end end -- If we got this far then we're not on the blacklist -- The blacklist overrides any locally supplied parameter as well -- If a non-blank input parameter was supplied return it if input_parm then return false, input_parm end -- We can filter out non-valid properties if property_id:sub(1,1):upper() ~="P" or property_id == "P0" then return false, nil end -- Otherwise see if this field is on the whitelist: -- needs a bit more logic because find will return its second value = 0 if fieldname is "" -- but nil if fieldname not found on whitelist local _, found = whitelist:find(fieldname) found = ((found or 0) > 0) if whitelist ~= 'ALL' and (whitelist:upper() == "NONE" or not found) then return false, nil end -- See what's on Wikidata (the call always returns a table, but it may be empty): local props = {} if args.reqranks.b then props = mw.wikibase.getBestStatements(qid, property_id) else props = mw.wikibase.getAllStatements(qid, property_id) end if props[1] then return qid, props end -- no property on Wikidata return false, nil end ------------------------------------------------------------------------------- -- createicon assembles the "Edit at Wikidata" pen icon. -- It returns a wikitext string inside a span class="penicon" -- if entityID is nil or empty, the ID associated with current page is used -- langcode and propertyID may be nil or empty ------------------------------------------------------------------------------- -- Dependencies: i18n[]; ------------------------------------------------------------------------------- local createicon = function(langcode, entityID, propertyID) langcode = langcode or "" if not entityID or entityID == "" then entityID= mw.wikibase.getEntityIdForCurrentPage() end propertyID = propertyID or "" local icon = " <span class='penicon autoconfirmed-show'>[[" -- " <span data-bridge-edit-flow='overwrite' class='penicon'>[[" -> enable Wikidata Bridge .. i18n["filespace"] .. ":OOjs UI icon edit-ltr-progressive.svg |frameless |text-top |10px |alt=" .. i18n["editonwikidata"] .. "|link=https://www.wikidata.org/wiki/" .. entityID if langcode ~= "" then icon = icon .. "?uselang=" .. langcode end if propertyID ~= "" then icon = icon .. "#" .. propertyID end icon = icon .. "|" .. i18n["editonwikidata"] .. "]]</span>" return icon end ------------------------------------------------------------------------------- -- assembleoutput takes the sequence table containing the property values -- and formats it according to switches given. It returns a string or nil. -- It uses the entityID (and optionally propertyID) to create a link in the pen icon. ------------------------------------------------------------------------------- -- Dependencies: parseParam(); ------------------------------------------------------------------------------- local assembleoutput = function(out, args, entityID, propertyID) -- sorted is a boolean passed to enable sorting of the values returned -- if nothing or an empty string is passed set it false -- if "false" or "no" or "0" is passed set it false local sorted = parseParam(args.sorted, false) -- noicon is a boolean passed to suppress the trailing "edit at Wikidata" icon -- for use when the value is processed further by the infobox -- if nothing or an empty string is passed set it false -- if "false" or "no" or "0" is passed set it false local noic = parseParam(args.noicon, false) -- list is the name of a template that a list of multiple values is passed through -- examples include "hlist" and "ubl" -- setting it to "prose" produces something like "1, 2, 3, and 4" local list = args.list or "" -- sep is a string that is used to separate multiple returned values -- if nothing or an empty string is passed set it to the default -- any double-quotes " are stripped out, so that spaces may be passed -- e.g. |sep=" - " local sepdefault = i18n["list separator"] local separator = args.sep or "" separator = string.gsub(separator, '"', '') if separator == "" then separator = sepdefault end -- collapse is a number that determines the maximum number of returned values -- before the output is collapsed. -- Zero or not a number result in no collapsing (default becomes 0). local collapse = tonumber(args.collapse) or 0 -- replacetext (rt) is a string that is returned instead of any non-empty Wikidata value -- this is useful for tracking and debugging local replacetext = mw.text.trim(args.rt or args.replacetext or "") -- if there's anything to return, then return a list -- comma-separated by default, but may be specified by the sep parameter -- optionally specify a hlist or ubl or a prose list, etc. local strout if #out > 0 then if sorted then table.sort(out) end -- if there's something to display and a pen icon is wanted, add it the end of the last value local hasdisplay = false for i, v in ipairs(out) do if v ~= i18n.missinginfocat then hasdisplay = true break end end if not noic and hasdisplay then out[#out] = out[#out] .. createicon(args.langobj.code, entityID, propertyID) end if list == "" then strout = table.concat(out, separator) elseif list:lower() == "prose" then strout = mw.text.listToText( out ) else strout = mw.getCurrentFrame():expandTemplate{title = list, args = out} end if collapse >0 and #out > collapse then strout = collapsediv .. strout .. "</div>" end else strout = nil -- no items had valid reference end if replacetext ~= "" and strout then strout = replacetext end return strout end ------------------------------------------------------------------------------- -- rendersnak takes a table (propval) containing the information stored on one property value -- and returns the value as a string and its language if monolingual text. -- It handles data of type: -- wikibase-item -- time -- string, url, commonsMedia, external-id -- quantity -- globe-coordinate -- monolingualtext -- It also requires linked, the link/pre/postfixes, uabbr, and the arguments passed from frame. -- The optional filter parameter allows quantities to be be filtered by unit Qid. ------------------------------------------------------------------------------- -- Dependencies: parseParam(); labelOrId(); i18n[]; dateFormat(); -- roundto(); decimalPrecision(); decimalToDMS(); linkedItem(); ------------------------------------------------------------------------------- local rendersnak = function(propval, args, linked, lpre, lpost, pre, post, uabbr, filter) lpre = lpre or "" lpost = lpost or "" pre = pre or "" post = post or "" args.lang = args.lang or findLang().code -- allow values to display a fixed text instead of label local dtxt = args.displaytext or args.dt if dtxt == "" then dtxt = nil end -- switch to use display of short name (P1813) instead of label local snak = propval.mainsnak or propval local dtype = snak.datatype local dv = snak.datavalue dv = dv and dv.value -- value and monolingual text language code returned local val, mlt if propval.rank and not args.reqranks[propval.rank:sub(1, 1)] then -- val is nil: value has a rank that isn't requested ------------------------------------ elseif snak.snaktype == "somevalue" then -- value is unknown val = i18n["Unknown"] ------------------------------------ elseif snak.snaktype == "novalue" then -- value is none -- val = "No value" -- don't return anything ------------------------------------ elseif dtype == "wikibase-item" then -- data type is a wikibase item: -- it's wiki-linked value, so output as link if enabled and possible local qnumber = dv.id if linked then val = linkedItem(qnumber, args) else -- no link wanted so check for display-text, otherwise test for lang code local label, islabel if dtxt then label = dtxt else label, islabel = labelOrId(qnumber) local langlabel = mw.wikibase.getLabelByLang(qnumber, args.lang) if langlabel then label = mw.text.nowiki( langlabel ) end end val = pre .. label .. post end -- test for link required ------------------------------------ elseif dtype == "time" then -- data type is time: -- time is in timestamp format -- date precision is integer per mediawiki -- output formatting according to preferences (y/dmy/mdy) -- BC format as BC or BCE -- plaindate is passed to disable looking for "sourcing cirumstances" -- or to set the adjectival form -- qualifiers (if any) is a nested table or nil -- lang is given, or user language, or site language -- -- Here we can check whether args.df has a value -- If not, use code from Module:Sandbox/RexxS/Getdateformat to set it from templates like {{Use mdy dates}} val = dateFormat(dv.time, dv.precision, args.df, args.bc, args.pd, propval.qualifiers, args.lang, "", dv.calendarmodel) ------------------------------------ -- data types which are strings: elseif dtype == "commonsMedia" or dtype == "external-id" or dtype == "string" or dtype == "url" then -- commonsMedia or external-id or string or url -- all have mainsnak.datavalue.value as string if (lpre == "" or lpre == ":") and lpost == "" then -- don't link if no linkpre/postfix or linkprefix is just ":" val = pre .. dv .. post elseif dtype == "external-id" then val = "[" .. lpre .. dv .. lpost .. " " .. pre .. dv .. post .. "]" else val = "[[" .. lpre .. dv .. lpost .. "|" .. pre .. dv .. post .. "]]" end -- check for link requested (i.e. either linkprefix or linkpostfix exists) ------------------------------------ -- data types which are quantities: elseif dtype == "quantity" then -- quantities have mainsnak.datavalue.value.amount and mainsnak.datavalue.value.unit -- the unit is of the form http://www.wikidata.org/entity/Q829073 -- -- implement a switch to turn on/off numerical formatting later local fnum = true -- -- a switch to turn on/off conversions - only for en-wiki local conv = parseParam(args.conv or args.convert, false) -- if we have conversions, we won't have formatted numbers or scales if conv then uabbr = true fnum = false args.scale = "0" end -- -- a switch to turn on/off showing units, default is true local showunits = parseParam(args.su or args.showunits, true) -- -- convert amount to a number local amount = tonumber(dv.amount) or i18n["NaN"] -- -- scale factor for millions, billions, etc. local sc = tostring(args.scale or ""):sub(1,1):lower() local scale if sc == "a" then -- automatic scaling if amount > 1e15 then scale = 12 elseif amount > 1e12 then scale = 9 elseif amount > 1e9 then scale = 6 elseif amount > 1e6 then scale = 3 else scale = 0 end else scale = tonumber(args.scale) or 0 if scale < 0 or scale > 12 then scale = 0 end scale = math.floor(scale/3) * 3 end local factor = 10^scale amount = amount / factor -- ranges: local range = "" -- check if upper and/or lower bounds are given and significant local upb = tonumber(dv.upperBound) local lowb = tonumber(dv.lowerBound) if upb and lowb then -- differences rounded to 2 sig fig: local posdif = roundto(upb - amount, 2) / factor local negdif = roundto(amount - lowb, 2) / factor upb, lowb = amount + posdif, amount - negdif -- round scaled numbers to integers or 4 sig fig if (scale > 0 or sc == "a") then if amount < 1e4 then amount = roundto(amount, 4) else amount = math.floor(amount + 0.5) end end if fnum then amount = args.langobj:formatNum( amount ) end if posdif ~= negdif then -- non-symmetrical range = " +" .. posdif .. " -" .. negdif elseif posdif ~= 0 then -- symmetrical and non-zero range = " ±" .. posdif else -- otherwise range is zero, so leave it as "" end else -- round scaled numbers to integers or 4 sig fig if (scale > 0 or sc == "a") then if amount < 1e4 then amount = roundto(amount, 4) else amount = math.floor(amount + 0.5) end end if fnum then amount = args.langobj:formatNum( amount ) end end -- unit names and symbols: -- extract the qid in the form 'Qnnn' from the value.unit url -- and then fetch the label from that - or symbol if unitabbr is true local unit = "" local usep = "" local usym = "" local unitqid = string.match( dv.unit, "(Q%d+)" ) if filter and unitqid ~= filter then return nil end if unitqid and showunits then local uname = mw.wikibase.getLabelByLang(unitqid, args.lang) or "" if uname ~= "" then usep, unit = " ", uname end if uabbr then -- see if there's a unit symbol (P5061) local unitsymbols = mw.wikibase.getBestStatements(unitqid, "P5061") -- construct fallback table, add local lang and multiple languages local fbtbl = mw.language.getFallbacksFor( args.lang ) table.insert( fbtbl, 1, args.lang ) table.insert( fbtbl, 1, "mul" ) local found = false for idx1, us in ipairs(unitsymbols) do for idx2, fblang in ipairs(fbtbl) do if us.mainsnak.datavalue.value.language == fblang then usym = us.mainsnak.datavalue.value.text found = true break end if found then break end end -- loop through fallback table end -- loop through values of P5061 if found then usep, unit = " ", usym end end end -- format display: if conv then if range == "" then val = mw.getCurrentFrame():expandTemplate{title = "cvt", args = {amount, unit}} else val = mw.getCurrentFrame():expandTemplate{title = "cvt", args = {lowb, "to", upb, unit}} end elseif unit == "$" or unit == "£" then val = unit .. amount .. range .. i18n.multipliers[scale] else val = amount .. range .. i18n.multipliers[scale] .. usep .. unit end ------------------------------------ -- datatypes which are global coordinates: elseif dtype == "globe-coordinate" then -- 'display' parameter defaults to "inline, title" *** unused for now *** -- local disp = args.display or "" -- if disp == "" then disp = "inline, title" end -- -- format parameter switches from deg/min/sec to decimal degrees -- default is deg/min/sec -- decimal degrees needs |format = dec local form = (args.format or ""):lower():sub(1,3) if form ~= "dec" then form = "dms" end -- not needed for now -- -- show parameter allows just the latitude, or just the longitude, or both -- to be returned as a signed decimal, ignoring the format parameter. local show = (args.show or ""):lower() if show ~= "longlat" then show = show:sub(1,3) end -- local lat, long, prec = dv.latitude, dv.longitude, dv.precision if show == "lat" then val = decimalPrecision(lat, prec) elseif show == "lon" then val = decimalPrecision(long, prec) elseif show == "longlat" then val = decimalPrecision(long, prec) .. ", " .. decimalPrecision(lat, prec) else local ns = "N" local ew = "E" if lat < 0 then ns = "S" lat = - lat end if long < 0 then ew = "W" long = - long end if form == "dec" then lat = decimalPrecision(lat, prec) long = decimalPrecision(long, prec) val = lat .. "°" .. ns .. " " .. long .. "°" .. ew else local latdeg, latmin, latsec = decimalToDMS(lat, prec) local longdeg, longmin, longsec = decimalToDMS(long, prec) if latsec == 0 and longsec == 0 then if latmin == 0 and longmin == 0 then val = latdeg .. "°" .. ns .. " " .. longdeg .. "°" .. ew else val = latdeg .. "°" .. latmin .. "′" .. ns .. " " val = val .. longdeg .. "°".. longmin .. "′" .. ew end else val = latdeg .. "°" .. latmin .. "′" .. latsec .. "″" .. ns .. " " val = val .. longdeg .. "°" .. longmin .. "′" .. longsec .. "″" .. ew end end end ------------------------------------ elseif dtype == "monolingualtext" then -- data type is Monolingual text: -- has mainsnak.datavalue.value as a table containing language/text pairs -- collect all the values in 'out' and languages in 'mlt' and process them later val = pre .. dv.text .. post mlt = dv.language ------------------------------------ else -- some other data type so write a specific handler val = "unknown data type: " .. dtype end -- of datatype/unknown value/sourced check return val, mlt end ------------------------------------------------------------------------------- -- propertyvalueandquals takes a property object, the arguments passed from frame, -- and a qualifier propertyID. -- It returns a sequence (table) of values representing the values of that property -- and qualifiers that match the qualifierID if supplied. ------------------------------------------------------------------------------- -- Dependencies: parseParam(); sourced(); labelOrId(); i18n.latestdatequalifier(); -- roundto(); decimalPrecision(); decimalToDMS(); assembleoutput(); ------------------------------------------------------------------------------- local function propertyvalueandquals(objproperty, args, qualID) -- needs this style of declaration because it's re-entrant -- onlysourced is a boolean passed to return only values sourced to other than Wikipedia -- if nothing or an empty string is passed set it true local onlysrc = parseParam(args.onlysourced or args.osd, true) -- linked is a a boolean that enables the link to a local page via sitelink -- if nothing or an empty string is passed set it true local linked = parseParam(args.linked, true) -- prefix is a string that may be nil, empty (""), or a string of characters -- this is prefixed to each value -- useful when when multiple values are returned -- any double-quotes " are stripped out, so that spaces may be passed local prefix = (args.prefix or ""):gsub('"', '') -- postfix is a string that may be nil, empty (""), or a string of characters -- this is postfixed to each value -- useful when when multiple values are returned -- any double-quotes " are stripped out, so that spaces may be passed local postfix = (args.postfix or ""):gsub('"', '') -- linkprefix is a string that may be nil, empty (""), or a string of characters -- this creates a link and is then prefixed to each value -- useful when when multiple values are returned and indirect links are needed -- any double-quotes " are stripped out, so that spaces may be passed local lprefix = (args.linkprefix or args.lp or ""):gsub('"', '') -- linkpostfix is a string that may be nil, empty (""), or a string of characters -- this is postfixed to each value when linking is enabled with lprefix -- useful when when multiple values are returned -- any double-quotes " are stripped out, so that spaces may be passed local lpostfix = (args.linkpostfix or ""):gsub('"', '') -- wdlinks is a boolean passed to enable links to Wikidata when no article exists -- if nothing or an empty string is passed set it false local wdl = parseParam(args.wdlinks or args.wdl, false) -- unitabbr is a boolean passed to enable unit abbreviations for common units -- if nothing or an empty string is passed set it false local uabbr = parseParam(args.unitabbr or args.uabbr, false) -- qualsonly is a boolean passed to return just the qualifiers -- if nothing or an empty string is passed set it false local qualsonly = parseParam(args.qualsonly or args.qo, false) -- maxvals is a string that may be nil, empty (""), or a number -- this determines how many items may be returned when multiple values are available -- setting it = 1 is useful where the returned string is used within another call, e.g. image local maxvals = tonumber(args.maxvals) or 0 -- pd (plain date) is a string: yes/true/1 | no/false/0 | adj -- to disable/enable "sourcing cirumstances" or use adjectival form for the plain date local pd = args.plaindate or args.pd or "no" args.pd = pd -- allow qualifiers to have a different date format; default to year args.qdf = args.qdf or args.qualifierdateformat or args.df or "y" local lang = args.lang or findlang().code -- qualID is a string list of wanted qualifiers or "ALL" qualID = qualID or "" -- capitalise list of wanted qualifiers and substitute "DATES" qualID = qualID:upper():gsub("DATES", "P580, P582") local allflag = (qualID == "ALL") -- create table of wanted qualifiers as key local qwanted = {} -- create sequence of wanted qualifiers local qorder = {} for q in mw.text.gsplit(qualID, "%p") do -- split at punctuation and iterate local qtrim = mw.text.trim(q) if qtrim ~= "" then qwanted[mw.text.trim(q)] = true qorder[#qorder+1] = qtrim end end -- qsep is the output separator for rendering qualifier list local qsep = (args.qsep or ""):gsub('"', '') -- qargs are the arguments to supply to assembleoutput() local qargs = { ["osd"] = "false", ["linked"] = tostring(linked), ["prefix"] = args.qprefix, ["postfix"] = args.qpostfix, ["linkprefix"] = args.qlinkprefix or args.qlp, ["linkpostfix"] = args.qlinkpostfix, ["wdl"] = "false", ["unitabbr"] = tostring(uabbr), ["maxvals"] = 0, ["sorted"] = tostring(args.qsorted), ["noicon"] = "true", ["list"] = args.qlist, ["sep"] = qsep, ["langobj"] = args.langobj, ["lang"] = args.langobj.code, ["df"] = args.qdf, } -- all proper values of a Wikidata property will be the same type as the first -- qualifiers don't have a mainsnak, properties do local datatype = objproperty[1].datatype or objproperty[1].mainsnak.datatype -- out[] holds the a list of returned values for this property -- mlt[] holds the language code if the datatype is monolingual text local out = {} local mlt = {} for k, v in ipairs(objproperty) do local hasvalue = true if (onlysrc and not sourced(v)) then -- no value: it isn't sourced when onlysourced=true hasvalue = false else local val, lcode = rendersnak(v, args, linked, lprefix, lpostfix, prefix, postfix, uabbr) if not val then hasvalue = false -- rank doesn't match elseif qualsonly and qualID then -- suppress value returned: only qualifiers are requested else out[#out+1], mlt[#out+1] = val, lcode end end -- See if qualifiers are to be returned: local snak = v.mainsnak or v if hasvalue and v.qualifiers and qualID ~= "" and snak.snaktype~="novalue" then -- collect all wanted qualifier values returned in qlist, indexed by propertyID local qlist = {} local timestart, timeend = "", "" -- loop through qualifiers for k1, v1 in pairs(v.qualifiers) do if allflag or qwanted[k1] then if k1 == "P1326" then local ts = v1[1].datavalue.value.time local dp = v1[1].datavalue.value.precision qlist[k1] = dateFormat(ts, dp, args.qdf, args.bc, pd, "", lang, "before") elseif k1 == "P1319" then local ts = v1[1].datavalue.value.time local dp = v1[1].datavalue.value.precision qlist[k1] = dateFormat(ts, dp, args.qdf, args.bc, pd, "", lang, "after") elseif k1 == "P580" then timestart = propertyvalueandquals(v1, qargs)[1] or "" -- treat only one start time as valid elseif k1 == "P582" then timeend = propertyvalueandquals(v1, qargs)[1] or "" -- treat only one end time as valid else local q = assembleoutput(propertyvalueandquals(v1, qargs), qargs) -- we already deal with circa via 'sourcing circumstances' if the datatype was time -- circa may be either linked or unlinked *** internationalise later *** if datatype ~= "time" or q ~= "circa" and not (type(q) == "string" and q:find("circa]]")) then qlist[k1] = q end end end -- of test for wanted end -- of loop through qualifiers -- set date separator local t = timestart .. timeend -- *** internationalise date separators later *** local dsep = "–" if t:find("%s") or t:find(" ") then dsep = " – " end -- set the order for the list of qualifiers returned; start time and end time go last if next(qlist) then local qlistout = {} if allflag then for k2, v2 in pairs(qlist) do qlistout[#qlistout+1] = v2 end else for i2, v2 in ipairs(qorder) do qlistout[#qlistout+1] = qlist[v2] end end if t ~= "" then qlistout[#qlistout+1] = timestart .. dsep .. timeend end local qstr = assembleoutput(qlistout, qargs) if qualsonly then out[#out+1] = qstr else out[#out] = out[#out] .. " (" .. qstr .. ")" end elseif t ~= "" then if qualsonly then out[#out+1] = timestart .. dsep .. timeend else out[#out] = out[#out] .. " (" .. timestart .. dsep .. timeend .. ")" end end end -- of test for qualifiers wanted if maxvals > 0 and #out >= maxvals then break end end -- of for each value loop -- we need to pick one value to return if the datatype was "monolingualtext" -- if there's only one value, use that -- otherwise look through the fallback languages for a match if datatype == "monolingualtext" and #out >1 then lang = mw.text.split( lang, '-', true )[1] local fbtbl = mw.language.getFallbacksFor( lang ) table.insert( fbtbl, 1, lang ) local bestval = "" local found = false for idx1, lang1 in ipairs(fbtbl) do for idx2, lang2 in ipairs(mlt) do if (lang1 == lang2) and not found then bestval = out[idx2] found = true break end end -- loop through values of property end -- loop through fallback languages if found then -- replace output table with a table containing the best value out = { bestval } else -- more than one value and none of them on the list of fallback languages -- sod it, just give them the first one out = { out[1] } end end return out end ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- -- Public functions ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- -- _getValue makes the functionality of getValue available to other modules ------------------------------------------------------------------------------- -- Dependencies: setRanks; parseInput; propertyvalueandquals; assembleoutput; parseParam; sourced; -- labelOrId; i18n.latestdatequalifier; roundto; decimalPrecision; decimalToDMS; ------------------------------------------------------------------------------- p._getValue = function(args) -- parameter sets for commonly used groups of parameters local paraset = tonumber(args.ps or args.parameterset or 0) if paraset == 1 then -- a common setting args.rank = "best" args.fetchwikidata = "ALL" args.onlysourced = "no" args.noicon = "true" elseif paraset == 2 then -- equivalent to raw args.rank = "best" args.fetchwikidata = "ALL" args.onlysourced = "no" args.noicon = "true" args.linked = "no" args.pd = "true" elseif paraset == 3 then -- third set goes here end -- implement eid parameter local eid = args.eid if eid == "" then return nil elseif eid then args.qid = eid end local propertyID = mw.text.trim(args[1] or "") args.reqranks = setRanks(args.rank) -- replacetext (rt) is a string that is returned instead of any non-empty Wikidata value -- this is useful for tracking and debugging, so we set fetchwikidata=ALL to fill the whitelist local replacetext = mw.text.trim(args.rt or args.replacetext or "") if replacetext ~= "" then args.fetchwikidata = "ALL" end local f = {} f.args = args local entityid, props = parseInput(f, f.args[2], propertyID) if not entityid then return props -- either the input parameter or nothing end -- qual is a string containing the property ID of the qualifier(s) to be returned -- if qual == "ALL" then all qualifiers returned -- if qual == "DATES" then qualifiers P580 (start time) and P582 (end time) returned -- if nothing or an empty string is passed set it nil -> no qualifiers returned local qualID = mw.text.trim(args.qual or ""):upper() if qualID == "" then qualID = nil end -- set a language object and code in the args table args.langobj = findLang(args.lang) args.lang = args.langobj.code -- table 'out' stores the return value(s): local out = propertyvalueandquals(props, args, qualID) -- format the table of values and return it as a string: return assembleoutput(out, args, entityid, propertyID) end ------------------------------------------------------------------------------- -- getValue is used to get the value(s) of a property -- The property ID is passed as the first unnamed parameter and is required. -- A locally supplied parameter may optionaly be supplied as the second unnamed parameter. -- The function will now also return qualifiers if parameter qual is supplied ------------------------------------------------------------------------------- -- Dependencies: _getValue; setRanks; parseInput; propertyvalueandquals; assembleoutput; parseParam; sourced; -- labelOrId; i18n.latestdatequalifier; roundto; decimalPrecision; decimalToDMS; ------------------------------------------------------------------------------- p.getValue = function(frame) local args= frame.args if not args[1] then args = frame:getParent().args if not args[1] then return i18n.errors["No property supplied"] end end return p._getValue(args) end ------------------------------------------------------------------------------- -- getPropOfProp takes two propertyIDs: prop1 and prop2 (as well as the usual parameters) -- If the value(s) of prop1 are of type "wikibase-item" then it returns the value(s) of prop2 -- of each of those wikibase-items. -- The usual whitelisting, blacklisting, onlysourced, etc. are implemented ------------------------------------------------------------------------------- -- Dependencies: parseParam; setRanks; parseInput; sourced; propertyvalueandquals assembleoutput; ------------------------------------------------------------------------------- p._getPropOfProp = function(args) -- parameter sets for commonly used groups of parameters local paraset = tonumber(args.ps or args.parameterset or 0) if paraset == 1 then -- a common setting args.rank = "best" args.fetchwikidata = "ALL" args.onlysourced = "no" args.noicon = "true" elseif paraset == 2 then -- equivalent to raw args.rank = "best" args.fetchwikidata = "ALL" args.onlysourced = "no" args.noicon = "true" args.linked = "no" args.pd = "true" elseif paraset == 3 then -- third set goes here end args.reqranks = setRanks(args.rank) args.langobj = findLang(args.lang) args.lang = args.langobj.code local pid1 = args.prop1 or args.pid1 or "" local pid2 = args.prop2 or args.pid2 or "" local localval = mw.text.trim(args[1] or "") if pid1 == "" or pid2 == "" then return nil end local f = {} f.args = args local qid1, statements1 = parseInput(f, localval, pid1) if not qid1 then return localval end local onlysrc = parseParam(args.onlysourced or args.osd, true) local maxvals = tonumber(args.maxvals) or 0 local qualID = mw.text.trim(args.qual or ""):upper() if qualID == "" then qualID = nil end local out = {} for k, v in ipairs(statements1) do if not onlysrc or sourced(v) then local snak = v.mainsnak if snak.datatype == "wikibase-item" and snak.snaktype == "value" then local qid2 = snak.datavalue.value.id local statements2 = {} if args.reqranks.b then statements2 = mw.wikibase.getBestStatements(qid2, pid2) else statements2 = mw.wikibase.getAllStatements(qid2, pid2) end if statements2[1] then local out2 = propertyvalueandquals(statements2, args, qualID) out[#out+1] = assembleoutput(out2, args, qid2, pid2) end end -- of test for valid property1 value end -- of test for sourced if maxvals > 0 and #out >= maxvals then break end end -- of loop through values of property1 return assembleoutput(out, args, qid1, pid1) end p.getPropOfProp = function(frame) local args= frame.args if not args.prop1 and not args.pid1 then args = frame:getParent().args if not args.prop1 and not args.pid1 then return i18n.errors["No property supplied"] end end return p._getPropOfProp(args) end return p ------------------------------------------------------------------------------- -- List of exported functions ------------------------------------------------------------------------------- --[[ _getValue getValue _getPropOfProp getPropOfProp --]] -------------------------------------------------------------------------------