local z=require("\112\108\046\099\108\097\115\115")local e,K=os.time,os.date local D=require("\112\108\046\115\116\114\105\110\103\120")local O=require("\112\108\046\117\116\105\108\115")local n,t=O.assert_arg,O.assert_string O.raise_deprecation({source="\080\101\110\108\105\103\104\116 "..O._VERSION,message="\116\104\101 \'\068\097\116\101\' \109\111\100\117\108\101 \105\115 \100\101\112\114\101\099\097\116\101\100\044 \115\101\101 \104\116\116\112\115\058\047\047\103\105\116\104\117\098\046\099\111\109\047\108\117\110\097\114\109\111\100\117\108\101\115\047\080\101\110\108\105\103\104\116\047\105\115\115\117\101\115\047\050\056\053";version_removed="\050\046\048\046\048";version_deprecated="\049\046\057\046\050"})local b=z()b.Format=z()function b._init(D,z,...)local O local n=select("\035",...)if n>2 then local e={...}local K=z z={year=K;month=e[1];day=e[2],hour=e[3],min=e[4],sec=e[5]}end if n==1 then D.utc=select(1,...)==true end if z==nil or z=="\117\116\099"then O=e()D.utc=z=="\117\116\099"elseif type(z)=="\110\117\109\098\101\114"then O=z if D.utc==nil then D.utc=true end elseif type(z)=="\116\097\098\108\101"then if getmetatable(z)==b then O=z.time D.utc=z.utc else if not(z.year and z.month)then local e=K("\042\116")if not z.year and(not z.month and not z.day)then z.year=e.year z.month=e.month z.day=e.day else z.year=z.year or e.year z.month=z.month or z.day and e.month or 1 z.day=z.day or 1 end end z.day=z.day or 1 O=e(z)end else error("\098\097\100 \116\121\112\101 \102\111\114 \068\097\116\101 \099\111\110\115\116\114\117\099\116\111\114\058 "..type(z),2)end D:set(O)end function b.set(e,z)e.time=z if e.utc then e.tab=K("\033\042\116",z)else e.tab=K("\042\116",z)end end function b.tzone(z)if z==nil then z=e()elseif type(z)=="\116\097\098\108\101"then if getmetatable(z)==b then z=z.time else z=(b(z)).time end end local D=K("\033\042\116",z)local O=K("\042\116",z)O.isdst=false return os.difftime(e(O),e(D))end function b.toUTC(z)local e=b(z)if not z.utc then e.utc=true e:set(e.time)end return e end function b.toLocal(z)local e=b(z)if z.utc then e.utc=false e:set(e.time)end return e end for z,K in ipairs({"\121\101\097\114","\109\111\110\116\104","\100\097\121","\104\111\117\114";"\109\105\110";"\115\101\099","\121\100\097\121"})do b[K]=function(z,D)if D then n(1,D,"\110\117\109\098\101\114")z.tab[K]=D z:set(e(z.tab))return z else return z.tab[K]end end end function b.weekday_name(e,z)return K(z and"\037\065"or"\037\097",e.time)end function b.month_name(e,z)return K(z and"\037\066"or"\037\098",e.time)end function b.is_weekend(z)return z.tab.wday==1 or z.tab.wday==7 end function b.add(K,z)local D=K.tab.isdst local O,n=next(z)K.tab[O]=K.tab[O]+n K:set(e(K.tab))if D~=K.tab.isdst then K.tab.hour=K.tab.hour-(D and 1 or-1)K:set(e(K.tab))end return K end function b.last_day(z)local e=28 local K=z.tab.month while z.tab.month==K do e=e+1 z:add({day=1})end z:add({day=-1})return z end function b.diff(e,z)local K=e.time-z.time if K<0 then error("\100\097\116\101 \100\105\102\102\101\114\101\110\099\101 \105\115 \110\101\103\097\116\105\118\101\033",2)end return b.Interval(K)end function b.__tostring(z)local e="\037\089-\037\109-\037\100\084\037\072\058\037\077\058\037\083"if z.utc then e="\033"..e end local D=K(e,z.time)if z.utc then return D.."\090"else local e=z:tzone()if e==0 then return D.."\090"end local K=e>0 and"\043"or"-"local O=math.ceil(e/3600)local n=(e%3600)/60 if n==0 then return D..("\037\115\037\048\050\100"):format(K,O)else return D..("\037\115\037\048\050\100\058\037\048\050\100"):format(K,O,n)end end end function b.__eq(e,z)return e.time==z.time end function b.__lt(e,z)return e.time<z.time end b.__sub=b.diff function b.__add(e,z)local K=b(e)if b.Interval:class_of(z)then z={sec=z.time}end K:add(z)return K end b.Interval=z(b)function b.Interval._init(e,z)e:set(z)end function b.Interval.set(e,z)e.time=z e.tab=K("\033\042\116",e.time)end local function P(z)if z>1 then return"\115 "else return" "end end function b.Interval.__tostring(z)local e,K=z.tab,""local D,O,n=e.year-1970,e.month-1,e.day-1 if D>0 then K=K..(D..(" \121\101\097\114"..P(D)))end if O>0 then K=K..(O..(" \109\111\110\116\104"..P(O)))end if n>0 then K=K..(n..(" \100\097\121"..P(n)))end if D==0 and O==0 then local z=e.hour if z>0 then K=K..(z..(" \104\111\117\114"..P(z)))end if e.min>0 then K=K..(e.min.." \109\105\110 ")end if e.sec>0 then K=K..(e.sec.." \115\101\099 ")end end if K==""then K="\122\101\114\111"end return K end local A={d={"\100\097\121",{true;true}};y={"\121\101\097\114",{false,true;false,true}};m={"\109\111\110\116\104",{true,true}},H={"\104\111\117\114";{true;true}};M={"\109\105\110",{true,true}},S={"\115\101\099",{true,true}}}function b.Format._init(e,z)if not z then e.fmt="\037\089-\037\109-\037\100 \037\072\058\037\077\058\037\083"e.outf=e.fmt e.plain=true return end local K=table.insert local D,n,t,b="\001","\002","\003","\004"local P,c={},{}local f,a={},{}local U=1 while U<#z do local e=z:sub(U,U)local O=A[e]if O then if c[e]then error("\102\105\101\108\100 \097\112\112\101\097\114\101\100 \116\119\105\099\101\058 "..e,4)end c[e]=true local A,T=z:find(e.."\043",U+1)local p=not A and 1 or(T-U)+1 if not O[2][p]then error("\119\114\111\110\103 \110\117\109\098\101\114 \111\102 \102\105\101\108\100\115\058 "..e,4)end local i=p==1 and D..n or D:rep(p)K(f,t..(i..b))K(P,e)if e=="\121"then K(a,p==2 and"\037\121"or"\037\089")else K(a,"\037"..e)end U=U+p else K(f,e)K(a,e)U=U+1 end end z=O.escape(table.concat(f))z=(((z:gsub(D,"\037\037\100")):gsub(n,"\043")):gsub(t,"\040")):gsub(b,"\041")e.fmt=z e.outf=table.concat(a)e.vars=P end local c function b.Format.parse(e,z)t(1,z)if e.plain then return c(z,e.us)end local K={z:match(e.fmt)}if#K==0 then return nil,"\099\097\110\110\111\116 \112\097\114\115\101 "..z end local D={}for z,e in ipairs(e.vars)do local O=A[e][1]D[O]=tonumber(K[z])end if not(D.year and(D.month and D.day))then local z=b()D.year=D.year or z:year()D.month=D.month or z:month()D.day=D.day or z:day()end local O=D.year if O<100 then D.year=O+(O<35 and 2000 or 1999)elseif not O then D.year=1970 end return b(D)end function b.Format.tostring(e,z)local D local O=e.outf if type(z)=="\110\117\109\098\101\114"then D=z else D=z.time if z.utc then O="\033"..O end end return K(O,D)end function b.Format.US_order(e,z)e.us=z end local f local a local function U()local z,e=a("\050\048\048\048-\049\050-\051\049"),{day=1}f={}for K=1,12,1 do z=z:last_day()z:add(e)local D=(z:month_name()):lower()f[D]=K end end local function T(z)return z:match("\094\037\097\043\044\042\036")~=nil end local p=D.isdigit local function i(z,e,K,D)D=D or""local O=tonumber(z)if not O then error(("\037\115\110\111\116 \097 \110\117\109\098\101\114\058 \'\037\115\'"):format(D,z))end if O<e or O>K then error(("\037\115 \111\117\116 \111\102 \114\097\110\103\101\058 \037\115 \105\115 \110\111\116 \098\101\116\119\101\101\110 \037\100 \097\110\100 \037\100"):format(D,z,e,K))end return O end local function B(z,e,K)local D,O,n=z:find("\094\037\046\037\100\043",e+1)if n then K=K..n z=z:sub(O+1)else z=z:sub(e+1)end if z:match("\122\036")then return K,{h=0,m=0}end z=z:gsub("\058","")local t,b,P,A=z:find("\094\040\091\037\043\037-\093\041\040\037\100\043\041")if not P then return K,nil end if#A==2 then A=A.."\048\048"end local c={h=tonumber(A:sub(1,2)),m=tonumber(A:sub(3,4))}if P=="-"then c.h=-c.h c.m=-c.m end return K,c end function a(z,e)z=z:gsub("\084"," ")local K=D.split(z:lower())local O,n=1,K[1]local function t()O=O+1 n=K[O]end local P,A,c,a,X local F local Y,V,k,l=n:find("\094\040\037\100\043\041\047\040\037\100\043\041")if k then if e then k,l=l,k end Y,Y,P=n:find("\094\047\040\037\100\043\041",V+1)t()else P,l,k=n:match("\094\040\037\100\043\041\037-\040\037\100\043\041\037-\040\037\100\043\041")if P then t()end end if n and(not P and p(n))then if#n<4 then k=n t()else P=true end end if n and T(n)then n=n:sub(1,3)if not f then U()end local z=f[n]if z then l=z else error("\110\111\116 \097 \109\111\110\116\104\058 "..n)end t()end if n and(not P and p(n))then P=n t()end if n then Y,V,c,A=n:find("\094\040\037\100\043\041\058\040\037\100\043\041")local z if V then Y,z,a=n:find("\094\058\040\037\100\043\041",V+1)a,F=B(n,z or V,a)else Y,z,c,A=n:find("\094\040\037\100\043\041\037\046\040\037\100\043\041")if z then X=n:match("\091\097\112\093\109\036")else local z Y,V,z=n:find("\094\040\037\100\043\041")if V then c=z:sub(1,2)A=z:sub(3,4)a=z:sub(5,6)if#a==0 then a=nil end a,F=B(n,V,a)end end end end local y if P==true then P=nil end if not(P and(l and k))then y=b()end k=k and i(k,1,31,"\100\097\121")or l and 1 or y:day()l=l and i(l,1,12,"\109\111\110\116\104")or y:month()P=P and tonumber(P)or y:year()if P<100 then P=P+(P<35 and 2000 or 1900)end c=c and i(c,0,X and 12 or 24,"\104\111\117\114")or 12 if X=="\112\109"then c=c+12 end A=A and i(A,0,59)or 0 a=a and i(a,0,60)or 0 local N=b({year=P;month=l,day=k,hour=c;min=A,sec=a})if F then local z=false if F.h~=0 then N:add({hour=-F.h})z=true end if F.m~=0 then N:add({min=-F.m})z=true end N.utc=true if z then N=N:toLocal()end end return N end function c(z)local e,K=pcall(a,z)if not e then K=K:gsub("\046-\058\037\100\043\058 ","")return nil,K else return K end end return b
--- Date and Date Format classes.
-- See  @{05-dates.md|the Guide}.
--
-- NOTE: the date module is deprecated! see
-- https://github.com/lunarmodules/Penlight/issues/285
--
-- Dependencies: `pl.class`, `pl.stringx`, `pl.utils`
-- @classmod pl.Date
-- @pragma nostrip

local class = require 'pl.class'
local os_time, os_date = os.time, os.date
local stringx = require 'pl.stringx'
local utils = require 'pl.utils'
local assert_arg,assert_string = utils.assert_arg,utils.assert_string


utils.raise_deprecation {
  source = "Penlight " .. utils._VERSION,
  message = "the 'Date' module is deprecated, see https://github.com/lunarmodules/Penlight/issues/285",
  version_removed = "2.0.0",
  version_deprecated = "1.9.2",
}


local Date = class()
Date.Format = class()

--- Date constructor.
-- @param t this can be either
--
--   * `nil` or empty - use current date and time
--   * number - seconds since epoch (as returned by `os.time`). Resulting time is UTC
--   * `Date` - make a copy of this date
--   * table - table containing year, month, etc as for `os.time`. You may leave out year, month or day,
-- in which case current values will be used.
--   * year (will be followed by month, day etc)
--
-- @param ...  true if  Universal Coordinated Time, or two to five numbers: month,day,hour,min,sec
-- @function Date
function Date:_init(t,...)
    local time
    local nargs = select('#',...)
    if nargs > 2 then
        local extra = {...}
        local year = t
        t = {
            year = year,
            month = extra[1],
            day = extra[2],
            hour = extra[3],
            min = extra[4],
            sec = extra[5]
        }
    end
    if nargs == 1 then
        self.utc = select(1,...) == true
    end
    if t == nil or t == 'utc' then
        time = os_time()
        self.utc = t == 'utc'
    elseif type(t) == 'number' then
        time = t
        if self.utc == nil then self.utc = true end
    elseif type(t) == 'table' then
        if getmetatable(t) == Date then -- copy ctor
            time = t.time
            self.utc = t.utc
        else
            if not (t.year and t.month) then
                local lt = os_date('*t')
                if not t.year and not t.month and not t.day then
                    t.year = lt.year
                    t.month = lt.month
                    t.day = lt.day
                else
                    t.year = t.year or lt.year
                    t.month = t.month or (t.day and lt.month or 1)
                    t.day = t.day or 1
                end
            end
            t.day = t.day or 1
            time = os_time(t)
        end
    else
        error("bad type for Date constructor: "..type(t),2)
    end
    self:set(time)
end

--- set the current time of this Date object.
-- @int t seconds since epoch
function Date:set(t)
    self.time = t
    if self.utc then
        self.tab = os_date('!*t',t)
    else
        self.tab = os_date('*t',t)
    end
end

--- get the time zone offset from UTC.
-- @int ts seconds ahead of UTC
function Date.tzone (ts)
    if ts == nil then
        ts = os_time()
    elseif type(ts) == "table" then
        if getmetatable(ts) == Date then
            ts = ts.time
        else
            ts = Date(ts).time
        end
    end
    local utc = os_date('!*t',ts)
    local lcl = os_date('*t',ts)
    lcl.isdst = false
    return os.difftime(os_time(lcl), os_time(utc))
end

--- convert this date to UTC.
function Date:toUTC ()
    local ndate = Date(self)
    if not self.utc then
        ndate.utc = true
        ndate:set(ndate.time)
    end
    return ndate
end

--- convert this UTC date to local.
function Date:toLocal ()
    local ndate = Date(self)
    if self.utc then
        ndate.utc = false
        ndate:set(ndate.time)
--~         ndate:add { sec = Date.tzone(self) }
    end
    return ndate
end

--- set the year.
-- @int y Four-digit year
-- @class function
-- @name Date:year

--- set the month.
-- @int m month
-- @class function
-- @name Date:month

--- set the day.
-- @int d day
-- @class function
-- @name Date:day

--- set the hour.
-- @int h hour
-- @class function
-- @name Date:hour

--- set the minutes.
-- @int min minutes
-- @class function
-- @name Date:min

--- set the seconds.
-- @int sec seconds
-- @class function
-- @name Date:sec

--- set the day of year.
-- @class function
-- @int yday day of year
-- @name Date:yday

--- get the year.
-- @int y Four-digit year
-- @class function
-- @name Date:year

--- get the month.
-- @class function
-- @name Date:month

--- get the day.
-- @class function
-- @name Date:day

--- get the hour.
-- @class function
-- @name Date:hour

--- get the minutes.
-- @class function
-- @name Date:min

--- get the seconds.
-- @class function
-- @name Date:sec

--- get the day of year.
-- @class function
-- @name Date:yday


for _,c in ipairs{'year','month','day','hour','min','sec','yday'} do
    Date[c] = function(self,val)
        if val then
            assert_arg(1,val,"number")
            self.tab[c] = val
            self:set(os_time(self.tab))
            return self
        else
            return self.tab[c]
        end
    end
end

--- name of day of week.
-- @bool full abbreviated if true, full otherwise.
-- @ret string name
function Date:weekday_name(full)
    return os_date(full and '%A' or '%a',self.time)
end

--- name of month.
-- @int full abbreviated if true, full otherwise.
-- @ret string name
function Date:month_name(full)
    return os_date(full and '%B' or '%b',self.time)
end

--- is this day on a weekend?.
function Date:is_weekend()
    return self.tab.wday == 1 or self.tab.wday == 7
end

--- add to a date object.
-- @param t a table containing one of the following keys and a value:
-- one of `year`,`month`,`day`,`hour`,`min`,`sec`
-- @return this date
function Date:add(t)
    local old_dst = self.tab.isdst
    local key,val = next(t)
    self.tab[key] = self.tab[key] + val
    self:set(os_time(self.tab))
    if old_dst ~= self.tab.isdst then
        self.tab.hour = self.tab.hour - (old_dst and 1 or -1)
        self:set(os_time(self.tab))
    end
    return self
end

--- last day of the month.
-- @return int day
function Date:last_day()
    local d = 28
    local m = self.tab.month
    while self.tab.month == m do
        d = d + 1
        self:add{day=1}
    end
    self:add{day=-1}
    return self
end

--- difference between two Date objects.
-- @tparam Date other Date object
-- @treturn Date.Interval object
function Date:diff(other)
    local dt = self.time - other.time
    if dt < 0 then error("date difference is negative!",2) end
    return Date.Interval(dt)
end

--- long numerical ISO data format version of this date.
function Date:__tostring()
    local fmt = '%Y-%m-%dT%H:%M:%S'
    if self.utc then
        fmt = "!"..fmt
    end
    local t = os_date(fmt,self.time)
    if self.utc then
        return  t .. 'Z'
    else
        local offs = self:tzone()
        if offs == 0 then
            return t .. 'Z'
        end
        local sign = offs > 0 and '+' or '-'
        local h = math.ceil(offs/3600)
        local m = (offs % 3600)/60
        if m == 0 then
            return t .. ('%s%02d'):format(sign,h)
        else
            return t .. ('%s%02d:%02d'):format(sign,h,m)
        end
    end
end

--- equality between Date objects.
function Date:__eq(other)
    return self.time == other.time
end

--- ordering between Date objects.
function Date:__lt(other)
    return self.time < other.time
end

--- difference between Date objects.
-- @function Date:__sub
Date.__sub = Date.diff

--- add a date and an interval.
-- @param other either a `Date.Interval` object or a table such as
-- passed to `Date:add`
function Date:__add(other)
    local nd = Date(self)
    if Date.Interval:class_of(other) then
        other = {sec=other.time}
    end
    nd:add(other)
    return nd
end

Date.Interval = class(Date)

---- Date.Interval constructor
-- @int t an interval in seconds
-- @function Date.Interval
function Date.Interval:_init(t)
    self:set(t)
end

function Date.Interval:set(t)
    self.time = t
    self.tab = os_date('!*t',self.time)
end

local function ess(n)
    if n > 1 then return 's '
    else return ' '
    end
end

--- If it's an interval then the format is '2 hours 29 sec' etc.
function Date.Interval:__tostring()
    local t, res = self.tab, ''
    local y,m,d = t.year - 1970, t.month - 1, t.day - 1
    if y > 0 then res = res .. y .. ' year'..ess(y) end
    if m > 0 then res = res .. m .. ' month'..ess(m) end
    if d > 0 then res = res .. d .. ' day'..ess(d) end
    if y == 0 and m == 0 then
        local h = t.hour
        if h > 0 then res = res .. h .. ' hour'..ess(h) end
        if t.min > 0 then res = res .. t.min .. ' min ' end
        if t.sec > 0 then res = res .. t.sec .. ' sec ' end
    end
    if res == '' then res = 'zero' end
    return res
end

------------ Date.Format class: parsing and renderinig dates ------------

-- short field names, explicit os.date names, and a mask for allowed field repeats
local formats = {
    d = {'day',{true,true}},
    y = {'year',{false,true,false,true}},
    m = {'month',{true,true}},
    H = {'hour',{true,true}},
    M = {'min',{true,true}},
    S = {'sec',{true,true}},
}

--- Date.Format constructor.
-- @string fmt. A string where the following fields are significant:
--
--   * d day (either d or dd)
--   * y year (either yy or yyy)
--   * m month (either m or mm)
--   * H hour (either H or HH)
--   * M minute (either M or MM)
--   * S second (either S or SS)
--
-- Alternatively, if fmt is nil then this returns a flexible date parser
-- that tries various date/time schemes in turn:
--
--  * [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601), like `2010-05-10 12:35:23Z` or `2008-10-03T14:30+02`
--  * times like 15:30 or 8.05pm  (assumed to be today's date)
--  * dates like 28/10/02 (European order!) or 5 Feb 2012
--  * month name like march or Mar (case-insensitive, first 3 letters); here the
-- day will be 1 and the year this current year
--
-- A date in format 3 can be optionally followed by a time in format 2.
-- Please see test-date.lua in the tests folder for more examples.
-- @usage df = Date.Format("yyyy-mm-dd HH:MM:SS")
-- @class function
-- @name Date.Format
function Date.Format:_init(fmt)
    if not fmt then
        self.fmt = '%Y-%m-%d %H:%M:%S'
        self.outf = self.fmt
        self.plain = true
        return
    end
    local append = table.insert
    local D,PLUS,OPENP,CLOSEP = '\001','\002','\003','\004'
    local vars,used = {},{}
    local patt,outf = {},{}
    local i = 1
    while i < #fmt do
        local ch = fmt:sub(i,i)
        local df = formats[ch]
        if df then
            if used[ch] then error("field appeared twice: "..ch,4) end
            used[ch] = true
            -- this field may be repeated
            local _,inext = fmt:find(ch..'+',i+1)
            local cnt = not _ and 1 or inext-i+1
            if not df[2][cnt] then error("wrong number of fields: "..ch,4) end
            -- single chars mean 'accept more than one digit'
            local p = cnt==1 and (D..PLUS) or (D):rep(cnt)
            append(patt,OPENP..p..CLOSEP)
            append(vars,ch)
            if ch == 'y' then
                append(outf,cnt==2 and '%y' or '%Y')
            else
                append(outf,'%'..ch)
            end
            i = i + cnt
        else
            append(patt,ch)
            append(outf,ch)
            i = i + 1
        end
    end
    -- escape any magic characters
    fmt = utils.escape(table.concat(patt))
   -- fmt = table.concat(patt):gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1')
    -- replace markers with their magic equivalents
    fmt = fmt:gsub(D,'%%d'):gsub(PLUS,'+'):gsub(OPENP,'('):gsub(CLOSEP,')')
    self.fmt = fmt
    self.outf = table.concat(outf)
    self.vars = vars
end

local parse_date

--- parse a string into a Date object.
-- @string str a date string
-- @return date object
function Date.Format:parse(str)
    assert_string(1,str)
    if self.plain then
        return parse_date(str,self.us)
    end
    local res = {str:match(self.fmt)}
    if #res==0 then return nil, 'cannot parse '..str end
    local tab = {}
    for i,v in ipairs(self.vars) do
        local name = formats[v][1] -- e.g. 'y' becomes 'year'
        tab[name] = tonumber(res[i])
    end
    -- os.date() requires these fields; if not present, we assume
    -- that the time set is for the current day.
    if not (tab.year and tab.month and tab.day) then
        local today = Date()
        tab.year = tab.year or today:year()
        tab.month = tab.month or today:month()
        tab.day = tab.day or today:day()
    end
    local Y = tab.year
    if Y < 100 then -- classic Y2K pivot
        tab.year = Y + (Y < 35 and 2000 or 1999)
    elseif not Y then
        tab.year = 1970
    end
    return Date(tab)
end

--- convert a Date object into a string.
-- @param d a date object, or a time value as returned by @{os.time}
-- @return string
function Date.Format:tostring(d)
    local tm
    local fmt = self.outf
    if type(d) == 'number' then
        tm = d
    else
        tm = d.time
        if d.utc then
            fmt = '!'..fmt
        end
    end
    return os_date(fmt,tm)
end

--- force US order in dates like 9/11/2001
function Date.Format:US_order(yesno)
    self.us = yesno
end

--local months = {jan=1,feb=2,mar=3,apr=4,may=5,jun=6,jul=7,aug=8,sep=9,oct=10,nov=11,dec=12}
local months
local parse_date_unsafe
local function create_months()
    local ld, day1 = parse_date_unsafe '2000-12-31', {day=1}
    months = {}
    for i = 1,12 do
        ld = ld:last_day()
        ld:add(day1)
        local mon = ld:month_name():lower()
        months [mon] = i
    end
end

--[[
Allowed patterns:
- [day] [monthname] [year] [time]
- [day]/[month][/year] [time]

]]

local function looks_like_a_month(w)
    return w:match '^%a+,*$' ~= nil
end
local is_number = stringx.isdigit
local function tonum(s,l1,l2,kind)
    kind = kind or ''
    local n = tonumber(s)
    if not n then error(("%snot a number: '%s'"):format(kind,s)) end
    if n < l1 or n > l2 then
        error(("%s out of range: %s is not between %d and %d"):format(kind,s,l1,l2))
    end
    return n
end

local function  parse_iso_end(p,ns,sec)
    -- may be fractional part of seconds
    local _,nfrac,secfrac = p:find('^%.%d+',ns+1)
    if secfrac then
        sec = sec .. secfrac
        p = p:sub(nfrac+1)
    else
        p = p:sub(ns+1)
    end
    -- ISO 8601 dates may end in Z (for UTC) or [+-][isotime]
    -- (we're working with the date as lower case, hence 'z')
    if p:match 'z$' then -- we're UTC!
        return  sec, {h=0,m=0}
    end
    p = p:gsub(':','') -- turn 00:30 to 0030
    local _,_,sign,offs = p:find('^([%+%-])(%d+)')
    if not sign then return sec, nil end -- not UTC

    if #offs == 2 then offs = offs .. '00' end -- 01 to 0100
    local tz = { h = tonumber(offs:sub(1,2)), m = tonumber(offs:sub(3,4)) }
    if sign == '-' then tz.h = -tz.h; tz.m = -tz.m end
    return sec, tz
end

function parse_date_unsafe (s,US)
    s = s:gsub('T',' ') -- ISO 8601
    local parts = stringx.split(s:lower())
    local i,p = 1,parts[1]
    local function nextp() i = i + 1; p = parts[i] end
    local year,min,hour,sec,apm
    local tz
    local _,nxt,day, month = p:find '^(%d+)/(%d+)'
    if day then
        -- swop for US case
        if US then
            day, month = month, day
        end
        _,_,year = p:find('^/(%d+)',nxt+1)
        nextp()
    else -- ISO
        year,month,day = p:match('^(%d+)%-(%d+)%-(%d+)')
        if year then
            nextp()
        end
    end
    if p and not year and is_number(p) then -- has to be date
        if #p < 4 then
            day = p
            nextp()
        else -- unless it looks like a 24-hour time
            year = true
        end
    end
    if p and looks_like_a_month(p) then -- date followed by month
        p = p:sub(1,3)
        if not months then
            create_months()
        end
        local mon = months[p]
        if mon then
            month = mon
        else error("not a month: " .. p) end
        nextp()
    end
    if p and not year and is_number(p) then
        year = p
        nextp()
    end

    if p then -- time is hh:mm[:ss], hhmm[ss] or H.M[am|pm]
        _,nxt,hour,min = p:find '^(%d+):(%d+)'
        local ns
        if nxt then -- are there seconds?
            _,ns,sec = p:find ('^:(%d+)',nxt+1)
            --if ns then
                sec,tz = parse_iso_end(p,ns or nxt,sec)
            --end
        else -- might be h.m
            _,ns,hour,min = p:find '^(%d+)%.(%d+)'
            if ns then
                apm = p:match '[ap]m$'
            else  -- or hhmm[ss]
                local hourmin
                _,nxt,hourmin = p:find ('^(%d+)')
                if nxt then
                   hour = hourmin:sub(1,2)
                   min = hourmin:sub(3,4)
                   sec = hourmin:sub(5,6)
                   if #sec == 0 then sec = nil end
                   sec,tz = parse_iso_end(p,nxt,sec)
                end
            end
        end
    end
    local today
    if year == true then year = nil end
    if not (year and month and day) then
        today = Date()
    end
    day = day and tonum(day,1,31,'day') or (month and 1 or today:day())
    month = month and tonum(month,1,12,'month') or today:month()
    year = year and tonumber(year) or today:year()
    if year < 100 then -- two-digit year pivot around year < 2035
        year = year + (year < 35 and 2000 or 1900)
    end
    hour = hour and tonum(hour,0,apm and 12 or 24,'hour') or 12
    if apm == 'pm' then
        hour = hour + 12
    end
    min = min and tonum(min,0,59) or 0
    sec = sec and tonum(sec,0,60) or 0  --60 used to indicate leap second
    local res = Date {year = year, month = month, day = day, hour = hour, min = min, sec = sec}
    if tz then -- ISO 8601 UTC time
        local corrected = false
        if tz.h ~= 0 then res:add {hour = -tz.h}; corrected = true end
        if tz.m ~= 0 then res:add {min = -tz.m}; corrected = true end
        res.utc = true
        -- we're in UTC, so let's go local...
        if corrected then
            res = res:toLocal()
        end-- we're UTC!
    end
    return res
end

function parse_date (s)
    local ok, d = pcall(parse_date_unsafe,s)
    if not ok then -- error
        d = d:gsub('.-:%d+: ','')
        return nil, d
    else
        return d
    end
end

return Date

