Module:Date: Difference between revisions
tweak formatted output; extract_date to parse an input date string
(formatted output and strftime) |
(tweak formatted output; extract_date to parse an input date string) |
||
Line 152: | Line 152: | ||
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) | ||
end | |||
local function set_date_from_numbers(date, numbers, options) | |||
-- Set the fields of table date from numeric values. | |||
-- Return true if date is valid. | |||
if type(numbers) ~= 'table' then | |||
return | |||
end | |||
local y = numbers.y or numbers[1] | |||
local m = numbers.m or numbers[2] | |||
local d = numbers.d or numbers[3] | |||
local H = numbers.H or numbers[4] | |||
local M = numbers.M or numbers[5] or 0 | |||
local S = numbers.S or numbers[6] or 0 | |||
if not (y and m and d) then | |||
return | |||
end | |||
if not (-9999 <= y and y <= 9999 and 1 <= m and m <= 12 and | |||
1 <= d and d <= days_in_month(y, m, date.calname)) then | |||
return | |||
end | |||
if H then | |||
date.hastime = true | |||
else | |||
H = 0 | |||
end | |||
if not (0 <= H and H <= 23 and | |||
0 <= M and M <= 59 and | |||
0 <= S and S <= 59) then | |||
return | |||
end | |||
date.year = y -- -9999 to 9999 ('n BC' → year = 1 - n) | |||
date.month = m -- 1 to 12 | |||
date.day = d -- 1 to 31 | |||
date.hour = H -- 0 to 59 | |||
date.minute = M -- 0 to 59 | |||
date.second = S -- 0 to 59 | |||
date.isvalid = true | |||
if type(options) == 'table' then | |||
for _, k in ipairs({ 'am', 'era' }) do | |||
if options[k] then | |||
date.options[k] = options[k] | |||
end | |||
end | |||
end | |||
return true | |||
end | end | ||
Line 177: | Line 223: | ||
return '(invalid)' | return '(invalid)' | ||
end | end | ||
local shortcuts = { | |||
['%c'] = '%-I:%M %p %-d %B %Y%{era}', -- date and time: 2:30 pm 1 April 2016 | |||
['%x'] = '%-d %B %Y%{era}', -- date: 1 April 2016 | |||
['%X'] = '%-I:%M %p', -- time: 2:30 pm | |||
} | |||
local codes = { | local codes = { | ||
a = { field = 'dayabbr' }, | a = { field = 'dayabbr' }, | ||
Line 184: | Line 235: | ||
u = { fmt = '%d' , field = 'dowiso' }, | u = { fmt = '%d' , field = 'dowiso' }, | ||
w = { fmt = '%d' , field = 'dow' }, | w = { fmt = '%d' , field = 'dow' }, | ||
d = { fmt = '%02d', field = 'day' }, | d = { fmt = '%02d', fmt2 = '%d', field = 'day' }, | ||
m = { fmt = '%02d', field = 'month' }, | m = { fmt = '%02d', fmt2 = '%d', field = 'month' }, | ||
Y = { fmt = '%04d', field = 'year' }, | Y = { fmt = '%04d', fmt2 = '%d', field = 'year' }, | ||
H = { fmt = '%02d', field = 'hour' }, | H = { fmt = '%02d', fmt2 = '%d', field = 'hour' }, | ||
M = { fmt = '%02d', field = 'minute' }, | M = { fmt = '%02d', fmt2 = '%d', field = 'minute' }, | ||
S = { fmt = '%02d', field = 'second' }, | S = { fmt = '%02d', fmt2 = '%d', field = 'second' }, | ||
j = { fmt = '%03d', field = 'doy' }, | j = { fmt = '%03d', fmt2 = '%d', field = 'doy' }, | ||
I = { fmt = '%02d', 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 or date.options) | options = make_option_table(options or date.options) | ||
local amopt = options.am | local amopt = options.am | ||
local eraopt = options.era | local eraopt = options.era | ||
local function replace_code(id) | local function replace_code(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 | |||
fmt = code.fmt2 | |||
end | |||
local value = date[code.field] | local value = date[code.field] | ||
local special = code.special | local special = code.special | ||
Line 212: | Line 261: | ||
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 = ({ | ||
Line 258: | Line 305: | ||
-- This occurs, for example, if id is the name of a function. | -- This occurs, for example, if id is the name of a function. | ||
return nil | return nil | ||
end | |||
if shortcuts[format] then | |||
format = shortcuts[format] | |||
end | end | ||
local PERCENT = '\127PERCENT\127' | local PERCENT = '\127PERCENT\127' | ||
Line 263: | Line 313: | ||
:gsub('%%%%', PERCENT) | :gsub('%%%%', PERCENT) | ||
:gsub('%%{(%w+)}', replace_property) | :gsub('%%{(%w+)}', replace_property) | ||
:gsub('%%(-?%a)', replace_code) | :gsub('%%(-?)(%a)', replace_code) | ||
:gsub(PERCENT, '%%') | :gsub(PERCENT, '%%') | ||
) | ) | ||
Line 402: | Line 452: | ||
['C.E.'] = { 'B.C.E.', 'C.E.' }, | ['C.E.'] = { 'B.C.E.', 'C.E.' }, | ||
} | } | ||
local function extract_date(text) | |||
-- Parse the date/time in text and return n, o where | |||
-- n = table of numbers with date/time fields | |||
-- o = table of options for AM/PM or AD/BC, if any | |||
-- or return nothing if date is known to be invalid. | |||
-- Caller determines if the values in n are valid. | |||
-- Dates of form d/m/y, m/d/y, y/m/d are rejected as ambiguous and undesirable. | |||
local date, options = {}, {} | |||
local function extract_ymd(item) | |||
local ystr, mstr, dstr = item:match('^(%d%d%d%d)-(%w+)-(%d%d?)$') | |||
if ystr then | |||
local m | |||
if mstr:match('^%d%d?$') then | |||
m = tonumber(mstr) | |||
else | |||
m = month_number(mstr) | |||
end | |||
if m then | |||
date.y = tonumber(ystr) | |||
date.m = m | |||
date.d = tonumber(dstr) | |||
return true | |||
end | |||
end | |||
end | |||
local function extract_month(item) | |||
-- A month must be given as a name or abbreviation; a number would be ambiguous. | |||
local m = month_number(item) | |||
if m then | |||
date.m = m | |||
return true | |||
end | |||
end | |||
local function extract_time(item) | |||
local h, m, s = item:match('^(%d%d?):(%d%d)(:?%d*)$') | |||
if date.H or not h then | |||
return | |||
end | |||
if s ~= '' then | |||
s = s:match('^:(%d%d)$') | |||
if not s then | |||
return | |||
end | |||
end | |||
date.H = tonumber(h) | |||
date.M = tonumber(m) | |||
date.S = tonumber(s) -- nil if empty string | |||
return true | |||
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 index_time | |||
local function set_ampm(item) | |||
local H = date.H | |||
if H and not options.am and index_time + 1 == item_count then | |||
options.am = ampm_options[item] | |||
if item:match('^[Aa]') then | |||
if not (1 <= H and H <= 12) then | |||
return | |||
end | |||
if H == 12 then | |||
date.H = 0 | |||
end | |||
else | |||
if not (1 <= H and H <= 23) then | |||
return | |||
end | |||
if H <= 11 then | |||
date.H = H + 12 | |||
end | |||
end | |||
return true | |||
end | |||
end | |||
for item in text:gsub(',', ' '):gmatch('%S+') do | |||
-- Accept options in peculiar places; if duplicated, last wins. | |||
item_count = item_count + 1 | |||
if era_text[item] then | |||
options.era = item | |||
elseif ampm_options[item] then | |||
if not set_ampm(item) then | |||
return | |||
end | |||
elseif item:find(':', 1, true) then | |||
if not extract_time(item) then | |||
return | |||
end | |||
index_time = item_count | |||
elseif date.d and date.m then | |||
if date.y then | |||
return -- should be nothing more so item is invalid | |||
end | |||
if not item:match('^(%d%d?%d?%d?)$') then | |||
return | |||
end | |||
date.y = tonumber(item) | |||
elseif date.d then | |||
if not extract_month(item) then | |||
return | |||
end | |||
elseif date.m then | |||
if not item:match('^(%d%d?)$') then | |||
return | |||
end | |||
date.d = tonumber(item) | |||
elseif not extract_ymd(item) then | |||
if item:match('^(%d%d?)$') then | |||
date.d = tonumber(item) | |||
elseif not extract_month(item) then | |||
return | |||
end | |||
end | |||
end | |||
return date, options | |||
end | |||
-- Metatable for some operations on dates. | -- Metatable for some operations on dates. | ||
Line 469: | Line 644: | ||
--[[ Examples of syntax to construct a date: | --[[ Examples of syntax to construct a date: | ||
Date(y, m, d, 'julian') | 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 | ||
Line 475: | Line 650: | ||
Date('currentdatetime') | Date('currentdatetime') | ||
LATER: Following are not yet implemented: | LATER: Following are not yet implemented: | ||
Date('currentdate', H, M, S) | Date('currentdate', H, M, S) current date with given time | ||
Date('1 April 1995', 'julian') | Date('1 April 1995', 'julian') parse date from text | ||
Date('1 April 1995 AD', 'julian') AD, CE, BC, BCE (using one of these sets a flag to do same for output) | Date('1 April 1995 AD', 'julian') AD, CE, BC, BCE (using one of these sets a flag to do same for output) | ||
Date('04:30:59 1 April 1995', 'julian') | Date('04:30:59 1 April 1995', 'julian') | ||
Line 500: | Line 675: | ||
text = date_text, | text = date_text, | ||
} | } | ||
local argtype, datetext | |||
local numbers = collection() | local numbers = collection() | ||
for _, v in ipairs({...}) do | for _, v in ipairs({...}) do | ||
v = strip_to_nil(v) | v = strip_to_nil(v) | ||
Line 546: | Line 720: | ||
end | end | ||
if argtype == 'datetext' then | if argtype == 'datetext' then | ||
if numbers.n | if not (numbers.n == 0 and | ||
set_date_from_numbers(result, | |||
extract_date(datetext))) then | |||
return {} | return {} | ||
end | end | ||
elseif argtype == 'juliandate' then | elseif argtype == 'juliandate' then | ||
if numbers.n == 1 then | if numbers.n == 1 then | ||
Line 570: | Line 745: | ||
result.isvalid = true | result.isvalid = true | ||
elseif argtype == 'setdate' then | elseif argtype == 'setdate' then | ||
if not ( | if not set_date_from_numbers(result, numbers) then | ||
return {} | return {} | ||
end | end | ||
else | else | ||
return {} | return {} |