Jump to content

Module:Calendar date/sandbox

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by GreenC (talk | contribs) at 02:44, 31 August 2018. The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
--[[ 

Display Gregorian date of a holiday that moves year to year. Date data can be obtained from multiple sources as configured in ~/Configuration.js

  "localfile" = local JSON data file (eg. https://en.wikipedia.org/wiki/Template:Calendar_date/holidays/Hanukkah.js)
  "calculator" = user-supplied date calculator (eg. )
  "wikidata" = for holidays with their own date entity page such as https://www.wikidata.org/wiki/Q51224536
               
 ]]

local p = {}
local cfg
local eventdata

--[[--------------------------< inlineError >-----------------------

     Critical error. Render output completely in red. Add to tracking category.

 ]]

local function inlineError(arg, msg, tname)

  track["Category:Calendar date template errors"] = 1
  return '<span style="font-size:100%" class="error citation-comment">Error in {{' .. tname .. '}} - Check <code style="color:inherit; border:inherit; padding:inherit;">&#124;' .. arg .. '=</code>  ' .. msg .. '</span>'

end

--[[--------------------------< trimArg >-----------------------

     trimArg returns nil if arg is "" while trimArg2 returns 'true' if arg is "" 
     trimArg2 is for args that might accept an empty value, as an on/off switch like nolink=

 ]]

local function trimArg(arg)
  if arg == "" or arg == nil then
    return nil
  else
    return mw.text.trim(arg)
  end
end
local function trimArg2(arg)
  if arg == nil then
    return nil
  else
    return mw.text.trim(arg)
  end
end

--[[--------------------------< tableLength >-----------------------

      Given a 1-D table, return number of elements

  ]]

local function tableLength(T)
  local count = 0
  for _ in pairs(T) do count = count + 1 end
  return count
end

--[[--------------------------< createTracking >-----------------------

     Return data in track[] ie. tracking categories

  ]]

local function createTracking()

  local sand = ""
  if tableLength(track) > 0 then                        
    for key,_ in pairs(track) do
      sand = sand .. "[[" .. key .. "]]"
    end
  end
  return sand

end

--[[--------------------------< isValidDate >----------------------------------------------------

Returns true if date is after 31 December 1899 , not after 2100, and represents a valid date (29 February 2017 is not a valid date).  Applies Gregorian leapyear rules. All arguments are required.

]]

local function isValidDate (year, month, day)

    local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
    local month_length
    local y, m, d
    local today = os.date ('*') -- fetch a table of current date parts

    if not year or year == '' or not month or month == '' or not day or day == '' then
        return false            -- something missing
    end

    y = tonumber (year)
    m = tonumber (month)
    d = tonumber (day)

    if 1900 > y or 2100 < y or 1 > m or 12 < m then                 -- year and month are within bounds
      return false
    end

    if (2==m) then                                        -- if February
      month_length = 28                                   -- then 28 days unless
      if (0==(y%4) and (0~=(y%100) or 0==(y%400))) then   -- is a leap year?
        month_length = 29                                 -- if leap year then 29 days in February
      end
    else
      month_length=days_in_month[m];
    end

    if 1 > d or month_length < d then                     -- day is within bounds
      return false
    end
    -- here when date parts represent a valid date
    -- return os.time({['hour']=0, ['day']=d, ['month']=m, ['year']=y}) <= os.time()  -- date at midnight must be less than or equal to current date/time
    return true
end

--[[--------------------------< makeDate >-----------------------

     Given a zero-padded 4-digit year, 2-digit month and 2-digit day, return a full date in df format
     df = mdy, dmy, iso, ymd

 ]]

local function makeDate(year, month, day, df, format)
        local formatFull = {
                ['dmy'] = 'j F Y',
                ['mdy'] = 'F j, Y',
                ['ymd'] = 'Y F j',
                ['iso'] = 'Y-m-d'
        }
        local formatInfobox = {
                ['dmy'] = 'j F',
                ['mdy'] = 'F j',
                ['ymd'] = 'F j',
                ['iso'] = 'Y-m-d'
        }

        if not year or year == "" or not month or month == "" or not day or day == "" and format[df] then
          return nil
        end

        local date = table.concat ({year, month, day})               -- assemble iso format date
        if format ~= "infobox" then
          return mw.getContentLanguage():formatDate (formatFull[df], date)
        else
          return mw.getContentLanguage():formatDate (formatInfobox[df], date)
        end
end

--[[--------------------------< dateOffset >-----------------------

     Given a 'origdate' in ISO format, return the date offset by number of days in 'offset' 
        eg. given "2018-02-01" and "-1" it will return "2018-01-30"
     On error, return origdate

  ]]

function dateOffset(origdate, offset)

    local year, month, day = origdate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
    local now = os.time{year = year, month = month, day = day}
    local newdate = os.date("%Y-%m-%d", now + (tonumber(offset) * 24 * 3600))
    return newdate and newdate or origdate

  end

--[[--------------------------< renderHoli >-----------------------

     Render the data

  ]]
  
function renderHoli(cfg,eventdata,calcdate,date,df,format,tname,cite)

  local hits = 0
  local matchdate = "^" .. date
  local startdate,enddate,outoffset,endoutoffset = nil
  local starttitle,endtitle = ""  

  -- user-supplied date calculator 
  if cfg.datatype == "calculator" then
    if cfg.datasource then
      startdate = calcdate
      enddate = dateOffset(startdate, cfg.days - 1)
    else
      return inlineError("holiday", 'Invalid calculator result', tname )
    end

  -- read dates from localfile -- it assumes dates in the json are in chrono order, needs a more flexible method
  elseif cfg.datatype == "localfile" then                                              
    local numRecords = tableLength(eventdata) -- Get first and last date of holiday
    for i = 1, numRecords do
      if mw.ustring.find( eventdata[i].date, matchdate ) then
        if hits == 0 then
          startdate = eventdata[i].date
          hits = 1
        end
        if hits >= tonumber(cfg.days) then
          enddate = eventdata[i].date
          break
        end
        hits = hits + 1
      end
    end
  end
     
  -- Verify data is OK
  if startdate == nil or enddate == nil then 
    if cfg.holiday == "Hanukkah" and startdate and not enddate then  -- Hanukkah bug, template doesn't support cross-year boundary
      enddate = dateOffset(startdate, 8)
    else
      return nil
    end
  end
     
  -- Generate start-date offset (ie. holiday starts the evening before the given date)
  if cfg.startoffset then
    startdate = dateOffset(startdate, cfg.startoffset)
    if startdate ~= enddate then
      enddate = dateOffset(enddate, cfg.startoffset)
    else
      cfg.days = (cfg.days == "1") and "2"
    end
  end
 
  -- Generate end-date outside-Irael offset (ie. outside Israel the holiday ends +1 day later)
  endoutoffset = cfg.endoutoffset and dateOffset(enddate, cfg.endoutoffset)

  -- Format dates into df format 
  local year, month, day = startdate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
  startdate = makeDate(year, month, day, df, format)
  year, month, day = enddate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
  enddate = makeDate(year, month, day, df, format)
  if startdate == nil or enddate == nil then return nil end

  -- Add "outside of Israel" notices
  if endoutoffset then
    year, month, day = endoutoffset:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
    local leader = ((format == "infobox") and "<br>") or " "
    endoutoffset = leader .. "(" .. makeDate(year, month, day, df, "infobox") .. " outside of Israel)"
  end
  if not endoutoffset then
    endoutoffset = ""
  end

  --- Determine format string
  format = ((format == "infobox") and " –<br>") or " – "

  --- Determine pre-pended text string eg. "sunset, <date>"
  local prepend1 = (cfg.prepend1 and (cfg.prepend1 .. ", ")) or ""
  local prepend2 = (cfg.prepend2 and (cfg.prepend2 .. ", ")) or ""

  -- return output
  if startdate == enddate or cfg.days == "1" then            -- single date
    return prepend1 .. startdate .. endoutoffset .. cite
  else
    return prepend1 .. startdate .. format .. prepend2 .. enddate .. endoutoffset .. cite
  end
      
end


--[[--------------------------< calendardate >-----------------------

     Main function

  ]]

function p.calendardate(frame)

  local pframe = frame:getParent()
  local args = pframe.args

  local tname = "Calendar date"                  -- name of calling template. Change if template rename.
  local holiday = nil                            -- name of holiday
  local date = nil                               -- date of holiday (year) 
  local df = nil                                 -- date format (mdy, dmy, iso - default: iso)
  local format = nil                             -- template display format options
  local cite = nil                               -- leave a citation at end 
  local calcdate = ""             

  track = {}                                     -- global tracking-category table

  --- Determine holiday
  holiday = trimArg(args.holiday)                -- required
  if not holiday then
    holiday = trimArg(args.event)                -- event alias
    if not holiday then
      return inlineError("holiday", "Missing holiday argument", tname) .. createTracking()
    end
  end

  --- Determine date
  date = trimArg(args.year)                      -- required
  if not date then
    return inlineError("year", "Missing year argument", tname) .. createTracking()
  elseif not isValidDate(date, "01", "01") then
    return inlineError("year", "Invalid year", tname) .. createTracking()
  end

  --- Determine format type
  format = trimArg(args.format)
  if not format then
    format = "none"
  elseif format ~= "infobox" then
    format = "none"
  end 

  -- Load configuration file
  eventsfile = mw.loadData ('Module:Calendar date/Events')
  if eventsfile.hebrew_calendar[holiday] then
    cfg = eventsfile.hebrew_calendar[holiday]
  elseif eventsfile.misc_events[holiday] then
    cfg = eventsfile.misc_events[holiday]
  else
    return inlineError("holiday", '{{Calendar date}} – unknown holiday ' .. holiday, tname) .. createTracking()
  end

  -- If datatype = localfile 
  if cfg.datatype == "localfile" then
    eventfile = mw.loadData ('Module:Calendar date/localfiles/' .. holiday)
    if eventfile.event then
      eventdata = eventfile.event
    else
      return inlineError("holiday", '{{Calendar date}} – unknown holiday file Module:Calendar date/localfiles/' .. holiday .. '</span>', tname) .. createTracking()
    end

  -- If datatype = calculator
  elseif cfg.datatype == "calculator" then
    calcdate = frame:preprocess(cfg.datasource:gsub("YYYY", date))
    local year, month, day = calcdate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
    if not isValidDate(year, month, day) then
      return inlineError("holiday", '{{Calendar date}} – invalid calculated date ' .. calcdate, tname) .. createTracking()
    end
  else
    return inlineError("holiday", '{{Calendar date}} – unknown "datatype" in configuration', tname) .. createTracking()
  end

  --- Determine df - priority to |df in template, otherwise df in datafile, otherwise default to dmy
  df = trimArg(args.df)
  if not df then
    df = (cfg.df and cfg.df) or "dmy"
  end
  if df ~= "mdy" and df ~= "dmy" and df ~= "iso" then
    df = "dmy"
  end

  -- Determine citation
  cite = trimArg2(args.cite)
  if cite then
    cite = ""
    if cfg.datatype == "localfile" then
      if cfg.citeurl and cfg.accessdate and cfg.source and cfg.holiday then
        cite = frame:preprocess('<ref name="' .. holiday .. ' dates">{{cite web |url=' .. cfg.citeurl .. ' |title=Dates for ' .. cfg.name .. ' |publisher=' .. cfg.source .. ' |via=[[Template:' .. tname .. '|' .. tname .. ']] and [[Module:' .. tname .. '/localfiles/' .. holiday .. '|' .. holiday .. ']] |accessdate=' .. cfg.accessdate .. '}}</ref>')
      end
    elseif cfg.datatype == "calculator" then
      cite = (cfg.source and (frame:preprocess('<ref name="' .. holiday .. ' dates">' .. cfg.source .. '</ref>')))
    end
  else
    cite = ""
  end

  -- Render 
  local rend = renderHoli( cfg,eventdata,calcdate,date,df,format,tname,cite)
  if not rend then
    rend = '<span style="font-size:100%" class="error citation-comment">Error in [[:Template:' .. tname .. ']]: Unknown problem. Please report on template talk page.</span>'
    track["Category:Webarchive template errors"] = 1 
  end

  return rend .. createTracking()

end

return p