Module:Convert/wikidata/sandbox: Difference between revisions

Content deleted Content added
adjust error returns to hide problems for input=P666
use mw.wikibase.getLabel(X) not deprecated label
 
(10 intermediate revisions by the same user not shown)
Line 1:
-- Functions to access Wikidata for Module:Convert.
 
local functionCollection collection()= {}
Collection.__index = Collection
-- Return a table to hold items.
do
return {
function Collection:add(item)
n = 0,
if item ~= nil then
add = function (self, item)
self.n = self.n + 1
self[self.n] = item
end,
end
join = function (self, sep)
function Collection:join(sep)
return table.concat(self, sep)
return table.concat(self, sep)
end,
}end
function Collection:remove(pos)
if self.n > 0 and (pos == nil or (0 < pos and pos <= self.n)) then
self.n = self.n - 1
return table.remove(self, pos)
end
end
function Collection:sort(comp)
table.sort(self, comp)
end
function Collection.new()
return setmetatable({n = 0}, Collection)
end
end
 
Line 22 ⟶ 34:
end
end
 
local function frequency_unit(value, unit_table)
-- For use when converting m to Hz.
-- Return true, s where s = name of unit's default output unit,
-- or return false, t where t is an error message table.
-- However, for simplicity a valid result is always returned.
local unit
if unit_table._symbol == 'm' then
-- c = speed of light in a vacuum = 299792458 m/s
-- frequency = c / wavelength
local w = value * (unit_table.scale or 1)
local f = 299792458 / w -- if w == 0, f = math.huge which works here
if f >= 1e12 then
unit = 'THz'
elseif f >= 1e9 then
unit = 'GHz'
elseif f >= 1e6 then
unit = 'MHz'
elseif f >= 1e3 then
unit = 'kHz'
else
unit = 'Hz'
end
end
return true, unit or 'Hz'
end
 
local function wavelength_unit(value, unit_table)
-- Like frequency_unit but for use when converting Hz to m.
local unit
if unit_table._symbol == 'Hz' then
-- Using 0.9993 rather than 1 avoids rounding which would give results
-- like converting 300 MHz to 100 cm instead of 1 m.
local w = 1 / (value * (unit_table.scale or 1)) -- Hz scale is inverted
if w >= 0.9993e6 then
unit = 'Mm'
elseif w >= 0.9993e3 then
unit = 'km'
elseif w >= 0.9993 then
unit = 'm'
elseif w >= 0.9993e-2 then
unit = 'cm'
elseif w >= 0.9993e-3 then
unit = 'mm'
else
unit = 'um'
end
end
return true, unit or 'm'
end
 
local specials = {
frequency = { frequency_unit },
wavelength = { wavelength_unit },
--------------------------------------------------------------------------------
-- Following is a removed experiment to show two values as a range
-- using '-' as the separator.
-- frequencyrange = { frequency_unit, '-' },
-- wavelengthrange = { wavelength_unit, '-' },
}
 
local function make_unit(units, parms, uid)
Line 75 ⟶ 147:
end
 
local function get_statementsmatches_qualifier(qidstatement, pidqual)
-- Return:
-- Get item for qid and return a list of statements for property pid.
-- false, nil : if statement does not match specification
-- Statements are in Wikidata's order except that those with preferred
-- true, nil : if matches, and statement has no qualifier
-- rank are first, then normal rank. Any other rank is ignored.
-- qid is niltrue, forsq the current page's: itemif matches, orwhere sq is anthe item idstatement's (expensive).qualifier
-- A match means that no qualifier was specified (qual == nil), or that
local result, n = {}, 0
-- the statement has a qualifier matching the specification.
-- If a match occurs, the caller needs the statement's qualifier (if any)
-- so statements that duplicate the qualifier are not used, after the first.
-- Then, if convert is showing all values for a property such as the diameter
-- of a telescope's mirror (diameters of primary and secondary mirrors), it
-- will not show alternative values that could in principle be present for the
-- same item (telescope) and property (diameter) and qualifier (primary/secondary).
local target = (statement.qualifiers or {}).P518 -- P518 is "applies to part"
if type(target) == 'table' then
for _, q in ipairs(target) do
if type(q) == 'table' then
local value = (q.datavalue or {}).value
if value then
if qual == nil or qual == value.id then
return true, value.id
end
end
end
end
end
if qual == nil then
return true, nil -- only occurs if statement has no qualifier
end
return false, nil -- statement's qualifier is not relevant because statement will be skipped
end
 
local function get_statements(parms, pid)
-- Get specified item and return a list of tables with each statement for property pid.
-- Each table is of form {statqual=sq, stmt=statement} where sq = statement qualifier (nil if none).
-- Statements are in Wikidata's order except that those with preferred rank
-- are first, then normal rank. Any other rank is ignored.
local stored = {} -- qualifiers of statements that are first for the qualifier, and will be returned
local qid = strip_to_nil(parms.qid) -- nil for current page's item, or an item id (expensive)
local qual = strip_to_nil(parms.qual) -- nil or id of wanted P518 (applies to part) item in qualifiers
local result = Collection.new()
local entity = mw.wikibase.getEntity(qid)
if type(entity) == 'table' then
Line 88 ⟶ 195:
for _, statement in ipairs(statements) do
if type(statement) == 'table' and rank == statement.rank then
local is_match, statqual = matches_qualifier(statement, qual)
n = n + 1
result[n]if =is_match statementthen
result:add({ statqual = statqual, stmt = statement })
end
end
end
Line 98 ⟶ 207:
end
 
local function input_from_property(tdata, parms, qid, pid)
-- Given that pid is a Wikidata property identifier like 'P123',
-- return valuea collection of {amount, ucode} pairs (two strings) for the item/property,
-- for each matching item/property, or return nothing.
--------------------------------------------------------------------------------
for _, statement in ipairs(get_statements(qid, pid)) do
-- There appear to be few restrictions on how Wikidata is organized so it is
-- very likely that any decision a module makes about how to handle data
-- will be wrong for some cases at some time. This meets current requirements.
-- For each qualifier (or if no qualifier), if there are any preferred
-- statements, use them and ignore any normal statements.
-- For each qualifier, for the preferred statements if any, or for
-- the normal statements (but not both):
-- * Accept each statement if it has no qualifier (this will not occur
-- if qual=x is specified because other code already ensures that in that
-- case, only statements with a qualifier matching x are considered).
-- * Ignore any statements after the first if it has a qualifier.
-- The rationale is that for the diameter at [[South Pole Telescope]], want
-- convert to show the diameters for both the primary and secondary mirrors
-- if the convert does not specify which diameter is wanted.
-- However, if convert is given the wanted qualifier, only one value
-- (_the_ diameter) is wanted. For simplicity/consistency, that is also done
-- even if no qual=x is specified. Unclear what should happen.
-- For the wavelength at [[Nançay Radio Telescope]], want to show all three
-- values, and the values have no qualifiers.
--------------------------------------------------------------------------------
local result = Collection.new()
local done = {}
local skip_normal
for _, t in ipairs(get_statements(parms, pid)) do
local statement = t.stmt
if statement.mainsnak and statement.mainsnak.datatype == 'quantity' then
local value = (statement.mainsnak.datavalue or {}).value
Line 114 ⟶ 248:
local unit = value.unit
if type(unit) == 'string' then
unit = unit:match('Q%d+$') -- qidunit item id is at end of URL
local ucode = make_unit(tdata.wikidata_units, parms, unit)
if ucode then
returnlocal amount, ucodeskip
if t.statqual then
if done[t.statqual] then
skip = true
else
done[t.statqual] = true
end
else
if statement.rank == 'preferred' then
skip_normal = true
elseif skip_normal then
skip = true
end
end
if not skip then
result:add({ amount, ucode })
end
end
end
Line 124 ⟶ 274:
end
end
return result
end
 
local function input_from_text(tdata, parms, text, insert2)
-- Given string should be of form "<value><space><unit>". or
-- "<value1><space>ft<space><value2><space>in" for a special case (feet and inches).
-- Return value, ucode (two strings), or return nothing.
-- Return true if values/units were extracted and inserted, or return nothing.
text = text:gsub('&nbsp;', ' '):gsub(' +', ' ')
text = text:gsub('&nbsp;', ' '):gsub('%s+', ' ')
local pos = text:find(' ', 1, true)
if pos then
Line 135 ⟶ 287:
local value = text:sub(1, pos - 1)
local uid = text:sub(pos + 1)
if uid:sub(1, 3) == 'ft ' and uid:sub(-3) == ' in' then
local ucode = make_unit(tdata.wikidata_units, parms, uid)
-- Special case for enwiki to allow {{convert|input=5 ft 10+1/2 in}}
return value, ucode or uid
insert2(uid:sub(4, -4), 'in')
insert2(value, 'ft')
else
insert2(value, make_unit(tdata.wikidata_units, parms, uid) or uid)
end
return true
end
end
Line 145 ⟶ 303:
-- This is intended mainly for use in infoboxes where the input might be
-- <value><space><unit> or
-- <wikidata-property-id> (uses an optional qid item id)
-- If successful, insert valuevalues and unitunits in parms, before given index.
local amount, ucode
local text = parms.input -- should be a trimmed, non-empty string
local qid = strip_to_nil(parms.qid)
local pid = text:match('^P%d+$')
local sep = ','
local special = specials[parms[index]]
if special then
parms.out_unit = special[1]
sep = special[2] or sep
table.remove(parms, index)
end
local function quit()
return false, pid and { 'cvt_no_output' } or { 'cvt_bad_input', text }
end
local function insert2(first, second)
table.insert(parms, index, second)
table.insert(parms, index, first)
end
if pid then
parms.input_text = '' -- output an empty string if an error occurs
amount,local ucoderesult = input_from_property(tdata, parms, qid, pid)
if result.n == 0 then
else
return quit()
amount, ucode = input_from_text(tdata, parms, text)
end
if amount and local ucode then
for i, t in ipairs(result) do
table.insert(parms, index, ucode)
-- Convert requires each input unit to be identical.
table.insert(parms, index, amount)
if i == 1 then
ucode = t[2]
elseif ucode ~= t[2] then
return quit()
end
end
local item = ucode
if item == parms[index] then
-- Remove specified output unit if it is the same as the Wikidata unit.
-- For example, {{convert|input=P2044|km}} with property "12 km".
table.remove(parms, index)
end
for i = result.n, 1, -1 do
insert2(result[i][1], item)
item = sep
end
return true
else
if input_from_text(tdata, parms, text, insert2) then
return true
end
end
return quit()
return false, pid and { 'cvt_no_output' } or { 'cvt_bad_input', text }
end
 
Line 245 ⟶ 435:
local speckeys = { 'base', 'alias', 'unknown', 'known' }
for _, sid in ipairs(speckeys) do
specifications[sid].units = collectionCollection.new()
end
local keys, n = {}, 0Collection.new()
for k, v in pairs(wdunits) do
keys:add(k)
n = n + 1
keys[n] = k
end
table.sort(keys)
Line 274 ⟶ 463:
local spec = specifications[sid]
local fields = spec.fields
local note = collectionCollection.new()
for k, v in pairs(unit) do
if fields[k] then
Line 294 ⟶ 483:
end
if k == 'label' then
local wdl = mw.wikibase.labelgetLabel(key)
if wdl ~= value then
note:add('label changed to ' .. tostring(wdl))
Line 314 ⟶ 503:
spec.units:add(result)
end
local results = collectionCollection.new()
if note_count > 0 then
local text = note_count .. (note_count >== 1 and ' notesnote' or ' notenotes')
results:add("'''Search for * to see " .. text .. "'''\n")
end