モジュール:DateTime

local DateTime   = { serial = "2020-04-08",
suite  = "DateTime",
item   = 20652535 }
-- Date and time objects
local Failsafe   = DateTime
local GlobalMod  = DateTime
local Calc       = { }
local Meta       = { }
local Parser     = { }
local Private    = { }
local Prototypes = { }
local Templates  = { }
local World      = { slang       = "en",
monthsLong  = { },
monthsParse = { },
months4     = { } }
local MaxYear    = 2099
local 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 foreignModule = function ( access, advanced, append, alt, alert )
-- Fetch global module
-- Precondition:
--     access    -- string, with name of base module
--     advanced  -- true, for require(); else mw.loadData()
--     append    -- string, with subpage part, if any; or false
--     alt       -- number, of wikidata item of root; or false
--     alert     -- true, for throwing error on data problem
-- Postcondition:
--     Returns whatever, probably table
-- 2019-10-20
local storage = access
local fun, lucky, r
if advanced then
fun = require
else
fun = mw.loadData
end
if append then
storage = string.format( "%s/%s", storage, append )
end
lucky, r = pcall( fun,  "Module:" .. storage )
if not lucky then
local suited
GlobalMod.globalModules = GlobalMod.globalModules or { }
suited = GlobalMod.globalModules access 
if not suited  and
type( alt ) == "number"  and
alt > 0 then
suited = string.format( "Q%d", alt )
suited = mw.wikibase.getSitelink( suited )
GlobalMod.globalModules access  = suited or true
end
if type( suited ) == "string" then
storage = suited
if append then
storage = string.format( "%s/%s", storage, append )
end
lucky, r = pcall( fun, storage )
end
if not lucky and alert then
error( "Missing or invalid page: " .. storage, 0 )
end
end
return r
end -- foreignModule()
local function capitalize( a )
-- Upcase first character, downcase anything else
-- Parameter:
--     a  -- string
-- Returns:
--     string
return  mw.ustring.upper( mw.ustring.sub( a, 1, 1 ) )
.. mw.ustring.lower( mw.ustring.sub( a, 2 ) )
end -- capitalize()
local function fault( a )
-- Format error message by class=error
-- Parameter:
--     a  -- string, error message
-- Returns:
--     string, HTML span
local e = mw.html.create( "span" )
:addClass( "error" )
:wikitext( a )
return tostring( e )
end -- fault()
local function frame()
if not Frame then
Frame = mw.getCurrentFrame()
end
return Frame
end -- frame()
Meta.localized  = false
Meta.serial     = DateTime.serial
Meta.signature  = "__datetime"
Meta.suite      = "{DateTime}"
Meta.components = { lang  = "string",
bc    = "boolean",
year  = "number",
month = "number",
week  = "number",
dom   = "number",
hour  = "number",
min   = "number",
sec   = "number",
msec  = "number",
mysec = "number",
zone  = false,
leap  = "boolean",
jul   = "boolean" }
Meta.order      = { "bc", "year", "month", "week", "dom",
"hour", "min", "sec", "msec", "mysec" }
Meta.tableI    = {    -- instance metatable
__index    = function ( self, access )
local r = self Meta.signature  access 
if r == nil then
if access == "serial" then
r = Meta.serial
elseif access == "suite" then
r = "DateTime"
else
r = Prototypes access 
end
end
return r
end,
__newindex = function ( self, access, assign )
if type( access ) == "string" then
local data = self Meta.signature 
if assign == nil then
local val = data access 
data access  = nil
if not Prototypes.fair( data ) then
data access  = val
end
elseif Prototypes.fair( data,
access,
assign ) then
data access  = assign
end
end
return
end,
__add      = function ( op1, op2 )
return Prototypes.future( op1, op2, true )
end,
__eq       = function ( op1, op2 )
return Prototypes.flow( op1, op2, "eq" )
end,
__lt       = function ( op1, op2 )
return Prototypes.flow( op1, op2, "lt" )
end,
__le       = function ( op1, op2 )
return Prototypes.flow( op1, op2, "le" )
end,
__tostring = function ( e )
return Prototypes.tostring( e )
end,
__call     = function ( func, ... )
return Meta.fiat( ... )
end
} -- Meta.tableI
Meta.tableL    = {    -- library metatable
__index    = function ( self, access )
local r
if access == "serial" then
r = Meta.serial
elseif access == "suite" then
r = Meta.suite
end
return r
end,
__newindex = function ()
return
end,
__tostring = function ()
return Meta.suite
end,
__call     = function ( func, ... )
return Meta.fiat( ... )
end
} -- Meta.tableL
Meta.fiat = function ( assign, alien, add )
-- Create instance object (constructor)
-- Parameter:
--     assign  -- string, with initial timestamp, or nil
--                nil    -- now
--                false  -- empty object
--                table  -- clone this object, or copy from raw
--                          ignore remaining parameters
--     alien   -- string, with language code, or nil
--     add     -- string, with interval (PHP strtotime), or nil
-- Returns:
--     table, as DateTime object
--     string or false, if failed
local r
Private.foreign()
if type( assign ) == "table" then
if assign.suite == Meta.suite  and
getmetatable( assign ) == Meta.tableI then
r = assign Meta.signature 
else
r = Private.from( assign )
end
else
r = Private.factory( assign, alien, add )
end
if type( r ) == "table" then
r = {  Meta.signature  = r }
setmetatable( r, Meta.tableI )
end
return r
end -- Meta.fiat()
setmetatable( DateTime, Meta.tableL )
DateTime.serial = nil
Calc.months = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
--  Calc.fast = function ( at )
--      -- Quick scan of full ISO stamp
--      -- Parameter:
--      --     apply  -- string, ISO
--      -- Returns:
--      --     table, with numeric components
--      local r = { }
--      r.year  = tonumber( at:sub(  1, 4 ) )
--      r.month = tonumber( at:sub(  6, 2 ) )
--      r.dom   = tonumber( at:sub(  9, 2 ) )
--      r.hour  = tonumber( at:sub( 12, 2 ) )
--      r.min   = tonumber( at:sub( 14, 2 ) )
--      r.sec   = tonumber( at:sub( 17, 2 ) )
--      if at:sub( 19, 1 ) == "." then
--          r.msec = tonumber( at:sub( 20, 3 ) )
--          if #at > 22 then
--              r.mysec = tonumber( at:sub( 23, 3 ) )
--          end
--      end
--      return r
--  end -- Calc.fast()
Calc.fair = function ( adjust )
-- Normalize numeric components
-- Parameter:
--     adjust  -- table, with raw numbers
local ranges = { year  = { min = -999,
max = 9999 },
month = { min =  1,
max = 12,
mod = 12 },
dom   = { min =  1,
max = 28 },
hour  = { mod = 24 },
min   = { mod = 60 },
sec   = { mod = 60 },
msec  = { mod = 1000 },
mysec = { mod = 1000 } }
local m, max, min, move, n, range, s
for i = 10, 2, -1 do
s = Meta.order i 
n = adjust s 
if n or move then
range = ranges s 
if range then
min = range.min or 0
max = range.max  or  ( range.mod - 1 )
if move then
n    = ( n or 0 )  +  move
move = false
end
if n  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  1900 then
if n > 0 then
max = Calc.final( adjust )
while n > max do
n = n - max
if adjust.month  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  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( "^(0120-9)%-" )
if s then
n = tonumber( s )
if n >= 1  and  n = 1  and  n  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 = 6 then
r.mysec = tonumber( s:sub( 4, 6 ) )
end
else
r = false
end
end
else
r = false
end
end
return r
end -- Parser.time()
Parser.wikiDate = function ( analyse, assign )
-- String analysis, for date after wiki ~~~~~ signature time
--     dmy    10:28, 30. Dez. 2013
--     ymd    10:28, 2013 Dez. 30
-- Parameter:
--     analyse  -- string
--     assign   -- table
-- Returns:
--     table, extended if parsed
--     false, if invalid text format
local r
local s = analyse:match( "^(2%d%d%d) " )
local sx
if s then
-- ymd    "10:28, 2013 Dez. 30"
local n = false
r = assign
r.year = tonumber( s )
s = analyse:sub( 6 )
s, sx = mw.ustring.match( analyse:sub( 6 ),
"^(%a&;+)%.? (%d%d?)$" )
if s then
n = Parser.monthNumber( s )
if n then
r.month = n
end
end
if n then
r.dom  = tonumber( sx )
r.dom2 = ( #sx == 2 )
else
r = false
end
else
-- dmy    "10:28, 30. Dez. 2013"
local sep
s, sep, sx = analyse:match( "^(%d%d?)(%.? ?)(%a.+)$" )
if s then
r = Parser.european( s, sep, sx, assign )
else
r = false
end
end
return r
end -- Parser.wikiDate()
Parser.yearTime = function ( analyse, assign )
-- String analysis, for possible year and possible time
-- Parameter:
--     analyse  -- string, starting with year
--     assign   -- table
-- Returns:
--     table, extended if parsed
--     false, if invalid text format
local r = assign
local n = #analyse
if n > 0 then
local s, sx
if n == 4 then
if analyse:match( "^%d%d%d%d$" ) then
s  = analyse
sx = false
end
else
s = analyse:match( "^(%d%d%d%d) ," )
if s then
sx = analyse:sub( 5 )
else
local suffix
s, sx, suffix = analyse:match( "^(%d+)( ,?)(.*)$" )
if s then
local j = #sx
n = #s
if n = 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  v2 then
r = 1
end
elseif life then
if v2 then
r = -1
else
r = 1
end
else
r = nil
end
if r ~= 0 then
if at1.bc and r then
r = r * -1
end
break    -- for i
end
life = true
end
end -- for i
end
return r
end -- Private.flow()
Private.foreign = function ()
-- Retrieve localization submodule
if not Meta.localized then
local d = foreignModule( DateTime.suite,
false,
"local",
DateTime.item )
if type( d ) == "table" then
local wk
if d.slang then
Meta.suite  = string.format( "%s %s",
Meta.suite, d.slang )
World.slang = d.slang
end
for k, v in pairs( d ) do
wk = World k 
if wk  and  wk.en then
for subk, subv in pairs( v ) do
wk subk  = subv
end -- for k, v
else
World k  = v
end
end -- for k, v
end
Meta.localized = true
end
end -- Private.foreign()
Private.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 seek = Failsafe.serialProperty or "P348"
local vsn  = entity:formatPropertyValues( seek )
if type( vsn ) == "table"  and
type( vsn.value ) == "string"  and
vsn.value ~= "" then
if last   and
vsn.value == ( Meta.serial or DateTime.serial ) then
r = false
else
r = vsn.value
end
end
end
end
end
if type( r ) == "nil" then
if not since  or  since = min  and  v ", ">=", "=>", ">"
-- 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     = "",
"" = "",
"~=" = "",
ge     = ">=",
">=" = ">=",
"=>" = ">=",
gt     = ">",
">"  = ">" }
local same = trsl assert:lower() 
if same then
local s = "="
if 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 slang = self.lang or "en"
local opts  = { lang = slang }
local babel
if type( adapt ) == "table" then
if type( adapt.lang ) == "string" then
local i = adapt.lang:find( "-", 3, true )
if i then
slang = adapt.lang:lower()
opts.lang = slang:sub( 1,  i - 1 )
else
opts.lang = adapt.lang:lower()
end
end
opts.london = adapt.london
opts.lonely = adapt.lonely
end
babel = mw.language.new( opts.lang:lower() )
if babel then
local shift, show, stamp, suffix, limit4, locally
if 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", 1, true ) then
local mon4 = World.months4 opts.lang:lower() 
if mon4  and  mon4 self.month  then
limit4 = true
end
end
elseif self.year then
stamp = string.format( "%04d", self.year )
end
if self.hour then
if stamp then
stamp = stamp .. " "
else
stamp = ""
end
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", 1, true ) then
suite = "monthsLong"
elseif show:find( "M", 1, true ) then
suite = "monthsAbbr"
end
bucket = World suite 
if bucket then
m = bucket opts.lang:lower() 
if slang then
x = bucket slang:lower() 
end
if m then
local base = m 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
r = r:gsub( "%.$", "" )
return r
end -- Prototypes.tostring()
Prototypes.valueOf = function ( self )
-- Returns yourselves primitive value (primitive table)
-- Parameter:
--     self  -- table, to be dumped
-- Returns:
--     table, or false
local r
if type( self ) == "table" then
r = self Meta.signature 
end
return r or false
end -- Prototypes.valueOf()
Templates.flow = function ( frame, action )
-- Comparison invokation
-- Parameter:
--     frame  -- object
-- Returns:
--     string, either "" or "1"
local r
local s1 = frame.args 1 
local s2 = frame.args 2 
if s1 then
s1 = mw.text.trim( s1 )
if s1 == "" then
s1 = false
end
end
if s2 then
s2 = mw.text.trim( s2 )
if s2 == "" then
s2 = false
end
end
if s1 or s2 then
local l
Frame = frame
l, r = pcall( Prototypes.flow,
Meta.fiat( s1 ), s2, action )
if r == true then
r = "1"
end
end
return r or ""
end -- Templates.flow()
World.templates.formatter = function ( assigned, ask, adapt )
-- Retrieve format specification string
-- Parameter:
--     assigned  -- table, with numbers etc.
--     ask       -- string, format spec, or nil
--     adapt     -- table, with options
--                  .lang    -- string, with particular language code
--                  .lonely  -- true: permit lonely hour
-- Returns:
--     1  -- string
--     2  -- string or nil; append suffix (zone)
local r1, r2
if not ask  or  ask == "" then
r1 = "c"
elseif ask == "*" then
if World.present then
if assigned.hour then
if assigned.dom or assigned.month or assigned.year then
if World.present.both and
World.present.date and
World.present.time then
r1 = World.present.both
:gsub( "$date", World.present.date )
:gsub( "$time", World.present.time )
else
r1 = World.present.date
end
end
r1 = r1 or World.present.time
else
r1 = World.present.date
end
end
r1 = r1 or "c"
else
local template = World.templates ask 
r1 = ask
if not template then
local slang = ( adapt.lang or assigned.lang or World.slang )
local tmp   = World.templates slang 
if tmp then
template = tmp ask 
end
if not template then
local i = slang:find( "-", 3, true )
if i then
slang = slang:sub( 1,  i - 1 ):lower()
tmp   = World.templates slang 
if tmp then
template = tmp ask 
end
end
end
end
if type( template ) == "table" then
local low = ( ask == "ISO" or ask == "ISO-T" )
r1 = template.spec
if assigned.year then
if not assigned.dom then
r1 = r1:gsub( " .%-?dDjlNwz .,%-*", "" )
:gsub( "^ ", "" )
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", 1, true ) 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, 1, true )
if move then
move          = ( move - 13 ) * 100
assigned.zone = move
else
assigned.zone = false
end
else
local code = World.zones s 
if not code then
local slang = ( assigned.lang or
World.slang )
local tmp   = World.zones slang 
if tmp then
code = tmp s 
end
if not code  and
slang ~= "en"  and
World.zones.en then
code = World.zones.en s 
end
end
if code then
move          = code
assigned.zone = move
end
end
elseif s == "number" then
move = assigned.zone
end
end
if move then
local spec = "+-"
if align then
if align == "Z" then
if move % 100 == 0 then
r = World.zones "!" :sub( move / 100 + 13,  1 )
spec = false
end
elseif align ~= "+-" then
if move == 0 then
r    = " UTC"
spec = false
else
local part = World.zones align 
if part then
for k, v in pairs( part ) do
if v == move then
r    = string.format( " (%s)", k )
spec = false
break
end
end -- for k, v
end
end
end
end
if spec == "+-" then
if move < 0 then
spec = "%4.4d"
else
spec = "+%4.4d"
end
r = string.format( spec, move )
r = string.format( "%s:%s",
r:sub( 1, 3), r:sub( 4 ) )
end
end
return r
end -- World.zones.formatter()
-- Export
local p = { }
function p.test( args, alien )
local slang = args.lang or alien
local obj   = Meta.fiat( args 1 , false, args.shift )
local r
if type( obj ) == "table" then
local spec  = args 2 
local opt
if spec then
spec = mw.text.trim( spec )
end
if slang then
opt = { lang = mw.text.trim( slang ) }
end
r = obj:format( spec, opt )
else
r = ( args.noerror or "0" )
if r == "0" then
r = fault( "Format invalid" )
else
r = ""
end
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