Módulo:Propriedade Wikidata

wd = {}

-- Carrega as propriedades da página no Wikidata, se existir e não tiverem já sido carregadas
if not wdEntity then
    wdEntity = mw.wikibase.getEntity()
end
wd.props = wdEntity and wdEntity['claims']

-- Tabela para dados temporários, mantidos durante o processamento de uma propriedade
wd.temp = {}

-- Tabela para adicionar a data de cada propriedade, quando existir
wd.data = {}

meses = {['01']='janeiro', ['02']='fevereiro', ['03']='março', ['04']='abril', ['05']='maio', ['06']='junho',
	['07']='julho', ['08']='agosto', ['09']='setembro', ['10']='outubro', ['11']='novembro', ['12']='dezembro' }

-- Processa a propriedade ou qulificador pedido
wd.dados = function (prop)
    local p = {}
    local lingua = params and params['língua']
    for i in string.gmatch(prop, '[^:]+') do
        table.insert(p, i)
    end
    if not p[1] or not string.match(p[1], '^P%d+$') then
        wd.temp.debug = 'não é uma propriedade'
        return nil
    end
    if ext and ext.prop and ext.prop[p[1]] then
        wd.temp.debug = 'processada pela extensão'
        return ext.prop[p[1]](prop)
    end
    prop = wd.props and wd.props[p[1]]
    if not prop then
        wd.temp.debug = 'item não tem essa propriedade'
        return nil
    end
    local snak
    local arg = 2
    if p[2] then
        if string.match(p[2], '^P%d+$') then
            local qprop = p[2]  -- qualificador da propriedade (Pq)
            for _, propn in ipairs(prop) do
                if propn['qualifiers'] and propn['qualifiers'][qprop] then
                    if p[3] and string.match(p[3], '^Q%d+$') then
                        -- P:Pq:Qq
                        local qvalue = p[3]  -- valor do qualificador (Qq)
                        for i, v in ipairs(propn['qualifiers'][qprop]) do
                            if v['datatype'] == 'wikibase-item' and v['datavalue']['value']['id'] == qvalue then
                                snak = propn['mainsnak']
                                arg = 4
                                break
                            end
                        end
                    else
                        -- P:Pq
                        snak = propn['qualifiers'][qprop][1]
                        arg = 3
                        break
                    end
                end
            end
        elseif string.match(p[2], '^Q%d+$') then
            local pvalue = p[2]  -- valor da propriedade (Qv)
            if p[3] and string.match(p[3], '^P%d+$') then
                -- P:Qv:Pq
                local qprop = p[3]  -- qualificador (Pq)
                for _, propn in ipairs(prop) do
                    if propn['mainsnak']['datatype'] == 'wikibase-item' and
                      propn['mainsnak']['datavalue']['value']['id'] == pvalue then
                        if propn['qualifiers'] and propn['qualifiers'][qprop] then
                            snak = propn['qualifiers'][qprop][1]
                            arg = 4
                        else
                            wd.temp.debug = 'esse valor não possui o qualificador ' .. qprop
                            return nil
                        end
                    end
                end
            else
                wd.temp.debug = 'faltou o qualificador (último P do formato P:Qv:Pq)'
                return nil
            end
        end
    end
    if arg == 2 and p[2] == 'lista' then
        -- Listar todos valores de uma propriedade
        local lista = {}
        for _, propn in ipairs(prop) do
            local datatype = propn['mainsnak']['datatype']
            wd.temp.tipo = datatype
            if datatype == 'string' or datatype == 'external-id' then
                table.insert(lista, propn['mainsnak']['datavalue']['value'])
            elseif datatype == 'wikibase-item' then
                if not propn['mainsnak']['datavalue'] then
                    wd.temp.debug = 'propriedade não possui valor'
                    return nil
                end
                local qid = propn['mainsnak']['datavalue']['value']['id']
                local labels = mw.wikibase.getEntity(qid)['labels']
                if lingua and labels[lingua] then
                    table.insert(lista, labels[lingua])
                else
                    local pt, en, outra
                    for lang, v in pairs(labels) do
                        if lang == 'pt' then
                            pt = v['value']
                            break
                        elseif lang == 'pt-br' then
                            pt = v['value']
                        elseif lang == 'en' then
                            en = v['value']
                        elseif lang == 'es' or lang == 'fr' or lang == 'it' then -- outras linguas latinas
                            outra = v['value']
                        end
                    end
                    if pt or en or outra then
                        table.insert(lista, pt or en or outra)
                    end
                end
            else
                wd.temp.debug = 'esse tipo de dados em pt, pt-br, en, es, fr, ou it'
                return nil
            end
        end
        if #lista > 1 then
            wd.temp.debug = 'lista com ' .. #lista .. ' valores'
            return table.concat(lista, ', ', 1, #lista - 1) .. ' e ' .. lista[#lista]
        elseif #lista == 1 then
            wd.temp.debug = 'lista só possui um valor'
            return lista[1]
        else
            wd.temp.debug = 'não possui nenhum valor que pode ser retornado'
            return nil
        end
    end
    -- Escolher só um valor para retornar
    if not snak then
        if prop[1]['mainsnak']['datatype'] == 'monolingualtext' then
            wd.temp.tipo = 'monolingualtext' -- A FAZER: escolher língua
            local i = 1
            for n, v in pairs(prop) do
                if v['mainsnak']['datavalue']['value']['language'] == 'pt' then
                    i = n
                    break
                elseif v['mainsnak']['datavalue']['value']['language'] == 'pt-br' then
                    i = n
                end
            end
            wd.temp.debug = 'valor obtido'
            return prop[i]['mainsnak']['datavalue']['value']['text']
        else
        	local mostrecent = ""
            for _, propn in ipairs(prop) do
            	local time
                if arg == 2 and propn['qualifiers'] and propn['qualifiers']['P585'] then
                    time = propn['qualifiers']['P585'][1]['datavalue']['value']['time']
                    if time > mostrecent then
                        mostrecent = time
                        wd.data[p[1]] = time
                        snak = propn['mainsnak']
                    end
                end
                if propn['rank'] == 'preferred' then
                    snak = propn['mainsnak']
                    wd.data[p[1]] = time
                    break
                elseif not snak and propn['rank'] == 'normal' then
                    snak = propn['mainsnak']
                end
            end
        end
    end
    local args = {}
    for n, a in ipairs(p) do
        if n >= arg then
            table.insert(args, p[n])
        end
    end
    if snak then
        return wd.valorsnak(snak, args)
    end
end

-- Retorna o valor de um snak de acordo com o tipo de dado
wd.valorsnak = function(snak, args)
    local datatype = snak['datatype']
    wd.temp.tipo = datatype
    if datatype == 'string' or datatype == 'url' or datatype == 'external-id' or datatype == 'math' or
      datatype == 'commonsMedia' then
        wd.temp.debug = #args == 0 and 'valor obtido' or 'esse tipo de dado não recebe modificações'
        return snak['datavalue']['value']
    elseif datatype == 'monolingualtext' then
        wd.temp.debug = 'obtido valor na língua ' .. snak['datavalue']['value']['language']
        return snak['datavalue']['value']['text']
    elseif datatype == 'wikibase-item' then
        if not snak['datavalue'] then
            wd.temp.debug = 'propriedade não possui valor'
            return nil
        end
        local qid = snak['datavalue']['value']['id']
        local item = mw.wikibase.getEntity(qid)
        local labels = item['labels']
        local pt, en, outra
        for lang, v in pairs(labels) do
            if lang == 'pt' then
                pt = v['value']
                break
            elseif lang == 'pt-br' then
                pt = v['value']
            elseif lang == 'en' then
                en = v['value']
            elseif lang == 'es' or lang == 'fr' or lang == 'it' then -- outras linguas latinas
                outra = v['value']
            end
        end
        local valor = pt or en or outra
        if not valor then
            wd.temp.debug = 'item não tem rótulo em pt, pt-br, en, es, fr ou it'
            return qid
        end
        if args[1] == 'link' and item['sitelinks'] and item['sitelinks']['ptwiki'] then
            wd.temp.debug = 'valor obtido e adicionado link'
            return '[[' .. item['sitelinks']['ptwiki']['title'] .. '|' .. valor .. ']]'
        end
        wd.temp.debug = args[1] == 'link' and 'valor obtido sem link pois não possui artigo' or 'valor obtido'
        return valor
    elseif datatype == 'time' then
        local ac, ano, mes, dia, hora, minuto, segundo = string.match(
          snak['datavalue']['value']['time'], '([+-])(%d+)%-(%d%d)%-0?(%d%d?)T(%d%d):(%d%d):(%d%d)Z')
        if hora == '00' and minuto == '00' and segundo == '00' then
            if mes == '00' then
                wd.temp.debug = 'data só possui o ano'
                return ano .. (ac == '-' and ' AC' or '')
            end
            wd.temp.debug = 'data obtida, não possui hora'
            return dia .. ' de ' .. meses[mes] .. ' de ' .. ano .. (ac == '-' and ' AC' or '')
        else
            wd.temp.debug = 'obtida data e hora'
            return string.format('%d/%d/%d %d:%d:%d', dia, mes, ano, hora, minuto, segundo)
        end
    elseif datatype == 'quantity' then
        local amount = tonumber(snak['datavalue']['value']['amount'])
        local unit = string.match(snak['datavalue']['value']['unit'], '//www.wikidata.org/entity/(Q%d+)')
        return wd.quantidade(amount, unit, args)
    elseif datatype == 'globe-coordinate' then
        local lat = tonumber(snak['datavalue']['value']['latitude'])
        local long = tonumber(snak['datavalue']['value']['longitude'])
        return wd.coordenadas(lat, long, args)
    else
        wd.temp.debug = 'esse tipo de dado não é suportado'
        return nil
    end
end

-- Retorna número com determinados algarimos significativos
local algSig = function(n, alg)
    local a = alg - math.ceil(math.log10(math.abs(n)))
    return math.floor(n * 10 ^ a + 0.5) / 10 ^ a
end

-- Formata um número usando espaço como separador de milhar e vírgula como separador decimal
local formatnum = function(n)
    n = tostring(n)
    local m = 1
    while m > 0 do
        n, m = string.gsub(n, '^(%-?%d+)(%d%d%d)', '%1 %2')
    end
    return string.gsub(n, '(%d)%.(%d)', '%1,%2')
end

-- Retorna valor de uma quantidade de acordo com argumentos
wd.quantidade = function(n, qid, args)
    if args[1] == 'dividido' or args[1] == 'vezes' or args[1] == 'mais' or args[1] == 'menos' then
        wd.temp.debug = 'valor com operação matemática'
        resp = wd.matematica(n, args)
        return resp and formatnum(resp)
    elseif qid then
        return wd.unidade(n, qid, args)
    else
        wd.temp.debug = 'obtido valor sem unidade'
        return n and formatnum(n)
    end
end

-- Retorna unidade relacionada a um item do Wikidata baseado nos dados do Módulo:Unidades
wd.unidade = function(n, qid, args, loop)
    if not unidades then
        unidades = mw.loadData('Módulo:Unidades')
    end
    local u = type(unidades[qid]) == 'string' and unidades[unidades[qid]] or unidades[qid]
    if not u then
        wd.temp.debug = 'a unidade ' .. qid .. 'não é conhecida, se ela existir adicione no [[Módilo:Unidades]]'
        return n .. ' (unidade ' .. qid .. ')'
    end
    local nome = u['nome']
    local arg = table.remove(args, 1)
    -- Mudar a unidade se for pedido --
    if arg == 'unidade' and args[1] then
        nome = table.remove(args, 1)
            arg = table.remove(args, 1)
        local u2 = type(unidades[nome]) == 'string' and unidades[unidades[nome]] or unidades[nome]
        if u2 and u['grandeza'] ~= u2['grandeza'] then
            wd.temp.debug = 'tentando converter grandezas diferentes: ' .. u['nome'] .. ' (' .. u['grandeza'] ..
              ') para ' .. u2['nome'] .. ' (' .. u2['grandeza'] .. ')'
            return nil
        end
        if u2 and u ~= u2 then
            n = n * u['si'] / u2['si'] -- convertendo
            u = u2
        end
    end
    local unidade
    if u['unidade'] then
        unidade = u['unidade']
    elseif n ~= 1 and u['plural'] then
        unidade = u['plural']
    elseif n == 1 and u['nome'] then
        unidade = u['nome']
    else
        wd.temp.debug = 'unidade ' .. nome .. ' não possui nome da unidade' .. (n ~= 1 and ' nem plural' or '') ..
          ' para se colocado após o número, corrija no [[Módulo:Unidades]]'
        return ' (unidade ' .. qid .. ')'
    end
    local alg = 4
    if arg == 'alg' and tonumber(args[1]) then
        alg = math.max(1, math.min(12, math.floor(table.remove(args, 1))))
        arg = table.remove(args, 1)
    end
    wd.temp.debug = alg .. ' algarísmos significativos'
    local resp = formatnum(algSig(n, alg)) .. ' '
    if arg == 'link' and u['artigo'] then
        arg = table.remove(args, 1)
        resp = resp .. '[[' .. u['artigo'] .. '|' .. unidade .. ']]'
        wd.temp.debug = wd.temp.debug .. ', adicionado link'
    else
        resp = resp .. unidade
    end
    -- Exibir a conversão quando for pedido --
    if arg == 'converter' and args[1] and unidades[args[1]] and not loop then
        table.insert(args, 1, 'unidade')
        resp = resp .. ' (' .. wd.unidade(n, nome or qid, args, true) .. ')'
        wd.temp.debug = wd.temp.debug .. ', adicionado conversão'
    end
    return resp
end

-- Realiza operações matemáticas com quantidades
wd.matematica = function(n, args)
    local op = table.remove(args, 1)
    local prop = table.remove(args, 1)
    local n2
    if string.match(prop, '^%-?%d+$') then
        n2 = tonumber(prop)
        wd.temp.debug = 'valor ' .. n .. ' ' .. op .. (op == 'dividido' and ' por' or '') .. ' ' .. n2
    elseif string.match(prop, '^P%d+$') then
        local p2 = wd.props and wd.props[prop] and wd.props[prop][1]
        if not (p2 and p2['mainsnak']['datatype'] == 'quantity') then
            wd.temp.debug = p2 and prop .. ' não é do tipo quantity' or prop .. ' não existe no item'
            return
        end
        n2 = p2['mainsnak']['datavalue']['value']['amount']
        wd.temp.debug = 'operação com ' .. prop .. ' = ' .. n2
    else
        wd.temp.debug = 'operação matemática sem informar propriedade ou número'
        return nil
    end
    if op == 'dividido' then
        local r = n / n2
        if r ~= math.floor(r) then
            return tostring(algSig(r, 4))
        else
            return tostring(r)
        end
    elseif op == 'vezes' then
        return tostring(n * n2)
    elseif op == 'mais' then
        return tostring(n + n2)
    elseif op == 'menos' then
        return tostring(n - n2)
    end
end

-- Retorna as coordenadas no formato G° M' S" [NS] G° M' S" [LO] e coloca link quando pedido
wd.coordenadas = function(lat, long, args)
    local abs = math.abs(lat)
    local grau = math.floor(abs)
    local min = math.floor((abs - grau) * 60)
    local seg = math.floor(((abs - grau) * 60 - min) * 60)
    local hemi = lat >= 0 and 'N' or 'S'
    lat = string.format('%02d° %02d\' %02d" %s', grau, min, seg, hemi)
    abs = math.abs(long)
    grau = math.floor(abs)
    min = math.floor((abs - grau) * 60)
    seg = math.floor(((abs - grau) * 60 - min) * 60)
    hemi = long >= 0 and 'L' or 'O'
    long = string.format('%02d° %02d\' %02d" %s', grau, min, seg, hemi)
    local coor = string.gsub(lat, ' ', ' ') .. ' ' .. string.gsub(long, ' ', ' ')
    if args[1] == 'link' then
        local link = '<span class="plainlinks" style="white-space:nowrap" title="Mapas, fotos aéreas e outros dados para este local">[//tools.wmflabs.org/geohack/geohack.php?language=pt&pagename='
        local pagename =  mw.uri.encode(mw.title.getCurrentTitle().fullText)
        local coorlink = string.gsub(string.gsub(string.gsub(coor, '[^0-9NSLO]+', '_'), 'O', 'W'), 'L', 'E')
        local i = 2
        while #args > i do
            if args[i] == '' or args[i + 1] == '' then
                break
            end
            coorlink = coorlink .. '_' .. args[i] .. ':' .. string.gsub(args[i + 1], ' ', '_')
            i = i + 2
        end
        wd.temp.debug = 'obtida coordenada e adicionado link'
        return link .. pagename .. '&params=' .. coorlink .. ' ' .. coor .. ']</span>'
    end
    wd.temp.debug = 'obtida coordenada'
    return coor
end

wd.parser = function(s)
    local i = 1
    local grupos = {}
    local props = {}
    while i < #s do
        local abre = string.find(s, '{', i, true)
        local fecha = string.find(s, '}', i, true)
        if abre and not (fecha and fecha < abre) then
            table.insert(grupos, {abre})
            i = abre + 1
        else
            if fecha and #grupos > 0 then
                local encontrou
                for n = 0, #grupos - 1 do
                    if not grupos[#grupos - n][2] then
                        grupos[#grupos - n][2] = fecha
                        encontrou = true
                    end
                end
                if encontrou then
                    i = fecha + 1
                end
            elseif fecha then
                i = fecha + 1
            else
                break
            end
        end
    end
    i = 1
    while i < #s do
        local pos, fim = mw.ustring.find(s, 'P%d[%w:%-/]*', i)
        if pos then
            props[string.sub(s, pos, fim)] = pos
            i = fim + 1
        else
            break
        end
    end
    return grupos, props
end

-- Processa expressões que pedem dados do Wikidata
wd.expandir = function(str)
    local grupos, props = wd.parser(str)
    local expandido = {}
    local debug = {}
    for prop, pos in pairs(props) do
        local valor = wd.dados(prop)
        if valor then
            table.insert(expandido, pos)
            props[prop] = valor
        else
            props[prop] = false
        end
        table.insert(debug, prop .. ' [' .. (wd.temp.tipo or '?') .. ']: ' .. (wd.temp.debug or ''))
        wd.temp = {}
    end
    if #expandido == 0 then
        wd.temp.debug = #debug > 0 and table.concat(debug, '\n') or 'nenhuma propriedade em "' .. str .. '"'
        return nil
    end
    wd.temp.debug = #debug > 0 and table.concat(debug, '\n') or 'houve algum problema ao espandir "' .. str .. '"'
    for _, g in ipairs(grupos) do
        if #g == 2 then
            local apagar = true
            for _, pos in ipairs(expandido) do
                if g[1] < pos and g[2] > pos then
                    apagar = false
                    break
                end
            end
            if apagar then
                -- os grupos sem propriedades expandidas é preenchido com caracteres { para ser apagado
                str = str:sub(1, g[1]) .. string.rep('{', g[2] - g[1] - 1) .. str:sub(g[2])
            end
        end
    end
    str = str:gsub('[{}]+', '')  -- apaga caracteres { e }
    local oprops = {}
    -- Ordenando as propriedades pelo tamanho da string para evitar problemas quando uma é substring da outra
    for prop, valor in pairs(props) do
        local i = #oprops + 1
        for n, s in ipairs(oprops) do
            if #s < #prop then
                i = n
                break
            end
        end
        table.insert(oprops, i, prop)
    end
    -- Substituindo as propriedades pelo seu valor expandido
    for n, prop in ipairs(oprops) do
        local escprop = prop:gsub('[%^%$%(%)%%%.%[%]%*%+%-%?]','%%%1')  -- escapando caracteres especiais
        str = str:gsub(escprop, type(props[prop]) == 'string' and props[prop] or '')
    end
    return str
end

wd.formatardata = function(t)
	local ano, mes, dia = string.match(t, '^%+(%d+)%-(%d%d)%-0?(%d%d?)T')
	if not ano then
		return nil
	elseif mes == '00' then
		return ano
	else
		return meses[mes] .. ' de ' .. ano
	end
end

-- Tabela para funções invocadas diretamente
invoke = {}

invoke.dados = function(frame)
    local arg = frame.args['1']
    if arg and arg ~= '' then
        local resp = (wd.dados(arg) or '')
        if frame.args['2'] == 'debug' then
            resp = resp .. (wd.temp.debug and ' (' .. wd.temp.debug .. ')' or '')
        end
        return resp
    else
    	arg = frame:getParent().args['1']
    	if arg and arg ~= '' then
            local resp = (wd.dados(arg) or '')
            return resp
        end
    end
    return '(erro: nenhuma propriedade fornecida)'
end

invoke.expandir = function(frame)
    local arg = frame.args['1']
    if arg and arg ~= '' then
        local resp = (wd.expandir(arg) or '')
        if frame.args['2'] == 'debug' then
            resp = resp .. (wd.temp.debug and ' (' .. wd.temp.debug .. ')' or '')
        end
        return resp
    else
    	args = frame:getParent().args
    	if args['1'] and args['1'] ~= '' then
            local resp = (wd.expandir(args['1']) or '')
            if resp == '' and args['local'] then
            	return args['local']
            elseif args['data'] and wd.data[args['data']] then
            	local data = wd.formatardata(wd.data[args['data']])
            	if data then
            	    return resp ..' (' .. data .. ')'
            	end
            end
            return resp
        end
    end
    return '(erro: nenhuma propriedade fornecida)'
end

return invoke