Module:Sandbox/RexxS/GetDateValue

-- vim: set noexpandtab ft=lua ts=4 sw=4:

require('strict')

local p = {}

local debug = false

-- module local variables

local wiki =

{

langcode = mw.language.getContentLanguage().code

}

-- internationalisation

local i18n = {

["errors"] = {

["property-not-found"] = "Property not found.",

["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.",

["unknown-datetime-format"] = "Unknown datetime format.",

["local-article-not-found"] = "Article is not yet available in this wiki"

},

["datetime"] =

{

-- $1 is a placeholder for the actual number

[0] = "$1 billion years", -- precision: billion years

[1] = "$100 million years", -- precision: hundred million years

[2] = "$10 million years", -- precision: ten million years

[3] = "$1 million years", -- precision: million years

[4] = "$100,000 years", -- precision: hundred thousand years

[5] = "$10,000 years", -- precision: ten thousand years

[6] = "$1 millennium", -- precision: millennium

[7] = "$1 century", -- precision: century

[8] = "$1s", -- precision: decade

-- the following use the format of #time parser function

[9] = "Y", -- precision: year,

[10] = "F Y", -- precision: month

[11] = "F j, Y", -- precision: day

[12] = "F j, Y ga", -- precision: hour

[13] = "F j, Y g:ia", -- precision: minute

[14] = "F j, Y g:i:sa", -- precision: second

["beforenow"] = "$1 BCE", -- how to format negative numbers for precisions 0 to 5

["afternow"] = "$1 CE", -- how to format positive numbers for precisions 0 to 5

["bc"] = '$1 "BCE"', -- how print negative years

["ad"] = "$1", -- how print positive years

-- the following are for function getDateValue() and getQualifierDateValue()

["default-format"] = "dmy", -- default value of the #3 (getDateValue) or

-- #4 (getQualifierDateValue) argument

["default-addon"] = "BC", -- default value of the #4 (getDateValue) or

-- #5 (getQualifierDateValue) argument

["prefix-addon"] = false, -- set to true for languages put "BC" in front of the

-- datetime string; or the addon will be suffixed

["addon-sep"] = " ", -- separator between datetime string and addon (or inverse)

["format"] = -- options of the 3rd argument

{

["mdy"] = "F j, Y",

["my"] = "F Y",

["y"] = "Y",

["dmy"] = "j F Y",

["ymd"] = "Y-m-d",

["ym"] = "Y-m"

}

},

["monolingualtext"] = '%text',

["warnDump"] = "Category:Called function 'Dump' from module Wikidata"

}

-- Credit to http://stackoverflow.com/a/1283608/2644759

-- cc-by-sa 3.0

local function tableMerge(t1, t2)

for k,v in pairs(t2) do

if type(v) == "table" then

if type(t1[k] or false) == "table" then

tableMerge(t1[k] or {}, t2[k] or {})

else

t1[k] = v

end

else

t1[k] = v

end

end

return t1

end

local function loadI18n()

local exist, res = pcall(require, "Module:Wikidata-i18n")

if exist then

tableMerge(i18n, res.i18n)

end

end

loadI18n()

local function printError(code)

return '' .. (i18n.errors[code] or code) .. ''

end

if debug then

function p.inspectI18n(frame)

local val = i18n

for _, key in pairs(frame.args) do

key = mw.text.trim(key)

val = val[key]

end

return val

end

end

local function parseDateFull(timestamp, precision, date_format, date_addon)

local prefix_addon = i18n["datetime"]["prefix-addon"]

local addon_sep = i18n["datetime"]["addon-sep"]

local addon = ""

-- check for negative date

if string.sub(timestamp, 1, 1) == '-' then

timestamp = '+' .. string.sub(timestamp, 2)

addon = date_addon

end

-- get the next four characters after the + (should be the year now in all cases)

-- ok, so this is dirty, but let's get it working first

local intyear = tonumber(string.sub(timestamp, 2, 5))

if intyear == 0 and precision <= 9 then

return ""

end

-- precision is 10000 years or more

if precision <= 5 then

local factor = 10 ^ ((5 - precision) + 4)

local y2 = math.ceil(math.abs(intyear) / factor)

local relative = mw.ustring.gsub(i18n.datetime[precision], "$1", tostring(y2))

if addon ~= "" then

-- negative date

relative = mw.ustring.gsub(i18n.datetime.beforenow, "$1", relative)

else

relative = mw.ustring.gsub(i18n.datetime.afternow, "$1", relative)

end

return relative

end

-- precision is decades, centuries and millennia

local era

if precision == 6 then era = mw.ustring.gsub(i18n.datetime[6], "$1", tostring(math.floor((math.abs(intyear) - 1) / 1000) + 1)) end

if precision == 7 then era = mw.ustring.gsub(i18n.datetime[7], "$1", tostring(math.floor((math.abs(intyear) - 1) / 100) + 1)) end

if precision == 8 then era = mw.ustring.gsub(i18n.datetime[8], "$1", tostring(math.floor(math.abs(intyear) / 10) * 10)) end

if era then

if addon ~= "" then

era = mw.ustring.gsub(mw.ustring.gsub(i18n.datetime.bc, '"', ""), "$1", era)

else

era = mw.ustring.gsub(mw.ustring.gsub(i18n.datetime.ad, '"', ""), "$1", era)

end

return era

end

local _date_format = i18n["datetime"]["format"][date_format]

if _date_format ~= nil then

-- check for precision is year and override supplied date_format

if precision == 9 then

_date_format = i18n["datetime"][9]

end

local year_suffix

local tstr = ""

local lang_obj = mw.language.new(wiki.langcode)

local f_parts = mw.text.split(_date_format, 'Y', true)

for idx, f_part in pairs(f_parts) do

year_suffix = ''

if string.match(f_part, "x[mijkot]$") then

-- for non-Gregorian year

f_part = f_part .. 'Y'

elseif idx < #f_parts then

-- supress leading zeros in year

year_suffix = lang_obj:formatDate('Y', timestamp)

year_suffix = string.gsub(year_suffix, '^0+', '', 1)

end

tstr = tstr .. lang_obj:formatDate(f_part, timestamp) .. year_suffix

end

local fdate

if addon ~= "" and prefix_addon then

fdate = addon .. addon_sep .. tstr

elseif addon ~= "" then

fdate = tstr .. addon_sep .. addon

else

fdate = tstr

end

return fdate

else

return printError("unknown-datetime-format")

end

end

-- the "qualifiers" and "snaks" field have a respective "qualifiers-order" and "snaks-order" field

-- use these as the second parameter and this function instead of the built-in "pairs" function

-- to iterate over all qualifiers and snaks in the intended order.

local function orderedpairs(array, order)

if not order then return pairs(array) end

-- return iterator function

local i = 0

return function()

i = i + 1

if order[i] then

return order[i], array[order[i]]

end

end

end

-- precision: 0 - billion years, 1 - hundred million years, ..., 6 - millennia, 7 - century, 8 - decade, 9 - year, 10 - month, 11 - day, 12 - hour, 13 - minute, 14 - second

local function normalizeDate(date)

date = mw.text.trim(date, "+")

-- extract year

local yearstr = mw.ustring.match(date, "^\-?%d+")

local year = tonumber(yearstr)

-- remove leading zeros of year

return year .. mw.ustring.sub(date, #yearstr + 1), year

end

local function formatDate(date, precision, timezone)

precision = precision or 11

local date, year = normalizeDate(date)

if year == 0 and precision <= 9 then return "" end

-- precision is 10000 years or more

if precision <= 5 then

local factor = 10 ^ ((5 - precision) + 4)

local y2 = math.ceil(math.abs(year) / factor)

local relative = mw.ustring.gsub(i18n.datetime[precision], "$1", tostring(y2))

if year < 0 then

relative = mw.ustring.gsub(i18n.datetime.beforenow, "$1", relative)

else

relative = mw.ustring.gsub(i18n.datetime.afternow, "$1", relative)

end

return relative

end

-- precision is decades, centuries and millennia

local era

if precision == 6 then era = mw.ustring.gsub(i18n.datetime[6], "$1", tostring(math.floor((math.abs(year) - 1) / 1000) + 1)) end

if precision == 7 then era = mw.ustring.gsub(i18n.datetime[7], "$1", tostring(math.floor((math.abs(year) - 1) / 100) + 1)) end

if precision == 8 then era = mw.ustring.gsub(i18n.datetime[8], "$1", tostring(math.floor(math.abs(year) / 10) * 10)) end

if era then

if year < 0 then era = mw.ustring.gsub(mw.ustring.gsub(i18n.datetime.bc, '"', ""), "$1", era)

elseif year > 0 then era = mw.ustring.gsub(mw.ustring.gsub(i18n.datetime.ad, '"', ""), "$1", era) end

return era

end

-- precision is year

if precision == 9 then

return year

end

-- precision is less than years

if precision > 9 then

--[[ the following code replaces the UTC suffix with the given negated timezone to convert the global time to the given local time

timezone = tonumber(timezone)

if timezone and timezone ~= 0 then

timezone = -timezone

timezone = string.format("%.2d%.2d", timezone / 60, timezone % 60)

if timezone[1] ~= '-' then timezone = "+" .. timezone end

date = mw.text.trim(date, "Z") .. " " .. timezone

end

]]--

local formatstr = i18n.datetime[precision]

if year == 0 then formatstr = mw.ustring.gsub(formatstr, i18n.datetime[9], "")

elseif year < 0 then

-- Mediawiki formatDate doesn't support negative years

date = mw.ustring.sub(date, 2)

formatstr = mw.ustring.gsub(formatstr, i18n.datetime[9], mw.ustring.gsub(i18n.datetime.bc, "$1", i18n.datetime[9]))

elseif year > 0 and i18n.datetime.ad ~= "$1" then

formatstr = mw.ustring.gsub(formatstr, i18n.datetime[9], mw.ustring.gsub(i18n.datetime.ad, "$1", i18n.datetime[9]))

end

return mw.language.new(wiki.langcode):formatDate(formatstr, date)

end

end

local function printDatavalueEntity(data, parameter)

-- data fields: entity-type [string], numeric-id [int, Wikidata id]

local id

if data["entity-type"] == "item" then id = "Q" .. data["numeric-id"]

elseif data["entity-type"] == "property" then id = "P" .. data["numeric-id"]

else return printError("unknown-entity-type")

end

if parameter then

if parameter == "link" then

local linkTarget = mw.wikibase.sitelink(id)

local linkName = mw.wikibase.label(id)

if linkTarget then

-- if there is a local Wikipedia article link to it using the label or the article title

return "" .. (linkName or linkTarget) .. ""

else

-- if there is no local Wikipedia article output the label or link to the Wikidata object to let the user input a proper label

if linkName then return linkName else return "" .. id .. "" end

end

else

return data[parameter]

end

else

return mw.wikibase.label(id) or id

end

end

local function printDatavalueTime(data, parameter)

-- data fields: time [ISO 8601 time], timezone [int in minutes], before [int], after [int], precision [int], calendarmodel [wikidata URI]

-- precision: 0 - billion years, 1 - hundred million years, ..., 6 - millennia, 7 - century, 8 - decade, 9 - year, 10 - month, 11 - day, 12 - hour, 13 - minute, 14 - second

-- calendarmodel: e.g. http://www.wikidata.org/entity/Q1985727 for the proleptic Gregorian calendar or http://www.wikidata.org/wiki/Q11184 for the Julian calendar]

if parameter then

if parameter == "calendarmodel" then data.calendarmodel = mw.ustring.match(data.calendarmodel, "Q%d+") -- extract entity id from the calendar model URI

elseif parameter == "time" then data.time = normalizeDate(data.time) end

return data[parameter]

else

return formatDate(data.time, data.precision, data.timezone)

end

end

local function printDatavalueMonolingualText(data, parameter)

-- data fields: language [string], text [string]

if parameter then

return data[parameter]

else

local result = mw.ustring.gsub(mw.ustring.gsub(i18n.monolingualtext, "%%language", data["language"]), "%%text", data["text"])

return result

end

end

local function findClaims(entity, property)

if not property or not entity or not entity.claims then return end

if mw.ustring.match(property, "^P%d+$") then

-- if the property is given by an id (P..) access the claim list by this id

return entity.claims[property]

else

property = mw.wikibase.resolvePropertyId(property)

if not property then return end

return entity.claims[property]

end

end

local function getSnakValue(snak, parameter)

if snak.snaktype == "value" then

-- call the respective snak parser

if snak.datavalue.type == "string" then return snak.datavalue.value

elseif snak.datavalue.type == "globecoordinate" then return printDatavalueCoordinate(snak.datavalue.value, parameter)

elseif snak.datavalue.type == "quantity" then return printDatavalueQuantity(snak.datavalue.value, parameter)

elseif snak.datavalue.type == "time" then return printDatavalueTime(snak.datavalue.value, parameter)

elseif snak.datavalue.type == "wikibase-entityid" then return printDatavalueEntity(snak.datavalue.value, parameter)

elseif snak.datavalue.type == "monolingualtext" then return printDatavalueMonolingualText(snak.datavalue.value, parameter)

end

end

return mw.wikibase.renderSnak(snak)

end

local function getQualifierSnak(claim, qualifierId)

-- a "snak" is Wikidata terminology for a typed key/value pair

-- a claim consists of a main snak holding the main information of this claim,

-- as well as a list of attribute snaks and a list of references snaks

if qualifierId then

-- search the attribute snak with the given qualifier as key

if claim.qualifiers then

local qualifier = claim.qualifiers[qualifierId]

if qualifier then return qualifier[1] end

end

return nil, printError("qualifier-not-found")

else

-- otherwise return the main snak

return claim.mainsnak

end

end

local function getValueOfClaim(claim, qualifierId, parameter)

local error

local snak

snak, error = getQualifierSnak(claim, qualifierId)

if snak then

return getSnakValue(snak, parameter)

else

return nil, error

end

end

local function getReferences(frame, claim)

local result = ""

-- traverse through all references

for ref in pairs(claim.references or {}) do

local refparts

-- traverse through all parts of the current reference

for snakkey, snakval in orderedpairs(claim.references[ref].snaks or {}, claim.references[ref]["snaks-order"]) do

if refparts then refparts = refparts .. ", " else refparts = "" end

-- output the label of the property of the reference part, e.g. "imported from" for P143

refparts = refparts .. tostring(mw.wikibase.label(snakkey)) .. ": "

-- output all values of this reference part, e.g. "German Wikipedia" and "English Wikipedia" if the referenced claim was imported from both sites

for snakidx = 1, #snakval do

if snakidx > 1 then refparts = refparts .. ", " end

refparts = refparts .. getSnakValue(snakval[snakidx])

end

end

if refparts then result = result .. frame:extensionTag("ref", refparts) end

end

return result

end

function p.claim(frame)

local property = frame.args[1] or ""

local id = frame.args["id"] -- "id" must be nil, as access to other Wikidata objects is disabled in Mediawiki configuration

local qualifierId = frame.args["qualifier"]

local parameter = frame.args["parameter"]

local list = frame.args["list"]

local references = frame.args["references"]

local showerrors = frame.args["showerrors"]

local default = frame.args["default"]

if default then showerrors = nil end

-- get wikidata entity

local entity = mw.wikibase.getEntityObject(id)

if not entity then

if showerrors then return printError("entity-not-found") else return default end

end

-- fetch the first claim of satisfying the given property

local claims = findClaims(entity, property)

if not claims or not claims[1] then

if showerrors then return printError("property-not-found") else return default end

end

-- get initial sort indices

local sortindices = {}

for idx in pairs(claims) do

sortindices[#sortindices + 1] = idx

end

-- sort by claim rank

local comparator = function(a, b)

local rankmap = { deprecated = 2, normal = 1, preferred = 0 }

local ranka = rankmap[claims[a].rank or "normal"] .. string.format("%08d", a)

local rankb = rankmap[claims[b].rank or "normal"] .. string.format("%08d", b)

return ranka < rankb

end

table.sort(sortindices, comparator)

local result

local error

if list then

local value

-- iterate over all elements and return their value (if existing)

result = {}

for idx in pairs(claims) do

local claim = claims[sortindices[idx]]

value, error = getValueOfClaim(claim, qualifierId, parameter)

if not value and showerrors then value = error end

if value and references then value = value .. getReferences(frame, claim) end

result[#result + 1] = value

end

result = table.concat(result, list)

else

-- return first element

local claim = claims[sortindices[1]]

result, error = getValueOfClaim(claim, qualifierId, parameter)

if result and references then result = result .. getReferences(frame, claim) end

end

if result then return result else

if showerrors then return error else return default end

end

end

-- look into entity object

function p.ViewSomething(frame)

local f = (frame.args[1] or frame.args.id) and frame or frame:getParent()

local id = f.args.id

if id and (#id == 0) then

id = nil

end

local data = mw.wikibase.getEntityObject(id)

if not data then

return nil

end

local i = 1

while true do

local index = f.args[i]

if not index then

if type(data) == "table" then

return mw.text.jsonEncode(data, mw.text.JSON_PRESERVE_KEYS + mw.text.JSON_PRETTY)

else

return tostring(data)

end

end

data = data[index] or data[tonumber(index)]

if not data then

return

end

i = i + 1

end

end

-- getting sitelink of a given wiki

function p.getSiteLink(frame)

local f = frame.args[1]

local entity = mw.wikibase.getEntity()

if not entity then

return

end

local link = entity:getSitelink( f )

if not link then

return

end

return link

end

function p.Dump(frame)

local f = (frame.args[1] or frame.args.id) and frame or frame:getParent()

local data = mw.wikibase.getEntityObject(f.args.id)

if not data then

return i18n.warnDump

end

local i = 1

while true do

local index = f.args[i]

if not index then

return "

"..mw.dumpObject(data).."
".. i18n.warnDump

end

data = data[index] or data[tonumber(index)]

if not data then

return i18n.warnDump

end

i = i + 1

end

end

-- This is used to get a date value for date_of_birth (P569), etc. which won't be linked

-- Dates and times are stored in ISO 8601 format (sort of).

-- At present the local formatDate(date, precision, timezone) function doesn't handle timezone

-- So I'll just supply "Z" in the call to formatDate below:

p.getDateValue = function(frame)

local propertyID = mw.text.trim(frame.args[1] or "")

local input_parm = mw.text.trim(frame.args[2] or "")

local date_format = mw.text.trim(frame.args[3] or i18n["datetime"]["default-format"])

local date_addon = mw.text.trim(frame.args[4] or i18n["datetime"]["default-addon"])

if input_parm == "FETCH_WIKIDATA" then

local entity = mw.wikibase.getEntityObject()

if entity.claims[propertyID] ~= nil then

local out = {}

for k, v in pairs(entity.claims[propertyID]) do

if v.mainsnak.datavalue.type == 'time' then

local timestamp = v.mainsnak.datavalue.value.time

local dateprecision = v.mainsnak.datavalue.value.precision

out[#out + 1] = parseDateFull(timestamp, dateprecision, date_format, date_addon)

end

end

return table.concat(out, ", ")

else

return ""

end

else

return input_parm

end

end

return p