--[[-- remind(1) compatible formatting. --]]-- local inspect = require "ext.inspect" local iso8601dates = true --false --- write ISO 8601 dates local pcall = pcall local io = require "io" local iostdout = io.stdout local math = require "math" local mathfloor = math.floor local os = os local osdate = os.date local ostime = os.time local string = require "string" local stringformat = string.format local stringsub = string.sub local stringgsub = string.gsub local table = require "table" local tableconcat = table.concat local common = require "common" local println = common.println local errorln = common.errorln local noiseln = common.noiseln local debugln = common.debugln local trim_whitespace = common.trim_whitespace local unescape_string = common.unescape_string local internalize_value = common.internalize_value local seconds_of_time = function (t) return t % 60 end local minutes_of_time = function (t) return mathfloor (t / 60) % 60 end local hours_of_time = function (t) return mathfloor (t / (60 * 60)) % 60 end local days_of_time = function (t) return mathfloor (t / (24 * 60 * 60)) end local get_param = function (params, name) local np = #params for i = 1, np do local param = params [i] if param.name == name then return param end end return nil end local get_param_value = function (params, name) local param = get_param (params, name) if param ~= nil then return param.value end return nil end local get_param_value_1 = function (params, name) local val = get_param_value (params, name) if val ~= nil then return val [1] end return nil end local time_f_sec = 1 local time_f_min = 60 * time_f_sec local time_f_hour = 60 * time_f_min local time_f_day = 24 * time_f_hour local rem_of_vcalendars do --[[-- Date format “%Y%M%D” --]]-- local parse_date_value_DATE = function (st, tzid) local Y = tonumber (stringsub (st, 1, 4)) local M = tonumber (stringsub (st, 5, 6)) local D = tonumber (stringsub (st, 7, 8)) return ostime { year = Y, month = M, day = D } end --[[-- Date format 1 date with local time: “%Y%M%DT%h%m%s” Date format 2 date with local time: “%Y%M%DT%h%m%sZ%z” Date format 3 date with local time: “%Y%M%DT%h%m%s” with TZID parm --]]-- local parse_date_value_DATE_TIME = function (st, tzid) if #st < 15 then error (stringformat ("date-time specification [%s] is rubbish", st)) end local Y = tonumber (stringsub (st, 1, 4)) local M = tonumber (stringsub (st, 5, 6)) local D = tonumber (stringsub (st, 7, 8)) assert (stringsub (st, 9, 9) == "T") local h = tonumber (stringsub (st, 10, 11)) local m = tonumber (stringsub (st, 12, 13)) local s = tonumber (stringsub (st, 14, 15)) local t = { year = Y , month = M , day = D , hour = h , min = m , sec = s , isdst = false } if #st == 15 then --- format 3 (with time zone reference) if tzid then error (stringformat ("timezone specification (TZID) for dates \z not implemented, sorry")) end --- format 1 (local) return ostime (t), true end --- format 2 (UTC) assert (stringsub (st, 16, 16) == "Z", stringformat ("rubbish date-time specification %s", st)) return ostime (t), false end --[[-- if params lack a VALUE entry, DATE-TIME is assumed --]]-- local parse_date_value = function (val) local params = val.params local ptzid = get_param_value_1 (params, "TZID") local pdate = get_param_value_1 (params, "VALUE") if pdate == nil or pdate == "DATE-TIME" then return parse_date_value_DATE_TIME (val.value, ptzid) end if pdate == "DATE" then return parse_date_value_DATE (val.value, ptzid), false end return nil end local fmt_date_jumble_loc = "%b %d %Y" local fmt_date_iso8601_loc = "%F" local fmt_date_time_jumble_loc = "%b %d %Y AT %H:%M" local fmt_date_time_iso8601_loc = "%F AT %H:%M" local fmt_date_jumble_utc = "!%b %d %Y" local fmt_date_iso8601_utc = "!%F" local fmt_date_time_jumble_utc = "!%b %d %Y AT %H:%M" local fmt_date_time_iso8601_utc = "!%F AT %H:%M" local fmt_date_loc = iso8601dates and fmt_date_iso8601_loc or fmt_date_jumble_loc local fmt_date_utc = iso8601dates and fmt_date_iso8601_utc or fmt_date_jumble_utc local fmt_date_time_loc = iso8601dates and fmt_date_time_iso8601_loc or fmt_date_time_jumble_loc local fmt_date_time_utc = iso8601dates and fmt_date_time_iso8601_utc or fmt_date_time_jumble_utc local rem_date_time = function (d, loctime) if loctime == true then return osdate (fmt_date_time_loc, d) end return osdate (fmt_date_time_utc, d) end local rem_date = function (d, loctime) if loctime == true then return osdate (fmt_date_loc, d) end return osdate (fmt_date_utc, d) end local rem_duration = function (dt) --[[-- remind(1): Note that duration is specified in hours and minutes. --]]-- local s local ret if dt == 0 then return nil end ret = "" s = dt % 60 ret = ret .. stringformat ("%0.2d", s) dt = mathfloor (dt / 60) if dt == 0 then return ret end return stringformat ("%0.2d:%s", dt, ret) end local repeat_of_duration = function (t0, te) local from = rem_date (t0) local till = rem_date (te) return stringformat ("%s *1 UNTIL %s", from, till) end local rem_date_duration = function (t0, te, loctime) local dt = te - t0 if dt == 0 or dt == time_f_day then --- no duration or exactly 1d → no “at” clause return rem_date (t0, loctime) end if dt > time_f_day then --- duration of multiple days → needs repeat clause return repeat_of_duration (t0, te) end local s_t0 = rem_date_time (t0, loctime) local s_dt = rem_duration (dt) return stringformat ("%s DURATION %s", s_t0, s_dt) end local date_of_interval = function (dtstart, dtend) local t0, loctime = parse_date_value (dtstart) local te, _ignore = parse_date_value (dtend) if t0 ~= nil then if te ~= nil then return rem_date_duration (t0, te, loctime) end return rem_date_time (t0, loctime) end return nil end local string_of_textdata = function (val) local sane = internalize_value (val.value) return trim_whitespace (sane) end local msg_of_string = function (s) return stringgsub (s, "\n", "^M") end local rem_of_vevent = function (ev) local dtstart, dtend local date, msg, msg_desc local vals = ev.value local nvals = #vals for i = 1, nvals do local val = vals [i] if val.kind == "other" then local valname = val.name if valname == "summary" then msg = string_of_textdata (val) elseif valname == "description" then msg_desc = string_of_textdata (val) elseif valname == "dtstart" then dtstart = val elseif valname == "dtend" then dtend = val else debugln ("rem_of_vevent: ignoring value [%s]", valname) end else end end date = date_of_interval (dtstart, dtend) if date == nil then error ("could not derive date from event values") end if msg == nil then error ("could not derive message text from event values") end if msg_desc ~= nil then msg = stringformat ("%s (%s)", msg, msg_desc) end return stringformat ("REM %s MSG %s", date, msg_of_string (msg)) end local rem_of_vcalendar = function (vcal) local acc = { } local vals = vcal.value local nvals = #vals for i = 1, nvals do local val = vals [i] if val.kind ~= "scope" or val.name ~= "VEVENT" then noiseln ("rem_of_vcalendar: ignoring value %d of type [%s/%s]", i, val.kind, val.name) else local ok, res = pcall (rem_of_vevent, val) if ok then acc [#acc + 1] = res else errorln ("rem_of_vcalendar: vevent %d contains invalid entry: %s", i, tostring (res)) end end end return tableconcat (acc, "\n") end rem_of_vcalendars = function (vcals) local value = vcals.value local nvcal = #value local acc = { } for i = 1, nvcal do local vcal = value [i] if vcal.kind ~= "scope" or vcal.name ~= "VCALENDAR" then noiseln ("rem_of_vcalendars: ignoring object %d of type [%s/%s]", i, vcal.kind, vcal.name) else acc [#acc + 1] = rem_of_vcalendar (vcal) end end return tableconcat (acc, "\n") end end --- [rem_of_vcalendars] ------------------------------------------------------------------------------- --- API ------------------------------------------------------------------------------- local output_calendar = function (cal, fh) fh = fh or iostdout local ok, str = pcall (rem_of_vcalendars, cal) if ok then fh:write (str) end end return { output_calendar = output_calendar }