Module:Complex date/core

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Lua

CodeDiscussionEditHistoryLinksLink count Subpages:DocumentationTestsResultsSandboxLive code All modules


This module contains core functions of Module:Complex date separated in order to allow proper unit testing. It relies on the following tables:


Code

--[[
  __  __           _       _         ____                      _                 _
 |  \/  | ___   __| |_   _| | ___ _ / ___|___  _ __ ___  _ __ | | _____  __   __| | __ _| |_ ___ 
 | |\/| |/ _ \ / _` | | | | |/ _ (_) |   / _ \| '_ ` _ \| '_ \| |/ _ \ \/ /  / _` |/ _` | __/ _ \
 | |  | | (_) | (_| | |_| | |  __/_| |__| (_) | | | | | | |_) | |  __/>  <  | (_| | (_| | ||  __/
 |_|  |_|\___/ \__,_|\__,_|_|\___(_)\____\___/|_| |_| |_| .__/|_|\___/_/\_\  \__,_|\__,_|\__\___|
                                                        |_|

This module is intended for creation of complex date phrases in variety of languages.

Once deployed, please do not modify this code without applying the changes first at Module:Complex date/sandbox and testing
at Module:Complex date/sandbox/testcases.

Authors and maintainers:
* User:Sn1per - first draft of the original version
* User:Jarekt - corrections and expansion of the original version
]]

-- List of external modules and functions
local p = {Error = nil}
local cdate = {}
require('strict') -- used for debugging purposes as it detects cases of unintended global variables
local ISOdate = require('Module:ISOdate')._ISOdate -- date localization
local core    = require('Module:Core')

--local i18n     = require('Module:i18n/complex date')   -- used for translations of date related phrases
--local Calendar   -- loaded lazily

-- =======================================================================
local function Ordinal(...)
    -- Name of the Module:
	-- * Module:Ordinal    - on Commons and Wikidata
	-- * Module:Ordinal-cd - on English Wikipedia
	return require('Module:Ordinal')._Ordinal(...)
end

-- ===========================================================================
local function contains(element, list)
	for __, item in ipairs(list) do
		if item == element then
			return true
		end
	end	
	return false
end

-- =======================================================================
-- langSwitch with default
local function langSwitch(list, lang)
	local langList = mw.language.getFallbacksFor(lang)
	table.insert(langList,1,lang)
	table.insert(langList,math.max(#langList,2),'default')
	for i,language in ipairs(langList) do
		if list[language] then
			return list[language]
		end
	end
end

-- ==================================================
function cdate.formatnum(numStr, lang)
	-- same as [[Module:Formatnum]] except that it does not deal with floats, exponents, thousand-separators, etc.
	local number = tonumber(numStr)
	local digit = { -- substitution of decimal digits for languages not supported by mw.language:formatNum() in core Lua libraries for MediaWiki
	    ["ml-old"] = { '൦', '൧', '൨', '൩', '൪', '൫', '൬', '൭', '൮', '൯' },
	    ["mn"]     = { '᠐', '᠑', '᠒', '᠓', '᠔', '᠕', '᠖', '᠗', '᠘', '᠙'},
	    ["ta"]     = { '௦', '௧', '௨', '௩', '௪', '௫', '௬', '௭', '௮', '௯'},
	    ["te"]     = { '౦', '౧', '౨', '౩', '౪', '౫', '౬', '౭', '౮', '౯'},
	    ["th"]     = { '๐', '๑', '๒', '๓', '๔', '๕', '๖', '๗', '๘', '๙'}
	}
	if number and digit[lang] then
		for i, v in ipairs(digit[lang]) do
            numStr = mw.ustring.gsub(numStr, tostring(i - 1), v)
		end
    	return numStr
	elseif number then
		return mw.getLanguage(lang):formatNum(number, { noCommafy = true })
	else 
		return numStr
	end
end

-- ==================================================
--[[
This function returns a string containing the input value formatted as a Roman numeral.  
It works for values between 0 and 3999. 
]]
function cdate.Roman(value)
	local d0, d1, d2, d3, i0, i1, i2, i3
	d0 = { [0] = '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX' }
    d1 = { [0] = '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC' }
    d2 = { [0] = '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM' }
    d3 = { [0] = '', 'M', 'MM', 'MMM'}
	local result = ''
	value = tonumber(value)
	if ((value >= 0) and (value < 4000)) then
		i3 = math.floor(value / 1e3)  
		i2 = math.floor(value / 1e2) % 10
		i1 = math.floor(value / 1e1) % 10
		i0 = math.floor(value / 1e0) % 10 
		result = d3[i3] .. d2[i2] .. d1[i1] .. d0[i0]
	end
	
	return result
end

-- =======================================================================
-- initialize the class by loading JSON data from [[Data:Complex date.tab]]
function cdate.init()
	cdate.error = nil
	cdate.translations = {}
	for _, row in pairs(mw.ext.data.get('Complex date.tab', '_').data) do
		local key, tbl = unpack(row)
		cdate.translations[key] = tbl
	end
end

-- =======================================================================
--[[
INPUTS:
	date1_str - string with first  date 
	date2_str - string with second date 
	operation - 
	lang - language of the user, like "en"
OUTPUT:
	date_str - translated string
]]
function cdate.translatePhrase(date1_str, date2_str, operation, lang)
-- use tables in Module:i18n/complex date to translate a phrase
	if not cdate.translations[operation] then
		p.Error = string.format('<span style="background-color:red;">Error in [[Module:Complex date]]: input parameter "%s" is not recognized.</span>', operation or 'nil')
		return ''
	end
	local dateStr = langSwitch(cdate.translations[operation], lang)
	local date1_num, date2_num = tonumber(date1_str), tonumber(date1_str)
	if date1_num and dateStr:match('$date1_rom') then
		date1_str = cdate.Roman(date1_num)
	elseif date1_num and dateStr:match('$date1_ord') then
		date1_str = Ordinal(date1_num, lang)
	end
	if date2_num and dateStr:match('$date2_rom') then
		date2_str = cdate.Roman(date0_num)
	elseif date2_num and dateStr:match('$date2_ord') then
		date2_str = Ordinal(date2_num, lang)
	end
	if type(dateStr)=='string' then
		-- replace parts of the string '$date1' and '$date2' with date1 and date2 strings
		dateStr = mw.ustring.gsub(dateStr, '$date1[_%w]*', date1_str)
		dateStr = mw.ustring.gsub(dateStr, '$date2[_%w]*', date2_str)
	end
	return dateStr
end

-- =======================================================================
--[[
INPUTS:
	date1 - table with
		date1.adj    - adjective ("early", "middle", etc.) or preposition ("before", "after", "circa", etc.)
		date1.date   - date string - either YYYY-MM-DD or a number
		date1.precision  - 8="decade", 7="century", 6="millennium" or nil for "year", "month", "day"
		date1.era	 - "ad", "ah", "bc", "bp"
		date1.case	 - gramatical case: like "gen", "loc", or nil for basic
		data1.num    - first date or second
	lang - language of the user, like "en"
OUTPUT:
	date_str - translated string
]]
function cdate.oneDatePhrase(date1, lang)
	local case, case2, dateStr
	if date1.adj then
		dateStr = langSwitch(cdate.translations[date1.adj], lang)
		case2   = dateStr:match('$date1_(%w+)')	
	end
	case = case2 or date1.case
	if date1.precision==11 then
		case = nil -- dates with days usually use basic form
	end

	-- dateStr can have many forms: ISO date, year or a number for
	-- decade, century or millennium
	local lut = {[8]='decade', [7]='century', [6]='millennium'}
	local units = lut[date1.precision]
	if units then -- units is "decade", "century", "millennium''
		dateStr = cdate.translatePhrase(date1.date, '', units, lang)
	elseif date1.precision and date1.precision>=9 then -- unit is "year", "month", "day"
		local class, trim_year, _ = '', true, nil
		dateStr, _ = ISOdate(date1.date, lang, case, class, trim_year)
	else -- other units
		dateStr = date1.date
	end
	
	-- add adjective ("early", "mid", etc.) or preposition ("before", "after",
	-- "circa", etc.) to the date
	if date1.adj then
		dateStr = cdate.translatePhrase(dateStr, '', date1.adj, lang)
	else -- only era?
		dateStr = cdate.formatnum(dateStr, lang)
	end
	
	-- add era
	local eras = {'bc','bh', 'ad', 'ah'}
	if date1.era and contains(date1.era, eras) then
		dateStr = cdate.translatePhrase(dateStr, '', date1.era, lang)
	end
	return dateStr --.."/"..(case2 or 'x')
end

-- =======================================================================
function cdate.isodate2timestamp(dateStr, precision, era)
-- convert date string to timestamps used by Quick Statements
	local tStamp = nil
	era = era or ''
	if era == 'ah' or precision<6 then
		return nil
	elseif era ~= '' then
		local eraLUT = {ad='+', bc='-', bp='-' }
		era = eraLUT[era]
	else
		era='+'
	end

-- convert isodate to timestamp used by quick statements
	if precision>=9 then
		if string.match(dateStr,"^%d%d%d%d$") then               -- if YYYY  format
			tStamp = era .. dateStr .. '-00-00T00:00:00Z/9'
		elseif string.match(dateStr,"^%d%d%d%d%-%d%d$") then      -- if YYYY-MM format
			tStamp = era .. dateStr .. '-00T00:00:00Z/10'
		elseif string.match(dateStr,"^%d%d%d%d%-%d%d%-%d%d$") then  -- if YYYY-MM-DD format
			tStamp = era .. dateStr .. 'T00:00:00Z/11'
		end
	elseif precision==8 then -- decade
		tStamp = era .. dateStr .. '-00-00T00:00:00Z/8'
	elseif precision==7 then -- century
		local d = tostring(tonumber(dateStr)-1)
		tStamp = era .. d .. '50-00-00T00:00:00Z/7'
	elseif precision==6 then
		local d = tostring(tonumber(dateStr)-1)
		tStamp = era .. d .. '500-00-00T00:00:00Z/6'
	end
	
	return tStamp
end

-- =======================================================================
--[[ create QuickStatements string for "one date" dates
INPUTS:
	date1 - table with
		date1.adj    - adjective ("early", "middle", etc.) or preposition ("before", "after", "circa", etc.)
		date1.date   - date string - either YYYY-MM-DD or a number
		date1.precision  - 8="decade", 7="century", 6="millennium" or nil for "year", "month", "day"
		date1.era	 - "ad", "ah", "bc", "bp"

OUTPUT:
	outputStr - QuickStatements string
]]
function cdate.oneDateQScode(date1)

	local outputStr = ''

	local d = cdate.isodate2timestamp(date1.date, date1.precision, date1.era)
	if not d then
		return ''
	end
	local rLUT = {
		early='Q40719727', mid='Q40719748', late='Q40719766', first_half='Q40719687', second_half='Q40719707',
		first_quarter='Q40690303', second_quarter='Q40719649', third_quarter='Q40719662', forth_quarter='Q40719674',
		spring='Q40720559', summer='Q40720564', autumn='Q40720568', winter='Q40720553',		
	}
	local qLUT = {['from']='P580', ['until']='P582', ['after']='P1319', ['before']='P1326', ['by']='P1326'}

	local refine    = rLUT[date1.adj]
	local qualitier = qLUT[date1.adj]

	if date1.adj=='' then
		outputStr = d
	elseif date1.adj=='circa' then
		outputStr = d..",P1480,Q5727902"
	elseif refine then
		outputStr = d..",P4241,"..refine
	elseif date1.precision>7 and qualitier then
		local century = string.gsub(d, 'Z%/%d+', 'Z/7')
		outputStr = century ..",".. qualitier ..","..d
	end
	return outputStr
end

-- =======================================================================
function cdate.twoDateQScode(date1, date2, conj)
-- create QuickStatements string for "two date" dates
	if date1.adj or date2.adj or date1.era~=date2.era then
		return '' -- QuickStatements string are not generated for two date phrases with adjectives
	end
	local outputStr = ''
	local d1 = cdate.isodate2timestamp(date1.date, date1.precision, date1.era)
	local d2 = cdate.isodate2timestamp(date2.date, date2.precision, date2.era)
	if (not d1) or (not d2) then
		return ''
	end
	-- find date with lower precision in common to both dates
	local cd
	local year1 = tonumber(string.sub(d1,2,5))
	local year2 = tonumber(string.sub(d2,2,5))
	local k = 0
	for i = 1,10,1 do
		if string.sub(d1,1,i)==string.sub(d2,1,i) then
			k = i -- find last matching letter
		end
	end
	if k>=9 then              -- same month, since "+YYYY-MM-" is in common
		cd = cdate.isodate2timestamp(string.sub(d1,2,8), 10, date1.era)
	elseif k>=6 and k<9 then  -- same year, since "+YYYY-" is in common
		cd = cdate.isodate2timestamp(tostring(year1), 9, date1.era)
	elseif k==4 then          -- same decade(k=4, precision=8),  since "+YYY" is in common
		cd = cdate.isodate2timestamp(tostring(year1), 8, date1.era)
	elseif k==3 then          -- same century(k=3, precision=7) since "+YY" is in common
	  local d = tostring(math.floor(year1/100) +1) -- convert 1999 -> 20
		cd = cdate.isodate2timestamp( d, 7, date1.era)
	elseif k==2 then          -- same millennium (k=2, precision=6),  since "+Y" is in common
		local d = tostring(math.floor(year1/1000) +1) -- convert 1999 -> 2
		cd = cdate.isodate2timestamp( d, 6, date1.era)
	end
	if not cd then
		return ''
	end
	--if not cd then
	--	return ' <br/>error: ' .. d1.." / " .. d2.." / ".. (cd or '') .." / ".. string.sub(d1,2,5).." / " .. string.sub(d2,2,5).." / " .. tostring(k)
	--end

	--
	if (conj=='from-until') or (conj=='and' and year1==year2-1) then
		outputStr = cd ..",P580,".. d1 ..",P582,".. d2
	elseif (conj=='between') or (conj=='or' and year1==year2-1) then
		outputStr = cd ..",P1319,".. d1 ..",P1326,".. d2
	elseif conj=='circa2' then
		outputStr = cd ..",P1319,".. d1 ..",P1326,".. d2 ..",P1480,Q5727902"
	end

	return outputStr
end


return cdate