Difference between revisions of "Module:Convert/wikidata"
Jump to navigation
Jump to search
blackwiki>Johnuniq (will put sandbox code here after testing) |
blackwiki>Johnuniq (access Wikidata for convert per Template talk:Convert#Module version 14) |
||
| Line 1: | Line 1: | ||
| − | -- | + | -- Functions to access Wikidata for Module:Convert. |
| − | -- | + | |
| + | local function collection() | ||
| + | -- Return a table to hold items. | ||
| + | return { | ||
| + | n = 0, | ||
| + | add = function (self, item) | ||
| + | self.n = self.n + 1 | ||
| + | self[self.n] = item | ||
| + | end, | ||
| + | join = function (self, sep) | ||
| + | return table.concat(self, sep) | ||
| + | end, | ||
| + | } | ||
| + | end | ||
| + | |||
| + | local function strip_to_nil(text) | ||
| + | -- If text is a non-empty string, return its trimmed content, | ||
| + | -- otherwise return nothing (empty string or not a string). | ||
| + | if type(text) == 'string' then | ||
| + | return text:match('(%S.-)%s*$') | ||
| + | end | ||
| + | end | ||
| + | |||
| + | local function make_unit(units, parms, uid) | ||
| + | -- Return a unit code for convert or nil if unit unknown. | ||
| + | -- If necessary, add a dummy unit to parms so convert will use it | ||
| + | -- for the input without attempting a conversion since nothing | ||
| + | -- useful is available (for example, with unit volt). | ||
| + | local unit = units[uid] | ||
| + | if type(unit) ~= 'table' then | ||
| + | return nil | ||
| + | end | ||
| + | local ucode = unit.ucode | ||
| + | if ucode and not unit.si then | ||
| + | return ucode -- a unit known to convert | ||
| + | end | ||
| + | parms.opt_ignore_error = true | ||
| + | ucode = ucode or unit._ucode -- must be a non-empty string | ||
| + | local ukey, utable | ||
| + | if unit.si then | ||
| + | local base = units[unit.si] | ||
| + | ukey = base.symbol -- must be a non-empty string | ||
| + | local n1 = base.name1 | ||
| + | local n2 = base.name2 | ||
| + | if not n1 then | ||
| + | n1 = ukey | ||
| + | n2 = n2 or n1 -- do not append 's' | ||
| + | end | ||
| + | utable = { | ||
| + | _symbol = ukey, | ||
| + | _name1 = n1, | ||
| + | _name2 = n2, | ||
| + | link = unit.link or base.link, | ||
| + | utype = n1, | ||
| + | prefixes = 1, | ||
| + | } | ||
| + | else | ||
| + | ukey = ucode | ||
| + | utable = { | ||
| + | symbol = ucode, -- must be a non-empty string | ||
| + | name1 = unit.name1, -- if nil, uses symbol | ||
| + | name2 = unit.name2, -- if nil, uses name1..'s' | ||
| + | link = unit.link, -- if nil, uses name1 | ||
| + | utype = unit.name1 or ucode, | ||
| + | } | ||
| + | end | ||
| + | utable.scale = 1 | ||
| + | utable.default = '' | ||
| + | utable.defkey = '' | ||
| + | utable.linkey = '' | ||
| + | utable.bad_mcode = '' | ||
| + | parms.unittable = { [ukey] = utable } | ||
| + | return ucode | ||
| + | end | ||
| + | |||
| + | local function get_statements(qid, pid) | ||
| + | -- Get item for qid and return a list of statements for property pid. | ||
| + | -- Statements are in Wikidata's order except that those with preferred | ||
| + | -- rank are first, then normal rank. Any other rank is ignored. | ||
| + | -- qid is nil for the current page's item, or is an item id (expensive). | ||
| + | local result, n = {}, 0 | ||
| + | local entity = mw.wikibase.getEntity(qid) | ||
| + | if type(entity) == 'table' then | ||
| + | local statements = (entity.claims or {})[pid] | ||
| + | if type(statements) == 'table' then | ||
| + | for _, rank in ipairs({ 'preferred', 'normal' }) do | ||
| + | for _, statement in ipairs(statements) do | ||
| + | if type(statement) == 'table' and rank == statement.rank then | ||
| + | n = n + 1 | ||
| + | result[n] = statement | ||
| + | end | ||
| + | end | ||
| + | end | ||
| + | end | ||
| + | end | ||
| + | return result | ||
| + | end | ||
| + | |||
| + | local function input_from_property(tdata, parms, qid, pid) | ||
| + | -- Given that pid is a Wikidata property identifier like 'P123', | ||
| + | -- return amount, ucode (two strings) for the item/property, | ||
| + | -- or return nothing. | ||
| + | for _, statement in ipairs(get_statements(qid, pid)) do | ||
| + | if statement.mainsnak and statement.mainsnak.datatype == 'quantity' then | ||
| + | local value = (statement.mainsnak.datavalue or {}).value | ||
| + | if value then | ||
| + | local amount = value.amount | ||
| + | if amount then | ||
| + | amount = tostring(amount) -- in case amount is ever a number | ||
| + | if amount:sub(1, 1) == '+' then | ||
| + | amount = amount:sub(2) | ||
| + | end | ||
| + | local unit = value.unit | ||
| + | if type(unit) == 'string' then | ||
| + | unit = unit:match('Q%d+$') -- unit qid is at end of URL | ||
| + | local ucode = make_unit(tdata.wikidata_units, parms, unit) | ||
| + | if ucode then | ||
| + | return amount, ucode | ||
| + | end | ||
| + | end | ||
| + | end | ||
| + | end | ||
| + | end | ||
| + | end | ||
| + | end | ||
| + | |||
| + | local function input_from_text(tdata, parms, text) | ||
| + | -- Given string should be of form "<value><space><unit>". | ||
| + | -- Return value, ucode (two strings), or return nothing. | ||
| + | text = text:gsub(' ', ' '):gsub(' +', ' ') | ||
| + | local pos = text:find(' ', 1, true) | ||
| + | if pos then | ||
| + | -- Leave checking of value to convert which can handle fractions. | ||
| + | local value = text:sub(1, pos - 1) | ||
| + | local uid = text:sub(pos + 1) | ||
| + | local ucode = make_unit(tdata.wikidata_units, parms, uid) | ||
| + | return value, ucode or uid | ||
| + | end | ||
| + | end | ||
| + | |||
| + | local function adjustparameters(tdata, parms, index) | ||
| + | -- For Module:Convert, adjust parms (a table of {{convert}} parameters). | ||
| + | -- Return true if successful or return false, t where t is an error message table. | ||
| + | -- 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 value and unit 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+$') | ||
| + | if pid then | ||
| + | parms.input_text = '' -- output an empty string if an error occurs | ||
| + | amount, ucode = input_from_property(tdata, parms, qid, pid) | ||
| + | else | ||
| + | amount, ucode = input_from_text(tdata, parms, text) | ||
| + | end | ||
| + | if amount and ucode then | ||
| + | table.insert(parms, index, ucode) | ||
| + | table.insert(parms, index, amount) | ||
| + | return true | ||
| + | end | ||
| + | return false, pid and { 'cvt_no_output' } or { 'cvt_bad_input', text } | ||
| + | end | ||
| + | |||
| + | -------------------------------------------------------------------------------- | ||
| + | --- List units and check syntax of definitions --------------------------------- | ||
| + | -------------------------------------------------------------------------------- | ||
| + | local specifications = { | ||
| + | -- seq = sequence in which fields are displayed | ||
| + | base = { | ||
| + | title = 'SI base units', | ||
| + | fields = { | ||
| + | symbol = { seq = 2, mandatory = true }, | ||
| + | name1 = { seq = 3, mandatory = true }, | ||
| + | name2 = { seq = 4 }, | ||
| + | link = { seq = 5 }, | ||
| + | }, | ||
| + | noteseq = 6, | ||
| + | header = '{| class="wikitable"\n!si !!symbol !!name1 !!name2 !!link !!note', | ||
| + | item = '|-\n|%s ||%s ||%s ||%s ||%s ||%s', | ||
| + | footer = '|}', | ||
| + | }, | ||
| + | alias = { | ||
| + | title = 'Aliases for convert', | ||
| + | fields = { | ||
| + | ucode = { seq = 2, mandatory = true }, | ||
| + | si = { seq = 3 }, | ||
| + | }, | ||
| + | noteseq = 4, | ||
| + | header = '{| class="wikitable"\n!alias !!ucode !!base !!note', | ||
| + | item = '|-\n|%s ||%s ||%s ||%s', | ||
| + | footer = '|}', | ||
| + | }, | ||
| + | known = { | ||
| + | title = 'Units known to convert', | ||
| + | fields = { | ||
| + | ucode = { seq = 2, mandatory = true }, | ||
| + | label = { seq = 3, mandatory = true }, | ||
| + | }, | ||
| + | noteseq = 4, | ||
| + | header = '{| class="wikitable"\n!qid !!ucode !!label !!note', | ||
| + | item = '|-\n|%s ||%s ||%s ||%s', | ||
| + | footer = '|}', | ||
| + | }, | ||
| + | unknown = { | ||
| + | title = 'Units not known to convert', | ||
| + | fields = { | ||
| + | _ucode = { seq = 2, mandatory = true }, | ||
| + | si = { seq = 3 }, | ||
| + | name1 = { seq = 4 }, | ||
| + | name2 = { seq = 5 }, | ||
| + | link = { seq = 6 }, | ||
| + | label = { seq = 7, mandatory = true }, | ||
| + | }, | ||
| + | noteseq = 8, | ||
| + | header = '{| class="wikitable"\n!qid !!_ucode !!base !!name1 !!name2 !!link !!label !!note', | ||
| + | item = '|-\n|%s ||%s ||%s ||%s ||%s ||%s ||%s ||%s', | ||
| + | footer = '|}', | ||
| + | }, | ||
| + | } | ||
| + | |||
| + | local function listunits(tdata, ulookup) | ||
| + | -- For Module:Convert, make wikitext to list the built-in Wikidata units. | ||
| + | -- Return true, wikitext if successful or return false, t where t is an | ||
| + | -- error message table. Currently, an error return never occurs. | ||
| + | -- The syntax of each unit definition is checked and a note is added if | ||
| + | -- a problem is detected. | ||
| + | local function safe_cells(t) | ||
| + | -- This is not currently needed, but in case definitions ever use wikitext | ||
| + | -- like '[[kilogram|kg]]', escape the text so it works in a table cell. | ||
| + | local result = {} | ||
| + | for i, v in ipairs(t) do | ||
| + | if v:find('|', 1, true) then | ||
| + | v = v:gsub('(%[%[[^%[%]]-)|(.-%]%])', '%1\0%2') -- replace pipe in piped link with a zero byte | ||
| + | v = v:gsub('|', '|') -- escape '|' | ||
| + | v = v:gsub('%z', '|') -- restore pipe in piped link | ||
| + | end | ||
| + | result[i] = v:gsub('{', '{') -- escape '{' | ||
| + | end | ||
| + | return unpack(result) | ||
| + | end | ||
| + | local wdunits = tdata.wikidata_units | ||
| + | local speckeys = { 'base', 'alias', 'unknown', 'known' } | ||
| + | for _, sid in ipairs(speckeys) do | ||
| + | specifications[sid].units = collection() | ||
| + | end | ||
| + | local keys, n = {}, 0 | ||
| + | for k, v in pairs(wdunits) do | ||
| + | n = n + 1 | ||
| + | keys[n] = k | ||
| + | end | ||
| + | table.sort(keys) | ||
| + | local note_count = 0 | ||
| + | for _, key in ipairs(keys) do | ||
| + | local unit = wdunits[key] | ||
| + | local ktext, sid | ||
| + | if key:match('^Q%d+$') then | ||
| + | ktext = '[[d:' .. key .. '|' .. key .. ']]' | ||
| + | if unit.ucode then | ||
| + | sid = 'known' | ||
| + | else | ||
| + | sid = 'unknown' | ||
| + | end | ||
| + | elseif unit.ucode then | ||
| + | ktext = key | ||
| + | sid = 'alias' | ||
| + | else | ||
| + | ktext = key | ||
| + | sid = 'base' | ||
| + | end | ||
| + | local result = { ktext } | ||
| + | local spec = specifications[sid] | ||
| + | local fields = spec.fields | ||
| + | local note = collection() | ||
| + | for k, v in pairs(unit) do | ||
| + | if fields[k] then | ||
| + | local seq = fields[k].seq | ||
| + | if result[seq] then | ||
| + | note:add('duplicate ' .. k) -- cannot happen since keys are unique | ||
| + | else | ||
| + | result[seq] = v | ||
| + | end | ||
| + | else | ||
| + | note:add('invalid ' .. k) | ||
| + | end | ||
| + | end | ||
| + | for k, v in pairs(fields) do | ||
| + | local value = result[v.seq] | ||
| + | if value then | ||
| + | if k == 'si' and not wdunits[value] then | ||
| + | note:add('need si ' .. value) | ||
| + | end | ||
| + | if k == 'label' then | ||
| + | local wdl = mw.wikibase.label(key) | ||
| + | if wdl ~= value then | ||
| + | note:add('label changed to ' .. tostring(wdl)) | ||
| + | end | ||
| + | end | ||
| + | else | ||
| + | result[v.seq] = '' | ||
| + | if v.mandatory then | ||
| + | note:add('missing ' .. k) | ||
| + | end | ||
| + | end | ||
| + | end | ||
| + | local text | ||
| + | if note.n > 0 then | ||
| + | note_count = note_count + 1 | ||
| + | text = '*' .. note:join('<br />') | ||
| + | end | ||
| + | result[spec.noteseq] = text or '' | ||
| + | spec.units:add(result) | ||
| + | end | ||
| + | local results = collection() | ||
| + | if note_count > 0 then | ||
| + | local text = note_count .. (note_count == 1 and ' note' or ' notes') | ||
| + | results:add("'''Search for * to see " .. text .. "'''\n") | ||
| + | end | ||
| + | for _, sid in ipairs(speckeys) do | ||
| + | local spec = specifications[sid] | ||
| + | results:add("'''" .. spec.title .. "'''") | ||
| + | results:add(spec.header) | ||
| + | local fmt = spec.item | ||
| + | for _, unit in ipairs(spec.units) do | ||
| + | results:add(string.format(fmt, safe_cells(unit))) | ||
| + | end | ||
| + | results:add(spec.footer) | ||
| + | end | ||
| + | return true, results:join('\n') | ||
| + | end | ||
| + | |||
| + | return { _adjustparameters = adjustparameters, _listunits = listunits } | ||
Revision as of 01:09, 1 June 2016
Documentation for this module may be created at Module:Convert/wikidata/doc
-- Functions to access Wikidata for Module:Convert.
local function collection()
-- Return a table to hold items.
return {
n = 0,
add = function (self, item)
self.n = self.n + 1
self[self.n] = item
end,
join = function (self, sep)
return table.concat(self, sep)
end,
}
end
local function strip_to_nil(text)
-- If text is a non-empty string, return its trimmed content,
-- otherwise return nothing (empty string or not a string).
if type(text) == 'string' then
return text:match('(%S.-)%s*$')
end
end
local function make_unit(units, parms, uid)
-- Return a unit code for convert or nil if unit unknown.
-- If necessary, add a dummy unit to parms so convert will use it
-- for the input without attempting a conversion since nothing
-- useful is available (for example, with unit volt).
local unit = units[uid]
if type(unit) ~= 'table' then
return nil
end
local ucode = unit.ucode
if ucode and not unit.si then
return ucode -- a unit known to convert
end
parms.opt_ignore_error = true
ucode = ucode or unit._ucode -- must be a non-empty string
local ukey, utable
if unit.si then
local base = units[unit.si]
ukey = base.symbol -- must be a non-empty string
local n1 = base.name1
local n2 = base.name2
if not n1 then
n1 = ukey
n2 = n2 or n1 -- do not append 's'
end
utable = {
_symbol = ukey,
_name1 = n1,
_name2 = n2,
link = unit.link or base.link,
utype = n1,
prefixes = 1,
}
else
ukey = ucode
utable = {
symbol = ucode, -- must be a non-empty string
name1 = unit.name1, -- if nil, uses symbol
name2 = unit.name2, -- if nil, uses name1..'s'
link = unit.link, -- if nil, uses name1
utype = unit.name1 or ucode,
}
end
utable.scale = 1
utable.default = ''
utable.defkey = ''
utable.linkey = ''
utable.bad_mcode = ''
parms.unittable = { [ukey] = utable }
return ucode
end
local function get_statements(qid, pid)
-- Get item for qid and return a list of statements for property pid.
-- Statements are in Wikidata's order except that those with preferred
-- rank are first, then normal rank. Any other rank is ignored.
-- qid is nil for the current page's item, or is an item id (expensive).
local result, n = {}, 0
local entity = mw.wikibase.getEntity(qid)
if type(entity) == 'table' then
local statements = (entity.claims or {})[pid]
if type(statements) == 'table' then
for _, rank in ipairs({ 'preferred', 'normal' }) do
for _, statement in ipairs(statements) do
if type(statement) == 'table' and rank == statement.rank then
n = n + 1
result[n] = statement
end
end
end
end
end
return result
end
local function input_from_property(tdata, parms, qid, pid)
-- Given that pid is a Wikidata property identifier like 'P123',
-- return amount, ucode (two strings) for the item/property,
-- or return nothing.
for _, statement in ipairs(get_statements(qid, pid)) do
if statement.mainsnak and statement.mainsnak.datatype == 'quantity' then
local value = (statement.mainsnak.datavalue or {}).value
if value then
local amount = value.amount
if amount then
amount = tostring(amount) -- in case amount is ever a number
if amount:sub(1, 1) == '+' then
amount = amount:sub(2)
end
local unit = value.unit
if type(unit) == 'string' then
unit = unit:match('Q%d+$') -- unit qid is at end of URL
local ucode = make_unit(tdata.wikidata_units, parms, unit)
if ucode then
return amount, ucode
end
end
end
end
end
end
end
local function input_from_text(tdata, parms, text)
-- Given string should be of form "<value><space><unit>".
-- Return value, ucode (two strings), or return nothing.
text = text:gsub(' ', ' '):gsub(' +', ' ')
local pos = text:find(' ', 1, true)
if pos then
-- Leave checking of value to convert which can handle fractions.
local value = text:sub(1, pos - 1)
local uid = text:sub(pos + 1)
local ucode = make_unit(tdata.wikidata_units, parms, uid)
return value, ucode or uid
end
end
local function adjustparameters(tdata, parms, index)
-- For Module:Convert, adjust parms (a table of {{convert}} parameters).
-- Return true if successful or return false, t where t is an error message table.
-- 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 value and unit 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+$')
if pid then
parms.input_text = '' -- output an empty string if an error occurs
amount, ucode = input_from_property(tdata, parms, qid, pid)
else
amount, ucode = input_from_text(tdata, parms, text)
end
if amount and ucode then
table.insert(parms, index, ucode)
table.insert(parms, index, amount)
return true
end
return false, pid and { 'cvt_no_output' } or { 'cvt_bad_input', text }
end
--------------------------------------------------------------------------------
--- List units and check syntax of definitions ---------------------------------
--------------------------------------------------------------------------------
local specifications = {
-- seq = sequence in which fields are displayed
base = {
title = 'SI base units',
fields = {
symbol = { seq = 2, mandatory = true },
name1 = { seq = 3, mandatory = true },
name2 = { seq = 4 },
link = { seq = 5 },
},
noteseq = 6,
header = '{| class="wikitable"\n!si !!symbol !!name1 !!name2 !!link !!note',
item = '|-\n|%s ||%s ||%s ||%s ||%s ||%s',
footer = '|}',
},
alias = {
title = 'Aliases for convert',
fields = {
ucode = { seq = 2, mandatory = true },
si = { seq = 3 },
},
noteseq = 4,
header = '{| class="wikitable"\n!alias !!ucode !!base !!note',
item = '|-\n|%s ||%s ||%s ||%s',
footer = '|}',
},
known = {
title = 'Units known to convert',
fields = {
ucode = { seq = 2, mandatory = true },
label = { seq = 3, mandatory = true },
},
noteseq = 4,
header = '{| class="wikitable"\n!qid !!ucode !!label !!note',
item = '|-\n|%s ||%s ||%s ||%s',
footer = '|}',
},
unknown = {
title = 'Units not known to convert',
fields = {
_ucode = { seq = 2, mandatory = true },
si = { seq = 3 },
name1 = { seq = 4 },
name2 = { seq = 5 },
link = { seq = 6 },
label = { seq = 7, mandatory = true },
},
noteseq = 8,
header = '{| class="wikitable"\n!qid !!_ucode !!base !!name1 !!name2 !!link !!label !!note',
item = '|-\n|%s ||%s ||%s ||%s ||%s ||%s ||%s ||%s',
footer = '|}',
},
}
local function listunits(tdata, ulookup)
-- For Module:Convert, make wikitext to list the built-in Wikidata units.
-- Return true, wikitext if successful or return false, t where t is an
-- error message table. Currently, an error return never occurs.
-- The syntax of each unit definition is checked and a note is added if
-- a problem is detected.
local function safe_cells(t)
-- This is not currently needed, but in case definitions ever use wikitext
-- like '[[kilogram|kg]]', escape the text so it works in a table cell.
local result = {}
for i, v in ipairs(t) do
if v:find('|', 1, true) then
v = v:gsub('(%[%[[^%[%]]-)|(.-%]%])', '%1\0%2') -- replace pipe in piped link with a zero byte
v = v:gsub('|', '|') -- escape '|'
v = v:gsub('%z', '|') -- restore pipe in piped link
end
result[i] = v:gsub('{', '{') -- escape '{'
end
return unpack(result)
end
local wdunits = tdata.wikidata_units
local speckeys = { 'base', 'alias', 'unknown', 'known' }
for _, sid in ipairs(speckeys) do
specifications[sid].units = collection()
end
local keys, n = {}, 0
for k, v in pairs(wdunits) do
n = n + 1
keys[n] = k
end
table.sort(keys)
local note_count = 0
for _, key in ipairs(keys) do
local unit = wdunits[key]
local ktext, sid
if key:match('^Q%d+$') then
ktext = '[[d:' .. key .. '|' .. key .. ']]'
if unit.ucode then
sid = 'known'
else
sid = 'unknown'
end
elseif unit.ucode then
ktext = key
sid = 'alias'
else
ktext = key
sid = 'base'
end
local result = { ktext }
local spec = specifications[sid]
local fields = spec.fields
local note = collection()
for k, v in pairs(unit) do
if fields[k] then
local seq = fields[k].seq
if result[seq] then
note:add('duplicate ' .. k) -- cannot happen since keys are unique
else
result[seq] = v
end
else
note:add('invalid ' .. k)
end
end
for k, v in pairs(fields) do
local value = result[v.seq]
if value then
if k == 'si' and not wdunits[value] then
note:add('need si ' .. value)
end
if k == 'label' then
local wdl = mw.wikibase.label(key)
if wdl ~= value then
note:add('label changed to ' .. tostring(wdl))
end
end
else
result[v.seq] = ''
if v.mandatory then
note:add('missing ' .. k)
end
end
end
local text
if note.n > 0 then
note_count = note_count + 1
text = '*' .. note:join('<br />')
end
result[spec.noteseq] = text or ''
spec.units:add(result)
end
local results = collection()
if note_count > 0 then
local text = note_count .. (note_count == 1 and ' note' or ' notes')
results:add("'''Search for * to see " .. text .. "'''\n")
end
for _, sid in ipairs(speckeys) do
local spec = specifications[sid]
results:add("'''" .. spec.title .. "'''")
results:add(spec.header)
local fmt = spec.item
for _, unit in ipairs(spec.units) do
results:add(string.format(fmt, safe_cells(unit)))
end
results:add(spec.footer)
end
return true, results:join('\n')
end
return { _adjustparameters = adjustparameters, _listunits = listunits }