Modul:DateTime

Aus MeinFlöha-Wiki
Zur Navigation springen Zur Suche springen

Die Dokumentation für dieses Modul kann unter Modul:DateTime/Doku erstellt werden

local DateTime   = { serial = "2020-08-30",
                     suite  = "DateTime",
                     item   = 20652535 }
-- Date and time objects
local Failsafe   = DateTime
local GlobalMod  = DateTime
local Calc       = { }
local Meta       = { }
local Parser     = { }
local Private    = { }
local Prototypes = { }
local Templates  = { }
local World      = { slang       = "en",
                     monthsLong  = { },
                     monthsParse = { },
                     months4     = { } }
local MaxYear    = 2099
local Frame
DateTime.char = { nbsp = mw.ustring.char( 160 ),
                  tab  = mw.ustring.char( 9 ) }
World.era = { en = { "BC", "AD" } }
World.monthsAbbr = {  en = { n = 3 }  }
World.monthsLong.en = { "January",
                        "February",
                        "March",
                        "April",
                        "May",
                        "June",
                        "July",
                        "August",
                        "September",
                        "October",
                        "November",
                        "December"
                      }
World.monthsParse.en = { [ "Apr" ] =  4,
                         [ "Aug" ] =  8,
                         [ "Dec" ] = 12,
                         [ "Feb" ] =  2,
                         [ "Jan" ] =  1,
                         [ "Jul" ] =  7,
                         [ "Jun" ] =  6,
                         [ "Mar" ] =  3,
                         [ "May" ] =  5,
                         [ "Nov" ] = 11,
                         [ "Oct" ] = 10,
                         [ "Sep" ] =  9
                       }
World.months4.en = { [ 6 ] = true,
                     [ 7 ] = true }
World.templates = { [ "ISO" ] =
                        { spec = "Y-m-d",
                          lift = true },
                    [ "ISO-T" ] =
                        { spec = "c" },
                    [ "timestamp" ] =
                        { spec = "YmdHis" },
                    [ "default" ] =
                        { spec = "H:i, j M Y",
                          long = true },
                    [ "$dmy" ] =
                        { spec = "H:i, j M Y",
                          long = true },
                    [ "$ymd" ] =
                        { spec = "H:i, Y M j",
                          long = true  },
                    [ "$dmyt" ] =
                        { spec = "j M Y, H:i",
                          long = true  },
                    [ "$dmyts" ] =
                        { spec = "j M Y, H:i:s",
                          long = true  },
                    [ "data-sort-type:date" ] =
                        { spec = "j M Y" }
                  }
World.templates.en = { }
World.zones = {
    [ "!" ] = "YXWVUTSRQPONZABCDEFGHIKLM",
    UTC  =   0,
    GMT  =   0       -- Greenwich Mean Time
}
World.zones.en = {
    BST  =   100,    -- British Summer Time
    IST  =   100,    -- Irish Summer Time
    WET  =   0,      -- Western Europe Time
    WEST =   100,    -- Western Europe Summer Time
    CET  =   100,    -- Central Europe Time
    CEST =   200,    -- Central Europe Summer Time
    EET  =   200,    -- Eastern Europe Time
    EEST =   300,    -- Eastern Europe Summer Time
    MSK  =   300,    -- Moscow Time
    MSD  =   400,    -- Moscow Summer Time
    NST  =  -330,    -- Newfoundland Standard Time
    NDT  =  -230,    -- Newfoundland Daylight Time
    AST  =  -400,    -- Atlantic Standard Time
    ADT  =  -300,    -- Atlantic Daylight Time
    EST  =  -500,    -- Eastern Standard Time
    EDT  =  -400,    -- Eastern Daylight Saving Time
    CST  =  -600,    -- Central Standard Time
    CDT  =  -500,    -- Central Daylight Saving Time
    MST  =  -700,    -- Mountain Standard Time
    MDT  =  -600,    -- Mountain Daylight Saving Time
    PST  =  -800,    -- Pacific Standard Time
    PDT  =  -700,    -- Pacific Daylight Saving Time
    AKST =  -900,    -- Alaska Standard Time
    AKDT =  -800,    -- Alaska Standard Daylight Saving Time
    HST  = -1000     -- Hawaiian Standard Time
}



local foreignModule = function ( access, advanced, append, alt, alert )
    -- Fetch global module
    -- Precondition:
    --     access    -- string, with name of base module
    --     advanced  -- true, for require(); else mw.loadData()
    --     append    -- string, with subpage part, if any; or false
    --     alt       -- number, of wikidata item of root; or false
    --     alert     -- true, for throwing error on data problem
    -- Postcondition:
    --     Returns whatever, probably table
    -- 2020-01-01
    local storage = access
    local fun, lucky, r
    if advanced then
        fun = require
    else
        fun = mw.loadData
    end
    if append then
        storage = string.format( "%s/%s", storage, append )
    end
    lucky, r = pcall( fun,  "Module:" .. storage )
    if not lucky then
        local suited
        GlobalMod.globalModules = GlobalMod.globalModules or { }
        suited = GlobalMod.globalModules[ access ]
        if not suited  and
           type( alt ) == "number"  and
           alt > 0 then
            suited = string.format( "Q%d", alt )
            suited = mw.wikibase.getSitelink( suited )
            GlobalMod.globalModules[ access ] = suited or true
        end
        if type( suited ) == "string" then
            storage = suited
            if append then
                storage = string.format( "%s/%s", storage, append )
            end
            lucky, r = pcall( fun, storage )
        end
        if not lucky and alert then
            error( "Missing or invalid page: " .. storage )
        end
    end
    return r
end -- foreignModule()



local function capitalize( a )
    -- Upcase first character, downcase anything else
    -- Parameter:
    --     a  -- string
    -- Returns:
    --     string
    return  mw.ustring.upper( mw.ustring.sub( a, 1, 1 ) )
            .. mw.ustring.lower( mw.ustring.sub( a, 2 ) )
end -- capitalize()



local function fault( a )
    -- Format error message by class=error
    -- Parameter:
    --     a  -- string, error message
    -- Returns:
    --     string, HTML span
    local e = mw.html.create( "span" )
                     :addClass( "error" )
                     :wikitext( a )
    return tostring( e )
end -- fault()



local function frame()
    if not Frame then
        Frame = mw.getCurrentFrame()
    end
    return Frame
end -- frame()



Meta.localized  = false
Meta.serial     = DateTime.serial
Meta.signature  = "__datetime"
Meta.suite      = "{DateTime}"
Meta.components = { lang  = "string",
                    bc    = "boolean",
                    year  = "number",
                    month = "number",
                    week  = "number",
                    dom   = "number",
                    hour  = "number",
                    min   = "number",
                    sec   = "number",
                    msec  = "number",
                    mysec = "number",
                    zone  = false,
                    leap  = "boolean",
                    jul   = "boolean" }
Meta.order      = { "bc", "year", "month", "week", "dom",
                    "hour", "min", "sec", "msec", "mysec" }
Meta.tableI    = {    -- instance metatable
    __index    = function ( self, access )
                     local r = self[ Meta.signature ][ access ]
                     if r == nil then
                         if access == "serial" then
                             r = Meta.serial
                         elseif access == "suite" then
                             r = "DateTime"
                         else
                             r = Prototypes[ access ]
                         end
                     end
                     return r
                 end,
    __newindex = function ( self, access, assign )
                     if type( access ) == "string" then
                         local data = self[ Meta.signature ]
                         if assign == nil then
                             local val = data[ access ]
                             data[ access ] = nil
                             if not Prototypes.fair( data ) then
                                 data[ access ] = val
                             end
                         elseif Prototypes.fair( data,
                                                 access,
                                                 assign ) then
                             data[ access ] = assign
                         end
                     end
                     return
                 end,
    __add      = function ( op1, op2 )
                     return Prototypes.future( op1, op2, true )
                 end,
    __eq       = function ( op1, op2 )
                     return Prototypes.flow( op1, op2, "eq" )
                 end,
    __lt       = function ( op1, op2 )
                     return Prototypes.flow( op1, op2, "lt" )
                 end,
    __le       = function ( op1, op2 )
                     return Prototypes.flow( op1, op2, "le" )
                 end,
    __tostring = function ( e )
                     return Prototypes.tostring( e )
                 end,
    __call     = function ( func, ... )
                     return Meta.fiat( ... )
                 end
} -- Meta.tableI
Meta.tableL    = {    -- library metatable
    __index    = function ( self, access )
                     local r
                     if access == "serial" then
                         r = Meta.serial
                     elseif access == "suite" then
                         r = Meta.suite
                     end
                     return r
                 end,
    __newindex = function ()
                     return
                 end,
    __tostring = function ()
                     return Meta.suite
                 end,
    __call     = function ( func, ... )
                     return Meta.fiat( ... )
                 end
} -- Meta.tableL
Meta.fiat = function ( assign, alien, add )
    -- Create instance object (constructor)
    -- Parameter:
    --     assign  -- string, with initial timestamp, or nil
    --                nil    -- now
    --                false  -- empty object
    --                table  -- clone this object, or copy from raw
    --                          ignore remaining parameters
    --     alien   -- string, with language code, or nil
    --     add     -- string, with interval (PHP strtotime), or nil
    -- Returns:
    --     table, as DateTime object
    --     string or false, if failed
    local r
    Private.foreign()
    if type( assign ) == "table" then
        if assign.suite == Meta.suite  and
           getmetatable( assign ) == Meta.tableI then
            r = assign[ Meta.signature ]
        else
            r = Private.from( assign )
        end
    else
        r = Private.factory( assign, alien, add )
    end
    if type( r ) == "table" then
        r = { [ Meta.signature ] = r }
        setmetatable( r, Meta.tableI )
    end
    return r
end -- Meta.fiat()
setmetatable( DateTime, Meta.tableL )
DateTime.serial   = nil


Calc.months = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }



--  Calc.fast = function ( at )
--      -- Quick scan of full ISO stamp
--      -- Parameter:
--      --     apply  -- string, ISO
--      -- Returns:
--      --     table, with numeric components
--      local r = { }
--      r.year  = tonumber( at:sub(  1, 4 ) )
--      r.month = tonumber( at:sub(  6, 2 ) )
--      r.dom   = tonumber( at:sub(  9, 2 ) )
--      r.hour  = tonumber( at:sub( 12, 2 ) )
--      r.min   = tonumber( at:sub( 14, 2 ) )
--      r.sec   = tonumber( at:sub( 17, 2 ) )
--      if at:sub( 19, 1 ) == "." then
--          r.msec = tonumber( at:sub( 20, 3 ) )
--          if #at > 22 then
--              r.mysec = tonumber( at:sub( 23, 3 ) )
--          end
--      end
--      return r
--  end -- Calc.fast()



Calc.fair = function ( adjust )
    -- Normalize numeric components
    -- Parameter:
    --     adjust  -- table, with raw numbers
    local ranges = { year  = { min = -999,
                               max = 9999 },
                     month = { min =  1,
                               max = 12,
                               mod = 12 },
                     dom   = { min =  1,
                               max = 28 },
                     hour  = { mod = 24 },
                     min   = { mod = 60 },
                     sec   = { mod = 60 },
                     msec  = { mod = 1000 },
                     mysec = { mod = 1000 } }
    local m, max, min, move, n, range, s
    for i = 10, 2, -1 do
        s = Meta.order[ i ]
        n = adjust[ s ]
        if n or move then
            range = ranges[ s ]
            if range then
                min = range.min or 0
                max = range.max  or  ( range.mod - 1 )
                if move then
                    n    = ( n or 0 )  +  move
                    move = false
                end
                if n < min  or  n > max then
                    if range.mod then
                        m    = n % range.mod
                        move = ( n - m )  /  range.mod
                        n    = min + m
                    else    -- dom
                        if adjust.month and adjust.year  and
                           adjust.month >= 1  and
                           adjust.month <= 12 and
                           adjust.year > 1900 then
                            if n > 0 then
                                max = Calc.final( adjust )
                                while n > max do
                                    n = n - max
                                    if adjust.month < 12 then
                                        adjust.month = adjust.month + 1
                                    else
                                        adjust.month = 1
                                        adjust.year  = adjust.year + 1
                                    end
                                    max = Calc.final( adjust )
                                end    -- while n <= max
                            else
                                while n < 1 do
                                    if adjust.month == 1 then
                                        adjust.month = 12
                                        adjust.year  = adjust.year - 1
                                    else
                                        adjust.month = adjust.month - 1
                                    end
                                    max = Calc.final( adjust )
                                    n   = n + max
                                end    -- while n < 1
                            end
                        end
                    end
                end
                adjust[ s ] = n
            end
        end
    end -- for i
end -- Calc.fair()



Calc.final = function ( adjust )
    -- Retrieve number of days in particular month
    -- Parameter:
    --     adjust   -- table, with date specification
    -- Returns:
    --     number, of days in month
    local r = Calc.months[ adjust.month ]
    if adjust.month == 2  and
       ( adjust.year % 4 ~= 0  or
         adjust.year % 400 == 0 ) then
        r = 28
    end
    return r
end -- Calc.final()



Calc.future = function ( add )
    -- Parse move interval
    -- Parameter:
    --     add   -- string, with GNU relative items
    -- Returns:
    --     table, with numeric components, or false
    local r, token
    local units = { year      = true,
                    month     = true,
                    fortnight = { slot = "dom", mult = 14 },
                    week      = { slot = "dom", mult = 7 },
                    dom       = true,
                    hour      = true,
                    min       = true,
                    sec       = true }
    local story = string.format( " %s ", add:lower() )
                        :gsub( "%s+", " " )
                        :gsub( " yesterday ", " -1 dom " )
                        :gsub( " tomorrow ",   " 1 dom " )
                        :gsub( "(%l)s ", "%1 " )
                        :gsub( " day ",    " dom " )
                        :gsub( " minute ", " min " )
                        :gsub( " second ", " sec " )
    local feed  = function ()
                      local slice
                      token, slice = story:match( "^( (%S+)) " )
                      return slice
                  end
    local fed   = function ()
                      story = story:sub( #token + 1 )
                  end
    local m, n, s, u
    while true do
        s = feed()
        if s then
            n = 1
            if s:match( "^[+-]?%d+$" ) then
                n = tonumber( s )
                fed()
                s = feed()
            end
            if s then
                u = units[ s ]
            end
            if s and u then
                fed()
                if u ~= true then
                    s = u.slot
                    n = n * u.mult
                end
                if feed() == "ago" then
                    if n > 0 then
                        n = - n
                    end
                    fed()
                end
                r      = r  or  { }
                r[ s ] = ( r[ s ] or 0 )  +  n
            else
                r = false
                break    -- while true
            end
        else
            break    -- while true
        end
    end    -- while true
    return r
end -- Calc.future()



Parser.digitsHeading = function ( analyse, alone, amount, add )
    -- String analysis, if digits only or at least 4 digits heading
    -- Parameter:
    --     analyse  -- string to be scanned, starting with digit
    --                 digits only, else starting with exactly 4 digits
    --     alone    -- true, if only digits
    --     amount   -- number of heading digits
    --     add      -- table, to be extended
    -- Returns:
    --     table, extended if parsed
    --     false, if invalid text format
    local r = add
    if alone then
        -- digits only
        if amount <= 4 then
            r.year = tonumber( analyse )
        elseif amount == 14 then
            -- timestamp
            r.year   = tonumber( analyse:sub(  1,  4 ) )
            r.month  = tonumber( analyse:sub(  5,  6 ) )
            r.dom    = tonumber( analyse:sub(  7,  8 ) )
            r.hour   = tonumber( analyse:sub(  9, 10 ) )
            r.min    = tonumber( analyse:sub( 11, 12 ) )
            r.sec    = tonumber( analyse:sub( 13, 14 ) )
        else
            r = false
        end
    elseif amount == 4 then
        local s, sep, sx = analyse:match( "^(%d+)([%-%.:Ww]?)(.*)$" )
        r.year = tonumber( s )
        if sep == "-" then
            -- ISO
            s, sep, sx = sx:match( "^(%d%d)(-?)(.*)$" )
            if s then
                r.month  = tonumber( s )
                r.month2 = true
                if sep == "-" then
                    s, sep, sx = sx:match( "^(%d%d?)([ T]?)(.*)$" )
                    if s then
                        r.dom = tonumber( s )
                        if sep == "T" then
                            r.month2 = nil
                        else
                            r.dom2 = ( #s == 2 )
                        end
                        if sep then
                            r = Parser.time( sx,  r,  sep == "T" )
                        end
                    else
                        r = false
                    end
                elseif sx and sx ~= "" then
                    r = false
                end
            else
                r = false
            end
        elseif sep:lower() == "w" then
            if sx then
                s = sx:match( "^(%d%d?)$" )
                if s then
                    r.week = tonumber( s )
                    if r.week < 1  or  r.week > 53 then
                        r = false
                    end
                else
                    r = false
                end
            else
                r = false
            end
        else
            r = false
        end
        if r then
            r.iso = true
        end
    elseif amount == 8 then
        -- ISO compact
        local s, sz = analyse:match( "^%d+T(%d+)([.+-]?%d*%a*)$" )
        if s then
            local n = #s
            if n == 2  or  n == 4  or  n == 6 then
                r.year  = tonumber( analyse:sub(  1,  4 ) )
                r.month = tonumber( analyse:sub(  5,  6 ) )
                r.dom   = tonumber( analyse:sub(  7,  8 ) )
                r.hour  = tonumber( analyse:sub( 10, 11 ) )
                if n > 2 then
                    r.min = tonumber( s:sub( 3, 4 ) )
                    if n == 6 then
                        r.sec = tonumber( s:sub( 5, 6 ) )
                    end
                    n, s = sz:match( "^(%.%d+)([+-]?[%a%d]*)$" )
                    if n then
                        n      = n .. "00"
                        r.msec = tonumber( n:sub( 1, 3 ) )
                        if #n >= 6 then
                            r.mysec = tonumber( n:sub( 4, 6 ) )
                        end
                        sz = s
                    end
                end
                if sz ~= "" then
                    s, sz = sz:match( "^([+-]?)(%a*)$" )
                    if s == "" then
                        if sz:match( "^(%u)$" ) then
                            r.zone = sz
                        else
                            s = false
                        end
                    elseif #s == 1 then
                        r.zone = s .. sz
                    else
                        s = false
                    end
                end
            else
                s = false
            end
        end
        if s then
            r = false
        end
    end
    return r
end -- Parser.digitsHeading()



Parser.eraGermanEnglish = function ( analyse )
    -- String analysis, for German and English era
    -- v. Chr.   v. u. Z.  n. Chr.   AD BC   A.D. B.C. B.C.E.
    -- Parameter:
    --     analyse  -- string
    -- Returns:
    --     1  -- table, with boolean era, if any
    --     2  -- string, with era stripped off, if any
    local rO = { }
    local rS = analyse
    local s, switch = analyse:match( "^(.+) ([vn])%. ?Chr%.$" )
    if switch then
        rS    = s
        rO.bc = ( switch == "v" )
    elseif analyse:find( " v%. ?u%. ?Z%.$" ) then
        rS    = analyse:match( "^(.+) v%. ?u%. ?Z%.$" )
        rO.bc = true
    elseif analyse:find( " B%.? ?C%.? ?E?%.?$" ) then
        rS    = analyse:match( "^(.+) B%.? ?C%.? ?E?%.?$" )
        rO.bc = true
    elseif analyse:find( "^A%.? ?D%.? " ) then
        rS    = analyse:match( "^A%.? ?D%.? (.*)$" )
        rO.bc = false
    end
    return rO, rS
end -- Parser.eraGermanEnglish()



Parser.european = function ( ahead, adhere, analyse, assign )
    -- String analysis, retrieve date style: DOM MONTH YEAR
    -- Parameter:
    --     ahead    -- string, with first digits, not more than 2
    --     adhere   -- string, with first separator; not ":"
    --     analyse  -- string, remainder following adhere
    --     assign   -- table
    -- Returns:
    --     table, extended if parsed
    local r = assign
    local s, s2, sx
    if adhere == "."  or  adhere == ". " then
        -- 23.12.2013
        -- 23. Dezember 2013
        s, sx = analyse:match( "^(%d%d?)%.(.*)$" )
        if s then
            r = Parser.putDate( false, s, ahead, assign )
            r = Parser.yearTime( sx, r )
        else
            s, sx = mw.ustring.match( analyse,
                                      "^ ?([%a&;]+%.?) ?(.*)$" )
            if s then
                local n = Parser.monthNumber( s )
                if n then
                    r.month = n
                    r.dom   = tonumber( ahead )
                    r.dom2  = ( #ahead == 2 )
                    r       = Parser.yearTime( sx, r )
                else
                    r = false
                end
            else
                r = false
            end
        end
    elseif adhere == " " then
        -- 23 Dec 2013
        s, sx = mw.ustring.match( analyse,
                                  "^([%a&;]+%.?) ?(.*)$" )
        if s then
            local n = Parser.monthNumber( s )
            if n then
                r.month = n
                r.dom   = tonumber( ahead )
                r.dom2  = ( #ahead == 2 )
                r       = Parser.yearTime( sx, r )
            else
                r = false
            end
        else
            r = false
        end
    else
        r = false
    end
    return r
end -- Parser.european()



Parser.isoDate = function ( analyse, assign )
    -- String analysis, retrieve month heading ISO date
    -- Parameter:
    --     analyse  -- string, with heading hyphen
    --     assign   -- table
    -- Returns:
    --     1  -- table, extended if parsed
    --     2  -- stripped string, or false, if invalid text format
    local rO, rS
    if analyse:match( "^%-%-?[0-9]" ) then
        local n, s
        rO = assign
        rS = analyse:sub( 2 )
        s  = rS:match( "^([012][0-9])%-" )
        if s then
            n = tonumber( s )
            if n >= 1  and  n <= 12 then
                rO.month = n
                rS       = rS:sub( 3 )
            else
                rO = false
            end
        end
        if rO then
            if rS:byte( 1, 1 ) == 45 then
                local suffix
                s = rS:match( "^%-([012][0-9])" )
                if s then
                    n  = tonumber( s )
                    if n >= 1  and  n <= 31 then
                        rO.dom = n
                        rS     = rS:sub( 4 )
                    else
                        rO = false
                    end
                else
                    rS:sub( 2 )
                end
            else
                rO = false
            end
            if rO then
                if #rS > 0 then
                    if rO.dom then
                        n = rS:byte( 1, 1 )
                        if n == 32  or  n == 84 then
                            rS = rS:sub( 2 )
                        else
                            rO = false
                        end
                    else
                        rO = false
                    end
                end
            end
        end
    else
        rO = false
    end
    if rO then
        rO.iso = true
    else
        rS = false
    end
    return rO, rS
end -- Parser.isoDate()



Parser.monthHeading = function ( analyse, assign )
    -- String analysis, retrieve month heading date (US only)
    -- Parameter:
    --     analyse  -- string, with heading word
    --     assign   -- table
    -- Returns:
    --     1  -- table, extended if parsed
    --     2  -- stripped string, or false, if invalid text format
    local rO = assign
    local rS = analyse
    local s, sep = mw.ustring.match( analyse, "^([%a&;]+%.?)([^%a%.]?)" )
    if s then
        -- might begin with month name   "December 23, 2013"
        local n = Parser.monthNumber( s )
        if n then
            rO.month = n
            if sep == "" then
                rS = ""
            else
                local s2, s3
                n = mw.ustring.len( s )  +  1
                s = mw.ustring.sub( analyse, n )
                s2 = s:match( "^ (%d%d%d?%d?)$" )
                if s2 then
                    rO.year = tonumber( s2 )
                    rS = ""
                else
                    s2, s3, rS = s:match( "^ (%d+), (%d+)( ?.*)$" )
                    if s2 and s3 then
                        n = #s2
                        if n <= 2  and  #s3 == 4 then
                            rO.dom  = tonumber( s2 )
                            rO.year = tonumber( s3 )
                            rO.dom2 = ( n == 2 )
                        else
                            rO = false
                        end
                    else
                        rO = false
                    end
                end
            end
        else
            rO = false
        end
    else
        rO = false
    end
    if not rO then
        rS = false
    end
    return rO, rS
end -- Parser.monthHeading()



Parser.monthNumber = function ( analyse )
    -- String analysis, retrieve month number
    -- Parameter:
    --     analyse  -- string, with month name including any period
    -- Returns:
    --     number, 1...12 if found
    --     false or nil, if not detected
    local r = false
    local s = mw.ustring.match( analyse, "^([%a&;]+)%.?$" )
    if s then
        local given
        s = capitalize( s )
        for k, v in pairs( World.monthsLong ) do
            given = World.monthsParse[ k ]
            if given then
                r = given[ s ]
            end
            if not r then
                given = World.monthsLong[ k ]
                for i = 1, 12 do
                    if given[ i ] == s then
                        r = i
                        break
                    end
                end -- for i
            end
            if r then
                break
            end
        end -- for k, v
    end
    return r
end -- Parser.monthNumber()



Parser.putDate = function ( aYear, aMonth, aDom, assign )
    -- Store date strings
    -- Parameter:
    --     aYear   -- string, with year, or false
    --     aMonth  -- string, with numeric month
    --     aDom    -- string, with day of month
    --     assign  -- table
    -- Returns:
    --     table, extended
    local r = assign
    if aYear then
        r.year   = tonumber( aYear )
    end
    r.month  = tonumber( aMonth )
    r.dom    = tonumber( aDom )
    r.month2 = ( #aMonth == 2 )
    r.dom2   = ( #aDom == 2 )
    return r
end -- Parser.putDate()



Parser.time = function ( analyse, assign, adjusted )
    -- String analysis, retrieve time components
    -- Parameter:
    --     analyse   -- string, with time part
    --     assign    -- table
    --     adjusted  -- true: fixed length of 2 digits expected
    -- Returns:
    --     table, extended if parsed
    --     false, if invalid text format
    local r = assign
    if analyse ~= "" then
        local s, sx = analyse:match( "^(%d+)(:?.*)$" )
        if s then
            local n = #s
            if n <= 2 then
                r.hour  = tonumber( s )
                if not adjusted then
                    r.hour2 = ( n == 2 )
                end
            else
                sx = false
                r  = false
            end
            if sx then
                s, sx = sx:match( "^:(%d+)(:?(.*))$"  )
                if s then
                    if #s == 2 then
                        r.min = tonumber( s )
                        if sx == "" then
                            sx = false
                        end
                    else
                        sx = false
                        r  = false
                    end
                    if sx then
                        local sep
                        local scan = "^([:,] ?)(%d+)(.*)$"
                        sep, s, sx = sx:match( scan )
                        if sep == ":" then
                            if #s == 2 then
                                r.sec = tonumber( s )
                            end
                        elseif sep == ", " then
                            r = Parser.wikiDate( s .. sx,  r )
                            sx = false
                        else
                            r = false
                        end
                    end
                else
                    r = false
                end
            end
            if sx  and  sx ~= "" then
                s = sx:match( "^%.(%d+)$" )
                if s then
                    s  = s .. "00"
                    r.msec = tonumber( s:sub( 1, 3 ) )
                    if #s >= 6 then
                        r.mysec = tonumber( s:sub( 4, 6 ) )
                    end
                else
                    r = false
                end
            end
        else
            r = false
        end
    end
    return r
end -- Parser.time()



Parser.wikiDate = function ( analyse, assign )
    -- String analysis, for date after wiki ~~~~~ signature time
    --     dmy    10:28, 30. Dez. 2013
    --     ymd    10:28, 2013 Dez. 30
    -- Parameter:
    --     analyse  -- string
    --     assign   -- table
    -- Returns:
    --     table, extended if parsed
    --     false, if invalid text format
    local r
    local s = analyse:match( "^(2%d%d%d) " )
    local sx
    if s then
        -- ymd    "10:28, 2013 Dez. 30"
        local n = false
        r = assign
        r.year = tonumber( s )
        s = analyse:sub( 6 )
        s, sx = mw.ustring.match( analyse:sub( 6 ),
                                  "^([%a&;]+)%.? (%d%d?)$" )
        if s then
            n = Parser.monthNumber( s )
            if n then
               r.month = n
            end
        end
        if n then
            r.dom  = tonumber( sx )
            r.dom2 = ( #sx == 2 )
        else
            r = false
        end
    else
        -- dmy    "10:28, 30. Dez. 2013"
        local sep
        s, sep, sx = analyse:match( "^(%d%d?)(%.? ?)(%a.+)$" )
        if s then
            r = Parser.european( s, sep, sx, assign )
        else
            r = false
        end
    end
    return r
end -- Parser.wikiDate()



Parser.yearTime = function ( analyse, assign )
    -- String analysis, for possible year and possible time
    -- Parameter:
    --     analyse  -- string, starting with year
    --     assign   -- table
    -- Returns:
    --     table, extended if parsed
    --     false, if invalid text format
    local r = assign
    local n = #analyse
    if n > 0 then
        local s, sx
        if n == 4 then
            if analyse:match( "^%d%d%d%d$" ) then
                s  = analyse
                sx = false
            end
        else
            s = analyse:match( "^(%d%d%d%d)[ ,]" )
            if s then
                sx = analyse:sub( 5 )
            else
                local suffix
                s, sx, suffix = analyse:match( "^(%d+)([ ,]?)(.*)$" )
                if s then
                    local j = #sx
                    n = #s
                    if n < 4  and  ( j == 1 or #suffix == 0 ) then
                        sx = analyse:sub( n + j )
                    else
                        s = false
                    end
                end
            end
        end
        if s then
            r.year = tonumber( s )
            if sx then
                s, sx = sx:match( "^(,? ?)(%d.*)$" )
                if #s >= 1 then
                    r = Parser.time( sx, r )
                end
            end
        else
            r = false
        end
    end
    return r
end -- Parser.yearTime()



Parser.zone = function ( analyse, assign )
    -- String analysis, for time zone
    -- +/-nn +/-nnnn (AAAa)
    -- Parameter:
    --     analyse  -- string
    --     assign   -- table
    -- Returns:
    --     1  -- table, with number or string zone, if any, or false
    --     2  -- string, with zone stripped off, if any
    local rO = assign
    local rS = analyse
    local s, sign, shift, sub
    s = "^(.+)([+-])([01]%d):?(%d?%d?)$"
    s, sign, shift, sub = analyse:match( s )
    if sign then
        if s:find( ":%d%d *$" ) then
            if sub then
                if #sub == 2 then
                    rO.zone = tonumber( shift .. sub )
                else
                    rO = false
                end
            else
                rO.zone = tonumber( shift ) * 100
            end
            if rO then
                if sign == "-" then
                    rO.zone = - rO.zone
                end
                rS = mw.text.trim( s )
            end
        end
    elseif analyse:find( "%(.*%)$" ) then
        s, shift = analyse:match( "^(.+)%((%a%a%a%a?)%)$" )
        if shift then
            rO.zone = shift:upper()
            rS      = mw.text.trim( s )
        else
            rO = false
        end
    else
        s, shift = analyse:match( "^(.+%d) ?(%a+)$" )
        if shift then
            local n = #shift
            if n == 1 then
                rO.zone = shift:upper()
            elseif n == 3 then
                if shift == "UTC"  or  shift == "GMT" then
                    rO.zone = 0
                end
            end
            if rO.zone then
                rS = s
            end
        end
    end
    return rO, rS
end -- Parser.zone()



Parser.GermanEnglish = function ( analyse )
    -- String analysis, for German and English formats
    -- Parameter:
    --     analyse  -- string, with date or time or parts of it
    -- Returns:
    --     table, if parsed
    --     false, if invalid text format
    local r, s = Parser.eraGermanEnglish( analyse )
    r, s = Parser.zone( s, r )
    if r then
        local start, sep, sx = s:match( "^(%d+)([ %-%.:WwT]?)(.*)$" )
        if start then
            -- begins with one or more digits (ASCII)
            local n    = #start
            local lazy = ( start == s   and
                           ( n >=4  or  type( r.bc == "boolean" ) ) )
            if n == 4  or  n == 8  or  lazy then
                r = Parser.digitsHeading( s, lazy, n, r )
            elseif n <= 2 then
                if sep == ":" then
                    r, s = Parser.time( s, r )
                elseif sep == "" then
                    r = false
                else
                    r = Parser.european( start, sep, sx, r )
                end
            else
                r = false
            end
        else
            local rM, sM = Parser.monthHeading( s, r )
            if rM then
                r = rM
            else
                r, sM = Parser.isoDate( s, r )
            end
            if r and sM ~= "" then
                r = Parser.time( sM, r )
            end
        end
    end
    return r
end -- Parser.GermanEnglish()



Private.factory = function ( assign, alien, add )
    -- Create DateTime table (constructor)
    -- Parameter:
    --     assign  -- string, with initial timestamp, or nil
    --                nil    -- now
    --                false  -- empty object
    --     alien   -- string, with language code, or nil
    --     add     -- string, with interval (PHP strtotime), or nil
    -- Returns:
    --     table, for DateTime object
    --     string or false, if failed
    local l     = true
    local slang = mw.text.trim( alien or World.slang or "en" )
    local r
    if assign == false then
        r = { }
    else
        local stamp = ( assign or "now" )
        local shift
        if add then
            shift = Private.future( add )
        end
        r = false
        if stamp == "now" then
            stamp = frame():callParserFunction( "#timel", "c", shift )
            shift = false
        else
            local seconds = stamp:match( "^#(%d+)$" )
            if seconds then
                stamp = os.date( "!%Y-%m-%dT%H:%M:%S",
                                 tonumber( seconds ) )
            end
        end
        l, r = pcall( Private.fetch, stamp, slang, shift )
    end
    if l  and  type( r ) == "table" then
        if slang ~= "" then
            r.lang = slang
        end
    end
    return r
end -- Private.factory()



Private.fetch = function ( analyse, alien, add )
    -- Retrieve object from string
    -- Parameter:
    --     analyse  -- string to be interpreted
    --     alien    -- string with language code, or nil
    --     add      -- table, with interval, or nil
    -- Returns:
    --     table, if parsed
    --     false, if invalid text format
    --     string, if serious error (args)
    local r
    if type( analyse ) == "string" then
        local strip = mw.ustring.char( 0x5B, 0x200E, 0x200F, 0x5D )
        r =  analyse:gsub( "&nbsp;", " " )
                    :gsub( "&#160;", " " )
                    :gsub( "&#x[aA]0;", " " )
                    :gsub( "&#32;", " " )
                    :gsub( DateTime.char.nbsp, " " )
                    :gsub( DateTime.char.tab,  " " )
                    :gsub( "  +", " " )
                    :gsub( "%[%[", "" )
                    :gsub( "%]%]", "" )
                    :gsub( strip, "" )
        r = mw.text.trim( r )
        if r == "" then
            r = { }
        else
            local slang  = ( alien or "" )
            local parser = { en  = "GermanEnglish",
                             de  = "GermanEnglish",
                             frr = "GermanEnglish",
                             nds = "GermanEnglish" }
            local suitable
            if slang == "" then
                slang = "en"
            else
                local s = slang:match( "^(%a+)%-" )
                if s then
                     slang = s
                end
            end
            slang    = slang:lower()
            suitable = parser[ slang ]
            if suitable then
                local l
                l, r = pcall( Parser[ suitable ], r )
                if l and r then
                    if not Prototypes.fair( r ) then
                        r = false
                    elseif add then
                        r = Prototypes.future( r, add )
                    end
                else
                    r = "invalid format"
                end
            else
                r = "unknown language: " .. slang
            end
        end
    else
        r = "bad type"
    end
    return r
end -- Private.fetch()



Private.field = function ( at, ask, adapt, atleast )
    -- Format object as string
    -- Parameter:
    --     at       -- DateTime
    --     ask      -- string, with format spec, or nil
    --     adapt    -- table, with options, or nil
    --                 .lang    -- string, with particular language code
    --                 .london  -- true: UTC output; default: local
    --                 .lonely  -- true: permit lonely hour
    --     atleast  -- string, with default value, or nil
    -- Returns:
    --     string, or false, if invalid, or number for julian date
    local r, spec
    if type( ask ) == "string" then
        if ask:sub( 1, 1 ) == "$" then
            if ask:sub( 1, 11 ) == "$JulianDate" then
                local luxury = ( ask:sub( -2 ) == ",$" )
                if ask:sub( 1, 14 ) == "$JulianDateJul" then
                    at.legacy = true
                elseif ask:sub( 1, 15 ) == "$JulianDateGreg" then
                else
                    at.legacy = Private.former( at )
                end
                r = Private.fixed( at, luxury )
            elseif ask:sub( 1, 11 ) == "$JulianCal$" then
                adapt.legacy = true
                spec = ask:sub( 12 )
            elseif ask:sub( 1, 3 ) == "$\"$" then
                r = ask:sub( 4 )
            else
                spec = ask
            end
        else
            spec = ask
        end
    else
        spec = false
    end
    if not r then
        r = Private.format( at, spec, adapt )
    end
    return r or atleast
end -- Private.field()



Private.fixed = function ( at, advanced )
    -- Compute julian date
    -- Parameter:
    --     at        -- DateTime
    --                  .legacy  -- true: at is in Julian calendar
    --     advanced  -- true: format long number
    -- Returns:
    --     number, or string
    local mM, mMY, mY, nY, r
    if at.year then
        mY = at.year * 12
    else    -- actually invalid
        mY = 0
    end
    if at.month then
        mMY = at.month
    else
        mMY = 1
    end
    mMY = mMY + 57609
    if at.dom then
        r = at.dom
    else
        r = 1
    end
    mM = ( mY + mMY )  *  0.08333333333    -- divided by 12 months
    nY = math.floor( mM - 1 )
    r  = math.floor( nY * 365.25 )
         + math.floor( ( mMY%12 + 4 )  *  30.6 )
         + r
        if at.legacy then
            r = r - 32205.5
        else
            r = r - math.floor( nY * 0.01 )     -- no leap day in century
                  + math.floor( nY * 0.0025 )   -- but every 400 years
                  - 32167.5
        end
    if at.hour then   -- divided by 24 hours per day
        r = r  +  at.hour * 0.0416666666666667
    else
        r = r + 0.5
    end
    if at.min then    -- divided by 1440 minutes per day
        r = r  +  at.min * 0.000694444444
    end
    if at.sec then    -- divided by 86400 seconds per day
        r = r  +  at.min * 0.00001157407407
    end
    if at.bc then
        r = 3442406 - r
        if at.legacy then
            r = r + 3
        end
    end
    if advanced then
        local slang = ( at.lang or World.slang )
        local o = mw.language.new( slang )
        r = o:formatNum( r )
    end
    return r
end -- Private.fixed()



Private.flow = function ( at1, at2 )
    -- Compare two objects
    -- Parameter:
    --     at1  -- DateTime
    --     at2  -- DateTime
    -- Returns:
    --     -1, 0, 1 or nil if not comparable
    local r = 0
    if at1.bc or at2.bc  and  at1.bc ~= at2.bc then
        if at1.bc then
            r = -1
        else
            r = 1
        end
    else
        local life  = false
        local s, v1, v2
        for i = 2, 10 do
            s  = Meta.order[ i ]
            v1 = at1[ s ]
            v2 = at2[ s ]
            if v1 or v2 then
                if v1 and v2 then
                    if v1 < v2 then
                        r = -1
                    elseif v1 > v2 then
                        r = 1
                    end
                elseif life then
                    if v2 then
                        r = -1
                    else
                        r = 1
                    end
                else
                    r = nil
                end
                if r ~= 0 then
                    if at1.bc and r then
                        r = r * -1
                    end
                    break    -- for i
                end
                life = true
            end
        end -- for i
    end
    return r
end -- Private.flow()



Private.foreign = function ()
    -- Retrieve localization submodule
    if not Meta.localized then
        local d = foreignModule( DateTime.suite,
                                 false,
                                 "local",
                                 DateTime.item )
        if type( d ) == "table" then
            local wk
            if d.slang then
                Meta.suite  = string.format( "%s %s",
                                             Meta.suite, d.slang )
                World.slang = d.slang
            end
            for k, v in pairs( d ) do
                wk = World[ k ]
                if wk  and  wk.en then
                    for subk, subv in pairs( v ) do
                        wk[ subk ] = subv
                    end -- for k, v
                else
                    World[ k ] = v
                end
            end -- for k, v
        end
        Meta.localized = true
    end
end -- Private.foreign()



Private.format = function ( at, ask, adapt )
    -- Format object as string
    -- Parameter:
    --     at     -- table, with numbers etc.
    --     ask    -- string, format spec, or nil
    --     adapt  -- table, with options, or nil
    --               .lang    -- string, with particular language code
    --               .london  -- true: UTC output; default: local
    --               .lonely  -- true: permit lonely hour
    -- Returns:
    --     string, or not
    local slang = at.lang or "en"
    local opts  = { lang = slang }
    local babel, r
    if type( adapt ) == "table" then
        if type( adapt.lang ) == "string" then
            local i = adapt.lang:find( "-", 3, true )
            if i then
                slang = adapt.lang:lower()
                opts.lang = slang:sub( 1,  i - 1 )
            else
                opts.lang = adapt.lang:lower()
            end
        end
        opts.london = adapt.london
        opts.lonely = adapt.lonely
    end
    babel = mw.language.new( opts.lang:lower() )
    if babel then
        local shift, show, stamp, suffix, limit4, locally
        if at.month then
            stamp = World.monthsLong.en[ at.month ]
            if at.year then
                stamp = string.format( "%s %04d", stamp, at.year )
            end
            if at.dom then
                stamp = string.format( "%d %s", at.dom, stamp )
            end
            if ask and ask:find( "Mon4", 1, true ) then
                local mon4 = World.months4[ opts.lang:lower() ]
                if mon4  and  mon4[ at.month ] then
                    limit4 = true
                end
            end
        elseif at.year then
            stamp = string.format( "%04d", at.year )
        end
        if at.hour then
            if stamp then
                stamp = stamp .. " "
            else
                stamp = ""
            end
            stamp = string.format( "%s%02d:", stamp, at.hour )
            if at.min then
                stamp = string.format( "%s%02d", stamp, at.min )
                if at.sec then
                    stamp = string.format( "%s:%02d",
                                           stamp, at.sec )
                    if at.msec then
                        stamp = string.format( "%s.%03d",
                                               stamp, at.msec )
                        if at.mysec then
                            stamp = string.format( "%s%03d",
                                                   stamp,
                                                   at.mysec )
                        end
                    end
                end
            else
                stamp = stamp .. "00"
            end
            if at.zone then
                stamp = stamp .. World.zones.formatter( at, "+-" )
            end
        end
        show, suffix = World.templates.formatter( at, ask, opts )
        if limit4 then
            show = show:gsub( "M", "F" )
        end
        if type( opts.london ) == "boolean" then
            locally = not opts.london
        else
            locally = true
        end
        r = babel:formatDate( show, stamp, locally )
        r = r:gsub( "&#160;$", "" )
        if at.year and at.year < 1000 then
            r = r:gsub( string.format( "%04d", at.year ),
                        tostring( at.year ) )
        end
        if at.month then
            local bucket, m, suite, x
            if show:find( "F", 1, true ) then
                suite = "monthsLong"
            elseif show:find( "M", 1, true ) then
                suite = "monthsAbbr"
            end
            bucket = World[ suite ]
            if bucket then
                m = bucket[ opts.lang:lower() ]
                if slang then
                    x = bucket[ slang:lower() ]
                end
                if m then
                    local base = m[ at.month ]
                    local ex
                    if x then
                        ex = x[ at.month ]
                    end
                    if suite == "monthsAbbr" then
                        local stop
                        if ex then
                            stop = x.suffix
                            base = ex
                        else
                            stop = m.suffix
                        end
                        if base and stop then
                            local shift, std
                            std   = string.format( "%s%%%s",
                                                   base[ 1 ], stop )
                            shift = string.format( "%s%s",
                                                   base[ 2 ], stop )
                            r = mw.ustring.gsub( r, std, shift )
                        end
                    elseif suite == "monthsLong" then
                        if base and ex then
                            r = mw.ustring.gsub( r, base, ex )
                        end
                    end
                end
            end
        end
        if suffix then
            r = r .. suffix
        end
    end
    return r
end -- Private.format()



Private.former = function ( at )
    -- Analyze whether Julian calendar
    -- Parameter:
    --     at  -- table, to be evaluated
    -- Returns:
    --     true, i
    local r
    if at.year then
        if at.year < 1582 then
            r = true
        elseif at.year == 1582 then
            if at.month then
                if at.month < 10 then
                    r = true
                elseif at.month == 10 then
                    r = ( at.dom <= 15 )
                end
            end
        end
    end
    return r
end -- Private.former()



Private.from = function ( attempt )
    -- Create valid raw table from arbitrary table
    -- Parameter:
    --     attempt  -- table, to be evaluated
    -- Returns:
    --     table, with valid components, or nil
    local data  = { }
    local r
    for k, v in pairs( Meta.components ) do
        if v then
            v = ( type( attempt[ k ] )  ==  v )
        else
            v = true
        end
        if v then
            data[ k ] = attempt[ k ]
        end
    end -- for k, v
    if Prototypes.fair( data ) then
        r = data
    end
    return r
end -- Private.from()



Private.future = function ( add )
    -- Normalize move interval
    -- Parameter:
    --     add   -- string or number, to be added
    -- Returns:
    --     table, with shift, or false/nil
    local r
    if add then
        local s = type( add )
        if s == "string"  and  add:match( "^%s*[+-]?%d+%.?%d*%s*$" ) then
            r = tonumber( add )
            s = "number"
        end
        if s == "number" then
            if r == 0 then
                r = false
            else
                r = string.format( "%d second",  r or add )
            end
        elseif s == "string" then
            r = add
        else
            r = false
        end
        if r then
            r = Calc.future( r )
        end
    end
    return r
end -- Private.future()



Prototypes.clone = function ( self )
    -- Clone object
    -- Parameter:
    --     self  -- table, with object, to be cloned
    -- Returns:
    --     table, with object
    local r = { [ Meta.signature ] = self[ Meta.signature ] }
    setmetatable( r, Meta.tableI )
    return r
end -- Prototypes.clone()



Prototypes.failsafe = function ( self, atleast )
    -- Retrieve versioning and check for compliance
    -- Precondition:
    --     self     -- table, or not, with DateTime object, unused
    --     atleast  -- string, with required version
    --                         or "wikidata" or "~" or "@" or false
    -- Postcondition:
    --     Returns  string  -- with queried version/item, also if problem
    --              false   -- if appropriate
    -- 2020-08-17
    local since = atleast
    local last    = ( since == "~" )
    local linked  = ( since == "@" )
    local link    = ( since == "item" )
    local r
    if last  or  link  or  linked  or  since == "wikidata" then
        local item = Meta.item
        since = false
        if type( item ) == "number"  and  item > 0 then
            local suited = string.format( "Q%d", item )
            if linkedlink then
                r = suited
            else
                local entity = mw.wikibase.getEntity( suited )
                if type( entity ) == "table" then
                    local seek = Failsafe.serialProperty or "P348"
                    local vsn  = entity:formatPropertyValues( seek )
                    if type( vsn ) == "table"  and
                       type( vsn.value ) == "string"  and
                       vsn.value ~= "" then
                        if last  and
                           vsn.value == ( Meta.serial or
                                          DateTime.serial ) then
                            r = false
                        elseif linked then
                            if mw.title.getCurrentTitle().prefixedText
                               ==  mw.wikibase.getSitelink( suited ) then
                                r = false
                            else
                                r = suited
                            end
                        else
                            r = vsn.value
                        end
                        if last then
                            r = false
                        else
                            r = vsn.value
                        end
                    end
                end
            end
        end
    end
    if type( r ) == "nil" then
        if not since  or  since <= Meta.serial then
            r = Meta.serial
        else
            r = false
        end
    end
    return r
end -- Prototypes.failsafe()



Prototypes.fair = function ( self, access, assign )
    -- Check formal validity of table
    -- Parameter:
    --     self    -- table, to be checked
    --     access  -- string or nil, single item to be checked
    --     assign  -- single access value to be checked
    -- Returns:
    --     true, if valid;  false, if not
    local r = ( type( self ) == "table" )
    if r then
        local defs = { year  = { max = MaxYear },
                       month = { min =  1,
                                 max = 12 },
                       week  = { min =  1,
                                 max = 53 },
                       dom   = { min =  1,
                                 max = 31 },
                       hour  = { max = 23 },
                       min   = { max = 59 },
                       sec   = { max = 61 },
                       msec  = { max = 999 },
                       mysec = { max = 999 }
        }
        local fNum =
            function ( k, v )
                local ret = true
                local dk  = defs[ k ]
                if dk then
                    if type( dk.max ) == "number" then
                        ret = ( type( v ) == "number" )
                        if ret then
                            local min
                            if dk.min then
                                min = dk.min
                            else
                                min = 0
                            end
                            ret = ( v >= min  and  v <= dk.max
                                    and  math.floor( v ) == v )
                            if ret and dk.f then
                                ret = dk.f( v )
                            end
                        end
                    end
                end
                return ret
            end -- fNum()
        if self.bc then
            defs.year.max = 999999
        end
        defs.dom.f =
            function ()
                local ret
                local d
                if access == "dom" then
                    d = assign
                else
                    d = self.dom
                end
                if d then
                    ret = ( d <= 28 )
                    if not ret then
                        local m
                        if access == "month" then
                            m = assign
                        else
                            m = self.month
                        end
                        if m then
                            ret = ( d <= Calc.months[ m ] )
                            if ret then
                                local y
                                if access == "year" then
                                    y = assign
                                else
                                    y = self.year
                                end
                                if d == 29  and  m == 2  and  y then
                                    if y % 4 ~= 0   or
                                       ( y % 100 == 0  and
                                         y % 400 ~= 0 ) then
                                        ret = false
                                    end
                                end
                            end
                        end
                    end
                else
                    ret = true
                end
                return ret
            end -- defs.dom.f()
        defs.sec.f =
            function ()
                local ret
                local second
                if access == "sec" then
                    second = assign
                else
                    second = self.sec
                end
                if second then
                    ret = ( second <= 59 )
                    if not ret and self.leap then
                        ret = true
                    end
                end
                return ret
            end -- defs.sec.f()
        if access or assign then
            r = ( type( access ) == "string" )
            if r then
                local def = defs[ access ]
                if def then
                    r = fNum( access, assign )
                    if r then
                        if def == "dom"  or
                           def == "month"  or
                           def == "year" then
                            r = defs.dom.f()
                        end
                    end
                elseif access == "lang" then
                    r = ( type( assign ) == "string" )
                    if r then
                        r = assign:match( "^%l%l%l?-?%a*$" )
                    end
                elseif access == "london" then
                    r = ( type( assign ) == "boolean" )
                end
            end
        else
            local life  = false
            local leak  = false
            local s, v
            for i = 1, 10 do
                s = Meta.order[ i ]
                v = self[ s ]
                if v then
                    if not life and leak then
                        -- gap detected
                        r = false
                        break
                    else
                        if not fNum( s, v ) then
                            r = false
                            break    -- for i
                        end
                        life = true
                        leak = true
                    end
                elseif i == 3 then
                    if not self.week then
                        life = false
                    end
                elseif i ~= 4 then
                    life = false
                end
            end -- for i
            if self.week  and  ( self.month or self.dom ) then
                r = false
            end
        end
    end
    return r
end -- Prototypes.fair()



Prototypes.figure = function ( self, assign )
    -- Assign month by name
    -- Parameter:
    --     self    -- table, to be filled
    --     assign  -- string, with month name
    -- Returns:
    --     number 1...12, if valid;  false, if not
    local r = false
    if type( self ) == "table"  and  type( assign ) == "string" then
        r = Parser.monthNumber( assign )
        if r then
            self.month = r
        end
    end
    return r
end -- Prototypes.figure()



Prototypes.first = function ( self )
    -- Retrieve abbreviated month name in current language
    -- Parameter:
    --     self  -- table, to be evaluated
    -- Returns:
    --     string, if defined;  false, if not
    local r
    if type( self ) == "table"  and  self.month then
        local slang = ( self.lang or World.slang )
        r = World.monthsLong[ slang ]
        if r then
            local brief = World.monthsAbbr[ slang ]
            r = r[ self.month ]
            if brief then
                local ex = brief[ self.month ]
                local s  = brief.suffix
                if ex then
                    r = ex[ 2 ]
                else
                    local n = brief.n or 3
                    r = mw.ustring.sub( r, 1, n )
                end
                if s then
                    r = r .. s
                end
            end
        end
    else
        r = false
    end
    return r
end -- Prototypes.first()



Prototypes.fix = function ( self )
    -- Adapt this object to local time if no explicit zone given
    -- Parameter:
    --     self  -- table, with numbers etc.
    if type( self ) == "table"  and
       not self.zone then
        local seconds = Prototypes.format( self, "Z" )
        Prototypes.future( self,  - tonumber( seconds ) )
    end
end -- Prototypes.fix()



Prototypes.flow = function ( self, another, assert )
    -- Compare this object with another timestamp
    -- Parameter:
    --     self     -- table, with numbers etc.
    --     another  -- DateTime or string or nil (now)
    --     assert   -- nil, or string with operator
    --                       "lt", "le", "eq", "ne", "ge", "gt",
    --                       "<", "<=", "==", "~=", "<>", ">=", "=>", ">"
    -- Returns:
    --     if assert: true or false
    --     else: -1, 0, 1
    --     nil if invalid
    local base, other, r
    if type( self ) == "table" then
        base  = self
        other = another
    elseif type( another ) == "table" then
        base  = another
        other = self
    end
    if base then
        if type( other ) ~= "table" then
            other = Meta.fiat( other )
        end
        if type( other ) == "table" then
            r = Private.flow( base, other )
            if r  and  type( assert ) == "string" then
                local trsl = { lt     = "<",
                               ["<"]  = "<",
                               le     = "<=",
                               ["<="] = "<=",
                               eq     = "=",
                               ["=="] = "=",
                               ne     = "<>",
                               ["<>"] = "<>",
                               ["~="] = "<>",
                               ge     = ">=",
                               [">="] = ">=",
                               ["=>"] = ">=",
                               gt     = ">",
                               [">"]  = ">" }
                local same = trsl[ assert:lower() ]
                if same then
                    local s = "="
                    if r < 0 then
                        s = "<"
                    elseif r > 0 then
                        s = ">"
                    end
                    r = ( same:find( s, 1, true )  ~=  nil )
                else
                    r = nil
                end
            end
        end
    end
    return r
end -- Prototypes.flow()



Prototypes.format = function ( self, ask, adapt )
    -- Format object as string
    -- Parameter:
    --     self   -- table, with numbers etc.
    --     ask    -- string, format spec, or nil
    --               table, with multiple formats
    --               string may contain multiple formats joined by "|||"
    --     adapt  -- table, with options, or nil
    --               .lang    -- string, with particular language code
    --               .london  -- true: UTC output; default: local
    --               .lonely  -- true: permit lonely hour
    -- Returns:
    --     string, or false, if invalid, or number for julian date
    local r
    if type( self ) == "table" then
        local s = type( ask )
        local poly
        if s == "string"  and  ask:find( "|||", 1, true ) then
            poly = mw.text.split( ask, "|||" )
        elseif s == "table" then
            poly = ask
        end
        if poly then
            r = ""
            for i = 1, #poly do
                r = r .. Private.field( self, poly[ i ], adapt )
            end -- for i
        else
            r = Private.field( self, ask, adapt )
        end
    end
    return r  or  false
end -- Prototypes.format()



Prototypes.full = function ( self )
    -- Retrieve month name in current language
    -- Parameter:
    --     self  -- table, to be evaluated
    -- Returns:
    --     string, if defined;  false, if not
    local r
    if type( self ) == "table"  and  self.month then
        local slang = ( self.lang or World.slang )
        r = World.monthsLong[ slang ]
        if r then
            r = r[ self.month ]
        end
    else
        r = false
    end
    return r
end -- Prototypes.full()



Prototypes.future = function ( self, add, allocate )
    -- Relative move by interval
    -- Parameter:
    --     self      -- table, to be used as base
    --     add       -- string or number, to be added
    --     allocate  -- true, if a clone shall be returned
    -- Returns:
    --     table, with shift
    local r, raw, rel, shift
    if type( self ) == "table" then
        r     = self
        shift = add
    elseif type( add ) == "table" then
        r     = add
        shift = self
    end
    if r then
        if r[ Meta.signature ] then
            raw = r[ Meta.signature ]
        else
            raw = r
        end
        if type( shift ) == "table" then
            rel = shift
        else
            rel = Private.future( shift )
        end
    end
    if raw and rel then
        if allocate then
            r   = Prototypes.clone( r )
            raw = r[ Meta.signature ]
        end
        for k, v in pairs( rel ) do
            raw[ k ] = ( raw[ k ] or 0 )  +  v
        end -- for k, v
        Calc.fair( raw )
        r[ Meta.signature ] = raw
    end
    return r
end -- Prototypes.future()



Prototypes.tostring = function ( self )
    -- Stringify yourself
    -- Parameter:
    --     self  -- table, to be stringified
    -- Returns:
    --     string
    local dels = { false, "", "-", "-", "", "", ":", ":", ".", "" }
    local wids = { false, 4,  2,   2,   2,  2,  2,   2,   3,  3  }
    local s    = ""
    local n, r, spec
    local f = function ( a )
                  n = self[ Meta.order[ a ] ]
                  s = s .. dels[ a ]
                  if n then
                      spec = string.format( "%%s%%0%dd", wids[ a ] )
                      s    = string.format( spec, s, n )
                  end
              end -- f()
    for i = 2, 5 do
        f( i )
    end -- for i
    r = s
    s = ""
    for i = 6, 10 do
        f( i )
    end -- for i
    if s == "::." then
        r = r:gsub( "%-+$", "" )
    else
        if r == "--" then
            r = s
        else
            r = string.format( "%sT%s", r, s )
        end
    end
    r = r:gsub( "%.$", "" )
    if self.bc then
        if self.year then
            r = "-" .. r
        else
            r = r .. " BC"
        end
    end
    return r
end -- Prototypes.tostring()



Prototypes.valueOf = function ( self )
    -- Returns yourselves primitive value (primitive table)
    -- Parameter:
    --     self  -- table, to be dumped
    -- Returns:
    --     table, or false
    local r
    if type( self ) == "table" then
        r = self[ Meta.signature ]
    end
    return r or false
end -- Prototypes.valueOf()



Templates.flow = function ( frame, action )
    -- Comparison invokation
    -- Parameter:
    --     frame  -- object
    -- Returns:
    --     string, either "" or "1"
    local r
    local s1 = frame.args[ 1 ]
    local s2 = frame.args[ 2 ]
    if s1 then
        s1 = mw.text.trim( s1 )
        if s1 == "" then
            s1 = false
        end
    end
    if s2 then
        s2 = mw.text.trim( s2 )
        if s2 == "" then
            s2 = false
        end
    end
    if s1 or s2 then
        local l
        Frame = frame
        l, r = pcall( Prototypes.flow,
                      Meta.fiat( s1 ), s2, action )
        if r == true then
            r = "1"
        end
    end
    return r or ""
end -- Templates.flow()



World.templates.formatter = function ( assigned, ask, adapt )
    -- Retrieve format specification string
    -- Parameter:
    --     assigned  -- table, with numbers etc.
    --     ask       -- string, format spec, or nil
    --     adapt     -- table, with options
    --                  .lang    -- string, with particular language code
    --                  .lonely  -- true: permit lonely hour
    -- Returns:
    --     1  -- string
    --     2  -- string or nil; append suffix (zone)
    local r1, r2
    if not ask  or  ask == "" then
        r1 = "c"
    elseif ask == "*" then
        if World.present then
            if assigned.hour then
                if assigned.dom or assigned.month or assigned.year then
                    if World.present.both and
                       World.present.date and
                       World.present.time then
                        r1 = World.present.both
                                     :gsub( "$date", World.present.date )
                                     :gsub( "$time", World.present.time )
                    else
                        r1 = World.present.date
                    end
                end
                r1 = r1 or World.present.time
            else
                r1 = World.present.date
            end
        end
        r1 = r1 or "c"
    else
        local template = World.templates[ ask ]
        r1 = ask
        if not template then
            local slang = ( adapt.lang or assigned.lang or World.slang )
            local tmp   = World.templates[ slang ]
            if tmp then
                template = tmp[ ask ]
            end
            if not template then
                local i = slang:find( "-", 3, true )
                if i then
                    slang = slang:sub( 1,  i - 1 ):lower()
                    tmp   = World.templates[ slang ]
                    if tmp then
                        template = tmp[ ask ]
                    end
                end
            end
        end
        if type( template ) == "table" then
            local low = ( ask == "ISO" or ask == "ISO-T" )
            r1 = template.spec
            if assigned.year then
                if not assigned.dom then
                    r1 = r1:gsub( "[ .%-]?[dDjlNwz][ .,%-]*", "" )
                           :gsub( "^&#160;", "" )
                    if not assigned.month then
                        r1 = r1:gsub( "[ .%-]?[FmMnt][ .%-]*", "" )
                    end
                end
            else
                r1 = r1:gsub( " ?[yY] ?", "" )
                if not assigned.dom then
                     r1 = r1:gsub( "[ .]?[dDjlNwz][ .,%-]*", "" )
                            :gsub( "^&#160;", "" )
                end
            end
            if template.lift and
               ( assigned.dom or
                 not  ( assigned.month or assigned.year or assigned.bc )
               ) then
                local stamp = false
                if assigned.hour then
                    if assigned.min then
                        stamp = "H:i"
                        if assigned.sec then
                            stamp = "H:i:s"
                            if assigned.msec then
                                stamp = string.format( "%s.%03d",
                                                       stamp,
                                                       assigned.msec )
                                if assigned.mysec then
                                    stamp = string.format( "%s.%03d",
                                                           stamp,
                                                         assigned.mysec )
                                end
                            end
                        end
                    elseif adapt.lonely then
                        stamp = "H"
                    end
                end
                if low or ask:find( "hh:mm:ss", 1, true ) then
                    if stamp then
                        r1 = string.format( "%s %s", r1, stamp )
                    end
                elseif ask:find( "hh:mm", 1, true )  and
                       stamp  and
                       #stamp > 3 then
                    r1 = string.format( "%s H:i", r1 )
                end
                if stamp then
                    if low or template.long then
                        local scheme
                        if template.long then
                            scheme = mw.language.getContentLanguage()
                            scheme = scheme.code
                        end
                        r2 = World.zones.formatter( assigned, scheme )
                    end
                end
            end
            if type ( assigned.bc ) == "boolean" then
                local eras = World.era[ adapt.lang ]  or  World.era.en
                local i
                if not r2 then
                    r2 = ""
                end
                if assigned.bc then
                    i = 1
                else
                    i = 2
                end
                r2 = string.format( "%s&#160;%s", r2, eras[ i ] )
            end
        end
    end
    return r1, r2
end -- World.templates.formatter()



World.zones.formatter =  function ( assigned, align )
    -- Retrieve time zone specification string
    -- Parameter:
    --     assigned  -- table, with numbers etc.
    --                  .zone should be available
    --     align     -- string, format spec, or nil
    --                  nil, false, "+-"  -- +/- 0000
    --                  "Z"               -- single letter
    --                  "UTC"             -- "UTC", if appropriate
    --                  "de"              -- try localized
    -- Returns:
    --     string
    local r    = ""
    local move = 0
    if assigned.zone then
        local s = type( assigned.zone )
        if s == "string" then
            s = assigned.zone:upper()
            if #s == 1 then
                -- "YXWVUTSRQPONZABCDEFGHIKLM"
                move = World.zones[ "!" ]:find( s, 1, true )
                if move then
                    move          = ( move - 13 ) * 100
                    assigned.zone = move
                else
                    assigned.zone = false
                end
            else
                local code = World.zones[ s ]
                if not code then
                   local slang = ( assigned.lang or
                                   World.slang )
                   local tmp   = World.zones[ slang ]
                   if tmp then
                       code = tmp[ s ]
                   end
                   if not code  and
                      slang ~= "en"  and
                      World.zones.en then
                       code = World.zones.en[ s ]
                   end
                end
                if code then
                    move          = code
                    assigned.zone = move
                end
            end
        elseif s == "number" then
            move = assigned.zone
        end
    end
    if move then
        local spec = "+-"
        if align then
            if align == "Z" then
                if move % 100 == 0 then
                    r = World.zones[ "!" ]:sub( move / 100 + 13,  1 )
                    spec = false
                end
            elseif align ~= "+-" then
                if move == 0 then
                    r    = " UTC"
                    spec = false
                else
                    local part = World.zones[ align ]
                    if part then
                        for k, v in pairs( part ) do
                            if v == move then
                                r    = string.format( " (%s)", k )
                                spec = false
                                break
                            end
                        end -- for k, v
                    end
                end
            end
        end
        if spec == "+-" then
            if move < 0 then
                spec = "%4.4d"
            else
                spec = "+%4.4d"
            end
            r = string.format( spec, move )
            r = string.format( "%s:%s",
                               r:sub( 1, 3), r:sub( 4 ) )
        end
    end
    return r
end -- World.zones.formatter()



-- Export
local p = { }

p.test = function ( args, alien )
    local slang = args.lang or alien
    local obj   = Meta.fiat( args[ 1 ], false, args.shift )
    local r
    if type( obj ) == "table" then
        local spec  = args[ 2 ]
        local opt
        if spec then
            spec = mw.text.trim( spec )
        end
        if slang then
            opt = { lang = mw.text.trim( slang ) }
        end
        r = obj:format( spec, opt )
    else
        r = ( args.noerror or "0" )
        if r == "0" then
            r = fault( "Format invalid" )
        else
            r = ""
        end
        if args.errCat then
            local cats = mw.text.split( args.errCat, "%s*|%s*" )
            for i = 1, #cats do
                r = string.format( "%s[[Category:%s]]",  r,  cats[ i ] )
            end -- for i
        end
    end
    return r
end -- p.test



p.failsafe = function ( frame )
    local s = type( frame )
    local r, since
    if s == "table" then
        since = frame.args[ 1 ]
    elseif s == "string" then
        since = mw.text.trim( since )
        if since == "" then
            since = false
        end
    end
    return Prototypes.failsafe( false, since )  or  ""
end -- p.failsafe



p.format = function ( frame )
    --    1       -- stamp
    --    2       -- spec
    --    lang
    --    shift
    --    noerror
    local l, r
    local v = { frame.args[ 1 ],
                frame.args[ 2 ],
                shift   = frame.args.shift,
                noerror = frame.args.noerror,
                errCat  = frame.args.errCat }
    if not v[ 1 ]  or  v[ 1 ] == "now" then
        v[ 1 ]  = frame:callParserFunction( "#timel", "c", v.shift )
        v.shift = false
    end
    Frame  = frame
    l, r = pcall( p.test,  v,  frame.args[ 3 ] or frame.args.lang )
    if not l then
        r = fault( r )
    end
    return r
end -- p.format



p.lt = function ( frame )
    return Templates.flow( frame, "lt" )
end -- p.lt
p.le = function ( frame )
    return Templates.flow( frame, "le" )
end -- p.le
p.eq = function ( frame )
    return Templates.flow( frame, "eq" )
end -- p.eq
p.ne = function ( frame )
    return Templates.flow( frame, "ne" )
end -- p.ne
p.ge = function ( frame )
    return Templates.flow( frame, "ge" )
end -- p.ge
p.gt = function ( frame )
    return Templates.flow( frame, "gt" )
end -- p.gt



p.DateTime = function ()
    return DateTime
end -- p.DateTime

return p