Diferencia entre revisiones de «Módulo:Date»

De Hispanopedia
Sin resumen de edición
Sin resumen de edición
Línea 1: Línea 1:
-- Date functions for use by other modules.
-- Date functions for use by other modules.
-- I18N and time zones are not supported.
-- I18N and time zones are not supported.


local MINUS = '−' -- Unicode U+2212 MINUS SIGN
local MINUS = '−' -- Unicode U+2212 MINUS SIGN
local floor = math.floor
local floor = math.floor


local Date, DateDiff, diffmt -- forward declarations
local Date, DateDiff, diffmt -- forward declarations
local uniq = { 'unique identifier' }
local uniq = { 'unique identifier' }


local function is_date(t)
local function is_date(t)
-- The system used to make a date read-only means there is no unique
  -- The system used to make a date read-only means there is no unique
-- metatable that is conveniently accessible to check.
  -- metatable that is conveniently accessible to check.
return type(t) == 'table' and t._id == uniq
  return type(t) == 'table' and t._id == uniq
end
end


local function is_diff(t)
local function is_diff(t)
return type(t) == 'table' and getmetatable(t) == diffmt
  return type(t) == 'table' and getmetatable(t) == diffmt
end
end


local function _list_join(list, sep)
local function _list_join(list, sep)
return table.concat(list, sep)
  return table.concat(list, sep)
end
end


local function collection()
local function collection()
-- Return a table to hold items.
  -- Return a table to hold items.
return {
  return {
n = 0,
    n = 0,
add = function (self, item)
    add = function (self, item)
self.n = self.n + 1
      self.n = self.n + 1
self[self.n] = item
      self[self.n] = item
end,
    end,
join = _list_join,
    join = _list_join,
}
  }
end
end


local function strip_to_nil(text)
local function strip_to_nil(text)
-- If text is a string, return its trimmed content, or nil if empty.
  -- If text is a string, return its trimmed content, or nil if empty.
-- Otherwise return text (convenient when Date fields are provided from
  -- Otherwise return text (convenient when Date fields are provided from
-- another module which may pass a string, a number, or another type).
  -- another module which may pass a string, a number, or another type).
if type(text) == 'string' then
  if type(text) == 'string' then
text = text:match('(%S.-)%s*$')
    text = text:match('(%S.-)%s*$')
end
  end
return text
  return text
end
end


local function is_leap_year(year, calname)
local function is_leap_year(year, calname)
-- Return true if year is a leap year.
  -- Return true if year is a leap year.
if calname == 'Juliano' then
  if calname == 'Juliano' then
return year % 4 == 0
    return year % 4 == 0
end
  end
return (year % 4 == 0 and year % 100 ~= 0) or year % 400 == 0
  return (year % 4 == 0 and year % 100 ~= 0) or year % 400 == 0
end
end


local function days_in_month(year, month, calname)
local function days_in_month(year, month, calname)
-- Return number of days (1..31) in given month (1..12).
  -- Return number of days (1..31) in given month (1..12).
if month == 2 and is_leap_year(year, calname) then
  if month == 2 and is_leap_year(year, calname) then
return 29
    return 29
end
  end
return ({ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 })[month]
  return ({ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 })[month]
end
end


local function h_m_s(time)
local function h_m_s(time)
-- Return hour, minute, second extracted from fraction of a day.
  -- Return hour, minute, second extracted from fraction of a day.
time = floor(time * 24 * 3600 + 0.5) -- number of seconds
  time = floor(time * 24 * 3600 + 0.5) -- number of seconds
local second = time % 60
  local second = time % 60
time = floor(time / 60)
  time = floor(time / 60)
return floor(time / 60), time % 60, second
  return floor(time / 60), time % 60, second
end
end


local function hms(date)
local function hms(date)
-- Return fraction of a day from date's time, where (0 <= fraction < 1)
  -- Return fraction of a day from date's time, where (0 <= fraction < 1)
-- if the values are valid, but could be anything if outside range.
  -- if the values are valid, but could be anything if outside range.
return (date.hour + (date.minute + date.second / 60) / 60) / 24
  return (date.hour + (date.minute + date.second / 60) / 60) / 24
end
end


local function julian_date(date)
local function julian_date(date)
-- 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 = same, but assume time is 00:00:00 if no time given
  -- 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!
-- JDN 0 is the 24-hour period starting at noon UTC on Monday
  -- JDN 0 is the 24-hour period starting at noon UTC on Monday
--   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
local offset
  local offset
local a = floor((14 - date.month)/12)
  local a = floor((14 - date.month)/12)
local y = date.year + 4800 - a
  local y = date.year + 4800 - a
if date.calendar == 'Juliano' then
  if date.calendar == 'Juliano' then
offset = floor(y/4) - 32083
    offset = floor(y/4) - 32083
else
  else
offset = floor(y/4) - floor(y/100) + floor(y/400) - 32045
    offset = floor(y/4) - floor(y/100) + floor(y/400) - 32045
end
  end
local m = date.month + 12*a - 3
  local m = date.month + 12*a - 3
local jd = date.day + floor((153*m + 2)/5) + 365*y + offset
  local jd = date.day + floor((153*m + 2)/5) + 365*y + offset
if date.hastime then
  if date.hastime then
jd = jd + hms(date) - 0.5
    jd = jd + hms(date) - 0.5
return jd, jd
    return jd, jd
end
  end
return jd, jd - 0.5
  return jd, jd - 0.5
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.
  -- 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.
-- Negative Julian dates are not defined but they work.
  -- Negative Julian dates are not defined but they work.
local calname = date.calendar
  local calname = date.calendar
local low, high -- min/max limits for date ranges −9999-01-01 to 9999-12-31
  local low, high -- min/max limits for date ranges −9999-01-01 to 9999-12-31
if calname == 'Gregoriano' then
  if calname == 'Gregoriano' then
low, high = -1930999.5, 5373484.49999
    low, high = -1930999.5, 5373484.49999
elseif calname == 'Juliano' then
  elseif calname == 'Juliano' then
low, high = -1931076.5, 5373557.49999
    low, high = -1931076.5, 5373557.49999
else
  else
return
    return
end
  end
local jd = date.jd
  local jd = date.jd
if not (type(jd) == 'number' and low <= jd and jd <= high) then
  if not (type(jd) == 'number' and low <= jd and jd <= high) then
return
    return
end
  end
local jdn = floor(jd)
  local jdn = floor(jd)
if date.hastime then
  if date.hastime then
local time = jd - jdn -- 0 <= time < 1
    local time = jd - jdn -- 0 <= time < 1
if time >= 0.5 then   -- if at or after midnight of next day
    if time >= 0.5 then -- if at or after midnight of next day
jdn = jdn + 1
      jdn = jdn + 1
time = time - 0.5
      time = time - 0.5
else
    else
time = time + 0.5
      time = time + 0.5
end
    end
date.hour, date.minute, date.second = h_m_s(time)
    date.hour, date.minute, date.second = h_m_s(time)
else
  else
date.second = 0
    date.second = 0
date.minute = 0
    date.minute = 0
date.hour = 0
    date.hour = 0
end
  end
local b, c
  local b, c
if calname == 'Juliano' then
  if calname == 'Juliano' then
b = 0
    b = 0
c = jdn + 32082
    c = jdn + 32082
else -- Gregorian
  else -- Gregorian
local a = jdn + 32044
    local a = jdn + 32044
b = floor((4*a + 3)/146097)
    b = floor((4*a + 3)/146097)
c = a - floor(146097*b/4)
    c = a - floor(146097*b/4)
end
  end
local d = floor((4*c + 3)/1461)
  local d = floor((4*c + 3)/1461)
local e = c - floor(1461*d/4)
  local e = c - floor(1461*d/4)
local m = floor((5*e + 2)/153)
  local m = floor((5*e + 2)/153)
date.day = e - floor((153*m + 2)/5) + 1
  date.day = e - floor((153*m + 2)/5) + 1
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
  return true
end
end


local function fix_numbers(numbers, y, m, d, H, M, S, partial, hastime, calendar)
local function fix_numbers(numbers, y, m, d, H, M, S, partial, hastime, calendar)
-- Put the result of normalizing the given values in table numbers.
  -- Put the result of normalizing the given values in table numbers.
-- The result will have valid m, d values if y is valid; caller checks y.
  -- The result will have valid m, d values if y is valid; caller checks y.
-- The logic of PHP mktime is followed where m or d can be zero to mean
  -- The logic of PHP mktime is followed where m or d can be zero to mean
-- the previous unit, and -1 is the one before that, etc.
  -- the previous unit, and -1 is the one before that, etc.
-- Positive values carry forward.
  -- Positive values carry forward.
local date
  local date
if not (1 <= m and m <= 12) then
  if not (1 <= m and m <= 12) then
date = Date(y, 1, 1)
    date = Date(y, 1, 1)
if not date then return end
    if not date then return end
date = date + ((m - 1) .. 'm')
    date = date + ((m - 1) .. 'm')
y, m = date.year, date.month
    y, m = date.year, date.month
end
  end
local days_hms
  local days_hms
if not partial then
  if not partial then
if hastime and H and M and S then
    if hastime and H and M and S then
if not (0 <= H and H <= 23 and
      if not (0 <= H and H <= 23 and
0 <= M and M <= 59 and
          0 <= M and M <= 59 and
0 <= S and S <= 59) then
          0 <= S and S <= 59) then
days_hms = hms({ hour = H, minute = M, second = S })
        days_hms = hms({ hour = H, minute = M, second = S })
end
      end
end
    end
if days_hms or not (1 <= d and d <= days_in_month(y, m, calendar)) then
    if days_hms or not (1 <= d and d <= days_in_month(y, m, calendar)) then
date = date or Date(y, m, 1)
      date = date or Date(y, m, 1)
if not date then return end
      if not date then return end
date = date + (d - 1 + (days_hms or 0))
      date = date + (d - 1 + (days_hms or 0))
y, m, d = date.year, date.month, date.day
      y, m, d = date.year, date.month, date.day
if days_hms then
      if days_hms then
H, M, S = date.hour, date.minute, date.second
        H, M, S = date.hour, date.minute, date.second
end
      end
end
    end
end
  end
numbers.year = y
  numbers.year = y
numbers.month = m
  numbers.month = m
numbers.day = d
  numbers.day = d
if days_hms then
  if days_hms then
-- Don't set H unless it was valid because a valid H will set hastime.
    -- Don't set H unless it was valid because a valid H will set hastime.
numbers.hour = H
    numbers.hour = H
numbers.minute = M
    numbers.minute = M
numbers.second = S
    numbers.second = S
end
  end
end
end


local function set_date_from_numbers(date, numbers, options)
local function set_date_from_numbers(date, numbers, options)
-- Set the fields of table date from numeric values.
  -- Set the fields of table date from numeric values.
-- Return true if date is valid.
  -- Return true if date is valid.
if type(numbers) ~= 'table' then
  if type(numbers) ~= 'table' then
return
    return
end
  end
local y = numbers.year   or date.year
  local y = numbers.year or date.year
local m = numbers.month or date.month
  local m = numbers.month or date.month
local d = numbers.day   or date.day
  local d = numbers.day or date.day
local H = numbers.hour
  local H = numbers.hour
local M = numbers.minute or date.minute or 0
  local M = numbers.minute or date.minute or 0
local S = numbers.second or date.second or 0
  local S = numbers.second or date.second or 0
local need_fix
  local need_fix
if y and m and d then
  if y and m and d then
date.partial = nil
    date.partial = nil
if not (-9999 <= y and y <= 9999 and
    if not (-9999 <= y and y <= 9999 and
1 <= m and m <= 12 and
      1 <= m and m <= 12 and
1 <= d and d <= days_in_month(y, m, date.calendar)) then
      1 <= d and d <= days_in_month(y, m, date.calendar)) then
if not date.want_fix then
        if not date.want_fix then
return
          return
end
        end
need_fix = true
        need_fix = true
end
    end
elseif y and date.partial then
  elseif y and date.partial then
if d or not (-9999 <= y and y <= 9999) then
    if d or not (-9999 <= y and y <= 9999) then
return
      return
end
    end
if m and not (1 <= m and m <= 12) then
    if m and not (1 <= m and m <= 12) then
if not date.want_fix then
      if not date.want_fix then
return
        return
end
      end
need_fix = true
      need_fix = true
end
    end
else
  else
return
    return
end
  end
if date.partial then
  if date.partial then
H = nil -- ignore any time
    H = nil -- ignore any time
M = nil
    M = nil
S = nil
    S = nil
else
  else
if H then
    if H then
-- It is not possible to set M or S without also setting H.
      -- It is not possible to set M or S without also setting H.
date.hastime = true
      date.hastime = true
else
    else
H = 0
      H = 0
end
    end
if not (0 <= H and H <= 23 and
    if not (0 <= H and H <= 23 and
0 <= M and M <= 59 and
        0 <= M and M <= 59 and
0 <= S and S <= 59) then
        0 <= S and S <= 59) then
if date.want_fix then
      if date.want_fix then
need_fix = true
        need_fix = true
else
      else
return
        return
end
      end
end
    end
end
  end
date.want_fix = nil
  date.want_fix = nil
if need_fix then
  if need_fix then
fix_numbers(numbers, y, m, d, H, M, S, date.partial, date.hastime, date.calendar)
    fix_numbers(numbers, y, m, d, H, M, S, date.partial, date.hastime, date.calendar)
return set_date_from_numbers(date, numbers, options)
    return set_date_from_numbers(date, numbers, options)
end
  end
date.year = y   -- -9999 to 9999 ('n BC' → year = 1 - n)
  date.year = y -- -9999 to 9999 ('n BC' → year = 1 - n)
date.month = m   -- 1 to 12 (may be nil if partial)
  date.month = m -- 1 to 12 (may be nil if partial)
date.day = d     -- 1 to 31 (* = nil if partial)
  date.day = d -- 1 to 31 (* = nil if partial)
date.hour = H   -- 0 to 59 (*)
  date.hour = H -- 0 to 59 (*)
date.minute = M -- 0 to 59 (*)
  date.minute = M -- 0 to 59 (*)
date.second = S -- 0 to 59 (*)
  date.second = S -- 0 to 59 (*)
if type(options) == 'table' then
  if type(options) == 'table' then
for _, k in ipairs({ 'am', 'era', 'format' }) do
    for _, k in ipairs({ 'am', 'era', 'format' }) do
if options[k] then
      if options[k] then
date.options[k] = options[k]
        date.options[k] = options[k]
end
      end
end
    end
end
  end
return true
  return true
end
end


local function make_option_table(options1, options2)
local function make_option_table(options1, options2)
-- If options1 is a string, return a table with its settings, or
  -- If options1 is a string, return a table with its settings, or
-- if it is a table, use its settings.
  -- if it is a table, use its settings.
-- Missing options are set from table options2 or defaults.
  -- Missing options are set from table options2 or defaults.
-- If a default is used, a flag is set so caller knows the value was not intentionally set.
  -- If a default is used, a flag is set so caller knows the value was not intentionally set.
-- Valid option settings are:
  -- Valid option settings are:
-- am: 'am', 'a.m.', 'AM', 'A.M.'
  -- am: 'am', 'a.m.', 'AM', 'A.M.'
--     'pm', 'p.m.', 'PM', 'P.M.' (each has same meaning as corresponding item above)
  -- 'pm', 'p.m.', 'PM', 'P.M.' (each has same meaning as corresponding item above)
-- era: 'BCMINUS', 'BCNEGATIVE', 'BC', 'B.C.', 'BCE', 'B.C.E.', 'AD', 'A.D.', 'CE', 'C.E.'
  -- 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,
  -- Option am = 'am' does not mean the hour is AM; it means 'am' or 'pm' is used, depending on the hour,
--   and am = 'pm' has the same meaning.
  -- and am = 'pm' has the same meaning.
-- Similarly, era = 'BC' means 'BC' is used if year <= 0.
  -- 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}.
  -- BCMINUS displays a MINUS if year < 0 and the display format does not include %{era}.
-- BCNEGATIVE is similar but displays a hyphen.
  -- BCNEGATIVE is similar but displays a hyphen.
local result = { bydefault = {} }
  local result = { bydefault = {} }
if type(options1) == 'table' then
  if type(options1) == 'table' then
result.am = options1.am
    result.am = options1.am
result.era = options1.era
    result.era = options1.era
elseif type(options1) == 'string' then
  elseif type(options1) == 'string' then
-- Example: 'am:AM era:BC' or 'am=AM era=BC'.
    -- Example: 'am:AM era:BC' or 'am=AM era=BC'.
for item in options1:gmatch('%S+') do
    for item in options1: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
end
  end
options2 = type(options2) == 'table' and options2 or {}
  options2 = type(options2) == 'table' and options2 or {}
local defaults = { am = 'am', era = 'BC' }
  local defaults = { am = 'am', era = 'BC' }
for k, v in pairs(defaults) do
  for k, v in pairs(defaults) do
if not result[k] then
    if not result[k] then
if options2[k] then
      if options2[k] then
result[k] = options2[k]
        result[k] = options2[k]
else
      else
result[k] = v
        result[k] = v
result.bydefault[k] = true
        result.bydefault[k] = true
end
      end
end
    end
end
  end
return result
  return result
end
end


local ampm_options = {
local ampm_options = {
-- lhs = input text accepted as an am/pm option
  -- lhs = input text accepted as an am/pm option
-- rhs = code used internally
  -- rhs = code used internally
['am']   = 'am',
  ['am'] = 'am',
['AM']   = 'AM',
  ['AM'] = 'AM',
['a.m.'] = 'a.m.',
  ['a.m.'] = 'a.m.',
['A.M.'] = 'A.M.',
  ['A.M.'] = 'A.M.',
['pm']   = 'am', -- same as am
  ['pm'] = 'am', -- same as am
['PM']   = 'AM',
  ['PM'] = 'AM',
['p.m.'] = 'a.m.',
  ['p.m.'] = 'a.m.',
['P.M.'] = 'A.M.',
  ['P.M.'] = 'A.M.',
}
}


local era_text = {
local era_text = {
-- Text for displaying an era with a positive year (after adjusting
  -- Text for displaying an era with a positive year (after adjusting
-- by replacing year with 1 - year if date.year <= 0).
  -- by replacing year with 1 - year if date.year <= 0).
-- options.era = { year<=0 , year>0 }
  -- options.era = { year<=0 , year>0 }
['BCMINUS']   = { 'BC'   , ''   , isbc = true, sign = MINUS },
  ['BCMINUS'] = { 'BC' , '' , isbc = true, sign = MINUS },
['BCNEGATIVE'] = { 'BC'   , ''   , isbc = true, sign = '-'   },
  ['BCNEGATIVE'] = { 'BC' , '' , isbc = true, sign = '-' },
['BC']         = { 'BC'   , ''   , isbc = true },
  ['BC'] = { 'BC' , '' , isbc = true },
['B.C.']       = { 'B.C.' , ''   , isbc = true },
  ['B.C.'] = { 'B.C.' , '' , isbc = true },
['BCE']       = { 'BCE'   , ''   , isbc = true },
  ['BCE'] = { 'BCE' , '' , isbc = true },
['B.C.E.']     = { 'B.C.E.', ''   , isbc = true },
  ['B.C.E.'] = { 'B.C.E.', '' , isbc = true },
['AD']         = { 'BC'   , 'AD'   },
  ['AD'] = { 'BC' , 'AD' },
['A.D.']       = { 'B.C.' , 'A.D.' },
  ['A.D.'] = { 'B.C.' , 'A.D.' },
['CE']         = { 'BCE'   , 'CE'   },
  ['CE'] = { 'BCE' , 'CE' },
['C.E.']       = { 'B.C.E.', 'C.E.' },
  ['C.E.'] = { 'B.C.E.', 'C.E.' },
}
}


local function get_era_for_year(era, year)
local function get_era_for_year(era, year)
return (era_text[era] or era_text['BC'])[year > 0 and 2 or 1] or ''
  return (era_text[era] or era_text['BC'])[year > 0 and 2 or 1] or ''
end
end


local function strftime(date, format, options)
local function strftime(date, format, options)
-- 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.
local sformat = string.format
  local sformat = string.format
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
  if shortcuts[format] then
format = shortcuts[format]
    format = shortcuts[format]
end
  end
local codes = {
  local codes = {
a = { field = 'dayabbr' },
    a = { field = 'dayabbr' },
A = { field = 'dayname' },
    A = { field = 'dayname' },
b = { field = 'monthabbr' },
    b = { field = 'monthabbr' },
B = { field = 'monthname' },
    B = { field = 'monthname' },
u = { fmt = '%d' , field = 'dowiso' },
    u = { fmt = '%d' , field = 'dowiso' },
w = { fmt = '%d' , field = 'dow' },
    w = { fmt = '%d' , field = 'dow' },
d = { fmt = '%02d', fmt2 = '%d', field = 'day' },
    d = { fmt = '%02d', fmt2 = '%d', field = 'day' },
m = { fmt = '%02d', fmt2 = '%d', field = 'month' },
    m = { fmt = '%02d', fmt2 = '%d', field = 'month' },
Y = { fmt = '%04d', fmt2 = '%d', field = 'year' },
    Y = { fmt = '%04d', fmt2 = '%d', field = 'year' },
H = { fmt = '%02d', fmt2 = '%d', field = 'hour' },
    H = { fmt = '%02d', fmt2 = '%d', field = 'hour' },
M = { fmt = '%02d', fmt2 = '%d', field = 'minute' },
    M = { fmt = '%02d', fmt2 = '%d', field = 'minute' },
S = { fmt = '%02d', fmt2 = '%d', field = 'second' },
    S = { fmt = '%02d', fmt2 = '%d', field = 'second' },
j = { fmt = '%03d', fmt2 = '%d', field = 'dayofyear' },
    j = { fmt = '%03d', fmt2 = '%d', field = 'dayofyear' },
I = { fmt = '%02d', fmt2 = '%d', field = 'hour', special = 'hour12' },
    I = { fmt = '%02d', fmt2 = '%d', field = 'hour', special = 'hour12' },
p = { field = 'hour', special = 'am' },
    p = { field = 'hour', special = 'am' },
}
  }
options = make_option_table(options, 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(spaces, modifier, id)
  local function replace_code(spaces, modifier, id)
local code = codes[id]
--print('spaces:' .. spaces .. ';')
if code then
--print('modifier:' .. modifier .. ';')
local fmt = code.fmt
--print('id:' .. id .. ';')
if modifier == '-' and code.fmt2 then
    local code = codes[id]
fmt = code.fmt2
    if code then
end
      local fmt = code.fmt
local value = date[code.field]
      if modifier == '-' and code.fmt2 then
if not value then
        fmt = code.fmt2
return nil -- an undefined field in a partial date
      end
end
      local value = date[code.field]
local special = code.special
      if not value then
if special then
        return nil -- an undefined field in a partial date
if special == 'hour12' then
      end
value = value % 12
      local special = code.special
value = value == 0 and 12 or value
      if special then
elseif special == 'am' then
        if special == 'hour12' then
local ap = ({
          value = value % 12
['a.m.'] = { 'a.m.', 'p.m.' },
          value = value == 0 and 12 or value
['AM'] = { 'AM', 'PM' },
        elseif special == 'am' then
['A.M.'] = { 'A.M.', 'P.M.' },
          local ap = ({
})[ampm_options[amopt]] or { 'am', 'pm' }
            ['a.m.'] = { 'a.m.', 'p.m.' },
return (spaces == '' and '' or '&nbsp;') .. (value < 12 and ap[1] or ap[2])
            ['AM'] = { 'AM', 'PM' },
end
            ['A.M.'] = { 'A.M.', 'P.M.' },
end
          })[ampm_options[amopt]] or { 'am', 'pm' }
if code.field == 'year' then
          return (spaces == '' and '' or '&nbsp;') .. (value < 12 and ap[1] or ap[2])
local sign = (era_text[eraopt] or {}).sign
        end
if not sign or format:find('%{era}', 1, true) then
      end
sign = ''
      if code.field == 'year' then
if value <= 0 then
        local sign = (era_text[eraopt] or {}).sign
value = 1 - value
        if not sign or format:find('%{era}', 1, true) then
end
          sign = ''
else
          if value <= 0 then
if value >= 0 then
            value = 1 - value
sign = ''
          end
else
        else
value = -value
          if value >= 0 then
end
            sign = ''
end
          else
return spaces .. sign .. sformat(fmt, value)
            value = -value
end
          end
return spaces .. (fmt and sformat(fmt, value) or value)
        end
end
        return spaces .. sign .. sformat(fmt, value)
end
      end
local function replace_property(spaces, id)
if code.field == 'monthname' then
if id == 'era' then
return spaces .. 'de ' .. (fmt and sformat(fmt, value) or value) .. ' de'
-- Special case so can use local era option.
end
local result = get_era_for_year(eraopt, date.year)
      return spaces .. (fmt and sformat(fmt, value) or value)
if result == '' then
    end
return ''
  end
end
  local function replace_property(spaces, id)
return (spaces == '' and '' or '&nbsp;') .. result
    if id == 'era' then
end
      -- Special case so can use local era option.
local result = date[id]
      local result = get_era_for_year(eraopt, date.year)
if type(result) == 'string' then
      if result == '' then
return spaces .. result
        return ''
end
      end
if type(result) == 'number' then
      return (spaces == '' and '' or '&nbsp;') .. result
return spaces .. tostring(result)
    end
end
    local result = date[id]
if type(result) == 'boolean' then
    if type(result) == 'string' then
return spaces .. (result and '1' or '0')
      return spaces .. result
end
    end
-- This occurs if id is an undefined field in a partial date, or is the name of a function.
    if type(result) == 'number' then
return nil
      return spaces .. tostring(result)
end
    end
local PERCENT = '\127PERCENT\127'
    if type(result) == 'boolean' then
return (format
      return spaces .. (result and '1' or '0')
:gsub('%%%%', PERCENT)
    end
:gsub('(%s*)%%{(%w+)}', replace_property)
    -- This occurs if id is an undefined field in a partial date, or is the name of a function.
:gsub('(%s*)%%(%-?)(%a)', replace_code)
    return nil
:gsub(PERCENT, '%%')
  end
)
  local PERCENT = '\127PERCENT\127'
 
format = format
    :gsub('%%%%', PERCENT)
:gsub('(%s*)%%{(%w+)}', replace_property)
 
--print(format)
format = format
    :gsub('(%s*)%%(%-?)(%a)', replace_code)
--print(format)
 
format = format
    :gsub(PERCENT, '%%')
 
  return format
end
end


local function _date_text(date, fmt, options)
local function _date_text(date, fmt, options)
-- Return a formatted string representing the given date.
  -- Return a formatted string representing the given date.
if not is_date(date) then
  if not is_date(date) then
error('date:text: need a date (use "date:text()" with a colon)', 2)
    error('date:text: need a date (use "date:text()" with a colon)', 2)
end
  end
if type(fmt) == 'string' and fmt:match('%S') then
  if type(fmt) == 'string' and fmt:match('%S') then
if fmt:find('%', 1, true) then
    if fmt:find('%', 1, true) then
return strftime(date, fmt, options)
      return strftime(date, fmt, options)
end
    end
elseif date.partial then
  elseif date.partial then
fmt = date.month and 'my' or 'y'
    fmt = date.month and 'my' or 'y'
else
  else
fmt = 'dmy'
    fmt = 'dmy'
if date.hastime then
    if date.hastime then
fmt = (date.second > 0 and 'hms ' or 'hm ') .. fmt
      fmt = (date.second > 0 and 'hms ' or 'hm ') .. fmt
end
    end
end
  end
local function bad_format()
  local function bad_format()
-- For consistency with other format processing, return given format
    -- For consistency with other format processing, return given format
-- (or cleaned format if original was not a string) if invalid.
    -- (or cleaned format if original was not a string) if invalid.
return mw.text.nowiki(fmt)
    return mw.text.nowiki(fmt)
end
  end
if date.partial then
  if date.partial then
-- Ignore days in standard formats like 'ymd'.
    -- Ignore days in standard formats like 'ymd'.
if fmt == 'ym' or fmt == 'ymd' then
    if fmt == 'ym' or fmt == 'ymd' then
fmt = date.month and '%Y-%m %{era}' or '%Y %{era}'
      fmt = date.month and '%Y-%m %{era}' or '%Y %{era}'
elseif fmt == 'my' or fmt == 'dmy' or fmt == 'mdy' then
    elseif fmt == 'my' or fmt == 'dmy' or fmt == 'mdy' then
fmt = date.month and '%B %-Y %{era}' or '%-Y %{era}'
      fmt = date.month and '%B %-Y %{era}' or '%-Y %{era}'
elseif fmt == 'y' then
    elseif fmt == 'y' then
fmt = date.month and '%-Y %{era}' or '%-Y %{era}'
      fmt = date.month and '%-Y %{era}' or '%-Y %{era}'
else
    else
return bad_format()
      return bad_format()
end
    end
return strftime(date, fmt, options)
    return strftime(date, fmt, options)
end
  end
local function hm_fmt()
  local function hm_fmt()
local plain = make_option_table(options, date.options).bydefault.am
    local plain = make_option_table(options, date.options).bydefault.am
return plain and '%H:%M' or '%-I:%M %p'
    return plain and '%H:%M' or '%-I:%M %p'
end
  end
local need_time = date.hastime
  local need_time = date.hastime
local t = collection()
  local t = collection()
for item in fmt:gmatch('%S+') do
  for item in fmt:gmatch('%S+') do
local f
    local f
if item == 'hm' then
    if item == 'hm' then
f = hm_fmt()
      f = hm_fmt()
need_time = false
      need_time = false
elseif item == 'hms' then
    elseif item == 'hms' then
f = '%H:%M:%S'
      f = '%H:%M:%S'
need_time = false
      need_time = false
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 bad_format()
      return bad_format()
end
    end
t:add(f)
    t:add(f)
end
  end
fmt = t:join(' ')
  fmt = t:join(' ')
if need_time then
  if need_time then
fmt = hm_fmt() .. ' ' .. fmt
    fmt = hm_fmt() .. ' ' .. fmt
end
  end
return strftime(date, fmt, options)
  return strftime(date, fmt, options)
end
end


local day_info = {
local day_info = {
-- 0=Sun to 6=Sat
  -- 0=Sun to 6=Sat
[0] = { 'do', 'domingo' },
  [0] = { 'do', 'domingo' },
{ 'lu', 'lunes' },
  { 'lu', 'lunes' },
{ 'ma', 'martes' },
  { 'ma', 'martes' },
{ 'mi', 'miércoles' },
  { 'mi', 'miércoles' },
{ 'ju', 'jueves' },
  { 'ju', 'jueves' },
{ 'vi', 'viernes' },
  { 'vi', 'viernes' },
{ 'sa', 'sábado' },
  { 'sa', 'sábado' },
}
}


local month_info = {
local month_info = {
-- 1=Jan to 12=Dec
  -- 1=Jan to 12=Dec
{ 'ene', 'enero' },
  { 'ene', 'enero' },
{ 'feb', 'febrero' },
  { 'feb', 'febrero' },
{ 'mar', 'marzo' },
  { 'mar', 'marzo' },
{ 'abr', 'abril' },
  { 'abr', 'abril' },
{ 'may', 'mayo' },
  { 'may', 'mayo' },
{ 'jun', 'junio' },
  { 'jun', 'junio' },
{ 'jul', 'julio' },
  { 'jul', 'julio' },
{ 'ago', 'agosto' },
  { 'ago', 'agosto' },
{ 'sep', 'septiembre' },
  { 'sep', 'septiembre' },
{ 'oct', 'octubre' },
  { 'oct', 'octubre' },
{ 'nov', 'noviembre' },
  { 'nov', 'noviembre' },
{ 'dic', 'diciembre' },
  { 'dic', 'diciembre' },
}
}


local function name_to_number(text, translate)
local function name_to_number(text, translate)
if type(text) == 'string' then
  if type(text) == 'string' then
return translate['xx' .. text:lower():gsub('é', 'e'):gsub('á', 'a')]
    return translate['xx' .. text:lower():gsub('é', 'e'):gsub('á', 'a')]
end
  end
end
end


local function day_number(text)
local function day_number(text)
return name_to_number(text, {
  return name_to_number(text, {
xxdo = 0, xxdomingo = 0,
    xxdo = 0, xxdomingo = 0,
xxlu = 1, xxlunes = 1,
    xxlu = 1, xxlunes = 1,
xxma = 2, xxmartes = 2,
    xxma = 2, xxmartes = 2,
xxmi = 3, xxmiercoles = 3,
    xxmi = 3, xxmiercoles = 3,
xxju = 4, xxjueves = 4,
    xxju = 4, xxjueves = 4,
xxvi = 5, xxviernes = 5,
    xxvi = 5, xxviernes = 5,
xxsat = 6, xxsabado = 6
    xxsat = 6, xxsabado = 6
})
  })
end
end


local function month_number(text)
local function month_number(text)
return name_to_number(text, {
  return name_to_number(text, {
xxene = 1, xxenero = 1,
    xxene = 1, xxenero = 1,
xxfeb = 2, xxfebrero = 2,
    xxfeb = 2, xxfebrero = 2,
xxmar = 3, xxmarzo = 3,
    xxmar = 3, xxmarzo = 3,
xxabr = 4, xxabril = 4,
    xxabr = 4, xxabril = 4,
xxmay = 5, xxmayo = 5,
    xxmay = 5, xxmayo = 5,
xxjun = 6, xxjunio = 6,
    xxjun = 6, xxjunio = 6,
xxjul = 7, xxjulio = 7,
    xxjul = 7, xxjulio = 7,
xxago = 8, xxagosto = 8,
    xxago = 8, xxagosto = 8,
xxsep = 9, xxseptiembre = 9, xxsept = 9,
    xxsep = 9, xxseptiembre = 9, xxsept = 9,
xxoct = 10, xxoctubre = 10,
    xxoct = 10, xxoctubre = 10,
xxnov = 11, xxnoviembre = 11,
    xxnov = 11, xxnoviembre = 11,
xxdic = 12, xxdiciembre = 12,
    xxdic = 12, xxdiciembre = 12,
})
  })
end
end


local function _list_text(list, fmt)
local function _list_text(list, fmt)
-- Return a list of formatted strings from a list of dates.
  -- Return a list of formatted strings from a list of dates.
if not type(list) == 'table' then
  if not type(list) == 'table' then
error('date:list:text: need "list:text()" with a colon', 2)
    error('date:list:text: need "list:text()" with a colon', 2)
end
  end
local result = { join = _list_join }
  local result = { join = _list_join }
for i, date in ipairs(list) do
  for i, date in ipairs(list) do
result[i] = date:text(fmt)
    result[i] = date:text(fmt)
end
  end
return result
  return result
end
end


local function _date_list(date, spec)
local function _date_list(date, spec)
-- Return a possibly empty numbered table of dates meeting the specification.
  -- Return a possibly empty numbered table of dates meeting the specification.
-- Dates in the list are in ascending order (oldest date first).
  -- Dates in the list are in ascending order (oldest date first).
-- The spec should be a string of form "<count> <day> <op>"
  -- The spec should be a string of form "<count> <day> <op>"
-- where each item is optional and
  -- where each item is optional and
--   count = number of items wanted in list
  -- count = number of items wanted in list
--   day = abbreviation or name such as Mon or Monday
  -- day = abbreviation or name such as Mon or Monday
--   op = >, >=, <, <= (default is > meaning after date)
  -- op = >, >=, <, <= (default is > meaning after date)
-- If no count is given, the list is for the specified days in date's month.
  -- If no count is given, the list is for the specified days in date's month.
-- The default day is date's day.
  -- The default day is date's day.
-- The spec can also be a positive or negative number:
  -- The spec can also be a positive or negative number:
--   -5 is equivalent to '5 <'
  -- -5 is equivalent to '5 <'
--   5 is equivalent to '5' which is '5 >'
  -- 5 is equivalent to '5' which is '5 >'
if not is_date(date) then
  if not is_date(date) then
error('date:list: need a date (use "date:list()" with a colon)', 2)
    error('date:list: need a date (use "date:list()" with a colon)', 2)
end
  end
local list = { text = _list_text }
  local list = { text = _list_text }
if date.partial then
  if date.partial then
return list
    return list
end
  end
local count, offset, operation
  local count, offset, operation
local ops = {
  local ops = {
['>='] = { before = false, include = true },
    ['>='] = { before = false, include = true },
['>'] = { before = false, include = false },
    ['>'] = { before = false, include = false },
['<='] = { before = true , include = true },
    ['<='] = { before = true , include = true },
['<'] = { before = true , include = false },
    ['<'] = { before = true , include = false },
}
  }
if spec then
  if spec then
if type(spec) == 'number' then
    if type(spec) == 'number' then
count = floor(spec + 0.5)
      count = floor(spec + 0.5)
if count < 0 then
      if count < 0 then
count = -count
        count = -count
operation = ops['<']
        operation = ops['<']
end
      end
elseif type(spec) == 'string' then
    elseif type(spec) == 'string' then
local num, day, op = spec:match('^%s*(%d*)%s*(%a*)%s*([<>=]*)%s*$')
      local num, day, op = spec:match('^%s*(%d*)%s*(%a*)%s*([<>=]*)%s*$')
if not num then
      if not num then
return list
        return list
end
      end
if num ~= '' then
      if num ~= '' then
count = tonumber(num)
        count = tonumber(num)
end
      end
if day ~= '' then
      if day ~= '' then
local dow = day_number(day:gsub('[sS]$', '')) -- accept plural days
        local dow = day_number(day:gsub('[sS]$', '')) -- accept plural days
if not dow then
        if not dow then
return list
          return list
end
        end
offset = dow - date.dow
        offset = dow - date.dow
end
      end
operation = ops[op]
      operation = ops[op]
else
    else
return list
      return list
end
    end
end
  end
offset = offset or 0
  offset = offset or 0
operation = operation or ops['>']
  operation = operation or ops['>']
local datefrom, dayfirst, daylast
  local datefrom, dayfirst, daylast
if operation.before then
  if operation.before then
if offset > 0 or (offset == 0 and not operation.include) then
    if offset > 0 or (offset == 0 and not operation.include) then
offset = offset - 7
      offset = offset - 7
end
    end
if count then
    if count then
if count > 1 then
      if count > 1 then
offset = offset - 7*(count - 1)
        offset = offset - 7*(count - 1)
end
      end
datefrom = date + offset
      datefrom = date + offset
else
    else
daylast = date.day + offset
      daylast = date.day + offset
dayfirst = daylast % 7
      dayfirst = daylast % 7
if dayfirst == 0 then
      if dayfirst == 0 then
dayfirst = 7
        dayfirst = 7
end
      end
end
    end
else
  else
if offset < 0 or (offset == 0 and not operation.include) then
    if offset < 0 or (offset == 0 and not operation.include) then
offset = offset + 7
      offset = offset + 7
end
    end
if count then
    if count then
datefrom = date + offset
      datefrom = date + offset
else
    else
dayfirst = date.day + offset
      dayfirst = date.day + offset
daylast = date.monthdays
      daylast = date.monthdays
end
    end
end
  end
if not count then
  if not count then
if daylast < dayfirst then
    if daylast < dayfirst then
return list
      return list
end
    end
count = floor((daylast - dayfirst)/7) + 1
    count = floor((daylast - dayfirst)/7) + 1
datefrom = Date(date, {day = dayfirst})
    datefrom = Date(date, {day = dayfirst})
end
  end
for i = 1, count do
  for i = 1, count do
if not datefrom then break end -- exceeds date limits
    if not datefrom then break end -- exceeds date limits
list[i] = datefrom
    list[i] = datefrom
datefrom = datefrom + 7
    datefrom = datefrom + 7
end
  end
return list
  return list
end
end


-- A table to get the current date/time (UTC), but only if needed.
-- A table to get the current date/time (UTC), but only if needed.
local current = setmetatable({}, {
local current = setmetatable({}, {
__index = function (self, key)
  __index = function (self, key)
local d = os.date('!*t')
    local d = os.date('!*t')
self.year = d.year
    self.year = d.year
self.month = d.month
    self.month = d.month
self.day = d.day
    self.day = d.day
self.hour = d.hour
    self.hour = d.hour
self.minute = d.min
    self.minute = d.min
self.second = d.sec
    self.second = d.sec
return rawget(self, key)
    return rawget(self, key)
end })
  end })


local function extract_date(newdate, text)
local function extract_date(newdate, text)
-- Parse the date/time in text and return n, o where
  -- Parse the date/time in text and return n, o where
--   n = table of numbers with date/time fields
  -- n = table of numbers with date/time fields
--   o = table of options for AM/PM or AD/BC or format, if any
  -- o = table of options for AM/PM or AD/BC or format, if any
-- 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.
-- A year must be positive ('1' to '9999'); use 'BC' for BC.
  -- 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
  -- 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
  -- ('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).
  -- 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 potentially ambiguous.
  -- Dates of form d/m/y, m/d/y, y/m/d are rejected as potentially ambiguous.
local date, options = {}, {}
  local date, options = {}, {}
if text:sub(-1) == 'Z' then
  if text:sub(-1) == 'Z' then
-- Extract date/time from a Wikidata timestamp.
    -- Extract date/time from a Wikidata timestamp.
-- The year can be 1 to 16 digits but this module handles 1 to 4 digits only.
    -- The year can be 1 to 16 digits but this module handles 1 to 4 digits only.
-- Examples: '+2016-06-21T14:30:00Z', '-0000000180-00-00T00:00:00Z'.
    -- Examples: '+2016-06-21T14:30:00Z', '-0000000180-00-00T00:00:00Z'.
local sign, y, m, d, H, M, S = text:match('^([+%-])(%d+)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)Z$')
    local sign, y, m, d, H, M, S = text:match('^([+%-])(%d+)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)Z$')
if sign then
    if sign then
y = tonumber(y)
      y = tonumber(y)
if sign == '-' and y > 0 then
      if sign == '-' and y > 0 then
y = -y
        y = -y
end
      end
if y <= 0 then
      if y <= 0 then
options.era = 'BCE'
        options.era = 'BCE'
end
      end
date.year = y
      date.year = y
m = tonumber(m)
      m = tonumber(m)
d = tonumber(d)
      d = tonumber(d)
H = tonumber(H)
      H = tonumber(H)
M = tonumber(M)
      M = tonumber(M)
S = tonumber(S)
      S = tonumber(S)
if m == 0 then
      if m == 0 then
newdate.partial = true
        newdate.partial = true
return date, options
        return date, options
end
      end
date.month = m
      date.month = m
if d == 0 then
      if d == 0 then
newdate.partial = true
        newdate.partial = true
return date, options
        return date, options
end
      end
date.day = d
      date.day = d
if H > 0 or M > 0 or S > 0 then
      if H > 0 or M > 0 or S > 0 then
date.hour = H
        date.hour = H
date.minute = M
        date.minute = M
date.second = S
        date.second = S
end
      end
return date, options
      return date, options
end
    end
return
    return
end
  end
local function extract_ymd(item)
  local function extract_ymd(item)
-- Called when no day or month has been set.
    -- Called when no day or month has been set.
local y, m, d = item:match('^(%d%d%d%d)%-(%w+)%-(%d%d?)$')
    local y, m, d = item:match('^(%d%d%d%d)%-(%w+)%-(%d%d?)$')
if y then
    if y then
if date.year then
      if date.year then
return
        return
end
      end
if m:match('^%d%d?$') then
      if m:match('^%d%d?$') then
m = tonumber(m)
        m = tonumber(m)
else
      else
m = month_number(m)
        m = month_number(m)
end
      end
if m then
      if m then
date.year = tonumber(y)
        date.year = tonumber(y)
date.month = m
        date.month = m
date.day = tonumber(d)
        date.day = tonumber(d)
return true
        return true
end
      end
end
    end
end
  end
local function extract_day_or_year(item)
  local function extract_day_or_year(item)
-- Called when a day would be valid, or
    -- Called when a day would be valid, or
-- when a year would be valid if no year has been set and partial is set.
    -- when a year would be valid if no year has been set and partial is set.
local number, suffix = item:match('^(%d%d?%d?%d?)(.*)$')
    local number, suffix = item:match('^(%d%d?%d?%d?)(.*)$')
if number then
    if number then
local n = tonumber(number)
      local n = tonumber(number)
if #number <= 2 and n <= 31 then
      if #number <= 2 and n <= 31 then
suffix = suffix:lower()
        suffix = suffix:lower()
if suffix == '' or suffix == 'st' or suffix == 'nd' or suffix == 'rd' or suffix == 'th' then
        if suffix == '' or suffix == 'st' or suffix == 'nd' or suffix == 'rd' or suffix == 'th' then
date.day = n
          date.day = n
return true
          return true
end
        end
elseif suffix == '' and newdate.partial and not date.year then
      elseif suffix == '' and newdate.partial and not date.year then
date.year = n
        date.year = n
return true
        return true
end
      end
end
    end
end
  end
local function extract_month(item)
  local function extract_month(item)
-- A month must be given as a name or abbreviation; a number could be ambiguous.
    -- A month must be given as a name or abbreviation; a number could be ambiguous.
local m = month_number(item)
    local m = month_number(item)
if m then
    if m then
date.month = m
      date.month = m
return true
      return true
end
    end
end
  end
local function extract_time(item)
  local function extract_time(item)
local h, m, s = item:match('^(%d%d?):(%d%d)(:?%d*)$')
    local h, m, s = item:match('^(%d%d?):(%d%d)(:?%d*)$')
if date.hour or not h then
    if date.hour or not h then
return
      return
end
    end
if s ~= '' then
    if s ~= '' then
s = s:match('^:(%d%d)$')
      s = s:match('^:(%d%d)$')
if not s then
      if not s then
return
        return
end
      end
end
    end
date.hour = tonumber(h)
    date.hour = tonumber(h)
date.minute = tonumber(m)
    date.minute = tonumber(m)
date.second = tonumber(s) -- nil if empty string
    date.second = tonumber(s) -- nil if empty string
return true
    return true
end
  end
local item_count = 0
  local item_count = 0
local index_time
  local index_time
local function set_ampm(item)
  local function set_ampm(item)
local H = date.hour
    local H = date.hour
if H and not options.am and index_time + 1 == item_count then
    if H and not options.am and index_time + 1 == item_count then
options.am = ampm_options[item] -- caller checked this is not nil
      options.am = ampm_options[item] -- caller checked this is not nil
if item:match('^[Aa]') then
      if item:match('^[Aa]') then
if not (1 <= H and H <= 12) then
        if not (1 <= H and H <= 12) then
return
          return
end
        end
if H == 12 then
        if H == 12 then
date.hour = 0
          date.hour = 0
end
        end
else
      else
if not (1 <= H and H <= 23) then
        if not (1 <= H and H <= 23) then
return
          return
end
        end
if H <= 11 then
        if H <= 11 then
date.hour = H + 12
          date.hour = H + 12
end
        end
end
      end
return true
      return true
end
    end
end
  end
for item in text:gsub(',', ' '):gsub('de', ' '):gsub('&nbsp;', ' '):gmatch('%S+') do
  for item in text:gsub(',', ' '):gsub('de', ' '):gsub('&nbsp;', ' '):gmatch('%S+') do
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.
      -- Era is accepted in peculiar places.
if options.era then
      if options.era then
return
        return
end
      end
options.era = item
      options.era = item
elseif ampm_options[item] then
    elseif ampm_options[item] then
if not set_ampm(item) then
      if not set_ampm(item) then
return
        return
end
      end
elseif item:find(':', 1, true) then
    elseif item:find(':', 1, true) then
if not extract_time(item) then
      if not extract_time(item) then
return
        return
end
      end
index_time = item_count
      index_time = item_count
elseif date.day and date.month then
    elseif date.day and date.month then
if date.year then
      if date.year then
return -- should be nothing more so item is invalid
        return -- should be nothing more so item is invalid
end
      end
if not item:match('^(%d%d?%d?%d?)$') then
      if not item:match('^(%d%d?%d?%d?)$') then
return
        return
end
      end
date.year = tonumber(item)
      date.year = tonumber(item)
elseif date.day then
    elseif date.day then
if not extract_month(item) then
      if not extract_month(item) then
return
        return
end
      end
elseif date.month then
    elseif date.month then
if not extract_day_or_year(item) then
      if not extract_day_or_year(item) then
return
        return
end
      end
elseif extract_month(item) then
    elseif extract_month(item) then
options.format = 'mdy'
      options.format = 'mdy'
elseif extract_ymd(item) then
    elseif extract_ymd(item) then
options.format = 'ymd'
      options.format = 'ymd'
elseif extract_day_or_year(item) then
    elseif extract_day_or_year(item) then
if date.day then
      if date.day then
options.format = 'dmy'
        options.format = 'dmy'
end
      end
else
    else
return
      return
end
    end
end
  end
if not date.year or date.year == 0 then
  if not date.year or date.year == 0 then
return
    return
end
  end
local era = era_text[options.era]
  local era = era_text[options.era]
if era and era.isbc then
  if era and era.isbc then
date.year = 1 - date.year
    date.year = 1 - date.year
end
  end
return date, options
  return date, options
end
end


local function autofill(date1, date2)
local function autofill(date1, date2)
-- Fill any missing month or day in each date using the
  -- Fill any missing month or day in each date using the
-- corresponding component from the other date, if present,
  -- corresponding component from the other date, if present,
-- or with 1 if both dates are missing the month or day.
  -- or with 1 if both dates are missing the month or day.
-- This gives a good result for calculating the difference
  -- This gives a good result for calculating the difference
-- between two partial dates when no range is wanted.
  -- between two partial dates when no range is wanted.
-- Return filled date1, date2 (two full dates).
  -- Return filled date1, date2 (two full dates).
local function filled(a, b)
  local function filled(a, b)
local fillmonth, fillday
    local fillmonth, fillday
if not a.month then
    if not a.month then
fillmonth = b.month or 1
      fillmonth = b.month or 1
end
    end
if not a.day then
    if not a.day then
fillday = b.day or 1
      fillday = b.day or 1
end
    end
if fillmonth or fillday then -- need to create a new date
    if fillmonth or fillday then -- need to create a new date
if (fillmonth or a.month) == 2 and (fillday or a.day) == 29 then
      if (fillmonth or a.month) == 2 and (fillday or a.day) == 29 then
-- Avoid invalid date, for example with {{age|2013|29 Feb 2016}} or {{age|Feb 2013|29 Jan 2015}}.
        -- Avoid invalid date, for example with {{age|2013|29 Feb 2016}} or {{age|Feb 2013|29 Jan 2015}}.
if not is_leap_year(a.year, a.calendar) then
        if not is_leap_year(a.year, a.calendar) then
fillday = 28
          fillday = 28
end
        end
end
      end
a = Date(a, { month = fillmonth, day = fillday })
      a = Date(a, { month = fillmonth, day = fillday })
end
    end
return a
    return a
end
  end
return filled(date1, date2), filled(date2, date1)
  return filled(date1, date2), filled(date2, date1)
end
end


local function date_add_sub(lhs, rhs, is_sub)
local function date_add_sub(lhs, rhs, is_sub)
-- Return a new date from calculating (lhs + rhs) or (lhs - rhs),
  -- Return a new date from calculating (lhs + rhs) or (lhs - rhs),
-- or return nothing if invalid.
  -- or return nothing if invalid.
-- The result is nil if the calculated date exceeds allowable limits.
  -- The result is nil if the calculated date exceeds allowable limits.
-- Caller ensures that lhs is a date; its properties are copied for the new date.
  -- Caller ensures that lhs is a date; its properties are copied for the new date.
if lhs.partial then
  if lhs.partial then
-- Adding to a partial is not supported.
    -- Adding to a partial is not supported.
-- Can subtract a date or partial from a partial, but this is not called for that.
    -- Can subtract a date or partial from a partial, but this is not called for that.
return
    return
end
  end
local function is_prefix(text, word, minlen)
  local function is_prefix(text, word, minlen)
local n = #text
    local n = #text
return (minlen or 1) <= n and n <= #word and text == word:sub(1, n)
    return (minlen or 1) <= n and n <= #word and text == word:sub(1, n)
end
  end
local function do_days(n)
  local function do_days(n)
local forcetime, jd
    local forcetime, jd
if floor(n) == n then
    if floor(n) == n then
jd = lhs.jd
      jd = lhs.jd
else
    else
forcetime = not lhs.hastime
      forcetime = not lhs.hastime
jd = lhs.jdz
      jd = lhs.jdz
end
    end
jd = jd + (is_sub and -n or n)
    jd = jd + (is_sub and -n or n)
if forcetime then
    if forcetime then
jd = tostring(jd)
      jd = tostring(jd)
if not jd:find('.', 1, true) then
      if not jd:find('.', 1, true) then
jd = jd .. '.0'
        jd = jd .. '.0'
end
      end
end
    end
return Date(lhs, 'juliandate', jd)
    return Date(lhs, 'juliandate', jd)
end
  end
if type(rhs) == 'number' then
  if type(rhs) == 'number' then
-- Add/subtract days, including fractional days.
    -- Add/subtract days, including fractional days.
return do_days(rhs)
    return do_days(rhs)
end
  end
if type(rhs) == 'string' then
  if type(rhs) == 'string' then
-- rhs is a single component like '26m' or '26 months' (with optional sign).
    -- rhs is a single component like '26m' or '26 months' (with optional sign).
-- Fractions like '3.25d' are accepted for the units which are handled as days.
    -- Fractions like '3.25d' are accepted for the units which are handled as days.
local sign, numstr, id = rhs:match('^%s*([+-]?)([%d%.]+)%s*(%a+)$')
    local sign, numstr, id = rhs:match('^%s*([+-]?)([%d%.]+)%s*(%a+)$')
if sign then
    if sign then
if sign == '-' then
      if sign == '-' then
is_sub = not (is_sub and true or false)
        is_sub = not (is_sub and true or false)
end
      end
local y, m, days
      local y, m, days
local num = tonumber(numstr)
      local num = tonumber(numstr)
if not num then
      if not num then
return
        return
end
      end
id = id:lower()
      id = id:lower()
if is_prefix(id, 'years') then
      if is_prefix(id, 'years') then
y = num
        y = num
m = 0
        m = 0
elseif is_prefix(id, 'months') then
      elseif is_prefix(id, 'months') then
y = floor(num / 12)
        y = floor(num / 12)
m = num % 12
        m = num % 12
elseif is_prefix(id, 'weeks') then
      elseif is_prefix(id, 'weeks') then
days = num * 7
        days = num * 7
elseif is_prefix(id, 'days') then
      elseif is_prefix(id, 'days') then
days = num
        days = num
elseif is_prefix(id, 'hours') then
      elseif is_prefix(id, 'hours') then
days = num / 24
        days = num / 24
elseif is_prefix(id, 'minutes', 3) then
      elseif is_prefix(id, 'minutes', 3) then
days = num / (24 * 60)
        days = num / (24 * 60)
elseif is_prefix(id, 'seconds') then
      elseif is_prefix(id, 'seconds') then
days = num / (24 * 3600)
        days = num / (24 * 3600)
else
      else
return
        return
end
      end
if days then
      if days then
return do_days(days)
        return do_days(days)
end
      end
if numstr:find('.', 1, true) then
      if numstr:find('.', 1, true) then
return
        return
end
      end
if is_sub then
      if is_sub then
y = -y
        y = -y
m = -m
        m = -m
end
      end
assert(-11 <= m and m <= 11)
      assert(-11 <= m and m <= 11)
y = lhs.year + y
      y = lhs.year + y
m = lhs.month + m
      m = lhs.month + m
if m > 12 then
      if m > 12 then
y = y + 1
        y = y + 1
m = m - 12
        m = m - 12
elseif m < 1 then
      elseif m < 1 then
y = y - 1
        y = y - 1
m = m + 12
        m = m + 12
end
      end
local d = math.min(lhs.day, days_in_month(y, m, lhs.calendar))
      local d = math.min(lhs.day, days_in_month(y, m, lhs.calendar))
return Date(lhs, y, m, d)
      return Date(lhs, y, m, d)
end
    end
end
  end
if is_diff(rhs) then
  if is_diff(rhs) then
local days = rhs.age_days
    local days = rhs.age_days
if (is_sub or false) ~= (rhs.isnegative or false) then
    if (is_sub or false) ~= (rhs.isnegative or false) then
days = -days
      days = -days
end
    end
return lhs + days
    return lhs + days
end
  end
end
end


local full_date_only = {
local full_date_only = {
dayabbr = true,
  dayabbr = true,
dayname = true,
  dayname = true,
dow = true,
  dow = true,
dayofweek = true,
  dayofweek = true,
dowiso = true,
  dowiso = true,
dayofweekiso = true,
  dayofweekiso = true,
dayofyear = true,
  dayofyear = true,
gsd = true,
  gsd = true,
juliandate = true,
  juliandate = true,
jd = true,
  jd = true,
jdz = true,
  jdz = true,
jdnoon = true,
  jdnoon = true,
}
}


-- Metatable for a date's calculated fields.
-- Metatable for a date's calculated fields.
local datemt = {
local datemt = {
__index = function (self, key)
  __index = function (self, key)
if rawget(self, 'partial') then
    if rawget(self, 'partial') then
if full_date_only[key] then return end
      if full_date_only[key] then return end
if key == 'monthabbr' or key == 'monthdays' or key == 'monthname' then
      if key == 'monthabbr' or key == 'monthdays' or key == 'monthname' then
if not self.month then return end
        if not self.month then return end
end
      end
end
    end
local value
    local value
if key == 'dayabbr' then
    if key == 'dayabbr' then
value = day_info[self.dow][1]
      value = day_info[self.dow][1]
elseif key == 'dayname' then
    elseif key == 'dayname' then
value = day_info[self.dow][2]
      value = day_info[self.dow][2]
elseif key == 'dow' then
    elseif key == 'dow' then
value = (self.jdnoon + 1) % 7 -- day-of-week 0=Sun to 6=Sat
      value = (self.jdnoon + 1) % 7 -- day-of-week 0=Sun to 6=Sat
elseif key == 'dayofweek' then
    elseif key == 'dayofweek' then
value = self.dow
      value = self.dow
elseif key == 'dowiso' then
    elseif key == 'dowiso' then
value = (self.jdnoon % 7) + 1 -- ISO day-of-week 1=Mon to 7=Sun
      value = (self.jdnoon % 7) + 1 -- ISO day-of-week 1=Mon to 7=Sun
elseif key == 'dayofweekiso' then
    elseif key == 'dayofweekiso' then
value = self.dowiso
      value = self.dowiso
elseif key == 'dayofyear' then
    elseif key == 'dayofyear' then
local first = Date(self.year, 1, 1, self.calendar).jdnoon
      local first = Date(self.year, 1, 1, self.calendar).jdnoon
value = self.jdnoon - first + 1 -- day-of-year 1 to 366
      value = self.jdnoon - first + 1 -- day-of-year 1 to 366
elseif key == 'era' then
    elseif key == 'era' then
-- Era text (never a negative sign) from year and options.
      -- Era text (never a negative sign) from year and options.
value = get_era_for_year(self.options.era, self.year)
      value = get_era_for_year(self.options.era, self.year)
elseif key == 'format' then
    elseif key == 'format' then
value = self.options.format or 'dmy'
      value = self.options.format or 'dmy'
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,
-- which is from jd 1721425.5 to 1721426.49999.
      -- which is from jd 1721425.5 to 1721426.49999.
value = floor(self.jd - 1721424.5)
      value = floor(self.jd - 1721424.5)
elseif key == 'juliandate' or key == 'jd' or key == 'jdz' then
    elseif key == 'juliandate' or key == 'jd' or key == 'jdz' then
local jd, jdz = julian_date(self)
      local jd, jdz = julian_date(self)
rawset(self, 'juliandate', jd)
      rawset(self, 'juliandate', jd)
rawset(self, 'jd', jd)
      rawset(self, 'jd', jd)
rawset(self, 'jdz', jdz)
      rawset(self, 'jdz', jdz)
return key == 'jdz' and jdz or jd
      return key == 'jdz' and jdz or jd
elseif key == 'jdnoon' then
    elseif key == 'jdnoon' then
-- Julian date at noon (an integer) on the calendar day when jd occurs.
      -- Julian date at noon (an integer) on the calendar day when jd occurs.
value = floor(self.jd + 0.5)
      value = floor(self.jd + 0.5)
elseif key == 'isleapyear' then
    elseif key == 'isleapyear' then
value = is_leap_year(self.year, self.calendar)
      value = is_leap_year(self.year, self.calendar)
elseif key == 'monthabbr' then
    elseif key == 'monthabbr' then
value = month_info[self.month][1]
      value = month_info[self.month][1]
elseif key == 'monthdays' then
    elseif key == 'monthdays' then
value = days_in_month(self.year, self.month, self.calendar)
      value = days_in_month(self.year, self.month, self.calendar)
elseif key == 'monthname' then
    elseif key == 'monthname' then
value = month_info[self.month][2]
      value = month_info[self.month][2]
end
    end
if value ~= nil then
    if value ~= nil then
rawset(self, key, value)
      rawset(self, key, value)
return value
      return value
end
    end
end,
  end,
}
}


-- Date operators.
-- Date operators.
local function mt_date_add(lhs, rhs)
local function mt_date_add(lhs, rhs)
if not is_date(lhs) then
  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)
    lhs, rhs = rhs, lhs -- put date on left (it must be a date for this to have been called)
end
  end
return date_add_sub(lhs, rhs)
  return date_add_sub(lhs, rhs)
end
end


local function mt_date_sub(lhs, rhs)
local function mt_date_sub(lhs, rhs)
if is_date(lhs) then
  if is_date(lhs) then
if is_date(rhs) then
    if is_date(rhs) then
return DateDiff(lhs, rhs)
      return DateDiff(lhs, rhs)
end
    end
return date_add_sub(lhs, rhs, true)
    return date_add_sub(lhs, rhs, true)
end
  end
end
end


local function mt_date_concat(lhs, rhs)
local function mt_date_concat(lhs, rhs)
return tostring(lhs) .. tostring(rhs)
  return tostring(lhs) .. tostring(rhs)
end
end


local function mt_date_tostring(self)
local function mt_date_tostring(self)
return self:text()
  return self:text()
end
end


local function mt_date_eq(lhs, rhs)
local function mt_date_eq(lhs, rhs)
-- Return true if dates identify same date/time where, for example,
  -- Return true if dates identify same date/time where, for example,
-- Date(-4712, 1, 1, 'Juliano') == Date(-4713, 11, 24, 'Gregoriano') is true.
  -- Date(-4712, 1, 1, 'Juliano') == Date(-4713, 11, 24, 'Gregoriano') is true.
-- This is called only if lhs and rhs have the same type and the same metamethod.
  -- This is called only if lhs and rhs have the same type and the same metamethod.
if lhs.partial or rhs.partial then
  if lhs.partial or rhs.partial then
-- One date is partial; the other is a partial or a full date.
    -- One date is partial; the other is a partial or a full date.
-- The months may both be nil, but must be the same.
    -- The months may both be nil, but must be the same.
return lhs.year == rhs.year and lhs.month == rhs.month and lhs.calendar == rhs.calendar
    return lhs.year == rhs.year and lhs.month == rhs.month and lhs.calendar == rhs.calendar
end
  end
return lhs.jdz == rhs.jdz
  return lhs.jdz == rhs.jdz
end
end


local function mt_date_lt(lhs, rhs)
local function mt_date_lt(lhs, rhs)
-- Return true if lhs < rhs, for example,
  -- Return true if lhs < rhs, for example,
-- Date('1 Jan 2016') < Date('06:00 1 Jan 2016') is true.
  -- Date('1 Jan 2016') < Date('06:00 1 Jan 2016') is true.
-- This is called only if lhs and rhs have the same type and the same metamethod.
  -- This is called only if lhs and rhs have the same type and the same metamethod.
if lhs.partial or rhs.partial then
  if lhs.partial or rhs.partial then
-- One date is partial; the other is a partial or a full date.
    -- One date is partial; the other is a partial or a full date.
if lhs.calendar ~= rhs.calendar then
    if lhs.calendar ~= rhs.calendar then
return lhs.calendar == 'Juliano'
      return lhs.calendar == 'Juliano'
end
    end
if lhs.partial then
    if lhs.partial then
lhs = lhs.partial.first
      lhs = lhs.partial.first
end
    end
if rhs.partial then
    if rhs.partial then
rhs = rhs.partial.first
      rhs = rhs.partial.first
end
    end
end
  end
return lhs.jdz < rhs.jdz
  return lhs.jdz < rhs.jdz
end
end


--[[ Examples of syntax to construct a date:
--[[ Examples of syntax to construct a date:
Date(y, m, d, 'julian')             default calendar is 'gregorian'
Date(y, m, d, 'julian') default calendar is 'gregorian'
Date(y, m, d, H, M, S, 'julian')
Date(y, m, d, H, M, S, 'julian')
Date('juliandate', jd, 'julian')   if jd contains "." text output includes H:M:S
Date('juliandate', jd, 'julian') if jd contains "." text output includes H:M:S
Date('currentdate')
Date('currentdate')
Date('currentdatetime')
Date('currentdatetime')
Date('1 April 1995', 'julian')     parse date from text
Date('1 April 1995', 'julian') parse date from text
Date('1 April 1995 AD', 'julian')   using an era sets a flag to do the 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
Date(date) copy of an existing date
Date(date, t)                       same, updated with y,m,d,H,M,S fields from table t
Date(date, t) same, updated with y,m,d,H,M,S fields from table t
Date(t)                       date with y,m,d,H,M,S fields from table t
Date(t)     date with y,m,d,H,M,S fields from table t
]]
]]
function Date(...) -- for forward declaration above
function Date(...) -- for forward declaration above
-- Return a table holding a date assuming a uniform calendar always applies
  -- Return a table holding a date assuming a uniform calendar always applies
-- (proleptic Gregorian calendar or proleptic Julian calendar), or
  -- (proleptic Gregorian calendar or proleptic Julian calendar), or
-- return nothing if date is invalid.
  -- return nothing if date is invalid.
-- A partial date has a valid year, however its month may be nil, and
  -- A partial date has a valid year, however its month may be nil, and
-- its day and time fields are nil.
  -- its day and time fields are nil.
-- Field partial is set to false (if a full date) or a table (if a partial date).
  -- Field partial is set to false (if a full date) or a table (if a partial date).
local calendars = { julian = 'Juliano', gregorian = 'Gregoriano' }
  local calendars = { julian = 'Juliano', gregorian = 'Gregoriano' }
local newdate = {
  local newdate = {
_id = uniq,
    _id = uniq,
calendar = 'Gregoriano', -- default is Gregorian calendar
    calendar = 'Gregoriano', -- default is Gregorian calendar
hastime = false, -- true if input sets a time
    hastime = false, -- true if input sets a time
hour = 0, -- always set hour/minute/second so don't have to handle nil
    hour = 0, -- always set hour/minute/second so don't have to handle nil
minute = 0,
    minute = 0,
second = 0,
    second = 0,
options = {},
    options = {},
list = _date_list,
    list = _date_list,
subtract = function (self, rhs, options)
    subtract = function (self, rhs, options)
return DateDiff(self, rhs, options)
      return DateDiff(self, rhs, options)
end,
    end,
text = _date_text,
    text = _date_text,
}
  }
local argtype, datetext, is_copy, jd_number, tnums
  local argtype, datetext, is_copy, jd_number, tnums
local numindex = 0
  local numindex = 0
local numfields = { 'year', 'month', 'day', 'hour', 'minute', 'second' }
  local numfields = { 'year', 'month', 'day', 'hour', 'minute', 'second' }
local numbers = {}
  local numbers = {}
for _, v in ipairs({...}) do
  for _, v in ipairs({...}) do
v = strip_to_nil(v)
    v = strip_to_nil(v)
local vlower = type(v) == 'string' and v:lower() or nil
    local vlower = type(v) == 'string' and v:lower() or nil
if v == nil then
    if v == nil then
-- Ignore empty arguments after stripping so modules can directly pass template parameters.
      -- Ignore empty arguments after stripping so modules can directly pass template parameters.
elseif calendars[vlower] then
    elseif calendars[vlower] then
newdate.calendar = calendars[vlower]
      newdate.calendar = calendars[vlower]
elseif vlower == 'partial' then
    elseif vlower == 'partial' then
newdate.partial = true
      newdate.partial = true
elseif vlower == 'fix' then
    elseif vlower == 'fix' then
newdate.want_fix = true
      newdate.want_fix = true
elseif is_date(v) then
    elseif is_date(v) then
-- Copy existing date (items can be overridden by other arguments).
      -- Copy existing date (items can be overridden by other arguments).
if is_copy or tnums then
      if is_copy or tnums then
return
        return
end
      end
is_copy = true
      is_copy = true
newdate.calendar = v.calendar
      newdate.calendar = v.calendar
newdate.partial = v.partial
      newdate.partial = v.partial
newdate.hastime = v.hastime
      newdate.hastime = v.hastime
newdate.options = v.options
      newdate.options = v.options
newdate.year = v.year
      newdate.year = v.year
newdate.month = v.month
      newdate.month = v.month
newdate.day = v.day
      newdate.day = v.day
newdate.hour = v.hour
      newdate.hour = v.hour
newdate.minute = v.minute
      newdate.minute = v.minute
newdate.second = v.second
      newdate.second = v.second
elseif type(v) == 'table' then
    elseif type(v) == 'table' then
if tnums then
      if tnums then
return
        return
end
      end
tnums = {}
      tnums = {}
local tfields = { year=1, month=1, day=1, hour=2, minute=2, second=2 }
      local tfields = { year=1, month=1, day=1, hour=2, minute=2, second=2 }
for tk, tv in pairs(v) do
      for tk, tv in pairs(v) do
if tfields[tk] then
        if tfields[tk] then
tnums[tk] = tonumber(tv)
          tnums[tk] = tonumber(tv)
end
        end
if tfields[tk] == 2 then
        if tfields[tk] == 2 then
newdate.hastime = true
          newdate.hastime = true
end
        end
end
      end
else
    else
local num = tonumber(v)
      local num = tonumber(v)
if not num and argtype == 'setdate' and numindex == 1 then
      if not num and argtype == 'setdate' and numindex == 1 then
num = month_number(v)
        num = month_number(v)
end
      end
if num then
      if num then
if not argtype then
        if not argtype then
argtype = 'setdate'
          argtype = 'setdate'
end
        end
if argtype == 'setdate' and numindex < 6 then
        if argtype == 'setdate' and numindex < 6 then
numindex = numindex + 1
          numindex = numindex + 1
numbers[numfields[numindex]] = num
          numbers[numfields[numindex]] = num
elseif argtype == 'juliandate' and not jd_number then
        elseif argtype == 'juliandate' and not jd_number then
jd_number = num
          jd_number = num
if type(v) == 'string' then
          if type(v) == 'string' then
if v:find('.', 1, true) then
            if v:find('.', 1, true) then
newdate.hastime = true
              newdate.hastime = true
end
            end
elseif num ~= floor(num) then
          elseif num ~= floor(num) then
-- The given value was a number. The time will be used
            -- The given value was a number. The time will be used
-- if the fractional part is nonzero.
            -- if the fractional part is nonzero.
newdate.hastime = true
            newdate.hastime = true
end
          end
else
        else
return
          return
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
argtype = v
          argtype = v
else
        else
argtype = 'datetext'
          argtype = 'datetext'
datetext = v
          datetext = v
end
        end
else
      else
return
        return
end
      end
end
    end
end
  end
if argtype == 'datetext' then
  if argtype == 'datetext' then
if tnums or not set_date_from_numbers(newdate, extract_date(newdate, datetext)) then
    if tnums or not set_date_from_numbers(newdate, extract_date(newdate, datetext)) then
return
      return
end
    end
elseif argtype == 'juliandate' then
  elseif argtype == 'juliandate' then
newdate.partial = nil
    newdate.partial = nil
newdate.jd = jd_number
    newdate.jd = jd_number
if not set_date_from_jd(newdate) then
    if not set_date_from_jd(newdate) then
return
      return
end
    end
elseif argtype == 'currentdate' or argtype == 'currentdatetime' then
  elseif argtype == 'currentdate' or argtype == 'currentdatetime' then
newdate.partial = nil
    newdate.partial = nil
newdate.year = current.year
    newdate.year = current.year
newdate.month = current.month
    newdate.month = current.month
newdate.day = current.day
    newdate.day = current.day
if argtype == 'currentdatetime' then
    if argtype == 'currentdatetime' then
newdate.hour = current.hour
      newdate.hour = current.hour
newdate.minute = current.minute
      newdate.minute = current.minute
newdate.second = current.second
      newdate.second = current.second
newdate.hastime = true
      newdate.hastime = true
end
    end
newdate.calendar = 'Gregoriano' -- ignore any given calendar name
    newdate.calendar = 'Gregoriano' -- ignore any given calendar name
elseif argtype == 'setdate' then
  elseif argtype == 'setdate' then
if tnums or not set_date_from_numbers(newdate, numbers) then
    if tnums or not set_date_from_numbers(newdate, numbers) then
return
      return
end
    end
elseif not (is_copy or tnums) then
  elseif not (is_copy or tnums) then
return
    return
end
  end
if tnums then
  if tnums then
newdate.jd = nil -- force recalculation in case jd was set before changes from tnums
    newdate.jd = nil -- force recalculation in case jd was set before changes from tnums
if not set_date_from_numbers(newdate, tnums) then
    if not set_date_from_numbers(newdate, tnums) then
return
      return
end
    end
end
  end
if newdate.partial then
  if newdate.partial then
local year = newdate.year
    local year = newdate.year
local month = newdate.month
    local month = newdate.month
local first = Date(year, month or 1, 1, newdate.calendar)
    local first = Date(year, month or 1, 1, newdate.calendar)
month = month or 12
    month = month or 12
local last = Date(year, month, days_in_month(year, month), newdate.calendar)
    local last = Date(year, month, days_in_month(year, month), newdate.calendar)
newdate.partial = { first = first, last = last }
    newdate.partial = { first = first, last = last }
else
  else
newdate.partial = false -- avoid index lookup
    newdate.partial = false -- avoid index lookup
end
  end
setmetatable(newdate, datemt)
  setmetatable(newdate, datemt)
local readonly = {}
  local readonly = {}
local mt = {
  local mt = {
__index = newdate,
    __index = newdate,
__newindex = function(t, k, v) error('date.' .. tostring(k) .. ' is read-only', 2) end,
    __newindex = function(t, k, v) error('date.' .. tostring(k) .. ' is read-only', 2) end,
__add = mt_date_add,
    __add = mt_date_add,
__sub = mt_date_sub,
    __sub = mt_date_sub,
__concat = mt_date_concat,
    __concat = mt_date_concat,
__tostring = mt_date_tostring,
    __tostring = mt_date_tostring,
__eq = mt_date_eq,
    __eq = mt_date_eq,
__lt = mt_date_lt,
    __lt = mt_date_lt,
}
  }
return setmetatable(readonly, mt)
  return setmetatable(readonly, mt)
end
end


local function _diff_age(diff, code, options)
local function _diff_age(diff, code, options)
-- Return a tuple of integer values from diff as specified by code, except that
  -- Return a tuple of integer values from diff as specified by code, except that
-- each integer may be a list of two integers for a diff with a partial date, or
  -- each integer may be a list of two integers for a diff with a partial date, or
-- return nil if the code is not supported.
  -- return nil if the code is not supported.
-- If want round, the least significant unit is rounded to nearest whole unit.
  -- If want round, the least significant unit is rounded to nearest whole unit.
-- For a duration, an extra day is added.
  -- For a duration, an extra day is added.
local wantround, wantduration, wantrange
  local wantround, wantduration, wantrange
if type(options) == 'table' then
  if type(options) == 'table' then
wantround = options.round
    wantround = options.round
wantduration = options.duration
    wantduration = options.duration
wantrange = options.range
    wantrange = options.range
else
  else
wantround = options
    wantround = options
end
  end
if not is_diff(diff) then
  if not is_diff(diff) then
local f = wantduration and 'duration' or 'age'
    local f = wantduration and 'duration' or 'age'
error(f .. ': need a date difference (use "diff:' .. f .. '()" with a colon)', 2)
    error(f .. ': need a date difference (use "diff:' .. f .. '()" with a colon)', 2)
end
  end
if diff.partial then
  if diff.partial then
-- Ignore wantround, wantduration.
    -- Ignore wantround, wantduration.
local function choose(v)
    local function choose(v)
if type(v) == 'table' then
      if type(v) == 'table' then
if not wantrange or v[1] == v[2] then
        if not wantrange or v[1] == v[2] then
-- Example: Date('partial', 2005) - Date('partial', 2001) gives
          -- Example: Date('partial', 2005) - Date('partial', 2001) gives
-- diff.years = { 3, 4 } to show the range of possible results.
          -- diff.years = { 3, 4 } to show the range of possible results.
-- If do not want a range, choose the second value as more expected.
          -- If do not want a range, choose the second value as more expected.
return v[2]
          return v[2]
end
        end
end
      end
return v
      return v
end
    end
if code == 'ym' or code == 'ymd' then
    if code == 'ym' or code == 'ymd' then
if not wantrange and diff.iszero then
      if not wantrange and diff.iszero then
-- This avoids an unexpected result such as
        -- This avoids an unexpected result such as
-- Date('partial', 2001) - Date('partial', 2001)
        -- Date('partial', 2001) - Date('partial', 2001)
-- giving diff = { years = 0, months = { 0, 11 } }
        -- giving diff = { years = 0, months = { 0, 11 } }
-- which would be reported as 0 years and 11 months.
        -- which would be reported as 0 years and 11 months.
return 0, 0
        return 0, 0
end
      end
return choose(diff.partial.years), choose(diff.partial.months)
      return choose(diff.partial.years), choose(diff.partial.months)
end
    end
if code == 'y' then
    if code == 'y' then
return choose(diff.partial.years)
      return choose(diff.partial.years)
end
    end
if code == 'm' or code == 'w' or code == 'd' then
    if code == 'm' or code == 'w' or code == 'd' then
return choose({ diff.partial.mindiff:age(code), diff.partial.maxdiff:age(code) })
      return choose({ diff.partial.mindiff:age(code), diff.partial.maxdiff:age(code) })
end
    end
return nil
    return nil
end
  end
local extra_days = wantduration and 1 or 0
  local extra_days = wantduration and 1 or 0
if code == 'wd' or code == 'w' or code == 'd' then
  if code == 'wd' or code == 'w' or code == 'd' then
local offset = wantround and 0.5 or 0
    local offset = wantround and 0.5 or 0
local days = diff.age_days + extra_days
    local days = diff.age_days + extra_days
if code == 'wd' or code == 'd' then
    if code == 'wd' or code == 'd' then
days = floor(days + offset)
      days = floor(days + offset)
if code == 'd' then
      if code == 'd' then
return days
        return days
end
      end
return floor(days/7), days % 7
      return floor(days/7), days % 7
end
    end
return floor(days/7 + offset)
    return floor(days/7 + offset)
end
  end
local H, M, S = diff.hours, diff.minutes, diff.seconds
  local H, M, S = diff.hours, diff.minutes, diff.seconds
if code == 'dh' or code == 'dhm' or code == 'dhms' or code == 'h' or code == 'hm' or code == 'hms' then
  if code == 'dh' or code == 'dhm' or code == 'dhms' or code == 'h' or code == 'hm' or code == 'hms' then
local days = floor(diff.age_days + extra_days)
    local days = floor(diff.age_days + extra_days)
local inc_hour
    local inc_hour
if wantround then
    if wantround then
if code == 'dh' or code == 'h' then
      if code == 'dh' or code == 'h' then
if M >= 30 then
        if M >= 30 then
inc_hour = true
          inc_hour = true
end
        end
elseif code == 'dhm' or code == 'hm' then
      elseif code == 'dhm' or code == 'hm' then
if S >= 30 then
        if S >= 30 then
M = M + 1
          M = M + 1
if M >= 60 then
          if M >= 60 then
M = 0
            M = 0
inc_hour = true
            inc_hour = true
end
          end
end
        end
else
      else
-- Nothing needed because S is an integer.
        -- Nothing needed because S is an integer.
end
      end
if inc_hour then
      if inc_hour then
H = H + 1
        H = H + 1
if H >= 24 then
        if H >= 24 then
H = 0
          H = 0
days = days + 1
          days = days + 1
end
        end
end
      end
end
    end
if code == 'dh' or code == 'dhm' or code == 'dhms' then
    if code == 'dh' or code == 'dhm' or code == 'dhms' then
if code == 'dh' then
      if code == 'dh' then
return days, H
        return days, H
elseif code == 'dhm' then
      elseif code == 'dhm' then
return days, H, M
        return days, H, M
else
      else
return days, H, M, S
        return days, H, M, S
end
      end
end
    end
local hours = days * 24 + H
    local hours = days * 24 + H
if code == 'h' then
    if code == 'h' then
return hours
      return hours
elseif code == 'hm' then
    elseif code == 'hm' then
return hours, M
      return hours, M
end
    end
return hours, M, S
    return hours, M, S
end
  end
if wantround then
  if wantround then
local inc_hour
    local inc_hour
if code == 'ymdh' or code == 'ymwdh' then
    if code == 'ymdh' or code == 'ymwdh' then
if M >= 30 then
      if M >= 30 then
inc_hour = true
        inc_hour = true
end
      end
elseif code == 'ymdhm' or code == 'ymwdhm' then
    elseif code == 'ymdhm' or code == 'ymwdhm' then
if S >= 30 then
      if S >= 30 then
M = M + 1
        M = M + 1
if M >= 60 then
        if M >= 60 then
M = 0
          M = 0
inc_hour = true
          inc_hour = true
end
        end
end
      end
elseif code == 'ymd' or code == 'ymwd' or code == 'yd' or code == 'md' then
    elseif code == 'ymd' or code == 'ymwd' or code == 'yd' or code == 'md' then
if H >= 12 then
      if H >= 12 then
extra_days = extra_days + 1
        extra_days = extra_days + 1
end
      end
end
    end
if inc_hour then
    if inc_hour then
H = H + 1
      H = H + 1
if H >= 24 then
      if H >= 24 then
H = 0
        H = 0
extra_days = extra_days + 1
        extra_days = extra_days + 1
end
      end
end
    end
end
  end
local y, m, d = diff.years, diff.months, diff.days
  local y, m, d = diff.years, diff.months, diff.days
if extra_days > 0 then
  if extra_days > 0 then
d = d + extra_days
    d = d + extra_days
if d > 28 or code == 'yd' then
    if d > 28 or code == 'yd' then
-- Recalculate in case have passed a month.
      -- Recalculate in case have passed a month.
diff = diff.date1 + extra_days - diff.date2
      diff = diff.date1 + extra_days - diff.date2
y, m, d = diff.years, diff.months, diff.days
      y, m, d = diff.years, diff.months, diff.days
end
    end
end
  end
if code == 'ymd' then
  if code == 'ymd' then
return y, m, d
    return y, m, d
elseif code == 'yd' then
  elseif code == 'yd' then
if y > 0 then
    if y > 0 then
-- It is known that diff.date1 > diff.date2.
      -- It is known that diff.date1 > diff.date2.
diff = diff.date1 - (diff.date2 + (y .. 'y'))
      diff = diff.date1 - (diff.date2 + (y .. 'y'))
end
    end
return y, floor(diff.age_days)
    return y, floor(diff.age_days)
elseif code == 'md' then
  elseif code == 'md' then
return y * 12 + m, d
    return y * 12 + m, d
elseif code == 'ym' or code == 'm' then
  elseif code == 'ym' or code == 'm' then
if wantround then
    if wantround then
if d >= 16 then
      if d >= 16 then
m = m + 1
        m = m + 1
if m >= 12 then
        if m >= 12 then
m = 0
          m = 0
y = y + 1
          y = y + 1
end
        end
end
      end
end
    end
if code == 'ym' then
    if code == 'ym' then
return y, m
      return y, m
end
    end
return y * 12 + m
    return y * 12 + m
elseif code == 'ymw' then
  elseif code == 'ymw' then
local weeks = floor(d/7)
    local weeks = floor(d/7)
if wantround then
    if wantround then
local days = d % 7
      local days = d % 7
if days > 3 or (days == 3 and H >= 12) then
      if days > 3 or (days == 3 and H >= 12) then
weeks = weeks + 1
        weeks = weeks + 1
end
      end
end
    end
return y, m, weeks
    return y, m, weeks
elseif code == 'ymwd' then
  elseif code == 'ymwd' then
return y, m, floor(d/7), d % 7
    return y, m, floor(d/7), d % 7
elseif code == 'ymdh' then
  elseif code == 'ymdh' then
return y, m, d, H
    return y, m, d, H
elseif code == 'ymwdh' then
  elseif code == 'ymwdh' then
return y, m, floor(d/7), d % 7, H
    return y, m, floor(d/7), d % 7, H
elseif code == 'ymdhm' then
  elseif code == 'ymdhm' then
return y, m, d, H, M
    return y, m, d, H, M
elseif code == 'ymwdhm' then
  elseif code == 'ymwdhm' then
return y, m, floor(d/7), d % 7, H, M
    return y, m, floor(d/7), d % 7, H, M
end
  end
if code == 'y' then
  if code == 'y' then
if wantround and m >= 6 then
    if wantround and m >= 6 then
y = y + 1
      y = y + 1
end
    end
return y
    return y
end
  end
return nil
  return nil
end
end


local function _diff_duration(diff, code, options)
local function _diff_duration(diff, code, options)
if type(options) ~= 'table' then
  if type(options) ~= 'table' then
options = { round = options }
    options = { round = options }
end
  end
options.duration = true
  options.duration = true
return _diff_age(diff, code, options)
  return _diff_age(diff, code, options)
end
end


-- Metatable for some operations on date differences.
-- Metatable for some operations on date differences.
diffmt = { -- for forward declaration above
diffmt = { -- for forward declaration above
__concat = function (lhs, rhs)
  __concat = function (lhs, rhs)
return tostring(lhs) .. tostring(rhs)
    return tostring(lhs) .. tostring(rhs)
end,
  end,
__tostring = function (self)
  __tostring = function (self)
return tostring(self.age_days)
    return tostring(self.age_days)
end,
  end,
__index = function (self, key)
  __index = function (self, key)
local value
    local value
if key == 'age_days' then
    if key == 'age_days' then
if rawget(self, 'partial') then
      if rawget(self, 'partial') then
local function jdz(date)
        local function jdz(date)
return (date.partial and date.partial.first or date).jdz
          return (date.partial and date.partial.first or date).jdz
end
        end
value = jdz(self.date1) - jdz(self.date2)
        value = jdz(self.date1) - jdz(self.date2)
else
      else
value = self.date1.jdz - self.date2.jdz
        value = self.date1.jdz - self.date2.jdz
end
      end
end
    end
if value ~= nil then
    if value ~= nil then
rawset(self, key, value)
      rawset(self, key, value)
return value
      return value
end
    end
end,
  end,
}
}


function DateDiff(date1, date2, options) -- for forward declaration above
function DateDiff(date1, date2, options) -- for forward declaration above
-- Return a table with the difference between two dates (date1 - date2).
  -- Return a table with the difference between two dates (date1 - date2).
-- The difference is negative if date1 is older than date2.
  -- The difference is negative if date1 is older than date2.
-- Return nothing if invalid.
  -- Return nothing if invalid.
-- If d = date1 - date2 then
  -- If d = date1 - date2 then
--     date1 = date2 + d
  -- date1 = date2 + d
-- If date1 >= date2 and the dates have no H:M:S time specified then
  -- If date1 >= date2 and the dates have no H:M:S time specified then
--     date1 = date2 + (d.years..'y') + (d.months..'m') + d.days
  -- date1 = date2 + (d.years..'y') + (d.months..'m') + d.days
-- where the larger time units are added first.
  -- where the larger time units are added first.
-- The result of Date(2015,1,x) + '1m' is Date(2015,2,28) for
  -- The result of Date(2015,1,x) + '1m' is Date(2015,2,28) for
-- x = 28, 29, 30, 31. That means, for example,
  -- x = 28, 29, 30, 31. That means, for example,
--     d = Date(2015,3,3) - Date(2015,1,31)
  -- d = Date(2015,3,3) - Date(2015,1,31)
-- gives d.years, d.months, d.days = 0, 1, 3 (excluding date1).
  -- gives d.years, d.months, d.days = 0, 1, 3 (excluding date1).
if not (is_date(date1) and is_date(date2) and date1.calendar == date2.calendar) then
  if not (is_date(date1) and is_date(date2) and date1.calendar == date2.calendar) then
return
    return
end
  end
local wantfill
  local wantfill
if type(options) == 'table' then
  if type(options) == 'table' then
wantfill = options.fill
    wantfill = options.fill
end
  end
local isnegative = false
  local isnegative = false
local iszero = false
  local iszero = false
if date1 < date2 then
  if date1 < date2 then
isnegative = true
    isnegative = true
date1, date2 = date2, date1
    date1, date2 = date2, date1
elseif date1 == date2 then
  elseif date1 == date2 then
iszero = true
    iszero = true
end
  end
-- It is known that date1 >= date2 (period is from date2 to date1).
  -- It is known that date1 >= date2 (period is from date2 to date1).
if date1.partial or date2.partial then
  if date1.partial or date2.partial then
-- Two partial dates might have timelines:
    -- Two partial dates might have timelines:
---------------------A=================B--- date1 is from A to B inclusive
    ---------------------A=================B--- date1 is from A to B inclusive
--------C=======D-------------------------- date2 is from C to D inclusive
    --------C=======D-------------------------- date2 is from C to D inclusive
-- date1 > date2 iff A > C (date1.partial.first > date2.partial.first)
    -- date1 > date2 iff A > C (date1.partial.first > date2.partial.first)
-- The periods can overlap ('April 2001' - '2001'):
    -- The periods can overlap ('April 2001' - '2001'):
-------------A===B------------------------- A=2001-04-01 B=2001-04-30
    -------------A===B------------------------- A=2001-04-01 B=2001-04-30
--------C=====================D------------ C=2001-01-01 D=2001-12-31
    --------C=====================D------------ C=2001-01-01 D=2001-12-31
if wantfill then
    if wantfill then
date1, date2 = autofill(date1, date2)
      date1, date2 = autofill(date1, date2)
else
    else
local function zdiff(date1, date2)
      local function zdiff(date1, date2)
local diff = date1 - date2
        local diff = date1 - date2
if diff.isnegative then
        if diff.isnegative then
return date1 - date1 -- a valid diff in case we call its methods
          return date1 - date1 -- a valid diff in case we call its methods
end
        end
return diff
        return diff
end
      end
local function getdate(date, which)
      local function getdate(date, which)
return date.partial and date.partial[which] or date
        return date.partial and date.partial[which] or date
end
      end
local maxdiff = zdiff(getdate(date1, 'last'), getdate(date2, 'first'))
      local maxdiff = zdiff(getdate(date1, 'last'), getdate(date2, 'first'))
local mindiff = zdiff(getdate(date1, 'first'), getdate(date2, 'last'))
      local mindiff = zdiff(getdate(date1, 'first'), getdate(date2, 'last'))
local years, months
      local years, months
if maxdiff.years == mindiff.years then
      if maxdiff.years == mindiff.years then
years = maxdiff.years
        years = maxdiff.years
if maxdiff.months == mindiff.months then
        if maxdiff.months == mindiff.months then
months = maxdiff.months
          months = maxdiff.months
else
        else
months = { mindiff.months, maxdiff.months }
          months = { mindiff.months, maxdiff.months }
end
        end
else
      else
years = { mindiff.years, maxdiff.years }
        years = { mindiff.years, maxdiff.years }
end
      end
return setmetatable({
      return setmetatable({
date1 = date1,
        date1 = date1,
date2 = date2,
        date2 = date2,
partial = {
        partial = {
years = years,
          years = years,
months = months,
          months = months,
maxdiff = maxdiff,
          maxdiff = maxdiff,
mindiff = mindiff,
          mindiff = mindiff,
},
        },
isnegative = isnegative,
        isnegative = isnegative,
iszero = iszero,
        iszero = iszero,
age = _diff_age,
        age = _diff_age,
duration = _diff_duration,
        duration = _diff_duration,
}, diffmt)
      }, diffmt)
end
    end
end
  end
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 = y1 - y2
  local years = y1 - y2
local months = m1 - m2
  local months = m1 - m2
local d1 = date1.day + hms(date1)
  local d1 = date1.day + hms(date1)
local d2 = date2.day + hms(date2)
  local d2 = date2.day + hms(date2)
local days, time
  local days, time
if d1 >= d2 then
  if d1 >= d2 then
days = d1 - d2
    days = d1 - d2
else
  else
months = months - 1
    months = months - 1
-- Get days in previous month (before the "to" date) given December has 31 days.
    -- Get days in previous month (before the "to" date) given December has 31 days.
local dpm = m1 > 1 and days_in_month(y1, m1 - 1, date1.calendar) or 31
    local dpm = m1 > 1 and days_in_month(y1, m1 - 1, date1.calendar) or 31
if d2 >= dpm then
    if d2 >= dpm then
days = d1 - hms(date2)
      days = d1 - hms(date2)
else
    else
days = dpm - d2 + d1
      days = dpm - d2 + d1
end
    end
end
  end
if months < 0 then
  if months < 0 then
years = years - 1
    years = years - 1
months = months + 12
    months = months + 12
end
  end
days, time = math.modf(days)
  days, time = math.modf(days)
local H, M, S = h_m_s(time)
  local H, M, S = h_m_s(time)
return setmetatable({
  return setmetatable({
date1 = date1,
    date1 = date1,
date2 = date2,
    date2 = date2,
partial = false, -- avoid index lookup
    partial = false, -- avoid index lookup
years = years,
    years = years,
months = months,
   
days = days,
 
hours = H,
months = months,
minutes = M,
    days = days,
seconds = S,
    hours = H,
isnegative = isnegative,
    minutes = M,
iszero = iszero,
    seconds = S,
age = _diff_age,
    isnegative = isnegative,
duration = _diff_duration,
    iszero = iszero,
}, diffmt)
    age = _diff_age,
    duration = _diff_duration,
  }, diffmt)
end
end


return {
return {
_current = current,
  _current = current,
_Date = Date,
  _Date = Date,
_days_in_month = days_in_month,
  _days_in_month = days_in_month,
}
}

Revisión del 19:10 11 jun 2020

Nota: Éste módulo es una traducción de Module:Date lo que significa que cualquier módulo que dependa de Module:Date se puede utilizar con éste módulo sin ningún problema (siempre que el módulo en cuestión se adapte para entender fechas en español si fuese necesario). Es por eso que a pesar de que las fechas que devuelve al utilizar :text() son en español todos los nombres de los métodos internos y los parámetros se han mantenido en el idioma original.


Este módulo provee funciones relacionadas con el manejo de fechas para que puedan usarse en otros módulos. El módulo soporta fechas en el Calendario Gregoriano y en el calendario Juliano, desde el 9999 a. C. al 9999 d. C.. Los calendarios son prolépticos, es decir, se aplican incluso antes de su creación sin irregularidades.

Una fecha , con su hora opcional, se puede especificar en varios formatos diferentes y se puede formatear para su impresión usando otra variedad de formatos, por ejemplo '1 de abril de 2016' o el 'abril 1, 2016'. Las propiedades de la fecha incluyen su Fecha juliana y los días desde el 1 A.D. así como el día de la semana y el día del año.

También es posible comparar fechas (por ejemplo, fecha1 <= fecha2), y se les pueden aplicar operadores como la suma o la resta (por ejemplo, fecha + '3 meses' o fecha1 - fecha2). Estas operaciones funcionan con fechas de los calendarios Juliano y Gregoriano pero será nil si intentas operar con dos fechas de diferentes calendarios.

El módulo provee los siguientes objetos.

Export Descripción
_current Tabla con el año, mes, día, hora, minuto y segundo actuales.
_Date Función que devuelve una tabla para una fecha concreta.
_days_in_month Función que retorna el número de días en un mes.

En Módulo:Date/ejemplos se encuentran los ejemplos de uso del módulo y en Módulo discusión:Date/ejemplos está el resultado de su ejecución.

Formato de la salida

Es posible formatear la representación textual de la fecha.

local Date = require('Módulo:Date')._Date
local text = Date(2016, 7, 1):text()          -- devolvería '1 de julio de 2016'
local text = Date(2016, 7, 1):text('%-d de %B')  -- devolvería '1 de julio'
local text = Date('1 de julio de 2016'):text('mdy')  -- devolvería 'julio 1, 2016'

Los siguientes son los códigos de formato simplificados disponibles.

Código Resultado
hm horas:minutos, utilizando "am" o "pm" o una variante especificada (14:30 o 2:30 pm o lo especificado)
hms horas:minutos:segundos (14:30:45)
ymd año-mes-día (2016-07-01)
mdy mes día, año (julio 1, 2016)
dmy día de mes de año (1 de julio de 2016)

También están disponibles los siguientes códigos están disponibles (similar a los utilizados por strftime).

Código Resultado
%a Abreviación del día: Lu, Ma, ...
%A Nombre del día: lunes, martes, ...
%u Día de la semana: de 1 a 7 (Lunes a Domingo)
%w Día de la semana: de 0 a 6 (Domingo a Sábado)
%d Día del mes relleno con ceros: 01 a 31
%b Abreviación del mes: ene a dic
%B Nombre del mes: enero a diciembre
%m Mes relleno con ceros: 01 a 12
%Y Año relleno con ceros: 0012, 0120, 1200
%H Hora (reloj de 24 horas) rellena con ceros: 00 a 23
%I Hora (reloj de 12 horas) rellena con ceros: 01 a 12
%p AM o PM como con las opciones
%M Minutos rellenos con ceros: 00 a 59
%S Segundos rellenos con ceros: 00 a 59
%j Día del año relleno con ceros: 001 a 366
%-d Día del mes: 1 a 31
%-m Mes: 1 a 12
%-Y Año: 12, 120, 1200
%-H Hora: 0 a 23
%-M Minutos: 0 a 59
%-S Segundos: 0 a 59
%-j Día del año: 1 a 366
%-I Hora: 1 a 12
%% %

Además también se puede utilizar %{property} (donde property es una de las propiedades de la fecha).

Por ejemplo, una fecha como Date('1 de febrero de 2015 14:30:45 d. C.') tiene las siguientes propiedades.

Código Resultado
%{calendar} Gregorian
%{year} 2015
%{month} 2
%{day} 1
%{hour} 14
%{minute} 30
%{second} 45
%{dayabbr} do
%{dayname} Domingo
%{dayofweek} 0
%{dow} 0 (igual que 'dayofweek')
%{dayofweekiso} 7
%{dowiso} 7 (igual que 'dayofweekiso')
%{dayofyear} 32
%{era} d. C.
%{gsd} 735630 (números de días desde 1 de enero de 1 d. C.; el primero es el 1)
%{juliandate} 2457055.1046875 (Fecha juliana)
%{jd} 2457055.1046875 (igual que 'juliandate')
%{isleapyear} false
%{monthdays} 28
%{monthabbr} feb
%{monthname} febrero

También hay disponibles algunos atajos. Dada fecha = Date('1 feb 2015 14:30'), obtendremos los siguientes resultados.

Código Descripción Resultado del ejemplo Formato equivalente
fecha:text('%c') fecha y hora 2:30 pm 1 de febrero de 2015 %-I:%M %p %-d %B %-Y %{era}
fecha:text('%C') fecha y hora 1 de febrero de 2015, 14:30 %-d de %B de %-Y, %-H:%-M:%-S
fecha:text('%x') fecha 1 de febrero de 2015 %-d %B %-Y %{era}
fecha:text('%X') hora 2:30 pm %-I:%M %p

Fecha Juliana

El siguiente código contiene un ejemplo de la conversión de una Fecha juliana y la posterior obtención de información sobre esa fecha.

-- Código                                               -- Resultado
Date = require('Módulo:Date')._Date
fecha = Date('juliandate', 320)
número = fecha.gsd                                       -- -1721105
número = fecha.jd                                        -- 320
texto = fecha.dayname                                    -- sábado
texto = fecha:text()                                     -- 9 de octubre de 4713&nbsp;a. C.
texto = fecha:text('%Y-%m-%d')                           -- 4713-10-09
texto = fecha:text('%{era} %Y-%m-%d')                    -- a. C. 4713-10-09
texto = fecha:text('%Y-%m-%d %{era}')                    -- 4713-10-09&nbsp;BC
texto = fecha:text('%Y-%m-%d %{era}', 'era=B.C.E.')      -- 4713-10-09&nbsp;B.C.E.
texto = fecha:text('%Y-%m-%d', 'era=BCNEGATIVE')         -- -4712-10-09
texto = fecha:text('%Y-%m-%d', 'era=BCMINUS')            -- −4712-10-09 (utiliza el símbolo menos de Unicode U+2212)
texto = Date('juliandate',320):text('%{gsd} %{jd}')     -- -1721105 320
texto = Date('oct 9, 4713 B.C.E.'):text('%{gsd} %{jd}') -- -1721105 320
texto = Date(-4712,10,9):text('%{gsd} %{jd}')           -- -1721105 320

Diferencia de fechas

La diferencia entre dos fechas se puede determinar utilizando fecha1 - fecha2. El resultado es válido si las dos fechas utilizan el mismo calendario siendo 'nil' en otro caso. Es posible calcular una edad o una duración a partir de la diferencia entre dos fechas.

Por ejemplo:

-- Código                                    -- Resultado
Date = require('Módulo:Date')._Date
fecha1 = Date('21 mar 2015')
fecha2 = Date('4 dic 1999')
diff = fecha1 - fecha2
d = diff.age_days                            -- 5586
y, m, d = diff.years, diff.months, diff.days -- 15, 3, 17 (15 años + 3 meses + 17 días)
y, m, d = diff:age('ymd')                    -- 15, 3, 17
y, m, w, d = diff:age('ymwd')                -- 15, 3, 2, 3 (15 años + 3 meses + 2 semanas + 3 días)
y, m, w, d = diff:duration('ymwd')           -- 15, 3, 2, 4
d = diff:duration('d')                       -- 5587 (duración en días incluyendo el último día)

Una diferencia de fechas mantiene las fechas originales pero están cambiadas de tal forma que diff.date1 >= diff.date2 (siendo siempre diff.date1 la más reciente). Esto se muestra a continuación.

fecha1 = Date('21 mar 2015')
fecha2 = Date('4 dic 1999')
diff = fecha1 - fecha2
neg = diff.isnegative                        -- false
text = diff.date1:text()                     -- 21 de marzo de 2015
text = diff.date2:text()                     -- 4 de diciembre de 1999
diff = fecha2 - fecha1
neg = diff.isnegative                        -- true (se han cambiado las fechas de orden)
text = diff.date1:text()                     -- 21 de marzo de 2015
text = diff.date2:text()                     -- 4 de diciembre de 1999

Una diferencia de fechas también guarda la diferencia de tiempo:

fecha1 = Date('8 mar 2016 0:30:45')
fecha2 = Date('19 ene 2014 22:55')
diff = fecha1 - fecha2
y, m, d = diff.years, diff.months, diff.days      -- 2, 1, 17
H, M, S = diff.hours, diff.minutes, diff.seconds  -- 1, 35, 45

Una diferencia de fechas también se puede añadir o restar a una fecha.

fecha1 = Date('8 mar 2016 0:30:45')
fecha2 = Date('19 ene 2014 22:55')
diff = fecha1 - fecha2
fecha3 = fecha2 + diff
fecha4 = fecha1 - diff
texto = date3:text('ymd hms')        -- 2016-03-08 00:30:45
texto = date4:text('ymd hms')        -- 2014-01-19 22:55:00
igualdad = (fecha1 == fecha3)        -- true
igualdad = (fecha2 == fecha4)        -- true

Los métodos de edad 'age' y duración 'duration' aceptan un código que identifica los componentes a retornar. En el caso de la duración se incluye un día extra (el último).

Código Valores retornados
'ymwd' años, meses, semanas, días
'ymd' años, meses, días
'ym' años, meses
'y' años
'm' meses
'wd' semanas, días
'w' semanas
'd' días

Compatibilidad

Este módulo implementa las funciones de Módulo:Fecha y Módulo:Fechas, los tests de ambos módulos se encuentran funcionando contra este en Módulo:Date/tests y el resultado de su ejecución en Módulo discusión:Date/tests.


-- Date functions for use by other modules.
-- I18N and time zones are not supported.

local MINUS = '−' -- Unicode U+2212 MINUS SIGN
local floor = math.floor

local Date, DateDiff, diffmt -- forward declarations
local uniq = { 'unique identifier' }

local function is_date(t)
  -- The system used to make a date read-only means there is no unique
  -- metatable that is conveniently accessible to check.
  return type(t) == 'table' and t._id == uniq
end

local function is_diff(t)
  return type(t) == 'table' and getmetatable(t) == diffmt
end

local function _list_join(list, sep)
  return table.concat(list, sep)
end

local function collection()
  -- Return a table to hold items.
  return {
    n = 0,
    add = function (self, item)
      self.n = self.n + 1
      self[self.n] = item
    end,
    join = _list_join,
  }
end

local function strip_to_nil(text)
  -- If text is a string, return its trimmed content, or nil if empty.
  -- Otherwise return text (convenient when Date fields are provided from
  -- another module which may pass a string, a number, or another type).
  if type(text) == 'string' then
    text = text:match('(%S.-)%s*$')
  end
  return text
end

local function is_leap_year(year, calname)
  -- Return true if year is a leap year.
  if calname == 'Juliano' then
    return year % 4 == 0
  end
  return (year % 4 == 0 and year % 100 ~= 0) or year % 400 == 0
end

local function days_in_month(year, month, calname)
  -- Return number of days (1..31) in given month (1..12).
  if month == 2 and is_leap_year(year, calname) then
    return 29
  end
  return ({ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 })[month]
end

local function h_m_s(time)
  -- Return hour, minute, second extracted from fraction of a day.
  time = floor(time * 24 * 3600 + 0.5) -- number of seconds
  local second = time % 60
  time = floor(time / 60)
  return floor(time / 60), time % 60, second
end

local function hms(date)
  -- Return fraction of a day from date's time, where (0 <= fraction < 1)
  -- if the values are valid, but could be anything if outside range.
  return (date.hour + (date.minute + date.second / 60) / 60) / 24
end

local function julian_date(date)
  -- Return jd, jdz from a Julian or Gregorian calendar date where
  -- jd = Julian date and its fractional part is zero at noon
  -- jdz = same, but assume time is 00:00:00 if no time given
  -- http://www.tondering.dk/claus/cal/julperiod.php#formula
  -- Testing shows this works for all dates from year -9999 to 9999!
  -- JDN 0 is the 24-hour period starting at noon UTC on Monday
  -- 1 January 4713 BC = (-4712, 1, 1) Julian calendar
  -- 24 November 4714 BC = (-4713, 11, 24) Gregorian calendar
  local offset
  local a = floor((14 - date.month)/12)
  local y = date.year + 4800 - a
  if date.calendar == 'Juliano' then
    offset = floor(y/4) - 32083
  else
    offset = floor(y/4) - floor(y/100) + floor(y/400) - 32045
  end
  local m = date.month + 12*a - 3
  local jd = date.day + floor((153*m + 2)/5) + 365*y + offset
  if date.hastime then
    jd = jd + hms(date) - 0.5
    return jd, jd
  end
  return jd, jd - 0.5
end

local function set_date_from_jd(date)
  -- 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
  -- This handles the proleptic Julian and Gregorian calendars.
  -- Negative Julian dates are not defined but they work.
  local calname = date.calendar
  local low, high -- min/max limits for date ranges −9999-01-01 to 9999-12-31
  if calname == 'Gregoriano' then
    low, high = -1930999.5, 5373484.49999
  elseif calname == 'Juliano' then
    low, high = -1931076.5, 5373557.49999
  else
    return
  end
  local jd = date.jd
  if not (type(jd) == 'number' and low <= jd and jd <= high) then
    return
  end
  local jdn = floor(jd)
  if date.hastime then
    local time = jd - jdn -- 0 <= time < 1
    if time >= 0.5 then -- if at or after midnight of next day
      jdn = jdn + 1
      time = time - 0.5
    else
      time = time + 0.5
    end
    date.hour, date.minute, date.second = h_m_s(time)
  else
    date.second = 0
    date.minute = 0
    date.hour = 0
  end
  local b, c
  if calname == 'Juliano' then
    b = 0
    c = jdn + 32082
  else -- Gregorian
    local a = jdn + 32044
    b = floor((4*a + 3)/146097)
    c = a - floor(146097*b/4)
  end
  local d = floor((4*c + 3)/1461)
  local e = c - floor(1461*d/4)
  local m = floor((5*e + 2)/153)
  date.day = e - floor((153*m + 2)/5) + 1
  date.month = m + 3 - 12*floor(m/10)
  date.year = 100*b + d - 4800 + floor(m/10)
  return true
end

local function fix_numbers(numbers, y, m, d, H, M, S, partial, hastime, calendar)
  -- Put the result of normalizing the given values in table numbers.
  -- The result will have valid m, d values if y is valid; caller checks y.
  -- The logic of PHP mktime is followed where m or d can be zero to mean
  -- the previous unit, and -1 is the one before that, etc.
  -- Positive values carry forward.
  local date
  if not (1 <= m and m <= 12) then
    date = Date(y, 1, 1)
    if not date then return end
    date = date + ((m - 1) .. 'm')
    y, m = date.year, date.month
  end
  local days_hms
  if not partial then
    if hastime and H and M and S then
      if not (0 <= H and H <= 23 and
          0 <= M and M <= 59 and
          0 <= S and S <= 59) then
        days_hms = hms({ hour = H, minute = M, second = S })
      end
    end
    if days_hms or not (1 <= d and d <= days_in_month(y, m, calendar)) then
      date = date or Date(y, m, 1)
      if not date then return end
      date = date + (d - 1 + (days_hms or 0))
      y, m, d = date.year, date.month, date.day
      if days_hms then
        H, M, S = date.hour, date.minute, date.second
      end
    end
  end
  numbers.year = y
  numbers.month = m
  numbers.day = d
  if days_hms then
    -- Don't set H unless it was valid because a valid H will set hastime.
    numbers.hour = H
    numbers.minute = M
    numbers.second = S
  end
end

local function set_date_from_numbers(date, numbers, options)
  -- Set the fields of table date from numeric values.
  -- Return true if date is valid.
  if type(numbers) ~= 'table' then
    return
  end
  local y = numbers.year or date.year
  local m = numbers.month or date.month
  local d = numbers.day or date.day
  local H = numbers.hour
  local M = numbers.minute or date.minute or 0
  local S = numbers.second or date.second or 0
  local need_fix
  if y and m and d then
    date.partial = nil
    if not (-9999 <= y and y <= 9999 and
      1 <= m and m <= 12 and
      1 <= d and d <= days_in_month(y, m, date.calendar)) then
        if not date.want_fix then
          return
        end
        need_fix = true
    end
  elseif y and date.partial then
    if d or not (-9999 <= y and y <= 9999) then
      return
    end
    if m and not (1 <= m and m <= 12) then
      if not date.want_fix then
        return
      end
      need_fix = true
    end
  else
    return
  end
  if date.partial then
    H = nil -- ignore any time
    M = nil
    S = nil
  else
    if H then
      -- It is not possible to set M or S without also setting H.
      date.hastime = true
    else
      H = 0
    end
    if not (0 <= H and H <= 23 and
        0 <= M and M <= 59 and
        0 <= S and S <= 59) then
      if date.want_fix then
        need_fix = true
      else
        return
      end
    end
  end
  date.want_fix = nil
  if need_fix then
    fix_numbers(numbers, y, m, d, H, M, S, date.partial, date.hastime, date.calendar)
    return set_date_from_numbers(date, numbers, options)
  end
  date.year = y -- -9999 to 9999 ('n BC' → year = 1 - n)
  date.month = m -- 1 to 12 (may be nil if partial)
  date.day = d -- 1 to 31 (* = nil if partial)
  date.hour = H -- 0 to 59 (*)
  date.minute = M -- 0 to 59 (*)
  date.second = S -- 0 to 59 (*)
  if type(options) == 'table' then
    for _, k in ipairs({ 'am', 'era', 'format' }) do
      if options[k] then
        date.options[k] = options[k]
      end
    end
  end
  return true
end

local function make_option_table(options1, options2)
  -- If options1 is a string, return a table with its settings, or
  -- if it is a table, use its settings.
  -- Missing options are set from table options2 or defaults.
  -- If a default is used, a flag is set so caller knows the value was not intentionally set.
  -- Valid option settings are:
  -- am: 'am', 'a.m.', 'AM', 'A.M.'
  -- 'pm', 'p.m.', 'PM', 'P.M.' (each has same meaning as corresponding item above)
  -- 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,
  -- and am = 'pm' has the same meaning.
  -- 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 = { bydefault = {} }
  if type(options1) == 'table' then
    result.am = options1.am
    result.era = options1.era
  elseif type(options1) == 'string' then
    -- Example: 'am:AM era:BC' or 'am=AM era=BC'.
    for item in options1:gmatch('%S+') do
      local lhs, rhs = item:match('^(%w+)[:=](.+)$')
      if lhs then
        result[lhs] = rhs
      end
    end
  end
  options2 = type(options2) == 'table' and options2 or {}
  local defaults = { am = 'am', era = 'BC' }
  for k, v in pairs(defaults) do
    if not result[k] then
      if options2[k] then
        result[k] = options2[k]
      else
        result[k] = v
        result.bydefault[k] = true
      end
    end
  end
  return result
end

local ampm_options = {
  -- lhs = input text accepted as an am/pm option
  -- rhs = code used internally
  ['am'] = 'am',
  ['AM'] = 'AM',
  ['a.m.'] = 'a.m.',
  ['A.M.'] = 'A.M.',
  ['pm'] = 'am', -- same as am
  ['PM'] = 'AM',
  ['p.m.'] = 'a.m.',
  ['P.M.'] = 'A.M.',
}

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 era_text['BC'])[year > 0 and 2 or 1] or ''
end

local function strftime(date, format, options)
  -- Return date formatted as a string using codes similar to those
  -- in the C strftime library function.
  local sformat = string.format
  local shortcuts = {
    ['%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'] = '%-I:%M %p', -- time: 2:30 pm
  }
  if shortcuts[format] then
    format = shortcuts[format]
  end
  local codes = {
    a = { field = 'dayabbr' },
    A = { field = 'dayname' },
    b = { field = 'monthabbr' },
    B = { field = 'monthname' },
    u = { fmt = '%d' , field = 'dowiso' },
    w = { fmt = '%d' , field = 'dow' },
    d = { fmt = '%02d', fmt2 = '%d', field = 'day' },
    m = { fmt = '%02d', fmt2 = '%d', field = 'month' },
    Y = { fmt = '%04d', fmt2 = '%d', field = 'year' },
    H = { fmt = '%02d', fmt2 = '%d', field = 'hour' },
    M = { fmt = '%02d', fmt2 = '%d', field = 'minute' },
    S = { fmt = '%02d', fmt2 = '%d', field = 'second' },
    j = { fmt = '%03d', fmt2 = '%d', field = 'dayofyear' },
    I = { fmt = '%02d', fmt2 = '%d', field = 'hour', special = 'hour12' },
    p = { field = 'hour', special = 'am' },
  }
  options = make_option_table(options, date.options)
  local amopt = options.am
  local eraopt = options.era
  local function replace_code(spaces, modifier, id)
--print('spaces:' .. spaces .. ';')
--print('modifier:' .. modifier .. ';')
--print('id:' .. id .. ';')
    local code = codes[id]
    if code then
      local fmt = code.fmt
      if modifier == '-' and code.fmt2 then
        fmt = code.fmt2
      end
      local value = date[code.field]
      if not value then
        return nil -- an undefined field in a partial date
      end
      local special = code.special
      if special then
        if special == 'hour12' then
          value = value % 12
          value = value == 0 and 12 or value
        elseif special == 'am' then
          local ap = ({
            ['a.m.'] = { 'a.m.', 'p.m.' },
            ['AM'] = { 'AM', 'PM' },
            ['A.M.'] = { 'A.M.', 'P.M.' },
          })[ampm_options[amopt]] or { 'am', 'pm' }
          return (spaces == '' and '' or '&nbsp;') .. (value < 12 and ap[1] or ap[2])
        end
      end
      if code.field == 'year' then
        local sign = (era_text[eraopt] or {}).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
            sign = ''
          else
            value = -value
          end
        end
        return spaces .. sign .. sformat(fmt, value)
      end
if code.field == 'monthname' then
return spaces .. 'de ' .. (fmt and sformat(fmt, value) or value) .. ' de'
end
      return spaces .. (fmt and sformat(fmt, value) or value)
    end
  end
  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]
    if type(result) == 'string' then
      return spaces .. result
    end
    if type(result) == 'number' then
      return spaces .. tostring(result)
    end
    if type(result) == 'boolean' then
      return spaces .. (result and '1' or '0')
    end
    -- This occurs if id is an undefined field in a partial date, or is the name of a function.
    return nil
  end
  local PERCENT = '\127PERCENT\127'

format = format
    :gsub('%%%%', PERCENT)
:gsub('(%s*)%%{(%w+)}', replace_property)

--print(format)
format = format
    :gsub('(%s*)%%(%-?)(%a)', replace_code)
--print(format)

format = format
    :gsub(PERCENT, '%%')

  return format
end

local function _date_text(date, fmt, options)
  -- Return a formatted string representing the given date.
  if not is_date(date) then
    error('date:text: need a date (use "date:text()" with a colon)', 2)
  end
  if type(fmt) == 'string' and fmt:match('%S') then
    if fmt:find('%', 1, true) then
      return strftime(date, fmt, options)
    end
  elseif date.partial then
    fmt = date.month and 'my' or 'y'
  else
    fmt = 'dmy'
    if date.hastime then
      fmt = (date.second > 0 and 'hms ' or 'hm ') .. fmt
    end
  end
  local function bad_format()
    -- For consistency with other format processing, return given format
    -- (or cleaned format if original was not a string) if invalid.
    return mw.text.nowiki(fmt)
  end
  if date.partial then
    -- Ignore days in standard formats like 'ymd'.
    if fmt == 'ym' or fmt == 'ymd' then
      fmt = date.month and '%Y-%m %{era}' or '%Y %{era}'
    elseif fmt == 'my' or fmt == 'dmy' or fmt == 'mdy' then
      fmt = date.month and '%B %-Y %{era}' or '%-Y %{era}'
    elseif fmt == 'y' then
      fmt = date.month and '%-Y %{era}' or '%-Y %{era}'
    else
      return bad_format()
    end
    return strftime(date, fmt, options)
  end
  local function hm_fmt()
    local plain = make_option_table(options, date.options).bydefault.am
    return plain and '%H:%M' or '%-I:%M %p'
  end
  local need_time = date.hastime
  local t = collection()
  for item in fmt:gmatch('%S+') do
    local f
    if item == 'hm' then
      f = hm_fmt()
      need_time = false
    elseif item == 'hms' then
      f = '%H:%M:%S'
      need_time = false
    elseif item == 'ymd' then
      f = '%Y-%m-%d %{era}'
    elseif item == 'mdy' then
      f = '%B %-d, %-Y %{era}'
    elseif item == 'dmy' then
      f = '%-d %B %-Y %{era}'
    else
      return bad_format()
    end
    t:add(f)
  end
  fmt = t:join(' ')
  if need_time then
    fmt = hm_fmt() .. ' ' .. fmt
  end
  return strftime(date, fmt, options)
end

local day_info = {
  -- 0=Sun to 6=Sat
  [0] = { 'do', 'domingo' },
  { 'lu', 'lunes' },
  { 'ma', 'martes' },
  { 'mi', 'miércoles' },
  { 'ju', 'jueves' },
  { 'vi', 'viernes' },
  { 'sa', 'sábado' },
}

local month_info = {
  -- 1=Jan to 12=Dec
  { 'ene', 'enero' },
  { 'feb', 'febrero' },
  { 'mar', 'marzo' },
  { 'abr', 'abril' },
  { 'may', 'mayo' },
  { 'jun', 'junio' },
  { 'jul', 'julio' },
  { 'ago', 'agosto' },
  { 'sep', 'septiembre' },
  { 'oct', 'octubre' },
  { 'nov', 'noviembre' },
  { 'dic', 'diciembre' },
}

local function name_to_number(text, translate)
  if type(text) == 'string' then
    return translate['xx' .. text:lower():gsub('é', 'e'):gsub('á', 'a')]
  end
end

local function day_number(text)
  return name_to_number(text, {
    xxdo = 0, xxdomingo = 0,
    xxlu = 1, xxlunes = 1,
    xxma = 2, xxmartes = 2,
    xxmi = 3, xxmiercoles = 3,
    xxju = 4, xxjueves = 4,
    xxvi = 5, xxviernes = 5,
    xxsat = 6, xxsabado = 6
  })
end

local function month_number(text)
  return name_to_number(text, {
    xxene = 1, xxenero = 1,
    xxfeb = 2, xxfebrero = 2,
    xxmar = 3, xxmarzo = 3,
    xxabr = 4, xxabril = 4,
    xxmay = 5, xxmayo = 5,
    xxjun = 6, xxjunio = 6,
    xxjul = 7, xxjulio = 7,
    xxago = 8, xxagosto = 8,
    xxsep = 9, xxseptiembre = 9, xxsept = 9,
    xxoct = 10, xxoctubre = 10,
    xxnov = 11, xxnoviembre = 11,
    xxdic = 12, xxdiciembre = 12,
  })
end

local function _list_text(list, fmt)
  -- Return a list of formatted strings from a list of dates.
  if not type(list) == 'table' then
    error('date:list:text: need "list:text()" with a colon', 2)
  end
  local result = { join = _list_join }
  for i, date in ipairs(list) do
    result[i] = date:text(fmt)
  end
  return result
end

local function _date_list(date, spec)
  -- Return a possibly empty numbered table of dates meeting the specification.
  -- Dates in the list are in ascending order (oldest date first).
  -- The spec should be a string of form "<count> <day> <op>"
  -- where each item is optional and
  -- count = number of items wanted in list
  -- day = abbreviation or name such as Mon or Monday
  -- op = >, >=, <, <= (default is > meaning after date)
  -- If no count is given, the list is for the specified days in date's month.
  -- The default day is date's day.
  -- The spec can also be a positive or negative number:
  -- -5 is equivalent to '5 <'
  -- 5 is equivalent to '5' which is '5 >'
  if not is_date(date) then
    error('date:list: need a date (use "date:list()" with a colon)', 2)
  end
  local list = { text = _list_text }
  if date.partial then
    return list
  end
  local count, offset, operation
  local ops = {
    ['>='] = { before = false, include = true },
    ['>'] = { before = false, include = false },
    ['<='] = { before = true , include = true },
    ['<'] = { before = true , include = false },
  }
  if spec then
    if type(spec) == 'number' then
      count = floor(spec + 0.5)
      if count < 0 then
        count = -count
        operation = ops['<']
      end
    elseif type(spec) == 'string' then
      local num, day, op = spec:match('^%s*(%d*)%s*(%a*)%s*([<>=]*)%s*$')
      if not num then
        return list
      end
      if num ~= '' then
        count = tonumber(num)
      end
      if day ~= '' then
        local dow = day_number(day:gsub('[sS]$', '')) -- accept plural days
        if not dow then
          return list
        end
        offset = dow - date.dow
      end
      operation = ops[op]
    else
      return list
    end
  end
  offset = offset or 0
  operation = operation or ops['>']
  local datefrom, dayfirst, daylast
  if operation.before then
    if offset > 0 or (offset == 0 and not operation.include) then
      offset = offset - 7
    end
    if count then
      if count > 1 then
        offset = offset - 7*(count - 1)
      end
      datefrom = date + offset
    else
      daylast = date.day + offset
      dayfirst = daylast % 7
      if dayfirst == 0 then
        dayfirst = 7
      end
    end
  else
    if offset < 0 or (offset == 0 and not operation.include) then
      offset = offset + 7
    end
    if count then
      datefrom = date + offset
    else
      dayfirst = date.day + offset
      daylast = date.monthdays
    end
  end
  if not count then
    if daylast < dayfirst then
      return list
    end
    count = floor((daylast - dayfirst)/7) + 1
    datefrom = Date(date, {day = dayfirst})
  end
  for i = 1, count do
    if not datefrom then break end -- exceeds date limits
    list[i] = datefrom
    datefrom = datefrom + 7
  end
  return list
end

-- A table to get the current date/time (UTC), but only if needed.
local current = setmetatable({}, {
  __index = function (self, key)
    local d = os.date('!*t')
    self.year = d.year
    self.month = d.month
    self.day = d.day
    self.hour = d.hour
    self.minute = d.min
    self.second = d.sec
    return rawget(self, key)
  end })

local function extract_date(newdate, text)
  -- Parse the date/time in text and return n, o where
  -- n = table of numbers with date/time fields
  -- o = table of options for AM/PM or AD/BC or format, if any
  -- or return nothing if date is known to be invalid.
  -- Caller determines if the values in n are valid.
  -- 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 potentially ambiguous.
  local date, options = {}, {}
  if text:sub(-1) == 'Z' then
    -- Extract date/time from a Wikidata timestamp.
    -- The year can be 1 to 16 digits but this module handles 1 to 4 digits only.
    -- Examples: '+2016-06-21T14:30:00Z', '-0000000180-00-00T00:00:00Z'.
    local sign, y, m, d, H, M, S = text:match('^([+%-])(%d+)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)Z$')
    if sign then
      y = tonumber(y)
      if sign == '-' and y > 0 then
        y = -y
      end
      if y <= 0 then
        options.era = 'BCE'
      end
      date.year = y
      m = tonumber(m)
      d = tonumber(d)
      H = tonumber(H)
      M = tonumber(M)
      S = tonumber(S)
      if m == 0 then
        newdate.partial = true
        return date, options
      end
      date.month = m
      if d == 0 then
        newdate.partial = true
        return date, options
      end
      date.day = d
      if H > 0 or M > 0 or S > 0 then
        date.hour = H
        date.minute = M
        date.second = S
      end
      return date, options
    end
    return
  end
  local function extract_ymd(item)
    -- Called when no day or month has been set.
    local y, m, d = item:match('^(%d%d%d%d)%-(%w+)%-(%d%d?)$')
    if y then
      if date.year then
        return
      end
      if m:match('^%d%d?$') then
        m = tonumber(m)
      else
        m = month_number(m)
      end
      if m then
        date.year = tonumber(y)
        date.month = m
        date.day = tonumber(d)
        return true
      end
    end
  end
  local function extract_day_or_year(item)
    -- Called when a day would be valid, or
    -- when a year would be valid if no year has been set and partial is set.
    local number, suffix = item:match('^(%d%d?%d?%d?)(.*)$')
    if number then
      local n = tonumber(number)
      if #number <= 2 and n <= 31 then
        suffix = suffix:lower()
        if suffix == '' or suffix == 'st' or suffix == 'nd' or suffix == 'rd' or suffix == 'th' then
          date.day = n
          return true
        end
      elseif suffix == '' and newdate.partial and not date.year then
        date.year = n
        return true
      end
    end
  end
  local function extract_month(item)
    -- A month must be given as a name or abbreviation; a number could be ambiguous.
    local m = month_number(item)
    if m then
      date.month = m
      return true
    end
  end
  local function extract_time(item)
    local h, m, s = item:match('^(%d%d?):(%d%d)(:?%d*)$')
    if date.hour or not h then
      return
    end
    if s ~= '' then
      s = s:match('^:(%d%d)$')
      if not s then
        return
      end
    end
    date.hour = tonumber(h)
    date.minute = tonumber(m)
    date.second = tonumber(s) -- nil if empty string
    return true
  end
  local item_count = 0
  local index_time
  local function set_ampm(item)
    local H = date.hour
    if H and not options.am and index_time + 1 == item_count then
      options.am = ampm_options[item] -- caller checked this is not nil
      if item:match('^[Aa]') then
        if not (1 <= H and H <= 12) then
          return
        end
        if H == 12 then
          date.hour = 0
        end
      else
        if not (1 <= H and H <= 23) then
          return
        end
        if H <= 11 then
          date.hour = H + 12
        end
      end
      return true
    end
  end
  for item in text:gsub(',', ' '):gsub('de', ' '):gsub('&nbsp;', ' '):gmatch('%S+') do
    item_count = item_count + 1
    if era_text[item] then
      -- Era is accepted in peculiar places.
      if options.era then
        return
      end
      options.era = item
    elseif ampm_options[item] then
      if not set_ampm(item) then
        return
      end
    elseif item:find(':', 1, true) then
      if not extract_time(item) then
        return
      end
      index_time = item_count
    elseif date.day and date.month then
      if date.year then
        return -- should be nothing more so item is invalid
      end
      if not item:match('^(%d%d?%d?%d?)$') then
        return
      end
      date.year = tonumber(item)
    elseif date.day then
      if not extract_month(item) then
        return
      end
    elseif date.month then
      if not extract_day_or_year(item) then
        return
      end
    elseif extract_month(item) then
      options.format = 'mdy'
    elseif extract_ymd(item) then
      options.format = 'ymd'
    elseif extract_day_or_year(item) then
      if date.day then
        options.format = 'dmy'
      end
    else
      return
    end
  end
  if not date.year or date.year == 0 then
    return
  end
  local era = era_text[options.era]
  if era and era.isbc then
    date.year = 1 - date.year
  end
  return date, options
end

local function autofill(date1, date2)
  -- Fill any missing month or day in each date using the
  -- corresponding component from the other date, if present,
  -- or with 1 if both dates are missing the month or day.
  -- This gives a good result for calculating the difference
  -- between two partial dates when no range is wanted.
  -- Return filled date1, date2 (two full dates).
  local function filled(a, b)
    local fillmonth, fillday
    if not a.month then
      fillmonth = b.month or 1
    end
    if not a.day then
      fillday = b.day or 1
    end
    if fillmonth or fillday then -- need to create a new date
      if (fillmonth or a.month) == 2 and (fillday or a.day) == 29 then
        -- Avoid invalid date, for example with {{age|2013|29 Feb 2016}} or {{age|Feb 2013|29 Jan 2015}}.
        if not is_leap_year(a.year, a.calendar) then
          fillday = 28
        end
      end
      a = Date(a, { month = fillmonth, day = fillday })
    end
    return a
  end
  return filled(date1, date2), filled(date2, date1)
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.
  -- The result is nil if the calculated date exceeds allowable limits.
  -- Caller ensures that lhs is a date; its properties are copied for the new date.
  if lhs.partial then
    -- Adding to a partial is not supported.
    -- Can subtract a date or partial from a partial, but this is not called for that.
    return
  end
  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)
    local forcetime, jd
    if floor(n) == n then
      jd = lhs.jd
    else
      forcetime = not lhs.hastime
      jd = lhs.jdz
    end
    jd = jd + (is_sub and -n or n)
    if forcetime then
      jd = tostring(jd)
      if not jd:find('.', 1, true) then
        jd = jd .. '.0'
      end
    end
    return Date(lhs, 'juliandate', jd)
  end
  if type(rhs) == 'number' then
    -- Add/subtract days, including fractional days.
    return do_days(rhs)
  end
  if type(rhs) == 'string' then
    -- rhs is a single component like '26m' or '26 months' (with optional sign).
    -- Fractions like '3.25d' are accepted for the units which are handled as days.
    local sign, numstr, id = rhs:match('^%s*([+-]?)([%d%.]+)%s*(%a+)$')
    if sign then
      if sign == '-' then
        is_sub = not (is_sub and true or false)
      end
      local y, m, days
      local num = tonumber(numstr)
      if not num then
        return
      end
      id = id:lower()
      if is_prefix(id, 'years') then
        y = num
        m = 0
      elseif is_prefix(id, 'months') then
        y = floor(num / 12)
        m = num % 12
      elseif is_prefix(id, 'weeks') then
        days = num * 7
      elseif is_prefix(id, 'days') then
        days = num
      elseif is_prefix(id, 'hours') then
        days = num / 24
      elseif is_prefix(id, 'minutes', 3) then
        days = num / (24 * 60)
      elseif is_prefix(id, 'seconds') then
        days = num / (24 * 3600)
      else
        return
      end
      if days then
        return do_days(days)
      end
      if numstr:find('.', 1, true) then
        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.calendar))
      return Date(lhs, y, m, d)
    end
  end
  if is_diff(rhs) then
    local days = rhs.age_days
    if (is_sub or false) ~= (rhs.isnegative or false) then
      days = -days
    end
    return lhs + days
  end
end

local full_date_only = {
  dayabbr = true,
  dayname = true,
  dow = true,
  dayofweek = true,
  dowiso = true,
  dayofweekiso = true,
  dayofyear = true,
  gsd = true,
  juliandate = true,
  jd = true,
  jdz = true,
  jdnoon = true,
}

-- Metatable for a date's calculated fields.
local datemt = {
  __index = function (self, key)
    if rawget(self, 'partial') then
      if full_date_only[key] then return end
      if key == 'monthabbr' or key == 'monthdays' or key == 'monthname' then
        if not self.month then return end
      end
    end
    local value
    if key == 'dayabbr' then
      value = day_info[self.dow][1]
    elseif key == 'dayname' then
      value = day_info[self.dow][2]
    elseif key == 'dow' then
      value = (self.jdnoon + 1) % 7 -- day-of-week 0=Sun to 6=Sat
    elseif key == 'dayofweek' then
      value = self.dow
    elseif key == 'dowiso' then
      value = (self.jdnoon % 7) + 1 -- ISO day-of-week 1=Mon to 7=Sun
    elseif key == 'dayofweekiso' then
      value = self.dowiso
    elseif key == 'dayofyear' then
      local first = Date(self.year, 1, 1, self.calendar).jdnoon
      value = self.jdnoon - first + 1 -- day-of-year 1 to 366
    elseif key == 'era' then
      -- Era text (never a negative sign) from year and options.
      value = get_era_for_year(self.options.era, self.year)
    elseif key == 'format' then
      value = self.options.format or 'dmy'
    elseif key == 'gsd' then
      -- GSD = 1 from 00:00:00 to 23:59:59 on 1 January 1 AD Gregorian calendar,
      -- which is from jd 1721425.5 to 1721426.49999.
      value = floor(self.jd - 1721424.5)
    elseif key == 'juliandate' or key == 'jd' or key == 'jdz' then
      local jd, jdz = julian_date(self)
      rawset(self, 'juliandate', jd)
      rawset(self, 'jd', jd)
      rawset(self, 'jdz', jdz)
      return key == 'jdz' and jdz or jd
    elseif key == 'jdnoon' then
      -- Julian date at noon (an integer) on the calendar day when jd occurs.
      value = floor(self.jd + 0.5)
    elseif key == 'isleapyear' then
      value = is_leap_year(self.year, self.calendar)
    elseif key == 'monthabbr' then
      value = month_info[self.month][1]
    elseif key == 'monthdays' then
      value = days_in_month(self.year, self.month, self.calendar)
    elseif key == 'monthname' then
      value = month_info[self.month][2]
    end
    if value ~= nil then
      rawset(self, key, value)
      return value
    end
  end,
}

-- Date operators.
local function mt_date_add(lhs, rhs)
  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

local function mt_date_sub(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

local function mt_date_concat(lhs, rhs)
  return tostring(lhs) .. tostring(rhs)
end

local function mt_date_tostring(self)
  return self:text()
end

local function mt_date_eq(lhs, rhs)
  -- Return true if dates identify same date/time where, for example,
  -- Date(-4712, 1, 1, 'Juliano') == Date(-4713, 11, 24, 'Gregoriano') is true.
  -- This is called only if lhs and rhs have the same type and the same metamethod.
  if lhs.partial or rhs.partial then
    -- One date is partial; the other is a partial or a full date.
    -- The months may both be nil, but must be the same.
    return lhs.year == rhs.year and lhs.month == rhs.month and lhs.calendar == rhs.calendar
  end
  return lhs.jdz == rhs.jdz
end

local function mt_date_lt(lhs, rhs)
  -- Return true if lhs < rhs, for example,
  -- Date('1 Jan 2016') < Date('06:00 1 Jan 2016') is true.
  -- This is called only if lhs and rhs have the same type and the same metamethod.
  if lhs.partial or rhs.partial then
    -- One date is partial; the other is a partial or a full date.
    if lhs.calendar ~= rhs.calendar then
      return lhs.calendar == 'Juliano'
    end
    if lhs.partial then
      lhs = lhs.partial.first
    end
    if rhs.partial then
      rhs = rhs.partial.first
    end
  end
  return lhs.jdz < rhs.jdz
end

--[[ Examples of syntax to construct a date:
Date(y, m, d, 'julian') default calendar is 'gregorian'
Date(y, m, d, H, M, S, 'julian')
Date('juliandate', jd, 'julian') if jd contains "." text output includes H:M:S
Date('currentdate')
Date('currentdatetime')
Date('1 April 1995', 'julian') parse date from text
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(date) copy of an existing date
Date(date, t) same, updated with y,m,d,H,M,S fields from table t
Date(t)     date with y,m,d,H,M,S fields from table t
]]
function Date(...) -- for forward declaration above
  -- Return a table holding a date assuming a uniform calendar always applies
  -- (proleptic Gregorian calendar or proleptic Julian calendar), or
  -- return nothing if date is invalid.
  -- A partial date has a valid year, however its month may be nil, and
  -- its day and time fields are nil.
  -- Field partial is set to false (if a full date) or a table (if a partial date).
  local calendars = { julian = 'Juliano', gregorian = 'Gregoriano' }
  local newdate = {
    _id = uniq,
    calendar = 'Gregoriano', -- default is Gregorian calendar
    hastime = false, -- true if input sets a time
    hour = 0, -- always set hour/minute/second so don't have to handle nil
    minute = 0,
    second = 0,
    options = {},
    list = _date_list,
    subtract = function (self, rhs, options)
      return DateDiff(self, rhs, options)
    end,
    text = _date_text,
  }
  local argtype, datetext, is_copy, jd_number, tnums
  local numindex = 0
  local numfields = { 'year', 'month', 'day', 'hour', 'minute', 'second' }
  local numbers = {}
  for _, v in ipairs({...}) do
    v = strip_to_nil(v)
    local vlower = type(v) == 'string' and v:lower() or nil
    if v == nil then
      -- Ignore empty arguments after stripping so modules can directly pass template parameters.
    elseif calendars[vlower] then
      newdate.calendar = calendars[vlower]
    elseif vlower == 'partial' then
      newdate.partial = true
    elseif vlower == 'fix' then
      newdate.want_fix = true
    elseif is_date(v) then
      -- Copy existing date (items can be overridden by other arguments).
      if is_copy or tnums then
        return
      end
      is_copy = true
      newdate.calendar = v.calendar
      newdate.partial = v.partial
      newdate.hastime = v.hastime
      newdate.options = v.options
      newdate.year = v.year
      newdate.month = v.month
      newdate.day = v.day
      newdate.hour = v.hour
      newdate.minute = v.minute
      newdate.second = v.second
    elseif type(v) == 'table' then
      if tnums then
        return
      end
      tnums = {}
      local tfields = { year=1, month=1, day=1, hour=2, minute=2, second=2 }
      for tk, tv in pairs(v) do
        if tfields[tk] then
          tnums[tk] = tonumber(tv)
        end
        if tfields[tk] == 2 then
          newdate.hastime = true
        end
      end
    else
      local num = tonumber(v)
      if not num and argtype == 'setdate' and numindex == 1 then
        num = month_number(v)
      end
      if num then
        if not argtype then
          argtype = 'setdate'
        end
        if argtype == 'setdate' and numindex < 6 then
          numindex = numindex + 1
          numbers[numfields[numindex]] = num
        elseif argtype == 'juliandate' and not jd_number then
          jd_number = num
          if type(v) == 'string' then
            if v:find('.', 1, true) then
              newdate.hastime = true
            end
          elseif num ~= floor(num) then
            -- The given value was a number. The time will be used
            -- if the fractional part is nonzero.
            newdate.hastime = true
          end
        else
          return
        end
      elseif argtype then
        return
      elseif type(v) == 'string' then
        if v == 'currentdate' or v == 'currentdatetime' or v == 'juliandate' then
          argtype = v
        else
          argtype = 'datetext'
          datetext = v
        end
      else
        return
      end
    end
  end
  if argtype == 'datetext' then
    if tnums or not set_date_from_numbers(newdate, extract_date(newdate, datetext)) then
      return
    end
  elseif argtype == 'juliandate' then
    newdate.partial = nil
    newdate.jd = jd_number
    if not set_date_from_jd(newdate) then
      return
    end
  elseif argtype == 'currentdate' or argtype == 'currentdatetime' then
    newdate.partial = nil
    newdate.year = current.year
    newdate.month = current.month
    newdate.day = current.day
    if argtype == 'currentdatetime' then
      newdate.hour = current.hour
      newdate.minute = current.minute
      newdate.second = current.second
      newdate.hastime = true
    end
    newdate.calendar = 'Gregoriano' -- ignore any given calendar name
  elseif argtype == 'setdate' then
    if tnums or not set_date_from_numbers(newdate, numbers) then
      return
    end
  elseif not (is_copy or tnums) then
    return
  end
  if tnums then
    newdate.jd = nil -- force recalculation in case jd was set before changes from tnums
    if not set_date_from_numbers(newdate, tnums) then
      return
    end
  end
  if newdate.partial then
    local year = newdate.year
    local month = newdate.month
    local first = Date(year, month or 1, 1, newdate.calendar)
    month = month or 12
    local last = Date(year, month, days_in_month(year, month), newdate.calendar)
    newdate.partial = { first = first, last = last }
  else
    newdate.partial = false -- avoid index lookup
  end
  setmetatable(newdate, datemt)
  local readonly = {}
  local mt = {
    __index = newdate,
    __newindex = function(t, k, v) error('date.' .. tostring(k) .. ' is read-only', 2) end,
    __add = mt_date_add,
    __sub = mt_date_sub,
    __concat = mt_date_concat,
    __tostring = mt_date_tostring,
    __eq = mt_date_eq,
    __lt = mt_date_lt,
  }
  return setmetatable(readonly, mt)
end

local function _diff_age(diff, code, options)
  -- Return a tuple of integer values from diff as specified by code, except that
  -- each integer may be a list of two integers for a diff with a partial date, or
  -- return nil if the code is not supported.
  -- If want round, the least significant unit is rounded to nearest whole unit.
  -- For a duration, an extra day is added.
  local wantround, wantduration, wantrange
  if type(options) == 'table' then
    wantround = options.round
    wantduration = options.duration
    wantrange = options.range
  else
    wantround = options
  end
  if not is_diff(diff) then
    local f = wantduration and 'duration' or 'age'
    error(f .. ': need a date difference (use "diff:' .. f .. '()" with a colon)', 2)
  end
  if diff.partial then
    -- Ignore wantround, wantduration.
    local function choose(v)
      if type(v) == 'table' then
        if not wantrange or v[1] == v[2] then
          -- Example: Date('partial', 2005) - Date('partial', 2001) gives
          -- diff.years = { 3, 4 } to show the range of possible results.
          -- If do not want a range, choose the second value as more expected.
          return v[2]
        end
      end
      return v
    end
    if code == 'ym' or code == 'ymd' then
      if not wantrange and diff.iszero then
        -- This avoids an unexpected result such as
        -- Date('partial', 2001) - Date('partial', 2001)
        -- giving diff = { years = 0, months = { 0, 11 } }
        -- which would be reported as 0 years and 11 months.
        return 0, 0
      end
      return choose(diff.partial.years), choose(diff.partial.months)
    end
    if code == 'y' then
      return choose(diff.partial.years)
    end
    if code == 'm' or code == 'w' or code == 'd' then
      return choose({ diff.partial.mindiff:age(code), diff.partial.maxdiff:age(code) })
    end
    return nil
  end
  local extra_days = wantduration and 1 or 0
  if code == 'wd' or code == 'w' or code == 'd' then
    local offset = wantround and 0.5 or 0
    local days = diff.age_days + extra_days
    if code == 'wd' or code == 'd' then
      days = floor(days + offset)
      if code == 'd' then
        return days
      end
      return floor(days/7), days % 7
    end
    return floor(days/7 + offset)
  end
  local H, M, S = diff.hours, diff.minutes, diff.seconds
  if code == 'dh' or code == 'dhm' or code == 'dhms' or code == 'h' or code == 'hm' or code == 'hms' then
    local days = floor(diff.age_days + extra_days)
    local inc_hour
    if wantround then
      if code == 'dh' or code == 'h' then
        if M >= 30 then
          inc_hour = true
        end
      elseif code == 'dhm' or code == 'hm' then
        if S >= 30 then
          M = M + 1
          if M >= 60 then
            M = 0
            inc_hour = true
          end
        end
      else
        -- Nothing needed because S is an integer.
      end
      if inc_hour then
        H = H + 1
        if H >= 24 then
          H = 0
          days = days + 1
        end
      end
    end
    if code == 'dh' or code == 'dhm' or code == 'dhms' then
      if code == 'dh' then
        return days, H
      elseif code == 'dhm' then
        return days, H, M
      else
        return days, H, M, S
      end
    end
    local hours = days * 24 + H
    if code == 'h' then
      return hours
    elseif code == 'hm' then
      return hours, M
    end
    return hours, M, S
  end
  if wantround then
    local inc_hour
    if code == 'ymdh' or code == 'ymwdh' then
      if M >= 30 then
        inc_hour = true
      end
    elseif code == 'ymdhm' or code == 'ymwdhm' then
      if S >= 30 then
        M = M + 1
        if M >= 60 then
          M = 0
          inc_hour = true
        end
      end
    elseif code == 'ymd' or code == 'ymwd' or code == 'yd' or code == 'md' then
      if H >= 12 then
        extra_days = extra_days + 1
      end
    end
    if inc_hour then
      H = H + 1
      if H >= 24 then
        H = 0
        extra_days = extra_days + 1
      end
    end
  end
  local y, m, d = diff.years, diff.months, diff.days
  if extra_days > 0 then
    d = d + extra_days
    if d > 28 or code == 'yd' then
      -- Recalculate in case have passed a month.
      diff = diff.date1 + extra_days - diff.date2
      y, m, d = diff.years, diff.months, diff.days
    end
  end
  if code == 'ymd' then
    return y, m, d
  elseif code == 'yd' then
    if y > 0 then
      -- It is known that diff.date1 > diff.date2.
      diff = diff.date1 - (diff.date2 + (y .. 'y'))
    end
    return y, floor(diff.age_days)
  elseif code == 'md' then
    return y * 12 + m, d
  elseif code == 'ym' or code == 'm' then
    if wantround then
      if d >= 16 then
        m = m + 1
        if m >= 12 then
          m = 0
          y = y + 1
        end
      end
    end
    if code == 'ym' then
      return y, m
    end
    return y * 12 + m
  elseif code == 'ymw' then
    local weeks = floor(d/7)
    if wantround then
      local days = d % 7
      if days > 3 or (days == 3 and H >= 12) then
        weeks = weeks + 1
      end
    end
    return y, m, weeks
  elseif code == 'ymwd' then
    return y, m, floor(d/7), d % 7
  elseif code == 'ymdh' then
    return y, m, d, H
  elseif code == 'ymwdh' then
    return y, m, floor(d/7), d % 7, H
  elseif code == 'ymdhm' then
    return y, m, d, H, M
  elseif code == 'ymwdhm' then
    return y, m, floor(d/7), d % 7, H, M
  end
  if code == 'y' then
    if wantround and m >= 6 then
      y = y + 1
    end
    return y
  end
  return nil
end

local function _diff_duration(diff, code, options)
  if type(options) ~= 'table' then
    options = { round = options }
  end
  options.duration = true
  return _diff_age(diff, code, options)
end

-- Metatable for some operations on date differences.
diffmt = { -- for forward declaration above
  __concat = function (lhs, rhs)
    return tostring(lhs) .. tostring(rhs)
  end,
  __tostring = function (self)
    return tostring(self.age_days)
  end,
  __index = function (self, key)
    local value
    if key == 'age_days' then
      if rawget(self, 'partial') then
        local function jdz(date)
          return (date.partial and date.partial.first or date).jdz
        end
        value = jdz(self.date1) - jdz(self.date2)
      else
        value = self.date1.jdz - self.date2.jdz
      end
    end
    if value ~= nil then
      rawset(self, key, value)
      return value
    end
  end,
}

function DateDiff(date1, date2, options) -- for forward declaration above
  -- Return a table with the difference between two dates (date1 - date2).
  -- The difference is negative if date1 is older than date2.
  -- Return nothing if invalid.
  -- If d = date1 - date2 then
  -- date1 = date2 + d
  -- If date1 >= date2 and the dates have no H:M:S time specified then
  -- date1 = date2 + (d.years..'y') + (d.months..'m') + d.days
  -- where the larger time units are added first.
  -- The result of Date(2015,1,x) + '1m' is Date(2015,2,28) for
  -- x = 28, 29, 30, 31. That means, for example,
  -- d = Date(2015,3,3) - Date(2015,1,31)
  -- gives d.years, d.months, d.days = 0, 1, 3 (excluding date1).
  if not (is_date(date1) and is_date(date2) and date1.calendar == date2.calendar) then
    return
  end
  local wantfill
  if type(options) == 'table' then
    wantfill = options.fill
  end
  local isnegative = false
  local iszero = false
  if date1 < date2 then
    isnegative = true
    date1, date2 = date2, date1
  elseif date1 == date2 then
    iszero = true
  end
  -- It is known that date1 >= date2 (period is from date2 to date1).
  if date1.partial or date2.partial then
    -- Two partial dates might have timelines:
    ---------------------A=================B--- date1 is from A to B inclusive
    --------C=======D-------------------------- date2 is from C to D inclusive
    -- date1 > date2 iff A > C (date1.partial.first > date2.partial.first)
    -- The periods can overlap ('April 2001' - '2001'):
    -------------A===B------------------------- A=2001-04-01 B=2001-04-30
    --------C=====================D------------ C=2001-01-01 D=2001-12-31
    if wantfill then
      date1, date2 = autofill(date1, date2)
    else
      local function zdiff(date1, date2)
        local diff = date1 - date2
        if diff.isnegative then
          return date1 - date1 -- a valid diff in case we call its methods
        end
        return diff
      end
      local function getdate(date, which)
        return date.partial and date.partial[which] or date
      end
      local maxdiff = zdiff(getdate(date1, 'last'), getdate(date2, 'first'))
      local mindiff = zdiff(getdate(date1, 'first'), getdate(date2, 'last'))
      local years, months
      if maxdiff.years == mindiff.years then
        years = maxdiff.years
        if maxdiff.months == mindiff.months then
          months = maxdiff.months
        else
          months = { mindiff.months, maxdiff.months }
        end
      else
        years = { mindiff.years, maxdiff.years }
      end
      return setmetatable({
        date1 = date1,
        date2 = date2,
        partial = {
          years = years,
          months = months,
          maxdiff = maxdiff,
          mindiff = mindiff,
        },
        isnegative = isnegative,
        iszero = iszero,
        age = _diff_age,
        duration = _diff_duration,
      }, diffmt)
    end
  end
  local y1, m1 = date1.year, date1.month
  local y2, m2 = date2.year, date2.month
  local years = y1 - y2
  local months = m1 - m2
  local d1 = date1.day + hms(date1)
  local d2 = date2.day + hms(date2)
  local days, time
  if d1 >= d2 then
    days = d1 - d2
  else
    months = months - 1
    -- Get days in previous month (before the "to" date) given December has 31 days.
    local dpm = m1 > 1 and days_in_month(y1, m1 - 1, date1.calendar) or 31
    if d2 >= dpm then
      days = d1 - hms(date2)
    else
      days = dpm - d2 + d1
    end
  end
  if months < 0 then
    years = years - 1
    months = months + 12
  end
  days, time = math.modf(days)
  local H, M, S = h_m_s(time)
  return setmetatable({
    date1 = date1,
    date2 = date2,
    partial = false, -- avoid index lookup
    years = years,
    

months = months,
    days = days,
    hours = H,
    minutes = M,
    seconds = S,
    isnegative = isnegative,
    iszero = iszero,
    age = _diff_age,
    duration = _diff_duration,
  }, diffmt)
end

return {
  _current = current,
  _Date = Date,
  _days_in_month = days_in_month,
}