Modul:DateTime

Aus BiodynWiki
Version vom 16. Mai 2021, 01:03 Uhr von Admin (Diskussion | Beiträge)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)

local DateTime = { serial = "2019-07-01",

                    suite  = "DateTime",
                    item   = 20652535 }

-- Date and time objects local Failsafe = DateTime local Calc = { } local Meta = { } local Parser = { } local Private = { } local Prototypes = { } local Templates = { } local World = { slang = "en",

                    monthsLong  = { },
                    monthsParse = { },
                    months4     = { } }

local MaxYear = 2099 local Nbsp = mw.ustring.char( 160 ) local Tab = mw.ustring.char( 9 ) local Frame 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 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 n == 14 then
           -- timestamp
           r.year   = tonumber( analyse:sub(  1, 4 ) )
           r.month  = tonumber( analyse:sub(  5, 2 ) )
           r.dom    = tonumber( analyse:sub(  7, 2 ) )
           r.hour   = tonumber( analyse:sub(  9, 2 ) )
           r.min    = tonumber( analyse:sub( 11, 2 ) )
           r.sec    = tonumber( analyse:sub( 13, 2 ) )
       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 03:03, 16. Mai 2021 (CEST) 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( " ", " " )
                   :gsub( " ", " " )
                   :gsub( "&#x[aA]0;", " " )
                   :gsub( " ", " " )
                   :gsub( Nbsp, " " )
                   :gsub( Tab, " " )
                   :gsub( "  +", " " )
                   :gsub( "%[%[", "" )
                   :gsub( "%]%]", "" )
                   :gsub( strip, "" )
       r = mw.text.trim( r )
       if r == "" then
           r = { }
       else
           local slang = ( alien or "" )
           if slang == "" then
               slang = "en"
           else
               local s = slang:match( "^(%a+)%-" )
               if s then
                    slang = s
               end
           end
           slang = slang:lower()
           if slang == "en" or slang == "de" then
               local l
               l, r = pcall( Parser.GermanEnglish, 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"
           end
       end
   else
       r = "bad type"
   end
   return r

end -- Private.fetch()


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 l, d = pcall( mw.loadData, "Module:DateTime/local" )
       if l 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%s %s
               else
                   World[ k ] = v
               end
           end -- for k, v
       end
       Meta.localized = true
   end

end -- Private.foreign()


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:
   --     atleast  -- string, with required version or "wikidata" or "~"
   --                 or false
   -- Postcondition:
   --     Returns  string  -- with queried version, also if problem
   --              false   -- if appropriate
   local last  = ( atleast == "~" )
   local since = atleast
   local r
   if last  or  since == "wikidata" then
       local item = Meta.item
       since = false
       if type( item ) == "number"  and  item > 0 then
           local entity = mw.wikibase.getEntity( string.format( "Q%d",
                                                                item ) )
           if type( entity ) == "table" then
               local vsn = entity:formatPropertyValues( "P348" )
               if type( vsn ) == "table"  and
                  type( vsn.value ) == "string"  and
                  vsn.value ~= "" then
                   if last  and  vsn.value == Meta.serial then
                       r = false
                   else
                       r = vsn.value
                   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()
       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
   --     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
   local r = false
   if type( self ) == "table" then
       local opts  = { lang = self.lang }
       local babel, slang
       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 )
       if babel then
           local shift, show, stamp, suffix, limit4, locally
           if self.month then
               stamp = World.monthsLong.en[ self.month ]
               if self.year then
                   stamp = string.format( "%s %04d", stamp, self.year )
               end
               if self.dom then
                   stamp = string.format( "%d %s", self.dom, stamp )
               end
               if ask and ask:find( "Mon4" ) then
                   local mon4 = World.months4[ opts.lang ]
                   if mon4 then
                       if mon4[ self.month ] then
                           limit4 = true
                       end
                   end
               end
           elseif self.year then
               stamp = string.format( "%04d", self.year )
           end
           if self.hour then
               stamp = string.format( "%s %02d:", stamp, self.hour )
               if self.min then
                   stamp = string.format( "%s%02d", stamp, self.min )
                   if self.sec then
                       stamp = string.format( "%s:%02d",
                                              stamp, self.sec )
                       if self.msec then
                           stamp = string.format( "%s.%03d",
                                                  stamp, self.msec )
                           if self.mysec then
                               stamp = string.format( "%s%03d",
                                                      stamp,
                                                      self.mysec )
                           end
                       end
                   end
               else
                   stamp = stamp .. "00"
               end
               if self.zone then
                   stamp = stamp .. World.zones.formatter( self, "+-" )
               end
           end
           show, suffix = World.templates.formatter( self, 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( " $", "" )
           if self.year and self.year < 1000 then
               r = r:gsub( string.format( "%04d", self.year ),
                           tostring( self.year ) )
           end
           if self.month then
               local bucket, m, suite, x
               if show:find( "F" ) then
                   suite = "monthsLong"
               elseif show:find( "M" ) then
                   suite = "monthsAbbr"
               end
               bucket = World[ suite ]
               if bucket then
                   m = bucket[ opts.lang ]
                   if slang then
                       x = bucket[ slang ]
                   end
                   if m then
                       local base = m[ self.month ]
                       local ex
                       if x then
                           ex = x[ self.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
   end
   return r

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
   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"
   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
       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( "^ ", "" )
                   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( "^ ", "" )
               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" ) then
                   if stamp then
                       r1 = string.format( "%s %s", r1, stamp )
                   end
               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 %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 )
               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 = { }

function p.test( args, alien )

   local slang = args.lang
   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
   end
   return r

end -- p.test


function p.failsafe( 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


function p.format( 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 }
   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


function p.lt( frame )

   return Templates.flow( frame, "lt" )

end -- p.lt function p.le( frame )

   return Templates.flow( frame, "le" )

end -- p.le function p.eq( frame )

   return Templates.flow( frame, "eq" )

end -- p.eq function p.ne( frame )

   return Templates.flow( frame, "ne" )

end -- p.ne function p.ge( frame )

   return Templates.flow( frame, "ge" )

end -- p.ge function p.gt( frame )

   return Templates.flow( frame, "gt" )

end -- p.gt


p.DateTime = function ()

   return DateTime

end -- p.DateTime

return p