Modul:DateTime

Aus Regiowiki
Zur Navigation springen Zur Suche springen
Diese Vorlage(n) wurde(n) fast unverändert von der deutschsprachigen Wikipedia übernommen. Es wurden nur geringfügige technische, stilistische und organisatorische Anpassungen an Regiowiki durchgeführt.

--[=[ 2014-02-22
Date and time utilities
]=]



-- local globals
local DateTime
local Parser     = { }
local Private    = { }
local Prototypes = { }
local World      = { slang       = "en",
                     monthsLong  = { },
                     monthsParse = { } }
local Nbsp       = mw.ustring.char( 160 )
local 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.templates = { [ "ISO" ] =
                        { spec = "Y-m-d",
                          lift = true },
                    [ "ISO-T" ] =
                        { spec = "c" },
                    [ "timestamp" ] =
                        { spec = "YmdHis" }
                  }
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 -- fault()



local function fault( a )
    -- Format error message by class=error
    -- Parameter:
    --     a  -- string, error message
    -- Returns:
    --     string, HTML span
    return string.format( "<span class=\"error\">%s</span>", a )
end -- fault()



DateTime = function ( assign, alien )
    -- Create metatable (constructor)
    -- Parameter:
    --     assign  -- string, with initial timestamp, or nil
    --                nil    -- now
    --                false  -- empty object
    --     alien   -- string, with language code, or nil
    -- Returns:
    --     table, as DateTime object
    --     string or false, if failed
    local r
    Private.foreign()
    r = Private.factory( assign, alien )
    if type( r ) == "table" then
        local meta = { }
        local s    = "__datetime"
        meta.__index    = function( self, access )
                              return self[ s ][ access ]
                          end
        meta.__newindex = function( self, access, assign )
                              if type( access ) == "string" then
                                  local data = self[ s ]
                                  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
        r               = { [ s ] = r }
        r.fair          = function ( ... )
                              return Prototypes.fair( ... )
                          end
        r.format        = function ( ... )
                              return Prototypes.format( ... )
                          end
        setmetatable( r, meta )
    end
    return r
end -- DateTime()



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+)([%-%.:w]?)(.*)$" )
        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 s then
                s = s:match( "^(%d%d?)$" )
                if s then
                    r.week = tonumber( s )
                else
                    r = false
                end
            else
                r = false
            end
        else
            r = false
        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 ) )
                        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.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( n )
                            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 )
                    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
                    r.msec = tonumber( s )
                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 )
            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
            else
                rO = false
            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
            r, s = Parser.monthHeading( s, r )
            if r and s ~= "" then
                r = Parser.time( s, r )
            end
        end
    end
    return r
end -- Parser.GermanEnglish()



Private.factory = function ( assign, alien )
    -- Create DateTime table (constructor)
    -- Parameter:
    --     assign  -- string, with initial timestamp, or nil
    --                nil    -- now
    --                false  -- empty object
    --     alien   -- string, with language code, or nil
    -- Returns:
    --     table, for DateTime object
    --     string or false, if failed
    local l     = true
    local r     = false
    local slang = mw.text.trim( alien or World.slang or "en" )
    if assign == false then
        r = { }
    else
        local stamp = ( assign or "now" )
        if stamp == "now" then
            stamp = mw.getCurrentFrame():callParserFunction( "#timel",
                                                             "c" )
        end
        l, r = pcall( Private.fetch, stamp, slang )
    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 )
    -- Retrieve object from string
    -- Parameter:
    --     analyse  -- string to be interpreted
    --     alien    -- string with language code, or nil
    -- Returns:
    --     table, if parsed
    --     false, if invalid text format
    --     string, if serious error (args)
    local r
    if type( analyse ) == "string" then
        r =  analyse:gsub( "&nbsp;", " " )
                    :gsub( "&#160;", " " )
                    :gsub( "&#32;", " " )
                    :gsub( Nbsp, " " )
                    :gsub( Tab, " " )
                    :gsub( "  +", " " )
        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
                    end
                end
            else
                r = "unknown language"
            end
        end
    else
        r = "bad type"
    end
    return r
end -- Private.fetch()



Private.foreign =  function ()
    -- Retrieve localization submodule
    if not World.localization then
        local l, d = pcall( mw.loadData, "Module:DateTime/local" )
        if l then
            local wk
            if d.slang then
                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
        World.localization = true
    end
end -- Private.foreign()



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 = 2099 },
                       month = { max = 12 },
                       dom   = { max = 31 },
                       hour  = { max = 23 },
                       min   = { max = 59 },
                       sec   = { max = 61 },
                       msec  = { max = 1000 }
        }
        local months = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
        local fNum =
            function ( k, v )
                local ret = true
                local dk  = defs[ k ]
                if dk then
                    local mx = dk.max
                    if type( mx ) == "number" then
                        ret = ( type( v ) == "number" )
                        if ret then
                            ret = ( v >= 0  and  v <= mx
                                    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 <= 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 % 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
                end
            end
        else
            local order = { "bc", "year", "month", "dom",
                            "hour", "min", "sec", "msec" }
            local life = false
            local leak = false
            local s, v
            for i = 1, 8 do
                s = 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
                        end
                        life = true
                        leak = true
                    end
                else
                    life = false
                end
            end -- for i
        end
    end
    return r
end -- Prototypes.fair()



Prototypes.format = function ( self, ask, alien )
    -- Format object as string
    -- Parameter:
    --     self   -- table, with numbers etc.
    --     ask    -- string, format spec, or nil
    --     alien  -- string, with language code, or nil
    -- Returns:
    --     string, or false, if invalid
    local r = false
    if type( self ) == "table" then
        local slang = ( alien or self.lang )
        local babel = mw.language.new( slang )
        if babel then
            local show
            local stamp
            local suffix
            local 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
            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.%d",
                                                   stamp, self.msec )
                        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, alien )
            if self.locally or alien then
                locally = true
            else
                locally = false
            end
            r = babel:formatDate( show, stamp, locally )
            if self.year and self.year < 1000 then
                r = r:gsub( string.format( "%04d", self.year ),
                            tostring( self.year ) )
            end
            if suffix then
                r = r .. suffix
            end
        end
    end
    return r
end -- Prototypes.format()



World.templates.formatter =  function ( assigned, ask, alien )
    -- Retrieve format specification string
    -- Parameter:
    --     assigned  -- table, with numbers etc.
    --     ask       -- string, format spec, or nil
    --     alien     -- string, with language code, or nil
    -- 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 = ( alien or assigned.lang or World.slang )
            local tmp   = World.templates[ slang ]
            if tmp then
                template = tmp[ ask ]
            end
        end
        if type( template ) == "table" then
            r1 = template.spec
            if assigned.year then
                if not assigned.dom then
                    r1 = r1:gsub( "[ .]?[jJ][ .,%-]*", "" )
                           :gsub( "^&#160;", "" )
                    if not assigned.month then
                        r1 = r1:gsub( "[ .%-]?[fFmM][ .%-]*", "" )
                    end
                end
            else
                r1 = r1:gsub( " ?[yY] ?", "" )
                if not assigned.dom then
                     r1 = r1:gsub( "[ .]?[jJ][ .,%-]*", "" )
                            :gsub( "^&#160;", "" )
                end
            end
            if template.lift then
                local spec  = "T. Monat JJJJ hh:mm:ss Zone"
                local stamp = false
                local low   = ( ask == "ISO" or ask == "ISO-T" )
                if ask ~= spec then
                    spec = false
                end
                if assigned.hour and assigned.min then
                    stamp = "H:i"
                    if assigned.sec then
                        stamp = "H:i:s"
                        if assigned.msec then
                            stamp = string.format( "%s.%d",
                                                   stamp, assigned.msec )
                        end
                    end
                end
                if low or spec then
                    if stamp then
                        r1 = string.format( "%s %s", r1, stamp )
                    end
                end
                if stamp then
                    local dewiki = ( ask == "dewiki" or spec )
                    if low or dewiki then
                        local scheme
                        if dewiki then
                            scheme = "de"
                        end
                        r2 = World.zones.formatter( assigned, scheme )
                    end
                end
            end
            if type ( assigned.bc ) == "boolean" then
                local eras = World.era[ alien ]  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 )
                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
                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 )
    local r
    local o = DateTime( args[ 1 ], "de" )
    if type( o ) == "table" then
        local spec  = args[ 2 ]
        local slang = args[ 3 ]
        if spec then
            spec = mw.text.trim( spec )
        end
        if slang then
            slang = mw.text.trim( slang )
        end
        r = o:format( spec, slang )
    else
        r = ( args.noerror or "0" )
        if r == "0" then
            r = fault( "Format nicht erkannt" )
        else
            r = ""
        end
    end
    return r
end -- test



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



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

return p