Modulo:Hours

El Vikivojaĝo

Versiokontrolo[redakti]

Versionomo en Vikidatumoj: 2024-01-11 Ok!


-- getting opening hours from Wikidata

-- module variable and administration
local hr = {
	moduleInterface = {
		suite  = 'Hours',
		serial = '2024-01-11',
		item   = 99600452
	},
	labelTable = nil
}

-- module import
-- require( 'strict' )
local hi = require( 'Module:Hours/i18n' )
local wu = require( 'Module:Wikidata utilities' )

-- local variables
local categIds    = {}
local showOptions = {}

local function isSet( s )
	return s and s ~= ''
end

-- insert a value into a table only if it is set
local function tableInsert( tab, value )
	if isSet( value ) then
		table.insert( tab, value )
	end
end

-- value count for any variable
local function getCount( tab )
	return type( tab ) == 'table' and #tab or 0
end

local function getLabelFromTables( id )
	local label = hi.dateIds[ id ]
	if not label and hr.labelTable then
		label = hr.labelTable[ id ]
	end
	return label
end

-- getting normalized time hh:dd
local function getNormalizedTime( s )
	local count
	s, count = mw.ustring.gsub( s, hi.texts.timePattern, '%1:%2' )
	return ( count > 0 ) and s or nil
end

function hr.formatTime( s )
	local t = getNormalizedTime( s )
	if not t then
		return s
	end

	local formatStr = hi.texts.formatTime
	t = mw.text.split( t, ':', true )
	if #t == 1 then
		t[ 2 ] = '00'
	end
	if hi.options.hour12 then
		local isAM = true
		local n = tonumber( t[ 1 ] )
		if n > 12 then
			isAM = false
			t[ 1 ] = '' .. ( n - 12 )
		end
		formatStr = isAM and hi.texts.formatAM or hi.texts.formatPM
	end
	s = mw.ustring.format( formatStr, mw.text.trim( t[ 1 ] ),
		mw.text.trim( t[ 2 ] ) )
	if hi.options.leadingZero then
		s = s:gsub( '^(%d):', '0%1:' )
	else
		s = s:gsub( '^0(%d):', '%1:' )
	end
	if hi.options.removeZeros then
		s = s:gsub( '^(%d%d?):00', '%1' )
	end
	return s
end

-- getting label for a qualifier id
-- to save computing time firstly the id will fetched from Hours/i18n table
-- if available, otherwise from Wikidata
local function getLabelFromId( id, wikilang, fallbackLang )
	if not isSet( id ) then
		return ''
	end

	-- from table
	local label = getLabelFromTables( id )

	-- from Wikidata
	if not label and mw.wikibase.isValidEntityId( id ) then
		label = wu.getLabel( id, wikilang )
		if not label and isSet( fallbackLang ) then
			label = wu.getLabel( id, fallbackLang )
			if label then
				categIds.fallbackLabel = 1
			end
		end
		if label then
			categIds.hoursLabelFromWikidata = 1
		end
	end

	-- abbreviate labels
	if isSet( label ) then
		for i, abbr in ipairs( hi.abbr ) do
			label = mw.ustring.gsub( label, abbr.f, abbr.a )
		end
		label = mw.ustring.gsub( label, '​', '' ) -- zero-width space
	end

	-- additional time formatting
	if isSet( label ) then
		if hi.months then
			for full, short in pairs( hi.months ) do
				label = mw.ustring.gsub( label, full, short )
			end
		end
		label = hr.formatTime( label )
	end
	return label or ''
end

local function abbreviateTimeStr( s, all, pattern, repl )
	if not isSet( s ) or not isSet( pattern ) or not repl then
        return s or ''
	end
	if all then
		s = mw.ustring.gsub( s, pattern, repl )
	else
		local matchPattern = mw.ustring.gsub( pattern, '%(%%d%)', '' )
		local first, stop = mw.ustring.find( s, pattern )
		if first then
			local second = mw.ustring.find( s, pattern, stop + 1 )
			if second and mw.ustring.match( s, matchPattern ) ==
				mw.ustring.match( s, matchPattern, stop + 1 ) then
        		s = mw.ustring.gsub( s, pattern, repl, 1 )
        	end
    	end
    end
    return s
end

-- getting time period string
-- i:  position in from and to arrays
-- id: label for P3035 value
local function getTimePeriod( from, to, i, id )
	local result = ''
	if id and ( id == getLabelFromTables( hi.times.daily )
		or id == getLabelFromTables( hi.times.is24_7 ) )
		and from and to and from[ i ] == getLabelFromTables( hi.times.Jan1 ) and
		to[ i ] == getLabelFromTables( hi.times.Dec31 ) then
		return ''
	end
	if from and isSet( from[ i ] ) and to and isSet( to[ i ] ) then
		result = mw.ustring.format( hi.texts.fromTo, from[ i ], to[ i ] )
		if isSet( hi.texts.hourPattern ) then
			result = abbreviateTimeStr( result, hi.texts.hourReplAll,
				hi.texts.hourPattern, hi.texts.hourRepl )
		end
	elseif from and isSet( from[ i ] ) then
		result = mw.ustring.format( hi.texts.from, from[ i ] )
	elseif to and isSet( to[ i ] ) then
		result = mw.ustring.format( hi.texts.to, to[ i ] )
	end
	return result
end

-- collecting all maintenance categories
function hr.getCategories( formatStr )
	local result = wu.getCategories( formatStr )
	for k, v in pairs( categIds ) do
		result = result .. ( hi.categories[ k ] or hi.categories.unknownError )
	end
	if showOptions.demo then
		-- remove category links
		result = result:gsub( '%[%[[^%[]*%]%]', '' )
	end
	return result
end

-- getting a string with listed days at which an establishment is closed
local function getClosed( arr )
	return ( arr and #arr > 0 ) and mw.ustring.format( hi.texts.closed or '%s', 
		table.concat( arr, hi.texts.comma ) ) or ''
end

-- converting day range from Mo, Tu, We to Mo–We, and so on
local function getRange( arr, minPos, maxPos )
	if maxPos > 0 and minPos > 0 and maxPos > minPos then
		arr[ minPos ] = mw.ustring.format( hi.texts.fromTo, arr[ minPos ],
			arr[ maxPos ] )
		for i = maxPos, minPos + 1, -1 do
			table.remove( arr, i )
		end
	end
end

-- looking for day ranges like Mo, Tu, We and so on and converting them
local function convertDayRange( arr )
	local count = #arr
	local minPos = 0
	local maxPos = 0
	local value, valueMin
	while count > 0 do
		value = hi.weekdays[ arr[ count ] ]
		if not value then
			getRange( arr, minPos, maxPos )
			maxPos = 0
		elseif maxPos == 0 then
			maxPos = count
			valueMin = value
		elseif maxPos > 0 and value == valueMin - 1 then
			minPos = count
			valueMin = value
		else
			getRange( arr, minPos, maxPos )
			maxPos = 0
		end
		count = count - 1
	end
	getRange( arr, minPos, maxPos )
end

-- concating non-empty strings
local function concatStrings( sep, str1, str2 )
	local tab = {}
	tableInsert( tab, str1 )
	tableInsert( tab, str2 )
	return table.concat( tab, sep )
end

-- add comment if not yet exists
local function addComment( tab, value )
	if not isSet( value ) then
		return
	elseif #tab == 0 then
		table.insert( tab, value )
	else
		for i = 1, #tab, 1 do
			if tab[ i ] == value then
				break
			end
			if i == #tab then
				table.insert( tab, value )
			end
		end
	end
end

-- main function for usage in Lua modules
-- entity: entity id or entity table
-- wikilang: content language of the wiki
-- fallbackLang: optional additional language for fallback
-- formatStr: optional format string for property categories
-- show: table of show options (addCategories, msg, nomsg) or nil
-- lastedit: dat of last edit. If false no date will be fetched
-- labelTabel: additional table with Q-id label pairs
function hr.getHoursFromWikidata( entity, wikilang, fallbackLang, formatStr,
	show, lastEdit, labelTable )

	-- collecting days at which an establishment is closed
	local closeDays = {}
	local closeDaysHelper = {}
	local function mergeDays( days )
		if not days or #days == 0 then
			return
		end
		for i, day in ipairs( days ) do
			if not closeDaysHelper[ day ] then
				table.insert( closeDays, day )
				closeDaysHelper[ day ] = ''
			end
		end
	end
	local function clearDays()
		closeDays = {}
		closeDaysHelper = {}
	end

	-- adding additional properties if an additional Q-id table is given
	hr.labelTable = labelTable

	-- preparing show options
	showOptions = show or {}
	showOptions.addCategories = hi.options.addCategories
	if showOptions.msg then
		showOptions.addCategories = true
	elseif showOptions.nomsg then
		showOptions.addCategories = false
	end

	-- format string for property categories
	if not isSet( formatStr ) then
		formatStr = hi.categories.properties
	end

	-- 1st step: getting statements for P3025: open days
	local statements = wu.getValuesWithQualifiers( entity, hi.wd.opened, nil,
		hi.wd.all, hi.wd.retrieved, nil, getLabelFromId, wikilang, fallbackLang )
	lastEdit = wu.getLastedit( lastEdit, statements )

	-- converting statements to human-readable strings
	local result = {}
	local comments, s
	local is24_7 = getLabelFromTables( hi.times.is24_7 )

	for i, statement in ipairs( statements ) do
		-- opening times
		local times = {}
		local count = math.max( getCount( statement[ hi.wd.hourOpen ] ),
			getCount( statement[ hi.wd.hourClosed ] ) )
		if count > 0 then
			for j = 1, count, 1 do
				s = getTimePeriod( statement[ hi.wd.hourOpen ],
					statement[ hi.wd.hourClosed ], j )
				if isSet( s ) then
					table.insert( times, s )
				elseif statement.value ~= is24_7 then
					categIds.withoutTime = 1
				end
			end
		elseif statement.value ~= is24_7 then
			categIds.withoutTime = 1
		end
		s = table.concat( times, hi.texts.comma )

		-- comments
		comments = {}
		count = math.max( getCount( statement[ hi.wd.dayOpen ] ),
			getCount( statement[ hi.wd.dayClosed ] ) )
		for j = 1, count, 1 do
			addComment( comments,
				getTimePeriod( statement[ hi.wd.dayOpen ],
					statement[ hi.wd.dayClosed ], j, statement.value ) )
		end
		for j, key in ipairs( hi.wd.comments ) do
			if statement[ key ] then
				addComment( comments, table.concat( statement[ key ],
					hi.texts.comma ) )
			end
		end

		-- concating times and comments
		times = {}
		tableInsert( times, s )
		s = table.concat( comments, hi.texts.comma )
		if isSet( s ) then
			table.insert( times, mw.ustring.format( hi.texts.parentheses, s ) )
		end

		local item = {}
		tableInsert( item, table.concat( times, hi.texts.space ) )

		-- close statements (P3026) as qualifiers for open days property (P3025)
		mergeDays( statement[ hi.wd.closed ] )
		if not hi.options.clusterClosed and ( i == #statements or
			statements[ i ].value ~= statements[ i + 1 ].value ) then
			convertDayRange( closeDays )
			tableInsert( item, getClosed( closeDays ) )
			clearDays()
		end
		s = table.concat( item, hi.texts.comma )
	
		-- copying each statement to result table
		if statement.sort2 == 1 then
			tableInsert( result, { value = { statement.value }, times = s } )
		elseif s ~= '' then
			result[ #result ].times = concatStrings( hi.texts.comma,
				result[ #result ].times, s )
		end
	end

	-- checking for duplicates
	for i = #result, 2, -1 do
		if result[ i ].times == result[ i - 1 ].times then
			for j, value in ipairs( result[ i ].value ) do
				table.insert( result[ i - 1 ].value, value )
			end
			table.remove( result, i )
		end
	end

	-- converting day range
	for i = 1, #result, 1 do
		local arr = result[ i ].value
		convertDayRange( arr )
		result[ i ] = concatStrings( hi.texts.space,
			table.concat( arr, hi.texts.comma ), result[ i ].times )
	end

	-- 2nd step: getting separated close statements (P3026)
	local statements = wu.getValuesWithQualifiers( entity, hi.wd.closed, nil,
		hi.wd.commentsForClosed, hi.wd.retrieved, nil, getLabelFromId, wikilang,
		fallbackLang )
	if #statements > 0 then
		lastEdit = wu.getLastedit( lastEdit, statements )

		-- getting closed values
		local closed = {}	
		for i, statement in ipairs( statements ) do
			local closedDate = {}
			table.insert( closedDate, statement.value )

			-- getting comments
			comments = {}
			for j, key in ipairs( hi.wd.commentsForClosed ) do
				if statement[ key ] then
					addComment( comments, table.concat( statement[ key ],
						hi.texts.comma ) )
				end
			end
			s = table.concat( comments, hi.texts.comma )
			if isSet( s ) then
				table.insert( closedDate, mw.ustring.format( hi.texts.parentheses, s ) )
			end

			table.insert( closed, table.concat( closedDate, hi.texts.space ) )
		end
		mergeDays( closed )
	end
	convertDayRange( closeDays )
	tableInsert( result, getClosed( closeDays ) )

	-- 3rd step: adding additional statements
	local statements = wu.getValuesWithQualifiers( entity, hi.wd.stateOfUse,
		nil, {}, hi.wd.retrieved, nil, getLabelFromId, wikilang, fallbackLang )
	if #statements > 0 then
		lastEdit = wu.getLastedit( lastEdit, statements )
		for i, statement in ipairs( statements ) do
			tableInsert( result, statement.value )
		end
	end

	-- merging all statements
	local result = table.concat( result, hi.texts.semicolon )

	-- adding maintenance categories
	if result ~= '' then
		categIds.hoursFromWikidata = 1
		if showOptions.addCategories then
			result = result .. hr.getCategories( formatStr )
		end
	end
	return result, lastEdit
end

-- invoke helper functions
-- splitting show parameters
local function splitAndCheck( s )
	local arr = {}
	local err = {}
	if isSet( s ) then
		for i, v in ipairs( mw.text.split( s, ',', true ) ) do
			v = mw.text.trim( v )
			if not hi.show[ v ] then
				table.insert( err, v )
			else
				arr[ v ] = ''
			end
		end
	end
	return arr, err
end

-- check if pareameters are valid
local function checkParameters( args )
	local err = {}
	if not args and type( args ) ~= 'table' then
		return err
	end
	for k, v in pairs( args ) do
		if not hi.params[ k ] then
			table.insert( err, k )
		end
	end
	return err
end

-- formating and concating error strings
local function getErrorStr( arr, formatStr )
	return #arr == 0 and '' or
		mw.ustring.format( formatStr, table.concat( arr, hi.texts.comma ) )
end

-- main function for template #invoke calls
-- id: Q id of an establishment
-- format: output format like 'opened at %s'
-- fallback: fallback language if labels are not available in content language
function hr.getHours( frame )
    local args = frame.args
    args.id = mw.text.trim( args.id or '' )
    if not isSet( args.id ) or not mw.wikibase.isValidEntityId( args.id ) then
        return hi.categories.invalidId
    end
    if not isSet( args.format ) then
    	args.format = hi.texts.format
    else
	    local s, count = args.format:gsub( '%%s', '%%s' )
    	if count ~= 1 then
        	args.format = hi.texts.format
    	end
    end
    args.fallback = args.fallback or ''
    local wikilang = mw.getContentLanguage():getCode()

	local paramErr = checkParameters( args )
	local show, showErr = splitAndCheck( args.show )

    local result, lastEdit = hr.getHoursFromWikidata( args.id, wikilang,
    	args.fallback, '', show, false, nil )
    if result ~= '' then
    	result = mw.ustring.format( args.format, result )
    end
    return result .. getErrorStr( paramErr, hi.categories.unknownParams )
    	.. getErrorStr( showErr, hi.categories.unknownShowOptions )
end

return hr