Jump to content

Module:Date: Difference between revisions

928 bytes removed ,  8 years ago
major refactor, mostly working now; add/subtract with a date; remove template handling which will be in Module:Age
(tweak formatted output; extract_date to parse an input date string)
(major refactor, mostly working now; add/subtract with a date; remove template handling which will be in Module:Age)
Line 19: Line 19:


local function strip_to_nil(text)
local function strip_to_nil(text)
-- Return nil if text is nil or is an empty string after trimming.
-- If text is a string, return its trimmed content, or nil.
-- If text is a non-blank string, return its content after trimming.
-- Otherwise return text (convenient when Date fields are provided from
-- Otherwise return text (convenient when accessed via another module).
-- another module which is able to pass, for example, a number).
if type(text) == 'string' then
if type(text) == 'string' then
local result = text:match("^%s*(.-)%s*$")
text = text:match('(%S.-)%s*$')
if result == '' then
return nil
end
return result
end
if text == nil then
return nil
end
end
return text
return text
Line 63: Line 56:
-- Return jd, jdz from a Julian or Gregorian calendar date where
-- Return jd, jdz from a Julian or Gregorian calendar date where
--  jd = Julian date and its fractional part is zero at noon
--  jd = Julian date and its fractional part is zero at noon
--  jdz = similar, but fractional part is zero at 00:00:00
--  jdz = same, but assume time is 00:00:00 if no time given
-- http://www.tondering.dk/claus/cal/julperiod.php#formula
-- http://www.tondering.dk/claus/cal/julperiod.php#formula
-- Testing shows this works for all dates from year -9999 to 9999!
-- Testing shows this works for all dates from year -9999 to 9999!
Line 69: Line 62:
--    1 January 4713 BC  = (-4712, 1, 1)  Julian calendar
--    1 January 4713 BC  = (-4712, 1, 1)  Julian calendar
--  24 November 4714 BC = (-4713, 11, 24) Gregorian calendar
--  24 November 4714 BC = (-4713, 11, 24) Gregorian calendar
if not date.isvalid then
return 0, 0  -- always return numbers to simplify usage
end
local floor = math.floor
local floor = math.floor
local offset
local offset
Line 82: Line 72:
end
end
local m = date.month + 12*a - 3
local m = date.month + 12*a - 3
local date_part = date.day + floor((153*m + 2)/5) + 365*y + offset
local jd = date.day + floor((153*m + 2)/5) + 365*y + offset
local time_part, zbias
if date.hastime then
if date.hastime then
time_part = (date.hour + (date.minute + date.second / 60) /60) / 24 - 0.5
jd = jd + (date.hour + (date.minute + date.second / 60) /60) / 24 - 0.5
zbias = 0
return jd, jd
else
time_part = 0
zbias = -0.5
end
end
local jd = date_part + time_part
return jd, jd - 0.5
return jd, jd + zbias
end
end


local function set_date_from_jd(date)
local function set_date_from_jd(date)
-- Set the fields of table date from its Julian date field.
-- Set the fields of table date from its Julian date field.
-- Return true if date is valid.
-- http://www.tondering.dk/claus/cal/julperiod.php#formula
-- http://www.tondering.dk/claus/cal/julperiod.php#formula
-- This handles the proleptic Julian and Gregorian calendars.
-- This handles the proleptic Julian and Gregorian calendars.
Line 112: Line 98:
end
end
if not (limits[1] <= jd and jd <= limits[2]) then
if not (limits[1] <= jd and jd <= limits[2]) then
date.isvalid = false
return
return
end
end
date.isvalid = true
local jdn = floor(jd)
local jdn = floor(jd)
if date.hastime then
if date.hastime then
Line 152: Line 136:
date.month = m + 3 - 12*floor(m/10)
date.month = m + 3 - 12*floor(m/10)
date.year = 100*b + d - 4800 + floor(m/10)
date.year = 100*b + d - 4800 + floor(m/10)
return true
end
end


Line 189: Line 174:
date.minute = M  -- 0 to 59
date.minute = M  -- 0 to 59
date.second = S  -- 0 to 59
date.second = S  -- 0 to 59
date.isvalid = true
if type(options) == 'table' then
if type(options) == 'table' then
for _, k in ipairs({ 'am', 'era' }) do
for _, k in ipairs({ 'am', 'era' }) do
Line 200: Line 184:
end
end


local function make_option_table(options)
local function make_option_table(options1, options2)
-- If options is a string, return a table with its settings.
-- If options1 is a string, return a table with its settings, or
-- Otherwise return options (it should already be a table).
-- if it is a table, use its settings.
if type(options) == 'string' then
-- Missing options are set from options2 or defaults.
-- Valid option settings are:
-- am: 'am', 'a.m.', 'AM', 'A.M.'
-- era: 'BCMINUS', 'BCNEGATIVE', 'BC', 'B.C.', 'BCE', 'B.C.E.', 'AD', 'A.D.', 'CE', 'C.E.'
-- Option am = 'am' does not mean the hour is AM; it means 'am' or 'pm' is used, depending on the hour.
-- Similarly, era = 'BC' means 'BC' is used if year < 0.
-- BCMINUS displays a MINUS if year < 0 and the display format does not include %{era}.
-- BCNEGATIVE is similar but displays a hyphen.
local result = {}
if type(options1) == 'table' then
result = options1
elseif type(options1) == 'string' then
-- Example: 'am:AM era:BC'
-- Example: 'am:AM era:BC'
local result = {}
for item in options1:gmatch('%S+') do
for item in options:gmatch('%S+') do
local lhs, rhs = item:match('^(%w+):(.+)$')
local lhs, rhs = item:match('^(%w+):(.*)$')
if lhs then
if lhs then
result[lhs] = rhs
result[lhs] = rhs
end
end
end
end
return result
end
end
return options
options2 = type(options2) == 'table' and options2 or {}
local defaults = { am = 'am', era = 'BC' }
for k, v in pairs(defaults) do
result[k] = result[k] or options2[k] or v
end
return result
end
 
local era_text = {
-- Text for displaying an era with a positive year (after adjusting
-- by replacing year with 1 - year if date.year <= 0).
-- options.era = { year<=0 , year>0 }
['BCMINUS']    = { 'BC'    , ''    , isbc = true, sign = MINUS },
['BCNEGATIVE'] = { 'BC'    , ''    , isbc = true, sign = '-'  },
['BC']        = { 'BC'    , ''    , isbc = true },
['B.C.']      = { 'B.C.'  , ''    , isbc = true },
['BCE']        = { 'BCE'  , ''    , isbc = true },
['B.C.E.']    = { 'B.C.E.', ''    , isbc = true },
['AD']        = { 'BC'    , 'AD'  },
['A.D.']      = { 'B.C.'  , 'A.D.' },
['CE']        = { 'BCE'  , 'CE'  },
['C.E.']      = { 'B.C.E.', 'C.E.' },
}
 
local function get_era_for_year(era, year)
return (era_text[era or 'BC'] or {})[year > 0 and 2 or 1] or ''
end
end


Line 220: Line 238:
-- Return date formatted as a string using codes similar to those
-- Return date formatted as a string using codes similar to those
-- in the C strftime library function.
-- in the C strftime library function.
if not date.isvalid then
local sformat = string.format
return '(invalid)'
end
local shortcuts = {
local shortcuts = {
['%c'] = '%-I:%M %p %-d %B %Y%{era}',  -- date and time: 2:30 pm 1 April 2016
['%c'] = '%-I:%M %p %-d %B %-Y %{era}',  -- date and time: 2:30 pm 1 April 2016
['%x'] = '%-d %B %Y%{era}',            -- date:          1 April 2016
['%x'] = '%-d %B %-Y %{era}',            -- date:          1 April 2016
['%X'] = '%-I:%M %p',                 -- time:          2:30 pm
['%X'] = '%-I:%M %p',                   -- time:          2:30 pm
}
}
if shortcuts[format] then
format = shortcuts[format]
end
local codes = {
local codes = {
a = { field = 'dayabbr' },
a = { field = 'dayabbr' },
Line 245: Line 264:
p = { field = 'hour', special = 'am' },
p = { field = 'hour', special = 'am' },
}
}
options = make_option_table(options or date.options)
options = make_option_table(options, date.options)
local amopt = options.am
local amopt = options.am
local eraopt = options.era
local eraopt = options.era
local function replace_code(modifier, id)
local function replace_code(spaces, modifier, id)
local code = codes[id]
local code = codes[id]
if code then
if code then
Line 267: Line 286:
['A.M.'] = { 'A.M.', 'P.M.' },
['A.M.'] = { 'A.M.', 'P.M.' },
})[amopt] or { 'am', 'pm' }
})[amopt] or { 'am', 'pm' }
return value < 12 and ap[1] or ap[2]
return (spaces == '' and '' or '&nbsp;') .. (value < 12 and ap[1] or ap[2])
end
end
end
end
if code.field == 'year' then
if code.field == 'year' then
if eraopt == 'BCMINUS' or eraopt == 'BCNEGATIVE' then
local sign = (era_text[eraopt] or {}).sign
local sign
if not sign or format:find('%{era}', 1, true) then
sign = ''
if value <= 0 then
value = 1 - value
end
else
if value >= 0 then
if value >= 0 then
sign = ''
sign = ''
else
else
sign = eraopt == 'BCMINUS' and MINUS or '-'
value = -value
value = -value
end
end
return sign .. string.format(fmt, value)
end
if value <= 0 then
value = 1 - value
end
end
return spaces .. sign .. sformat(fmt, value)
end
end
return fmt and string.format(fmt, value) or value
return spaces .. (fmt and sformat(fmt, value) or value)
end
end
end
end
local function replace_property(id)
local function replace_property(spaces, id)
if id == 'era' then
-- Special case so can use local era option.
local result = get_era_for_year(eraopt, date.year)
if result == '' then
return ''
end
return (spaces == '' and '' or '&nbsp;') .. result
end
local result = date[id]
local result = date[id]
if type(result) == 'string' then
if type(result) == 'string' then
if id == 'era' and result ~= '' then
return spaces .. result
-- Assume era follows a date.
return '&nbsp;' .. result
end
return result
end
end
if type(result) == 'number' then
if type(result) == 'number' then
return tostring(result)
return spaces .. tostring(result)
end
end
if type(result) == 'boolean' then
if type(result) == 'boolean' then
return result and '1' or '0'
return spaces .. (result and '1' or '0')
end
end
-- This occurs, for example, if id is the name of a function.
-- This occurs, for example, if id is the name of a function.
return nil
return nil
end
if shortcuts[format] then
format = shortcuts[format]
end
end
local PERCENT = '\127PERCENT\127'
local PERCENT = '\127PERCENT\127'
return (format
return (format
:gsub('%%%%', PERCENT)
:gsub('%%%%', PERCENT)
:gsub('%%{(%w+)}', replace_property)
:gsub('(%s*)%%{(%w+)}', replace_property)
:gsub('%%(-?)(%a)', replace_code)
:gsub('(%s*)%%(-?)(%a)', replace_code)
:gsub(PERCENT, '%%')
:gsub(PERCENT, '%%')
)
)
end
end


local function date_text(date, fmt, options)
local function _date_text(date, fmt, options)
-- Return formatted string from given date.
-- Return formatted string from given date.
if not (type(date) == 'table' and date.isvalid) then
return '(invalid)'
end
if type(fmt) ~= 'string' then
if type(fmt) ~= 'string' then
fmt = '%Y-%m-%d'
fmt = '%-d %B %-Y %{era}'
if date.hastime then
if date.hastime then
if date.second > 0 then
if date.second > 0 then
fmt = fmt .. ' %H:%M:%S'
fmt = '%H:%M:%S ' .. fmt
else
else
fmt = fmt .. ' %H:%M'
fmt = '%H:%M ' .. fmt
end
end
end
end
return strftime(date, fmt, options or { era = 'BCMINUS' })
return strftime(date, fmt, options)
end
end
if fmt:find('%', 1, true) then
if fmt:find('%', 1, true) then
Line 345: Line 363:
f = '%H:%M:%S'
f = '%H:%M:%S'
elseif item == 'ymd' then
elseif item == 'ymd' then
f = '%Y:%m:%d%{era}'
f = '%Y-%m-%d %{era}'
elseif item == 'mdy' then
elseif item == 'mdy' then
f = '%B %-d, %Y%{era}'
f = '%B %-d, %-Y %{era}'
elseif item == 'dmy' then
elseif item == 'dmy' then
f = '%-d %B %Y%{era}'
f = '%-d %B %-Y %{era}'
else
else
return '(invalid format)'
return '(invalid format)'
Line 418: Line 436:
end
end
})
})
local function date_component(named, positional, component)
-- Return the first of the two arguments (named like {{example|year=2001}}
-- or positional like {{example|2001}}) that is not nil and is not empty.
-- If both are nil, return the current date component, if specified.
-- This translates empty arguments passed to the template to nil, and
-- optionally replaces a nil argument with a value from the current date.
named = strip_to_nil(named)
if named then
return named
end
positional = strip_to_nil(positional)
if positional then
return positional
end
if component then
return current[component]
end
return nil
end
local era_text = {
-- options.era = { year<0  , year>0 }
['BCMINUS']    = { MINUS  , ''    },
['BCNEGATIVE'] = { '-'    , ''    },
['BC']        = { 'BC'    , ''    },
['B.C.']      = { 'B.C.'  , ''    },
['BCE']        = { 'BCE'  , ''    },
['B.C.E.']    = { 'B.C.E.', ''    },
['AD']        = { 'BC'    , 'AD'  },
['A.D.']      = { 'B.C.'  , 'A.D.' },
['CE']        = { 'BCE'  , 'CE'  },
['C.E.']      = { 'B.C.E.', 'C.E.' },
}


local function extract_date(text)
local function extract_date(text)
Line 459: Line 443:
-- or return nothing if date is known to be invalid.
-- or return nothing if date is known to be invalid.
-- Caller determines if the values in n are valid.
-- Caller determines if the values in n are valid.
-- Dates of form d/m/y, m/d/y, y/m/d are rejected as ambiguous and undesirable.
-- A year must be positive ('1' to '9999'); use 'BC' for BC.
-- In a y-m-d string, the year must be four digits to avoid ambiguity
-- ('0001' to '9999'). The only way to enter year <= 0 is by specifying
-- the date as three numeric parameters like ymd Date(-1, 1, 1).
-- Dates of form d/m/y, m/d/y, y/m/d are rejected as ambiguous.
local date, options = {}, {}
local date, options = {}, {}
local function extract_ymd(item)
local function extract_ymd(item)
Line 537: Line 525:
end
end
for item in text:gsub(',', ' '):gmatch('%S+') do
for item in text:gsub(',', ' '):gmatch('%S+') do
-- Accept options in peculiar places; if duplicated, last wins.
item_count = item_count + 1
item_count = item_count + 1
if era_text[item] then
if era_text[item] then
-- Era is accepted in peculiar places.
if options.era then
return
end
options.era = item
options.era = item
elseif ampm_options[item] then
elseif ampm_options[item] then
Line 574: Line 565:
end
end
end
end
end
if not date.y or date.y == 0 then
return
end
local era = era_text[options.era]
if era and era.isbc then
date.y = 1 - date.y
end
end
return date, options
return date, options
end
local Date, DateDiff, datemt  -- forward declarations
local function is_date(t)
return type(t) == 'table' and getmetatable(t) == datemt
end
local function date_add_sub(lhs, rhs, is_sub)
-- Return a new date from calculating (lhs + rhs) or (lhs - rhs),
-- or return nothing if invalid.
-- Caller ensures that lhs is a date; its properties are copied for the new date.
local function is_prefix(text, word, minlen)
local n = #text
return (minlen or 1) <= n and n <= #word and text == word:sub(1, n)
end
local function do_days(n)
if is_sub then
n = -n
end
return Date(lhs, 'juliandate', lhs.jd + n)
end
if type(rhs) == 'number' then
-- Add days, including fractional days.
return do_days(rhs)
end
if type(rhs) == 'string' then
-- rhs is a single component like '26m' or '26 months' (unsigned integer only).
local num, id = rhs:match('^%s*(%d+)%s*(%a+)$')
if num then
local y, m
num = tonumber(num)
id = id:lower()
if is_prefix(id, 'years') then
y = num
m = 0
elseif is_prefix(id, 'months') then
y = math.floor(num / 12)
m = num % 12
elseif is_prefix(id, 'weeks') then
return do_days(num * 7)
elseif is_prefix(id, 'days') then
return do_days(num)
elseif is_prefix(id, 'hours') then
return do_days(num / 24)
elseif is_prefix(id, 'minutes', 3) then
return do_days(num / (24 * 60))
elseif is_prefix(id, 'seconds') then
return do_days(num / (24 * 3600))
else
return
end
if is_sub then
y = -y
m = -m
end
assert(-11 <= m and m <= 11)
y = lhs.year + y
m = lhs.month + m
if m > 12 then
y = y + 1
m = m - 12
elseif m < 1 then
y = y - 1
m = m + 12
end
local d = math.min(lhs.day, days_in_month(y, m, lhs.calname))
return Date(lhs, y, m, d)
end
end
end
end


-- Metatable for some operations on dates.
-- Metatable for some operations on dates.
-- For Lua 5.1, __lt does not work if the metatable is an anonymous table.
datemt = {  -- for forward declaration above
local Date -- forward declaration
__add = function (lhs, rhs)
local datemt = {
if not is_date(lhs) then
lhs, rhs = rhs, lhs -- put date on left (it must be a date for this to have been called)
end
return date_add_sub(lhs, rhs)
end,
__sub = function (lhs, rhs)
if is_date(lhs) then
if is_date(rhs) then
return DateDiff(lhs, rhs)
end
return date_add_sub(lhs, rhs, true)
end
end,
__concat = function (lhs, rhs)
return tostring(lhs) .. tostring(rhs)
end,
__tostring = function (self)
return self:text()
end,
__eq = function (lhs, rhs)
__eq = function (lhs, rhs)
-- Return true if dates identify same date/time where, for example,
-- Return true if dates identify same date/time where, for example,
-- (-4712, 1, 1, 'Julian') == (-4713, 11, 24, 'Gregorian').
-- Date(-4712, 1, 1, 'Julian') == Date(-4713, 11, 24, 'Gregorian') is true.
return lhs.isvalid and rhs.isvalid and lhs.jdz == rhs.jdz
-- This is only called if lhs and rhs have the same metatable.
return lhs.jdz == rhs.jdz
end,
end,
__lt = function (lhs, rhs)
__lt = function (lhs, rhs)
-- Return true if lhs < rhs.
-- Return true if lhs < rhs, for example,
if not lhs.isvalid then
-- Date('1 Jan 2016') < Date('06:00 1 Jan 2016') is true.
return true
-- This is only called if lhs and rhs have the same metatable.
end
if not rhs.isvalid then
return false
end
return lhs.jdz < rhs.jdz
return lhs.jdz < rhs.jdz
end,
end,
Line 611: Line 694:
value = self.jd - first + 1  -- day-of-year 1 to 366
value = self.jd - first + 1  -- day-of-year 1 to 366
elseif key == 'era' then
elseif key == 'era' then
-- Era text from year and options.
-- Era text (not a negative sign) from year and options.
local eraopt = self.options.era
value = get_era_for_year(self.options.era, self.year)
local sign
if self.year == 0 then
sign = (eraopt == 'BCMINUS' or eraopt == 'BCNEGATIVE') and 2 or 1
else
sign = self.year > 0 and 2 or 1
end
value = era_text[eraopt][sign]
elseif key == 'gsd' then
elseif key == 'gsd' then
-- GSD = 1 from 00:00:00 to 23:59:59 on 1 January 1 AD Gregorian calendar,
-- GSD = 1 from 00:00:00 to 23:59:59 on 1 January 1 AD Gregorian calendar,
Line 642: Line 718:
end,
end,
}
}
local function _month_days(date, month)
return days_in_month(date.year, month, date.calname)
end


--[[ Examples of syntax to construct a date:
--[[ Examples of syntax to construct a date:
Line 649: Line 729:
Date('currentdate')
Date('currentdate')
Date('currentdatetime')
Date('currentdatetime')
LATER: Following are not yet implemented:
Date('currentdate', H, M, S)        current date with given time
Date('1 April 1995', 'julian')      parse date from text
Date('1 April 1995', 'julian')      parse date from text
Date('1 April 1995 AD', 'julian')  AD, CE, BC, BCE (using one of these sets a flag to do same for output)
Date('1 April 1995 AD', 'julian')  using an era sets a flag to do the same for output
Date('04:30:59 1 April 1995', 'julian')
Date('04:30:59 1 April 1995', 'julian')
Date(date)                          copy of an existing date
LATER: Following is not yet implemented:
Date('currentdate', H, M, S)        current date with given time
]]
]]
function Date(...)  -- for forward declaration above
function Date(...)  -- for forward declaration above
-- Return a table to hold a date assuming a uniform calendar always applies (proleptic).
-- Return a table holding a date assuming a uniform calendar always applies
-- If invalid, return an empty table which is regarded as invalid.
-- (proleptic Gregorian calendar or proleptic Julian calendar), or
-- return nothing if date is invalid.
local is_copy
local calendars = { julian = 'Julian', gregorian = 'Gregorian' }
local calendars = { julian = 'Julian', gregorian = 'Gregorian' }
local result = {
local result = {
isvalid = false,  -- false avoids __index lookup
calname = 'Gregorian',  -- default is Gregorian calendar
calname = 'Gregorian',  -- default is Gregorian calendar
hastime = false,  -- true if input sets a time
hastime = false,  -- true if input sets a time
Line 666: Line 748:
minute = 0,
minute = 0,
second = 0,
second = 0,
month_days = function (self, month)
month_days = _month_days,
return days_in_month(self.year, month, self.calname)
options = make_option_table(),
end,
text = _date_text,
-- Valid option settings are:
-- am: 'am', 'a.m.', 'AM', 'A.M.'
-- era: 'BCMINUS', 'BCNEGATIVE', 'BC', 'B.C.', 'BCE', 'B.C.E.', 'AD', 'A.D.', 'CE', 'C.E.'
options = { am = 'am', era = 'BC' },
text = date_text,
}
}
local argtype, datetext
local argtype, datetext
Line 684: Line 761:
elseif calendars[vlower] then
elseif calendars[vlower] then
result.calname = calendars[vlower]
result.calname = calendars[vlower]
elseif is_date(v) then
-- Copy existing date (items can be overridden by other arguments).
if is_copy then
return
end
is_copy = true
result.calname = v.calname
result.hastime = v.hastime
result.options = v.options
result.year = v.year
result.month = v.month
result.day = v.day
result.hour = v.hour
result.minute = v.minute
result.second = v.second
else
else
local num = tonumber(v)
local num = tonumber(v)
Line 706: Line 798:
end
end
elseif argtype then
elseif argtype then
return {}
return
elseif type(v) == 'string' then
elseif type(v) == 'string' then
if v == 'currentdate' or v == 'currentdatetime' or v == 'juliandate' then
if v == 'currentdate' or v == 'currentdatetime' or v == 'juliandate' then
Line 715: Line 807:
end
end
else
else
return {}
return
end
end
end
end
Line 723: Line 815:
set_date_from_numbers(result,
set_date_from_numbers(result,
extract_date(datetext))) then
extract_date(datetext))) then
return {}
return
end
end
elseif argtype == 'juliandate' then
elseif argtype == 'juliandate' then
if numbers.n == 1 then
result.jd = numbers[1]
result.jd = numbers[1]
if not (numbers.n == 1 and set_date_from_jd(result)) then
set_date_from_jd(result)
return
else
return {}
end
end
elseif argtype == 'currentdate' or argtype == 'currentdatetime' then
elseif argtype == 'currentdate' or argtype == 'currentdatetime' then
Line 743: Line 833:
end
end
result.calname = 'Gregorian'  -- ignore any given calendar name
result.calname = 'Gregorian'  -- ignore any given calendar name
result.isvalid = true
elseif argtype == 'setdate' then
elseif argtype == 'setdate' then
if not set_date_from_numbers(result, numbers) then
if not set_date_from_numbers(result, numbers) then
return {}
return
end
end
else
elseif not is_copy then
return {}
return
end
end
return setmetatable(result, datemt)
return setmetatable(result, datemt)
end
end


local function DateDiff(date1, date2)
function DateDiff(date1, date2) -- for forward declaration above
-- Return a table to with the difference between the two given dates.
-- Return a table with the difference between the two dates (date1 - date2).
-- Difference is negative if the second date is older than the first.
-- The difference is negative if date2 is more recent than date1.
-- TODO Replace with something using Julian dates?
-- Return nothing if invalid.
--      Who checks for isvalid()?
if not (date1 and date2 and date1.calname == date2.calname) then
--      Handle calname == 'Julian'
return
local calname = 'Gregorian'  -- TODO fix
end
local isnegative
local isnegative
if date2 < date1 then
if date1 < date2 then
isnegative = true
isnegative = true
date1, date2 = date2, date1
date1, date2 = date2, date1
end
end
-- It is known that date1 <= date2.
-- It is known that date1 >= date2.
local y1, m1 = date1.year, date1.month
local y1, m1 = date1.year, date1.month
local y2, m2 = date2.year, date2.month
local y2, m2 = date2.year, date2.month
local years, months, days = y2 - y1, m2 - m1, date2.day - date1.day
local years, months, days = y1 - y2, m1 - m2, date1.day - date2.day
if days < 0 then
if days < 0 then
days = days + days_in_month(y1, m1, calname)
days = days + days_in_month(y2, m2, date2.calname)
months = months - 1
months = months - 1
end
end
Line 804: Line 893:
end,
end,
}
}
end
local function message(msg, nocat)
-- Return formatted message text for an error.
-- Can append "#FormattingError" to URL of a page with a problem to find it.
local anchor = '<span id="FormattingError" />'
local category
if not nocat and mw.title.getCurrentTitle():inNamespaces(0, 10) then
-- Category only in namespaces: 0=article, 10=template.
category = '[[Category:Age error]]'
else
category = ''
end
return anchor ..
'<strong class="error">Error: ' ..
msg ..
'</strong>' ..
category .. '\n'
end
local function age_days(frame)
-- Return age in days between two given dates, or
-- between given date and current date.
-- This code implements the logic in [[Template:Age in days]].
-- Like {{Age in days}}, a missing argument is replaced from the current
-- date, so can get a bizarre mixture of specified/current y/m/d.
local args = frame:getParent().args
local date1 = Date(
date_component(args.year1 , args[1], 'year' ),
date_component(args.month1, args[2], 'month'),
date_component(args.day1  , args[3], 'day'  )
)
local date2 = Date(
date_component(args.year2 , args[4], 'year' ),
date_component(args.month2, args[5], 'month'),
date_component(args.day2  , args[6], 'day'  )
)
if not (date1.isvalid and date2.isvalid) then
return message('Need valid year, month, day')
end
local sign = ''
local result = date2.jd - date1.jd
if result < 0 then
sign = MINUS
result = -result
end
return sign .. tostring(result)
end
local function age_ym(frame)
-- Return age in years and months between two given dates, or
-- between given date and current date.
local args = frame:getParent().args
local fields = {}
for i = 1, 6 do
fields[i] = strip_to_nil(args[i])
end
local date1, date2
if fields[1] and fields[2] and fields[3] then
date1 = Date(fields[1], fields[2], fields[3])
end
if not (date1 and date1.isvalid) then
return message('Need valid year, month, day')
end
if fields[4] and fields[5] and fields[6] then
date2 = Date(fields[4], fields[5], fields[6])
if not date2.isvalid then
return message('Second date should be year, month, day')
end
else
date2 = Date('currentdate')
end
return DateDiff(date1, date2):age_ym()
end
local function gsd_ymd(frame)
-- Return Gregorian serial date of the given date, or the current date.
-- Like {{Gregorian serial date}}, a missing argument is replaced from the
-- current date, so can get a bizarre mixture of specified/current y/m/d.
-- This also accepts positional arguments, although the original template does not.
-- The returned value is negative for dates before 1 January 1 AD despite
-- the fact that GSD is not defined for earlier dates.
local args = frame:getParent().args
local date = Date(
date_component(args.year , args[1], 'year' ),
date_component(args.month, args[2], 'month'),
date_component(args.day  , args[3], 'day'  )
)
if date.isvalid then
return tostring(date.gsd)
end
return message('Need valid year, month, day')
end
local function ymd_from_jd(frame)
-- Return formatted date from a Julian date.
-- The result is y-m-d or y-m-d H:M:S if input includes a fraction.
-- The word 'Julian' is accepted for the Julian calendar.
local args = frame:getParent().args
local date = Date('juliandate', args[1], args[2])
if date.isvalid then
return date:text()
end
return message('Need valid Julian date number')
end
local function ymd_to_jd(frame)
-- Return Julian date (a number) from a date (y-m-d), or datetime (y-m-d H:M:S),
-- or the current date ('currentdate') or current datetime ('currentdatetime').
-- The word 'Julian' is accepted for the Julian calendar.
local args = frame:getParent().args
local date = Date(args[1], args[2], args[3], args[4], args[5], args[6], args[7])
if date.isvalid then
return tostring(date.jd)
end
return message('Need valid year/month/day or "currentdate"')
end
end


return {
return {
age_days = age_days,
_current = current,
age_ym = age_ym,
_Date = Date,
_Date = Date,
days_in_month = days_in_month,
_DateDiff = DateDiff,
gsd = gsd_ymd,
_days_in_month = days_in_month,
JULIANDAY = ymd_to_jd,
ymd_from_jd = ymd_from_jd,
ymd_to_jd = ymd_to_jd,
}
}
Anonymous user
Cookies help us deliver our services. By using our services, you agree to our use of cookies.