Module:Date: Difference between revisions

    (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' },
    X = { fmt = '%02d:%02d:%02d', field = 'hour', special = 'hms' },
    ['-d'] = { fmt = '%d', field = 'day' },
    ['-m'] = { fmt = '%d', field = 'month' },
    ['-Y'] = { fmt = '%d', field = 'year' },
    ['-j'] = { fmt = '%d', field = 'doy' },
    }
    }
    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 == 'hms' then
    return string.format(fmt, value, date.minute, date.second)
    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') 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
    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)     current date with given time
    Date('currentdate', H, M, S)       current date with given time
    Date('1 April 1995', 'julian')     parse date from text
    Date('1 April 1995', 'julian')     parse date from text
    Date('1 April 1995 AD', 'julian')  AD, CE, BC, BCE (using one of these sets a flag to do same for output)
    Date('1 April 1995 AD', 'julian')  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()
    local datetext
    local argtype
    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 > 0 then
    if not (numbers.n == 0 and
    set_date_from_numbers(result,
    extract_date(datetext))) then
    return {}
    return {}
    end
    end
    -- TODO Parse datetext to extract y,m,d,H,M,S.
    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 (3 <= numbers.n and numbers.n <= 6) then
    if not set_date_from_numbers(result, numbers) then
    return {}
    return {}
    end
    end
    local y, m, d = numbers[1], numbers[2], numbers[3]
    if not (-9999 <= y and y <= 9999 and 1 <= m and m <= 12 and
    1 <= d and d <= days_in_month(y, m, result.calname)) then
    return {}
    end
    local H = numbers[4]
    if H then
    result.hastime = true
    else
    H = 0
    end
    local M = numbers[5] or 0
    local S = numbers[6] or 0
    if not (0 <= H and H <= 23 and
    0 <= M and M <= 59 and
    0 <= S and S <= 59) then
    return {}
    end
    result.year = y    -- -9999 to 9999; '1 BC' → year = 0; 'n BC' → year = 1 - n
    result.month = m  -- 1 to 12
    result.day = d    -- 1 to 31
    result.hour = H    -- 0 to 59
    result.minute = M  -- 0 to 59
    result.second = S  -- 0 to 59
    result.isvalid = true
    else
    else
    return {}
    return {}