Modul:DateTime: Unterschied zwischen den Versionen

Aus ÖsterreichWiki
Zur Navigation springen Zur Suche springen
K (Schützte „Modul:DateTime“: Häufig eingebundenes Modul ([Bearbeiten=Nur angemeldete, nicht neue Benutzer] (unbeschränkt) [Verschieben=Nur Administratoren] (unbeschränkt)))
(update)
Zeile 1: Zeile 1:
--[=[ 2014-02-04
--[=[ 2014-02-22
Date and time utilities
Date and time utilities
]=]
]=]
Zeile 832: Zeile 832:
         local l, d = pcall( mw.loadData, "Module:DateTime/local" )
         local l, d = pcall( mw.loadData, "Module:DateTime/local" )
         if l then
         if l then
            local wk
             if d.slang then
             if d.slang then
                 World.slang = d.slang
                 World.slang = d.slang
             end
             end
             for k, v in pairs( d ) do
             for k, v in pairs( d ) do
                 if World[ k ].en then
                 wk = World[ k ]
                    local part = World[ k ]
                if wk  and  wk.en then
                     for subk, subv in pairs( v ) do
                     for subk, subv in pairs( v ) do
                         part[ subk ] = subv
                         wk[ subk ] = subv
                     end -- for k, v
                     end -- for k, v
                 else
                 else

Version vom 23. Februar 2014, 21:21 Uhr

Diese Vorlage(n) wurde(n) fast unverändert von der deutschsprachigen Wikipedia übernommen. Es wurden nur geringfügige technische, stilistische und organisatorische Anpassungen ans ÖsterreichWiki 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