Jump to content

Module:Date: Difference between revisions

686 bytes added ,  8 years ago
rework date differences for more consistent years/months/days; differences include hours/minutes/seconds; can add 'date + diff'; tweaks
(major refactor with fixes; force Date to be read-only (error on write); list of dates in a month on a particular day of week)
(rework date differences for more consistent years/months/days; differences include hours/minutes/seconds; can add 'date + diff'; tweaks)
Line 3: Line 3:


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


local Date, DateDiff, diffmt  -- forward declarations
local Date, DateDiff, diffmt  -- forward declarations
Line 34: Line 35:


local function strip_to_nil(text)
local function strip_to_nil(text)
-- If text is a string, return its trimmed content, or nil.
-- 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 is able to pass, for example, a number).
-- 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
local function number_name(number, singular, plural, sep)
-- Return the given number, converted to a string, with the
-- separator (default space) and singular or plural name appended.
plural = plural or (singular .. 's')
sep = sep or ' '
return tostring(number) .. sep .. ((number == 1) and singular or plural)
end
end


Line 65: Line 58:
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
local function h_m_s(time)
-- Return hour, minute, second extracted from fraction of a day.
time = floor(time * 24 * 3600 + 0.5)  -- number of seconds
local second = time % 60
time = floor(time / 60)
return floor(time / 60), time % 60, second
end
local function hms(date)
-- Return fraction of a day (0 <= fraction < 1) from date's time.
return (date.hour + (date.minute + date.second / 60) / 60) / 24
end
end


Line 76: Line 82:
--    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 floor = math.floor
local offset
local offset
local a = floor((14 - date.month)/12)
local a = floor((14 - date.month)/12)
Line 88: Line 93:
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 + (date.hour + (date.minute + date.second / 60) /60) / 24 - 0.5
jd = jd + hms(date) - 0.5
return jd, jd
return jd, jd
end
end
Line 100: Line 105:
-- 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 floor = math.floor
local calname = date.calname
local calname = date.calname
local limits -- 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 == 'Julian' then
if calname == 'Gregorian' then
limits = { -1931076.5, 5373557.49999 }
low, high = -1930999.5, 5373484.49999
elseif calname == 'Gregorian' then
elseif calname == 'Julian' then
limits = { -1930999.5, 5373484.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 limits[1] <= jd and jd <= limits[2]) 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
local time = jd - jdn -- 0 <= time < 1
local hour
if time >= 0.5 then   -- if at or after midnight of next day
if time >= 0.5 then
jdn = jdn + 1
jdn = jdn + 1
time = time - 0.5
time = time - 0.5
hour = 0
else
else
hour = 12
time = time + 0.5
end
end
time = floor(time * 24 * 3600 + 0.5)  -- number of seconds after hour
date.hour, date.minute, date.second = h_m_s(time)
date.second = time % 60
time = floor(time / 60)
date.minute = time % 60
date.hour = hour + floor(time / 60)
else
else
date.second = 0
date.second = 0
Line 172: Line 170:
end
end
if H then
if H then
-- It is not possible to set M or S without also setting H.
date.hastime = true
date.hastime = true
else
else
Line 203: Line 202:
-- 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)
-- 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,
-- Similarly, era = 'BC' means 'BC' is used if year < 0.
--    and am = 'pm' has the same meaning.
-- Similarly, era = 'BC' means 'BC' is used if year <= 0.
-- BCMINUS displays a MINUS if year < 0 and the display format does not include %{era}.
-- 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.
Line 212: Line 213:
result = options1
result = options1
elseif type(options1) == 'string' then
elseif type(options1) == 'string' then
-- Example: '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+)[:=](.+)$')
Line 227: Line 228:
return result
return result
end
end
local ampm_options = {
-- lhs = input text accepted as an am/pm option
-- rhs = code used internally
['am']  = 'am',
['AM']  = 'AM',
['a.m.'] = 'a.m.',
['A.M.'] = 'A.M.',
['pm']  = 'am',  -- same as am
['PM']  = 'AM',
['p.m.'] = 'a.m.',
['P.M.'] = 'A.M.',
}


local era_text = {
local era_text = {
Line 245: Line 259:


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


Line 298: Line 312:
['AM'] = { 'AM', 'PM' },
['AM'] = { 'AM', 'PM' },
['A.M.'] = { 'A.M.', 'P.M.' },
['A.M.'] = { 'A.M.', 'P.M.' },
})[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
Line 524: Line 538:
end
end
local list = { text = _list_text }
local list = { text = _list_text }
local count = math.floor((last - first)/7) + 1
local count = floor((last - first)/7) + 1
for i = 1, count do
for i = 1, count do
list[i] = Date(date, {day = first})
list[i] = Date(date, {day = first})
Line 600: Line 614:
return true
return true
end
end
local ampm_options = {
['am']  = 'am',
['AM']  = 'AM',
['a.m.'] = 'a.m.',
['A.M.'] = 'A.M.',
['pm']  = 'am',  -- same as am
['PM']  = 'AM',
['p.m.'] = 'a.m.',
['P.M.'] = 'A.M.',
}
local item_count = 0
local item_count = 0
local index_time
local index_time
Line 697: Line 701:
local function do_days(n)
local function do_days(n)
local forcetime, jd
local forcetime, jd
if math.floor(n) == n then
if floor(n) == n then
jd = lhs.jd
jd = lhs.jd
else
else
Line 727: Line 731:
m = 0
m = 0
elseif is_prefix(id, 'months') then
elseif is_prefix(id, 'months') then
y = math.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
Line 759: Line 763:
return Date(lhs, y, m, d)
return Date(lhs, y, m, d)
end
end
end
if is_diff(rhs) then
local days = rhs.daystotal
if (is_sub or false) ~= (rhs.isnegative or false) then
days = -days
end
return lhs + days
end
end
end
end
Line 789: Line 800:
-- 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 = math.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)
Line 798: Line 809:
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 = math.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.calname)
value = is_leap_year(self.year, self.calname)
Line 941: Line 952:
newdate.hastime = true
newdate.hastime = true
end
end
elseif num ~= math.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.
Line 1,009: Line 1,020:
}
}
return setmetatable(readonly, mt)
return setmetatable(readonly, mt)
end
local function _age_ym(diff)
-- Return text specifying date difference in years, months.
local sign = diff.isnegative and MINUS or ''
local mtext = number_name(diff.months, 'month')
local result
if diff.years > 0 then
local ytext = number_name(diff.years, 'year')
if diff.months == 0 then
result = ytext
else
result = ytext .. ',&nbsp;' .. mtext
end
else
if diff.months == 0 then
sign = ''
end
result = mtext
end
return sign .. result
end
end


Line 1,042: Line 1,032:
__index = function (self, key)
__index = function (self, key)
local value
local value
if key == 'age_ym' then
if key == 'daystotal' then
value = _age_ym(self)
elseif key == 'daystotal' then
value = self.date1.jdz - self.date2.jdz
value = self.date1.jdz - self.date2.jdz
end
end
Line 1,055: Line 1,043:


function DateDiff(date1, date2)  -- for forward declaration above
function DateDiff(date1, date2)  -- for forward declaration above
-- Return a table with the difference between the 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
--    date1 = date2 + d
-- If date1 >= date2 and the dates have no H:M:S time specified then
--    date1 = date2 + (d.years..'y') + (d.months..'m') + d.days
-- where the larger time units are added first.
-- The result of Date(2015,1,x) + '1m' is Date(2015,2,28) for
-- x = 28, 29, 30, 31. That means, for example,
--    d = Date(2015,3,3) - Date(2015,1,31)
-- gives d.years, d.months, d.days = 0, 1, 3 (excluding date1).
if not (is_date(date1) and is_date(date2) and date1.calname == date2.calname) then
if not (is_date(date1) and is_date(date2) and date1.calname == date2.calname) then
return
return
Line 1,066: Line 1,063:
date1, date2 = date2, date1
date1, date2 = date2, date1
end
end
-- It is known that date1 >= date2.
-- It is known that date1 >= date2 (period is from date2 to date1).
local y1, m1 = date1.year, date1.month
local y1, m1 = date1.year, date1.month
local y2, m2 = date2.year, date2.month
local y2, m2 = date2.year, date2.month
local years, months, days = y1 - y2, m1 - m2, date1.day - date2.day
local years = y1 - y2
if days < 0 then
local months = m1 - m2
days = days + days_in_month(y2, m2, date2.calname)
local d1 = date1.day + hms(date1)
local d2 = date2.day + hms(date2)
local days, time
if d1 >= d2 then
days = d1 - d2
else
months = months - 1
months = months - 1
-- Get days in previous month (before the "to" date) given December has 31 days.
local dpm = m1 > 1 and days_in_month(y1, m1 - 1, date1.calname) or 31
if d2 >= dpm then
days = d1
else
days = dpm - d2 + d1
end
end
end
if months < 0 then
if months < 0 then
years = years - 1
months = months + 12
months = months + 12
years = years - 1
end
end
days, time = math.modf(days)
local H, M, S = h_m_s(time)
return setmetatable({
return setmetatable({
date1 = date1,
date1 = date1,
Line 1,084: Line 1,095:
months = months,
months = months,
days = days,
days = days,
hours = H,
minutes = M,
seconds = S,
isnegative = isnegative,
isnegative = isnegative,
}, diffmt)
}, diffmt)
Anonymous user
Cookies help us deliver our services. By using our services, you agree to our use of cookies.