-- Functions to access Wikidata for Module:Convert.
local
Collection
=
{}
Collection
.
__index
=
Collection
do
function
Collection
:
add
(
item
)
if
item
~=
nil
then
self
.
n
=
self
.
n
+
1
self
[
self
.
n
]
=
item
end
end
function
Collection
:
join
(
sep
)
return
table.concat
(
self
,
sep
)
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
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
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
)
-- 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
matches_qualifier
(
statement
,
qual
)
-- Return:
-- false, nil : if statement does not match specification
-- true, nil : if matches, and statement has no qualifier
-- true, sq : if matches, where sq is the statement's qualifier
-- A match means that no qualifier was specified (qual == nil), or that
-- 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
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
local
is_match
,
statqual
=
matches_qualifier
(
statement
,
qual
)
if
is_match
then
result
:
add
({
statqual
=
statqual
,
stmt
=
statement
})
end
end
end
end
end
end
return
result
end
local
function
input_from_property
(
tdata
,
parms
,
pid
)
-- Given that pid is a Wikidata property identifier like 'P123',
-- return a collection of {amount, ucode} pairs (two strings)
-- for each matching item/property, or return nothing.
--------------------------------------------------------------------------------
-- 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 [[Nancay 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
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 item id is at end of URL
local
ucode
=
make_unit
(
tdata
.
wikidata_units
,
parms
,
unit
)
if
ucode
then
local
skip
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
end
end
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 true if values/units were extracted and inserted, or return nothing.
text
=
text
:
gsub
(
' '
,
' '
):
gsub
(
'%s+'
,
' '
)
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
)
if
uid
:
sub
(
1
,
3
)
==
'ft '
and
uid
:
sub
(
-
3
)
==
' in'
then
-- Special case for enwiki to allow {{convert|input=5 ft 10+1/2 in}}
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
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>
-- If successful, insert values and units in parms, before given index.
local
text
=
parms
.
input
-- should be a trimmed, non-empty string
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
local
result
=
input_from_property
(
tdata
,
parms
,
pid
)
if
result
.
n
==
0
then
return
quit
()
end
local
ucode
for
i
,
t
in
ipairs
(
result
)
do
-- Convert requires each input unit to be identical.
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
()
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
.
new
()
end
local
keys
=
Collection
.
new
()
for
k
,
v
in
pairs
(
wdunits
)
do
keys
:
add
(
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
.
new
()
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
.
new
()
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
}