Theme: iWiki Log in Register

Diff: Module:Date

Comparing revision #1 (2020-08-03 02:55:18) with revision #2 (2023-02-03 04:17:54).

OldNew
-- 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 == 'Julian' then
	if calname == 'Julian' 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 == 'Julian' then
	if date.calendar == 'Julian' 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 == 'Gregorian' then
	if calname == 'Gregorian' then
		low, high = -1930999.5, 5373484.49999
		low, high = -1930999.5, 5373484.49999
	elseif calname == 'Julian' then
	elseif calname == 'Julian' 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 == 'Julian' then
	if calname == 'Julian' 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]
		local code = codes[id]
		if code then
		if code then
			local fmt = code.fmt
			local fmt = code.fmt
			if modifier == '-' and code.fmt2 then
			if modifier == '-' and code.fmt2 then
				fmt = code.fmt2
				fmt = code.fmt2
			end
			end
			local value = date[code.field]
			local value = date[code.field]
			if not value then
			if not value then
				return nil  -- an undefined field in a partial date
				return nil  -- an undefined field in a partial date
			end
			end
			local special = code.special
			local special = code.special
			if special then
			if special then
				if special == 'hour12' then
				if special == 'hour12' then
					value = value % 12
					value = value % 12
					value = value == 0 and 12 or value
					value = value == 0 and 12 or value
				elseif special == 'am' then
				elseif special == 'am' then
					local ap = ({
					local ap = ({
						['a.m.'] = { 'a.m.', 'p.m.' },
						['a.m.'] = { 'a.m.', 'p.m.' },
						['AM'] = { 'AM', 'PM' },
						['AM'] = { 'AM', 'PM' },
						['A.M.'] = { 'A.M.', 'P.M.' },
						['A.M.'] = { 'A.M.', 'P.M.' },
					})[ampm_options[amopt]] or { 'am', 'pm' }
					})[ampm_options[amopt]] or { 'am', 'pm' }
					return (spaces == '' and '' or '&nbsp;') .. (value < 12 and ap[1] or ap[2])
					return (spaces == '' and '' or '&nbsp;') .. (value < 12 and ap[1] or ap[2])
				end
				end
			end
			end
			if code.field == 'year' then
			if code.field == 'year' then
				local sign = (era_text[eraopt] or {}).sign
				local sign = (era_text[eraopt] or {}).sign
				if not sign or format:find('%{era}', 1, true) then
				if not sign or format:find('%{era}', 1, true) then
					sign = ''
					sign = ''
					if value <= 0 then
					if value <= 0 then
						value = 1 - value
						value = 1 - value
					end
					end
				else
				else
					if value >= 0 then
					if value >= 0 then
						sign = ''
						sign = ''
					else
					else
						value = -value
						value = -value
					end
					end
				end
				end
				return spaces .. sign .. sformat(fmt, value)
				return spaces .. sign .. sformat(fmt, value)
			end
			end
			return spaces .. (fmt and sformat(fmt, value) or value)
			return spaces .. (fmt and sformat(fmt, value) or value)
		end
		end
	end
	end
	local function replace_property(spaces, id)
	local function replace_property(spaces, id)
		if id == 'era' then
		if id == 'era' then
			-- Special case so can use local era option.
			-- Special case so can use local era option.
			local result = get_era_for_year(eraopt, date.year)
			local result = get_era_for_year(eraopt, date.year)
			if result == '' then
			if result == '' then
				return ''
				return ''
			end
			end
			return (spaces == '' and '' or '&nbsp;') .. result
			return (spaces == '' and '' or '&nbsp;') .. result
		end
		end
		local result = date[id]
		local result = date[id]
		if type(result) == 'string' then
		if type(result) == 'string' then
			return spaces .. result
			return spaces .. result
		end
		end
		if type(result) == 'number' then
		if type(result) == 'number' then
			return  spaces .. tostring(result)
			return  spaces .. tostring(result)
		end
		end
		if type(result) == 'boolean' then
		if type(result) == 'boolean' then
			return  spaces .. (result and '1' or '0')
			return  spaces .. (result and '1' or '0')
		end
		end
		-- This occurs if id is an undefined field in a partial date, or is the name of a function.
		-- This occurs if id is an undefined field in a partial date, or is the name of a function.
		return nil
		return nil
	end
	end
	local PERCENT = '\127PERCENT\127'
	local PERCENT = '\127PERCENT\127'
	return (format
	return (format
		:gsub('%%%%', PERCENT)
		:gsub('%%%%', PERCENT)
		:gsub('(%s*)%%{(%w+)}', replace_property)
		:gsub('(%s*)%%{(%w+)}', replace_property)
		:gsub('(%s*)%%(%-?)(%a)', replace_code)
		:gsub('(%s*)%%(%-?)(%a)', replace_code)
		:gsub(PERCENT, '%%')
		:gsub(PERCENT, '%%')
	)
	)
end
end
local function _date_text(date, fmt, options)
local function _date_text(date, fmt, options)
	-- Return 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] = { 'Sun', 'Sunday' },
	[0] = { 'Sun', 'Sunday' },
	{ 'Mon', 'Monday' },
	{ 'Mon', 'Monday' },
	{ 'Tue', 'Tuesday' },
	{ 'Tue', 'Tuesday' },
	{ 'Wed', 'Wednesday' },
	{ 'Wed', 'Wednesday' },
	{ 'Thu', 'Thursday' },
	{ 'Thu', 'Thursday' },
	{ 'Fri', 'Friday' },
	{ 'Fri', 'Friday' },
	{ 'Sat', 'Saturday' },
	{ 'Sat', 'Saturday' },
}
}
local month_info = {
local month_info = {
	-- 1=Jan to 12=Dec
	-- 1=Jan to 12=Dec
	{ 'Jan', 'January' },
	{ 'Jan', 'January' },
	{ 'Feb', 'February' },
	{ 'Feb', 'February' },
	{ 'Mar', 'March' },
	{ 'Mar', 'March' },
	{ 'Apr', 'April' },
	{ 'Apr', 'April' },
	{ 'May', 'May' },
	{ 'May', 'May' },
	{ 'Jun', 'June' },
	{ 'Jun', 'June' },
	{ 'Jul', 'July' },
	{ 'Jul', 'July' },
	{ 'Aug', 'August' },
	{ 'Aug', 'August' },
	{ 'Sep', 'September' },
	{ 'Sep', 'September' },
	{ 'Oct', 'October' },
	{ 'Oct', 'October' },
	{ 'Nov', 'November' },
	{ 'Nov', 'November' },
	{ 'Dec', 'December' },
	{ 'Dec', 'December' },
}
}
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[text:lower()]
		return translate[text:lower()]
	end
	end
end
end
local function day_number(text)
local function day_number(text)
	return name_to_number(text, {
	return name_to_number(text, {
		sun = 0, sunday = 0,
		sun = 0, sunday = 0,
		mon = 1, monday = 1,
		mon = 1, monday = 1,
		tue = 2, tuesday = 2,
		tue = 2, tuesday = 2,
		wed = 3, wednesday = 3,
		wed = 3, wednesday = 3,
		thu = 4, thursday = 4,
		thu = 4, thursday = 4,
		fri = 5, friday = 5,
		fri = 5, friday = 5,
		sat = 6, saturday = 6,
		sat = 6, saturday = 6,
	})
	})
end
end
local function month_number(text)
local function month_number(text)
	return name_to_number(text, {
	return name_to_number(text, {
		jan = 1, january = 1,
		jan = 1, january = 1,
		feb = 2, february = 2,
		feb = 2, february = 2,
		mar = 3, march = 3,
		mar = 3, march = 3,
		apr = 4, april = 4,
		apr = 4, april = 4,
		may = 5,
		may = 5,
		jun = 6, june = 6,
		jun = 6, june = 6,
		jul = 7, july = 7,
		jul = 7, july = 7,
		aug = 8, august = 8,
		aug = 8, august = 8,
		sep = 9, september = 9, sept = 9,
		sep = 9, september = 9, sept = 9,
		oct = 10, october = 10,
		oct = 10, october = 10,
		nov = 11, november = 11,
		nov = 11, november = 11,
		dec = 12, december = 12,
		dec = 12, december = 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('&nbsp;', ' '):gmatch('%S+') do
	for item in text:gsub(',', ' '):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)
		-- Return date a filled, if necessary, with month and/or day from date b.
		-- Return date a filled, if necessary, with month and/or day from date b.
		-- The filled day is truncated to fit the number of days in the month.
		-- The filled day is truncated to fit the number of days in the month.
		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
			a = Date(a, {
			a = Date(a, {
				month = fillmonth,
				month = fillmonth,
				day = math.min(fillday or a.day, days_in_month(a.year, fillmonth or a.month, a.calendar))
				day = math.min(fillday or a.day, days_in_month(a.year, fillmonth or a.month, a.calendar))
			})
			})
		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, 'Julian') == Date(-4713, 11, 24, 'Gregorian') is true.
	-- Date(-4712, 1, 1, 'Julian') == Date(-4713, 11, 24, 'Gregorian') 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 == 'Julian'
			return lhs.calendar == 'Julian'
		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 = 'Julian', gregorian = 'Gregorian' }
	local calendars = { julian = 'Julian', gregorian = 'Gregorian' }
	local newdate = {
	local newdate = {
		_id = uniq,
		_id = uniq,
		calendar = 'Gregorian',  -- default is Gregorian calendar
		calendar = 'Gregorian',  -- 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 = 'Gregorian'  -- ignore any given calendar name
		newdate.calendar = 'Gregorian'  -- 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' or code == 'M' or code == 's' then
	if code == 'dh' or code == 'dhm' or code == 'dhms' or code == 'h' or code == 'hm' or code == 'hms' or code == 'M' or code == 's' 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
			elseif code == 'M' then
			elseif code == 'M' then
				if S >= 30 then
				if S >= 30 then
					M = M + 1
					M = M + 1
				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
		elseif code == 'M' or code == 's' then
		elseif code == 'M' or code == 's' then
			M = hours * 60 + M
			M = hours * 60 + M
			if code == 'M' then
			if code == 'M' then
				return M
				return M
			end
			end
			return M * 60 + S
			return M * 60 + S
		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,
		months = months,
		days = days,
		days = days,
		hours = H,
		hours = H,
		minutes = M,
		minutes = M,
		seconds = S,
		seconds = S,
		isnegative = isnegative,
		isnegative = isnegative,
		iszero = iszero,
		iszero = iszero,
		age = _diff_age,
		age = _diff_age,
		duration = _diff_duration,
		duration = _diff_duration,
	}, diffmt)
	}, 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,
}
}