Module:Events calendar/sandbox

Module documentation
local p = {}
local lang = nil

local function getDaysInMonth(month, year)
  local days_in_month = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }   
  local d = days_in_month[month]
   
  -- check for leap year
  if month == 2 then
    if math.mod(year,4) == 0 then
     if math.mod(year,100) == 0 then
      if math.mod(year,400) == 0 then                    
          d = 29
      end
     else
      d = 29
     end
    end
  end

  return d  
end

local function getFirstTimestamp(month, year, extended)
	first_timestamp = os.time({ year = year, month = month, day = 1, hour = 0, min = 0 })
	if not extended then
		return first_timestamp
	end
	first_day = (os.date("*t", first_timestamp).wday - 2)%7
	return first_timestamp - (first_day * 86400)
end

local function getLastTimestamp(month, year, extended)
	last_timestamp = os.time({ year = year, month = month, day = getDaysInMonth(tonumber(month), tonumber(year)), hour = 23, min = 59 })
	if not extended then
		return last_timestamp
	end
	last_day = (os.date("*t", last_timestamp).wday - 2)%7
	return last_timestamp + ((6-last_day) * 86400)
end

local function getFirstDayTimestamp(year, month, day)
	return os.time({ year = year, month = month, day = day, hour = 0 })
end

local function getLastDayTimestamp(year, month, day)
	return os.time({ year = year, month = month, day = day, hour = 23, min = 59, sec = 59 })
end

local function rruleToInterval(rrule)
	local days = 10000
	if rrule.freq == 'weekly' then
		days = 7
	end
	local interval = rrule.interval or 1
	return days * interval * 86400
end

local function currentDatesByRrule(dtstart, dtend, rrule, t1)
	local dtuntil = rrule['until'] or 2000000000 -- ~2033
	if dtuntil < t1 then
		return dtstart, dtend
	end
	
	local interval = rruleToInterval(rrule)
	local duration = dtend - dtstart
	while dtstart < t1 and dtstart + interval <= dtuntil do
		dtstart = dtstart + interval
	end
	dtend = dtstart + duration

	return dtstart, dtend
end

local function dateFilter(data, t1, t2)
	filtered_data = {}
	
	for _,value in pairs(data) do
		local dtstart = value.dtstart
		local dtend = value.dtend
		if value.rrule then
			dtstart, dtend = currentDatesByRrule(dtstart, dtend, value.rrule, t1)
		end
	    if (dtstart >= t1 and dtstart <= t2)
		    or (dtend >= t1 and dtend <= t2)
		    or (dtstart < t1 and dtend > t2)
	    then
	    	table.insert(filtered_data, value)
    	end
	end
	
	return filtered_data
end

local function tagFilter(data, searched_tags)
	if #searched_tags == 0 then
		return data
	end
	
	filtered_data = {}
	
	for _,value in pairs(data) do
		if value.tags ~= nil then
			for _,tag in pairs(value.tags) do
				local break_out = false
				for _,searched_tag in pairs(searched_tags) do
					if tag == searched_tag then
						table.insert(filtered_data, value)
						break_out = true
						break
					end
				end
				if break_out then break end
			end
		end
	end
	
	return filtered_data
end

local function locationFilter(data, searched_locations)
	if #searched_locations == 0 then
		return data
	end
	
	filtered_data = {}
	
	for _,value in pairs(data) do
		for _,location in pairs(value.location) do
			local break_out = false
			for _,searched_location in pairs(searched_locations) do
				if location == searched_location then
					table.insert(filtered_data, value)
					break_out = true
					break
				end
			end
			if break_out then break end
		end
	end
	
	return filtered_data
end

local function geolocFilter(data)
	filtered_data = {}
	
	for _,value in pairs(data) do
		if value.geoloc.lat ~= nil and value.geoloc.lng ~= nil then
			table.insert(filtered_data, value)
		end
	end

	return filtered_data
end

local function formatEvents(frame, data, short)
	local content = ""
	for _,event in pairs(data) do
		if content ~= "" then
			content = content .. "<br>"
		end
		local title = event.title
		if event.link ~= nil and event.link ~= "" then
			if mw.ustring.find(event.link, 'https?://') == 1 then
				title = "[" .. event.link .. " " .. event.title .. "]"
			else
				title = "[[" .. event.link .. "|" .. event.title .. "]]"
			end
		end
		local date = os.date("*t", event.dtstart)
		local timestring = frame:expandTemplate{ title = 'TZC event', args = {
			year = date.year,
			month = date.month,
			day = date.day,
			hour = string.format( "%02d", date.hour ),
			minute = string.format( "%02d", date.min ),
			format = 'timeonly',
		} }
		content = content .. "'''" .. event.location[#(event.location)] .. " " .. timestring .. "''' " .. title .. " <span style='display: none;' class='ec-pencil' id='"  .. mw.text.split(event.id, '@', true)[1]  ..  "'>[[File:Blue pencil.svg|12px|✏️|alt=Edit|link=]]</span>"
		if not short then
			content = content .. "<br>" .. event.description
		end
	end
	return content
end

local function arrayToText(array)
	if #array == 0 then
		return ""
	end
	
	local text = array[1]
	for i = 2, #array do
		text = array[i] .. "," .. text
	end
	
	return text
end


function p.ical(frame, year, month, locations, tags)
	local args = frame:getParent().args
	
	local today = os.date("*t")
	local timestamp = getFirstTimestamp(month, year, false)
	local last_timestamp = timestamp + 300000000
	
	local data = mw.text.jsonDecode(frame:expandTemplate{ title = ':Events calendar/events.json', args = {} })
	data = dateFilter(data, timestamp, last_timestamp)
	data = tagFilter(data, tags)
	data = locationFilter(data, locations)
	
	local content = "BEGIN:VCALENDAR"
		.. "\r\nVERSION:2.0"
		.. "\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN"
		.. "\r\nX-WR-CALNAME:" .. ( args.title == nil and "Wikimedia events" or args.title )
	
	for _,value in pairs(data) do
		content = content .. "\r\nBEGIN:VEVENT"
			.. "\r\nCREATED:" .. os.date("%Y%m%dT%H%M%SZ", value.dtcreated)
			.. "\r\nLAST-MODIFIED:" .. os.date("%Y%m%dT%H%M%SZ", value.dtmodified)
			.. "\r\nDTSTAMP:" .. os.date("%Y%m%dT%H%M%SZ", value.dtmodified)
			.. "\r\nUID:" .. value.id
			.. "\r\nSUMMARY:" .. value.title
		if value.description ~= nil then
			content = content .. "\r\nDESCRIPTION:" .. value.description:gsub("\n", ", ")
		end
		content = content .. " " .. mw.text.encode(value.link:gsub(" ", "_"))
		if value.tags ~= nil then
			content = content .. "\r\nCATEGORIES:" .. arrayToText(value.tags)
		end
		if value.location ~= nil then
			content = content .. "\r\nLOCATION:" .. arrayToText(value.location)
		end
		if value.link ~= nil then
			if mw.ustring.find(value.link, 'https?://') == 1 then
				content = content .. "\r\nURL:" .. mw.text.encode(value.link:gsub(" ", "_"))
			else
				content = content .. "\r\nURL:https://meta.wikimedia.org/wiki/" .. mw.text.encode(value.link:gsub(" ", "_"))
			end
		end
		if value.rrule ~= nil then
			local rrule = {}
			for key, value in pairs( value.rrule ) do
				table.insert( rrule, string.upper( key ) .. '=' .. string.upper( value ) )
			end
			content = content .. "\r\nRRULE:" .. table.concat( rrule, ';' )
		end
		content = content .. "\r\nDTSTART:" .. os.date("%Y%m%dT%H%M%S", value.dtstart)
			.. "\r\nDTEND:" .. os.date("%Y%m%dT%H%M%S", value.dtend)
			.. "\r\nEND:VEVENT"
	end
	content = content .. "\r\nEND:VCALENDAR"
	
	return content
end

function p.grid(frame, year, month, locations, tags)
	local args = frame:getParent().args
	
	local today = os.date("*t")
	local timestamp = getFirstTimestamp(month, year, true)
	local last_timestamp = getLastTimestamp(month, year, true)
	
	local data = mw.text.jsonDecode(frame:expandTemplate{ title = ':Events calendar/events.json', args = {} })
	data = dateFilter(data, timestamp, last_timestamp)
	data = tagFilter(data, tags)
	data = locationFilter(data, locations)
	
	local grid_content = ""
	while timestamp < last_timestamp do
		local line_content = ""
		
		for i=1,7 do
			local current_date = os.date("*t", timestamp)
			local events = formatEvents( frame, dateFilter(data, getFirstDayTimestamp(current_date.year, current_date.month, current_date.day), getLastDayTimestamp(current_date.year, current_date.month, current_date.day)), true )
			local is_today = current_date.month == today.month and current_date.day == today.day and "yes" or ""
			local is_grey = current_date.month ~= tonumber(month) and "yes" or ""
			line_content = line_content .. frame:expandTemplate{ title = 'Events calendar/Table-cell grid', args = { day = current_date.day, event = events, grey = is_grey, today = is_today } }
			timestamp = timestamp + 86400
		end
		
		grid_content = grid_content .. frame:expandTemplate{ title = 'Events calendar/Table-line grid', args = {cells = line_content} }
	end
    return frame:expandTemplate{ title = 'Events calendar/Table grid', args = {lines = grid_content, monday=lang:formatDate( "l", "20010101000000", false), tuesday=lang:formatDate( "l", "20010102000000", false), wednesday=lang:formatDate( "l", "20010103000000", false), thursday=lang:formatDate( "l", "20010104000000", false), friday=lang:formatDate( "l", "20010105000000", false), saturday=lang:formatDate( "l", "20010106000000", false), sunday=lang:formatDate( "l", "20010107000000", false)} }
end

function p.list(frame, year, month, locations, tags)
	local args = frame:getParent().args
	
	local today = os.date("*t")
	local timestamp = getFirstTimestamp(month, year, false)
	local last_timestamp = getLastTimestamp(month, year, false)
	
	local data = mw.text.jsonDecode(frame:expandTemplate{ title = ':Events calendar/events.json', args = {} })
	data = dateFilter(data, timestamp, last_timestamp)
	data = tagFilter(data, tags)
	data = locationFilter(data, locations)
	
	local list_content = ""
	while timestamp < last_timestamp do
		local current_date = os.date("*t", timestamp)
		local events = dateFilter(data, getFirstDayTimestamp(current_date.year, current_date.month, current_date.day), getLastDayTimestamp(current_date.year, current_date.month, current_date.day))
		if #events ~= 0 then
			local is_today = current_date.month == today.month and current_date.day == today.day and "yes" or ""
			list_content = list_content .. frame:expandTemplate{ title = 'Events calendar/Table-line list', args = { day = lang:formatDate( "l d", os.date("%Y%m%d000000", timestamp), false), events = formatEvents( frame, events, false ), today = is_today } }
		end
		timestamp = timestamp + 86400
	end
    return frame:expandTemplate{ title = 'Events calendar/Table list', args = {lines = list_content} }
end

function p.map(frame, year, month, locations, tags)
	local args = frame:getParent().args
	
	local today = os.date("*t")
	local data = mw.text.jsonDecode(frame:expandTemplate{ title = ':Events calendar/events.json', args = {} })
	data = dateFilter(data, getFirstTimestamp(month, year, false), getLastTimestamp(month, year, false))
	data = tagFilter(data, tags)
	data = locationFilter(data, locations)
	data = geolocFilter(data)
	
	if #data == 0 then
		return frame:extensionTag("mapframe", "", {width = "980", height = "500", align = "center", frameless = "", zoom = 2, latitude = 32 , longitude = 5 })
	end
	
	local geojson = {
		type = "FeatureCollection",
		features = {}
	}
	local first_timestamp_of_today = getFirstDayTimestamp(today.year, today.month, today.day)
	local last_timestamp_of_today = getLastDayTimestamp(today.year, today.month, today.day)
	for _,value in pairs(data) do
		local title = value.title
		if value.link ~= nil and value.link ~= "" then
			if mw.ustring.find(value.link, 'https?://') == 1 then
				title = "[[" .. value.link .. "|" .. value.title .. "]]"
			else
				title = "[[" .. value.link .. "|" .. value.title .. "]]"
			end
		end
		
		local properties = {
        	title = title,
        	description = "<small>" .. lang:formatDate( "l d F - H:i", os.date("%Y%m%d%H%M00", value.dtstart), false) .. "</small><br/>" .. value.description,
        }
		properties["marker-color"] = "#e23131"
		if value.dtend < first_timestamp_of_today then
			properties["marker-color"] = "#555555"
		elseif value.dtstart > last_timestamp_of_today then
			properties["marker-color"] = "#6d9ce8"
		end
		table.insert(geojson.features, {
	        type = "Feature",
	        properties = properties,
	        geometry = {
	            type = "Point",
	            coordinates = {
	            	tonumber(value.geoloc.lng), 
	            	tonumber(value.geoloc.lat),
	            }
	        }
	    })
	end
    return frame:extensionTag("mapframe", mw.text.jsonEncode(geojson), {width = "980", height = "500", align = "center", frameless = ""})
end

function p.calendar(frame)
	local args = frame:getParent().args
	local display = args.display
	local lang_code = args.lang == nil and "en" or args.lang
	local year = args.year == nil and os.date("%Y") or args.year
	local month = args.month == nil and os.date("%m") or args.month
	local locations = (args.locations == nil or args.locations == "") and {} or mw.text.split( args.locations, ",", true )
	local tags = (args.tags == nil or args.tags == "") and {} or mw.text.split( args.tags, ",", true )
	
	lang = mw.language.new( lang_code )
	
	local content = ""
	if display == "ical" then
		return p.ical(frame, year, month, locations, tags)
	elseif display == "list" then
		content = p.list(frame, year, month, locations, tags)
	elseif display == "map" then
		content = p.map(frame, year, month, locations, tags)
	else
		display = "grid"
		content = p.grid(frame, year, month, locations, tags)
	end
	
	local container_args = {}
	container_args["class"] = "events-calendar"
	container_args["data-display"] = display
	container_args["data-lang"] = lang_code
	container_args["data-year"] = year
	container_args["data-month"] = month
	container_args["data-locations"] = #locations == 0 and "" or args.locations
	container_args["data-tags"] = #tags == 0 and "" or args.tags
	return frame:extensionTag('div', frame:expandTemplate{ title = 'Events calendar/Header', args = {month = lang:formatDate( "F", year .. string.format("%02d", tonumber(month)) .. "01000000", false ), year = year} } .. content .. frame:expandTemplate{ title = 'Events calendar/Button', args = {} }, container_args)
end


return p