Модуль:Wikidata/date
Курӑнакан калӑплав
Для документации этого модуля может быть создана страница Модуль:Wikidata/date/doc
--settings
local nowLabel = 'наст. время'
local moduleDates = require( "Module:Dates" )
local moduleWikidata = require( "Module:Wikidata" )
local dateCat = require("Module:Infocards/dateCat")
local function deepcopy(orig)
local orig_type = type(orig)
local copy
if orig_type == 'table' then
copy = {}
for orig_key, orig_value in next, orig, nil do
copy[deepcopy(orig_key)] = deepcopy(orig_value)
end
setmetatable(copy, deepcopy(getmetatable(orig)))
else -- number, string, boolean, etc
copy = orig
end
return copy
end
local function ageImpl ( bStructure, bPrecision, dStructure, dPrecision )
if ( not bStructure or not dStructure or bPrecision < 10 or dPrecision < 10 ) then
return nil
end
local shift = 0
if ( bStructure.year < 0 and dStructure.year > 0 ) then
shift = -1
end
if ( bPrecision == 10 or dPrecision == 10 ) then
if ( bStructure.month < dStructure.month ) then
return dStructure.year - bStructure.year + shift
end
if ( bStructure.month == dStructure.month ) then
return nil
end
if ( bStructure.month > dStructure.month ) then
return dStructure.year - bStructure.year - 1 + shift
end
end
if ( bStructure.month < dStructure.month ) then
return dStructure.year - bStructure.year + shift
end
if ( bStructure.month == dStructure.month ) then
if ( bStructure.day <= dStructure.day ) then
return dStructure.year - bStructure.year + shift
else
return dStructure.year - bStructure.year - 1 + shift
end
end
if ( bStructure.month > dStructure.month ) then
return dStructure.year - bStructure.year - 1 + shift
end
return nil
end
-- accepts table of time+precision values
local function ageCurrent ( bTable )
local possibleAge = "NYA" -- it means "Not Yet Assigned", not what you imagined!
for bKey, bValue in pairs(bTable) do
if ( bValue.unknown ) then
return nil
end
local bStructure = bValue.structure
local bPrecision = bValue.precision
local dStructure = os.date( "*t" )
local calculatedAge = ageImpl ( bStructure, bPrecision, dStructure, 11 )
if ( possibleAge == "NYA" ) then
possibleAge = calculatedAge
else
if ( possibleAge ~= calculatedAge ) then
possibleAge = nil
end
end
end
return possibleAge
end
-- accepts tables of time+precision values
local function age ( bTable, dTable )
local possibleAge = "NYA" -- it means "Not Yet Assigned", not what you imagined!
for bKey, bValue in pairs( bTable ) do
if ( bValue.unknown ) then
return nil
end
local bStructure = bValue.structure
local bPrecision = bValue.precision
for dKey, dValue in pairs( dTable ) do
if ( dValue.unknown ) then
return nil
end
local dStructure = dValue.structure
local dPrecision = dValue.precision
if ( bValue.calendar == 'julian' and dValue.calendar == 'gregorian' ) then
-- to calculate age, need to adjust bStructure to gregorian calendar
local shift = math.floor(bStructure.year/100-2) - math.floor(bStructure.year/400)
-- TODO: re-implement this properly
bStructure.day = bStructure.day + shift
end
local calculatedAge = ageImpl ( bStructure, bPrecision, dStructure, dPrecision )
if ( possibleAge == "NYA" ) then
possibleAge = calculatedAge
else
if ( possibleAge ~= calculatedAge ) then
possibleAge = nil
end
end
end
end
return possibleAge
end
local function parseISO8601Date(str)
local pattern = "(%-?%d+)%-(%d+)%-(%d+)T"
local Y, M, D = mw.ustring.match( str, pattern )
return tonumber(Y), tonumber(M), tonumber(D)
end
local function parseISO8601Time(str)
local pattern = "T(%d+):(%d+):(%d+)%Z"
local H, M, S = mw.ustring.match( str, pattern)
return tonumber(H), tonumber(M), tonumber(S)
end
local function parseISO8601Offset(str)
if str:sub(-1)=="Z" then return 0,0 end -- ends with Z, Zulu time
-- matches ±hh:mm, ±hhmm or ±hh; else returns nils
local pattern = "([-+])(%d%d):?(%d?%d?)$"
local sign, oh, om = mw.ustring.match( str, pattern)
sign, oh, om = sign or "+", oh or "00", om or "00"
return tonumber(sign .. oh), tonumber(sign .. om)
end
local function parseISO8601(str)
if 'table' == type(str) then
if str.args and str.args[1] then
str = '' .. str.args[1]
else
return 'unknown argument type: ' .. type( str ) .. ': ' .. table.tostring( str )
end
end
local Y,M,D = parseISO8601Date(str)
local h,m,s = parseISO8601Time(str)
local oh,om = parseISO8601Offset(str)
if not Y or not M or not D or not h or not m or not s or not oh or not om then
return nil
end
return tonumber(os.time({year=Y, month=M, day=D, hour=(h+oh), min=(m+om), sec=s}))
end
local function parseClaim ( claim )
if ( claim.mainsnak.snaktype == "value" ) then
local timeISO8601 = string.gsub( string.gsub( tostring( claim.mainsnak.datavalue.value.time ), '-00%-', '-01-' ), '-00T', '-01T' )
local unixtime = parseISO8601( timeISO8601 )
local structure = os.date("*t", unixtime)
local precision = tonumber( claim.mainsnak.datavalue.value.precision )
local calendarmodel = 'gregorian'
if (mw.ustring.find(claim.mainsnak.datavalue.value.calendarmodel, 'Q1985786', 1, true)) then
calendarmodel = 'julian'
end
local item = { structure=structure, precision=precision, calendar = calendarmodel }
return item
elseif ( claim.mainsnak.snaktype == "novalue" ) then
-- novalue
return { unknown="novalue" }
else
--unknown
return { unknown="unknown" }
end
end
-- returns table of time+precision values for specified property
local function parseProperty ( context, options, propertyId )
if ( not context ) then error( 'context not specified'); end
if ( not options ) then error( 'options not specified'); end
if ( not options.entity ) then error( 'options.entity is missing'); end
if ( not propertyId ) then error( 'propertyId not specified'); end
local claims = context.selectClaims( options, propertyId )
if not claims then
return nil
end
local result = {}
for key, claim in pairs( claims ) do
table.insert ( result, parseClaim( claim ) )
end
return result
end
-- проверка на совпадающие даты с разной моделью календаря
local function checkDupDates( t )
if #t > 1 then
local removed = false
local j = 1
-- проверка на совпадающие даты с разной моделью календаря
while (j <= #t) do
local i = 1
while (i <= #t) do
if i ~= j then
if (os.time(t[j].structure) == os.time(t[i].structure)) then
if ((t[j].calendarmodel == 'gregorian') and
(t[i].calendarmodel == 'julian')) then
removed = true
break
else
table.remove(t, i)
end
else
i = i + 1
end
else
i = i + 1
end
end
if removed then
removed = false
table.remove(t, j)
else
j = j+1
end
end
end
end
-- returns first qualifier of specified propertyId
local function getQualifierWithDataValue( statement, qualifierPropertyId )
if ( statement.qualifiers
and statement.qualifiers[qualifierPropertyId] ) then
local qualifiers = statement.qualifiers[qualifierPropertyId]
for _, qualifier in ipairs( qualifiers ) do
if (qualifier.datavalue) then
return qualifier
end
end
end
return nil
end
local p = {}
local function formatDecade( time, categoryNamePrefix )
local bce = ''
local year
if time.year < 0 then
bce = ' до н. э.'
year = math.floor( math.abs( time.year ) / 10 ) * 10
else
year = math.floor( time.year / 10 ) * 10
end
local unit = '-е'
if isGenitive then
unit = '-х'
end
local value = '' .. year .. unit .. bce
if categoryNamePrefix then
return value .. '[[Category:' .. categoryNamePrefix .. ' в ' .. year .. '-е годы' .. bce .. ']]'
end
return value
end
local function formatCentury( time, categoryNamePrefix, isGenitive )
local moduleRoman = require( 'Module:RomanNumber' )
local bce = ''
local century
if time.year < 0 then
bce = ' до н. э.'
century = math.floor( ( math.abs( time.year ) - 1 ) / 100 ) + 1
else
century = math.floor( ( time.year - 1 ) / 100 ) + 1
end
local unit = 'век'
if isGenitive then
unit = 'века'
end
local infix = ' в '
if century == 2 then
infix = ' во '
end
if moduleRoman then
century = moduleRoman.toRomanNumber( century )
end
local value = '[[' .. century .. ' век' .. bce .. '|' .. century .. ' ' .. unit .. bce .. ']]'
if categoryNamePrefix then
return value .. '[[Category:' .. categoryNamePrefix .. infix .. century .. ' веке' .. bce .. ']]'
end
return value
end
local function formatMillenium( time, categoryNamePrefix, isGenitive )
local bce = ''
local millenium
if time.year < 0 then
bce = ' до н. э.'
millenium = math.floor( ( math.abs( time.year ) - 1 ) / 1000 ) + 1
else
millenium = math.floor( ( time.year - 1 ) / 1000 ) + 1
end
local unit = '-е тысячелетие'
if isGenitive then
unit = '-го тысячелетия'
end
local value = '[[' .. millenium .. '-е тысячелетие' .. bce .. '|' .. millenium .. unit .. bce .. ']]'
if categoryNamePrefix then
local infix = ' в '
if millenium == 2 then
infix = ' во '
end
return value .. '[[Category:' .. categoryNamePrefix .. infix .. millenium .. '-м тысячелетии' .. bce .. ']]'
else
return value
end
end
local function formatDateImpl( value, options, microformatClass, categoryPrefix, leftBracket, rightBracket, nolinks, isGenitive )
if ( not value ) then error( 'value not specified'); end
if ( not options ) then error( 'options not specified'); end
-- The calendar model used for saving the data is always the proleptic Gregorian calendar according to ISO 8601.
local timeISO8601 = string.gsub( string.gsub( tostring( value.time ), '-00%-', '-01-' ), '-00T', '-01T' )
local unixtime = parseISO8601( timeISO8601 )
if not unixtime then
return ''
end
local structure = os.date("*t", unixtime)
local precision = tonumber( value.precision )
if precision <= 6 then
return formatMillenium( structure, categoryPrefix, isGenitive )
end
if precision == 7 then
return formatCentury( structure, categoryPrefix, isGenitive )
end
if precision == 8 then
return formatDecade( structure, categoryPrefix, isGenitive )
end
if precision == 9 then
local tCopy = deepcopy( structure )
tCopy.day = nil
tCopy.month = nil
return moduleDates.formatWikiImpl( tCopy, tCopy, infoclass, categoryPrefix, leftBracket, rightBracket, nolinks )
end
-- year and month only
if precision == 10 then
local tCopy = deepcopy( structure )
tCopy.day = nil
return moduleDates.formatWikiImpl( tCopy, tCopy, infoclass, categoryPrefix, leftBracket, rightBracket, nolinks )
end
local calendarmodel = 'gregorian'
if (mw.ustring.find(value.calendarmodel, 'Q1985786', 1, true)) then
calendarmodel = 'julian'
end
if (calendarmodel == 'gregorian') then
return moduleDates.formatWikiImpl( structure, structure, microformatClass, categoryPrefix, leftBracket, rightBracket, nolinks )
else
return p.formatAsJulian( timeISO8601, infoclass, categoryPrefix, leftBracket, rightBracket, nolinks )
end
end
local function formatApproximateDateClaim( context, options, statement, unknownDateCategory )
if ( not context ) then error( 'context not specified'); end
if ( not options ) then error( 'options not specified'); end
if ( not options.entity ) then error( 'options.entity is missing'); end
if ( not statement ) then error( 'statement not specified'); end
if options.nocat then unknownDateCategory = "" end
local qNotSoonerThan = getQualifierWithDataValue( statement, 'P1319' )
local qNotLaterThan = getQualifierWithDataValue( statement, 'P1326' )
if ( qNotSoonerThan or qNotLaterThan ) then
local results = {}
if ( qNotSoonerThan ) then
local formattedDate = formatDateImpl( qNotSoonerThan.datavalue.value, {}, nil, nil, options.leftBracket, options.rightBracket, options.nolinks, true )
local value = 'не ранее ' .. context.wrapSnak( formattedDate, qNotSoonerThan.hash )
table.insert( results, context.wrapQualifier( value, 'P1319' ) )
end
if ( qNotLaterThan ) then
local formattedDate = formatDateImpl( qNotLaterThan.datavalue.value, {}, nil, nil, options.leftBracket, options.rightBracket, options.nolinks, true )
local value = 'не позднее ' .. context.wrapSnak( formattedDate, qNotLaterThan.hash )
table.insert( results, context.wrapQualifier( value, 'P1326' ) )
end
return mw.text.listToText( results, ' и ' , ' и ' ) .. unknownDateCategory .. context.formatRefs( options, statement )
end
return nil
end
function p.formatDateOfBirthClaim( context, options, statement )
local value = formatApproximateDateClaim( context, options, statement, dateCat.categoryUnknownBirthDate )
if value then
return value
end
options['conjunction'] = ' или '
options['value-module'] = 'Wikidata/date'
options['value-function'] = 'formatBirthDate'
options.i18n.somevalue = '\'\'неизвестно\'\'' .. dateCat.categoryUnknownBirthDate
local circumstances = context.getSourcingCircumstances( statement )
for _, itemId in ipairs( circumstances ) do
if itemId == 'Q5727902' then
options.isGenitive = true
break
end
end
local result = context.formatStatementDefault( context, options, statement )
local bTable = { parseClaim( statement ) }
local dTable = parseProperty ( context, options, 'P570' )
if ( bTable and not dTable ) then
local age = ageCurrent( bTable )
if ( age ) then
if ( options.suppressAge == nil or options.suppressAge == '' ) then
result = result .. ' <span style="white-space:nowrap;">(' .. age .. ' ' .. mw.language.new( 'cv' ):plural( age, 'çул', 'çул', 'çул') .. ')</span>'
end
if ( not options.nocat ) then
if ( age > 115 ) then
result = result .. dateCat.categoryBigCurrentAge
elseif (age >= 0) then
result = result .. dateCat.categoryBiographiesOfLivingPersons
else
result = result .. dateCat.categoryNegativeAge
end
end
end
end
return result
end
function p.formatDateOfDeathClaim( context, options, statement )
local value = formatApproximateDateClaim( context, options, statement, dateCat.categoryUnknownDeathDate )
if value then
return value
end
options['conjunction'] = ' или '
options['value-module'] = 'Wikidata/date'
options['value-function'] = 'formatDeathDate'
options.i18n.somevalue = '\'\'неизвестно\'\'' .. dateCat.categoryUnknownDeathDate
local circumstances = context.getSourcingCircumstances( statement )
for _, itemId in ipairs( circumstances ) do
if itemId == 'Q5727902' then
options.isGenitive = true
break
end
end
local result = context.formatStatementDefault( context, options, statement )
local bTable = parseProperty ( context, options, 'P569' )
local dTable = { parseClaim( statement ) }
if ( bTable and dTable ) then
local age = age( bTable, dTable )
if ( age ) then
if ( options.suppressAge == nil or options.suppressAge == '' ) then
result = result .. ' <span style="white-space:nowrap;">(' .. age .. ' ' .. mw.language.new( 'cv' ):plural( age, 'çул', 'çул', 'çул') .. ')</span>'
end
if ( not options.nocat and age < 0) then
result = result .. dateCat.categoryNegativeAge
end
end
-- returns category to recently deceased persons
local unixAvailable, unixDateOfDeath = pcall(function()
local r = os.time(dTable[1].structure)
if ( r ~= os.time() ) then
return r
end
error()
end)
if ( unixAvailable and os.time() - unixDateOfDeath < 31536000 and not options.nocat ) then
result = result .. dateCat.categoryRecentlyDeceased
end
end
return result
end
-- Reentry point for Wikidata Snak formatting
function p.formatBirthDate( context, options, value )
if ( not context ) then error( 'context not specified'); end
if ( not options ) then error( 'options not specified'); end
if ( not value ) then error( 'value not specified'); end
local microformatClass = nil
if options.microformat ~= '-' then
microformatClass = options.microformat or 'bday'
end
if ( options.nocat ) then
return formatDateImpl( value, options, microformatClass, nil, options.leftBracket, options.rightBracket, options.nolinks, options.isGenitive )
else
return formatDateImpl( value, options, microformatClass, 'çуралнисем', options.leftBracket, options.rightBracket, options.nolinks, options.isGenitive )
end
end
-- Reentry point for Wikidata Snak formatting
function p.formatDeathDate( context, options, value )
if ( not context ) then error( 'context not specified'); end
if ( not options ) then error( 'options not specified'); end
if ( not value ) then error( 'value not specified'); end
local microformatClass = nil
if options.microformat ~= '-' then
microformatClass = options.microformat or 'dday'
end
if ( options.nocat and options.nocat ~= '' ) then
return formatDateImpl( value, options, microformatClass, nil, options.leftBracket, options.rightBracket, options.nolinks, options.isGenitive )
else
return formatDateImpl( value, options, microformatClass, 'вилнисем', options.leftBracket, options.rightBracket, options.nolinks, options.isGenitive )
end
end
-- Reentry point for Wikidata Snak formatting -- default one
function p.formatDate( context, options, value )
if ( not context ) then error( 'context not specified'); end
if ( not options ) then error( 'options not specified'); end
if ( not value ) then error( 'value not specified'); end
local microformatClass = options.microformat or nil
if ( options.nocat and options.nocat ~= '' ) then
return formatDateImpl( value, options, microformatClass, nil, options.leftBracket, options.rightBracket, options.nolinks, options.isGenitive )
else
local categoryPrefix = options.categoryPrefix or nil
return formatDateImpl( value, options, microformatClass, categoryPrefix, options.leftBracket, options.rightBracket, options.nolinks, options.isGenitive )
end
end
function p.formatDateIntervalProperty( context, options )
if ( not context ) then error( 'context not specified' ); end
if ( not options ) then error( 'options not specified' ); end
if ( not options.entity ) then error( 'options.entity missing' ); end
-- Получение нужных утверждений
local WDS = require( 'Module:WikidataSelectors' )
local fromProperty = options.property
if options.from and options.from ~= '' then
fromProperty = options.from
end
local fromClaims = WDS.load( options.entityId, fromProperty )
local toClaims = WDS.load( options.entityId, options.to )
if fromClaims == nil and toClaims == nil then
return ''
end
local formattedFromClaims = {}
if fromClaims then
for i, claim in ipairs( fromClaims ) do
local formattedStatement = context.formatStatement( options, claim )
if formattedStatement then
formattedStatement = '<span class="wikidata-claim" data-wikidata-property-id="' .. string.upper( options.property ) .. '" data-wikidata-claim-id="' .. claim.id .. '">' .. formattedStatement .. '</span>'
table.insert( formattedFromClaims, formattedStatement )
end
end
end
local formattedToClaims = {}
local toOptions = deepcopy( options )
toOptions.property = options.to
toOptions.novalue = nowLabel
if toClaims then
for i, claim in ipairs( toClaims ) do
local formattedStatement = context.formatStatement( toOptions, claim )
if formattedStatement then
formattedStatement = '<span class="wikidata-claim" data-wikidata-property-id="' .. string.upper( toOptions.property ) .. '" data-wikidata-claim-id="' .. claim.id .. '">' .. formattedStatement .. '</span>'
table.insert( formattedToClaims, formattedStatement )
end
end
end
local out = ''
local fromOut = mw.text.listToText( formattedFromClaims, options.separator, options.conjunction )
local toOut = mw.text.listToText( formattedToClaims, options.separator, options.conjunction )
if fromOut ~= '' or toOut ~= '' then
if fromOut ~= '' then
out = fromOut
else
out = '?'
end
if toOut ~= '' then
out = out .. ' — ' .. toOut
else
local withinClaims = nil
if options.within then
WDS.load( options.entityId, options.within )
end
if withinClaims == nil then
out = 'с ' .. out
else
out = out .. ' — ' .. nowLabel
end
end
end
if out ~= '' then
if options.before then
out = options.before .. out
end
if options.after then
out = out .. options.after
end
end
return out
end
local lowestBoundary = '1582-10-05T00:00:00Z'
local lastBoundary = '1918-01-31T00:00:00Z'
local boundaries = {
-- from (G) till next will be diff(G = J + diff), at current
{ lowestBoundary, 10 },
{ '1700-02-29T00:00:00Z', 11 },
{ '1800-02-29T00:00:00Z', 12 },
{ '1900-02-29T00:00:00Z', 13 },
{ lastBoundary, '' },
}
-- Передаваемое время обязано быть по Юлианскому календарю (старому стилю)
function p.formatAsJulian( julTimeISO8601, infocardClass, categoryNamePrefix, leftBracket, rightBracket, nolinks )
if 'table' == type( julTimeISO8601 ) then
if julTimeISO8601.args and julTimeISO8601.args[1] then
julTimeISO8601 = julTimeISO8601.args[1]
else
return 'unknown argument type: ' .. type( julTime ) .. ': ' .. table.tostring( julTime )
end
end
julTimeISO8601 = mw.text.trim( julTimeISO8601 )
julTimeISO8601 = string.gsub( julTimeISO8601, '^+', '' )
local julTime = parseISO8601( julTimeISO8601 )
local t = os.date( "*t", julTime )
if ( julTime < parseISO8601( lowestBoundary ) ) then
-- only julian
if string.find( julTimeISO8601, '-02-29T', 1, true ) then
t = { year = t.year, month = 2, day = 29 }
end
return moduleDates.formatWikiImpl( t, t, infocardClass, categoryNamePrefix, leftBracket, rightBracket, nolinks )
end
if ( julTimeISO8601 >= lastBoundary ) then
return "''некорректная дата (юлианский календарь не используется после 1918-01-26)''"
end
-- julian and grigorian
for i = 1, #boundaries, 1 do
local b1 = boundaries[i][1]
local b2 = boundaries[i + 1][1]
if ( b1 <= julTimeISO8601 and julTimeISO8601 < b2 ) then
local diff = boundaries[i][2]
if string.sub( julTimeISO8601, 1, 10 ) == string.sub( boundaries[i][1], 1, 10 ) then
t = { year = t.year, month = 2, day = 29 }
diff = diff - 1
end
local gregTime = os.date( "*t", julTime + diff * 24 * 60 * 60 )
return moduleDates.formatWikiImpl( t, gregTime, infocardClass, categoryNamePrefix, leftBracket, rightBracket, nolinks )
end
end
return "''ошибка в модуле Модуль:Wikidata/date''"
end
return p