Difference between revisions of "Module:Age"

From annadreambrush.com/wiki
Jump to navigation Jump to search
imported>Dovid
(Change order of calculation of is_leap_year for tine efficiency tweak (tested with some manual code returns), and some if/else's on mutually exclusive combinations of neg year/month/day (tested with :Australian_Labor_Party))
imported>Salvidrim!
m (clarify)
Line 4: Line 4:
 
         {{Gregorian serial date}}      gsd_ymd
 
         {{Gregorian serial date}}      gsd_ymd
 
Calendar functions will be needed in many areas, so this may be superseded
 
Calendar functions will be needed in many areas, so this may be superseded
by some other system, perhaps using PHP functions accessed via mw.
+
by some other system, perhaps using PHP functions accessed via mediawiki.
 
]]
 
]]
  

Revision as of 05:59, 3 December 2013

Documentation for this module may be created at Module:Age/doc

--[[ Code for some date functions, including implementations of:
        {{Age in days}}                 age_days
        {{Age in years and months}}     age_ym
        {{Gregorian serial date}}       gsd_ymd
Calendar functions will be needed in many areas, so this may be superseded
by some other system, perhaps using PHP functions accessed via mediawiki.
]]

local MINUS = '−'  -- Unicode U+2212 MINUS SIGN

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)
    -- this uses an interesting trick of Lua:
    --  * and reurns false if the first argument is false, and the second otherwise, so (number==1) and singular returns singular if its 1, returns false if it is only 1
    --  * or returns the first argument if it is not false, and the second argument if the first is false
    --  * so, if number is 1, and evaluates (true and singular) returning (singular); or evaluates (singular or plural), finds singular non-false, and returns singular
    --  * but, if number is not 1, and evaluates (false and singular) returning (false); or evaluates (false or plural), and is forced to return plural
end

local function strip_to_nil(text)
    -- If text is a non-blank string, return its content with no leading
    -- or trailing whitespace.
    -- Otherwise return nil (a nil or empty string argument gives a nil
    -- result, as does a string argument of only whitespace).
    if type(text) == 'string' then
        local result = text:match("^%s*(.-)%s*$")
        if result ~= '' then
            return result
        end
    end
    return nil
end

local function is_leap_year(year)
    -- Return true if year is a leap year, assuming Gregorian calendar.
    return year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0)
end

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

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

local function date_component(named, positional, component)
    -- Return the first of the two arguments that is not nil and is not empty.
    -- If both are nil, return the current date component, if specified.
    -- The returned value is nil or is a number.
    -- This translates empty arguments passed to the template to nil, and
    -- optionally replaces a nil argument with a value from the current date.
    named = strip_to_nil(named)
    if named then
        return tonumber(named)
    end
    positional = strip_to_nil(positional)
    if positional then
        return tonumber(positional)
    end
    if component then
        return current[component]
    end
    return nil
end

local function gsd(year, month, day)
    -- Return the Gregorian serial day (an integer >= 1) for the given date,
    -- or return nil if the date is invalid (only check that year >= 1).
    -- This is the number of days from the start of 1 AD (there is no year 0).
    -- This code implements the logic in [[Template:Gregorian serial date]].
    if year < 1 then
        return nil
    end
    local floor = math.floor
    local days_this_year = (month - 1) * 30.5 + day
    if month > 2 then
        if is_leap_year(year) then
            days_this_year = days_this_year - 1
        else
            days_this_year = days_this_year - 2
        end
        if month > 8 then
            days_this_year = days_this_year + 0.9
        end
    end
    days_this_year = floor(days_this_year + 0.5)
    year = year - 1
    local days_from_past_years = year * 365
        + floor(year / 4)
        - floor(year / 100)
        + floor(year / 400)
    return days_from_past_years + days_this_year
end

local Date = {
    -- A naive date that assumes the Gregorian calendar always applied.
    year = 0,   -- 1 to 9999 (0 if never set)
    month = 1,  -- 1 to 12
    day = 1,    -- 1 to 31
    isvalid = false,
    new = function (self, o)
        o = o or {}
        setmetatable(o, self)
        self.__index = self
        return o
    end
}

function Date:__lt(rhs)
    -- Return true if self < rhs.
    if self.year < rhs.year then
        return true
    elseif self.year == rhs.year then
        if self.month < rhs.month then
            return true
        elseif self.month == rhs.month then
            return self.day < rhs.day
        end
    end
    return false
    -- probably simplify to return (self.year < rhs.year) or ((self.year == rhs.year) and ((self.month < rhs.month) or ((self.month == rhs.month) and (self.day < rhs.day))))
    -- would be just as efficient, as lua does not evaluate second argument of (true or second_argument)
    -- or similarly return self.year < rhs.year ? true : self.year > rhs.year ? false : self.month < rhs.month ? true : self.month > rhs.month ? false : self.day < rhs.day
end

function Date:set_current()
    -- Set date from current time (UTC) and return self.
    self.year = current.year
    self.month = current.month
    self.day = current.day
    self.isvalid = true
    return self
end

function Date:set_ymd(y, m, d)
    -- Set date from year, month, day (strings or numbers) and return self.
    -- LATER: If m is a name like "March" or "mar", translate it to a month.
    y = tonumber(y)
    m = tonumber(m)
    d = tonumber(d)
    if type(y) == 'number' and type(m) == 'number' and type(d) == 'number' then
        self.year = y
        self.month = m
        self.day = d
        self.isvalid = (1 <= y and y <= 9999 and 1 <= m and m <= 12 and
                        1 <= d and d <= days_in_month(y, m))
    end
    return self
end

local DateDiff = {
    -- Simple difference between two dates, assuming Gregorian calendar.
    isnegative = false,  -- true if second date is before first
    years = 0,
    months = 0,
    days = 0,
    new = function (self, o)
        o = o or {}
        setmetatable(o, self)
        self.__index = self
        return o
    end
}

function DateDiff:set(date1, date2)
    -- Set difference between the two dates, and return self.
    -- Difference is negative if the second date is older than the first.
    local isnegative
    if date2 < date1 then
        isnegative = true
        date1, date2 = date2, date1
    else
        isnegative = false
    end
    -- It is known that date1 <= date2.
    local y1, m1, d1 = date1.year, date1.month, date1.day
    local y2, m2, d2 = date2.year, date2.month, date2.day
    local years, months, days = y2 - y1, m2 - m1, d2 - d1
    if days < 0 then
        days = days + days_in_month(y1, m1)
        months = months - 1
    end
    if months < 0 then
        months = months + 12
        years = years - 1
    end
    self.years, self.months, self.days, self.isnegative = years, months, days, isnegative
    return self
end

function DateDiff:age_ym()
    -- Return text specifying difference in years, months.
    local sign = self.isnegative and MINUS or ''
    local mtext = number_name(self.months, 'month')
    local result
    if self.years > 0 then
        local ytext = number_name(self.years, 'year')
        if self.months == 0 then
            result = ytext
        else
            result = ytext .. ',&nbsp;' .. mtext
        end
    else
        if self.months == 0 then
            sign = ''
        end
        result = mtext
    end
    return sign .. result
end

local function error_wikitext(text)
    -- Return message for display when template parameters are invalid.
    local prefix = '[[Module talk:Age|Module error]]:'
    local cat = '[[Category:Age error]]'
    return '<span style="color:black; background-color:pink;">' ..
            prefix .. ' ' .. text .. cat .. '</span>'
end

local function age_days(frame)
    -- Return age in days between two given dates, or
    -- between given date and current date.
    -- This code implements the logic in [[Template:Age in days]].
    -- Like {{Age in days}}, a missing argument is replaced from the current
    -- date, so can get a bizarre mixture of specified/current y/m/d.
    local args = frame:getParent().args
    local year1  = date_component(args.year1 , args[1], 'year' )
    local month1 = date_component(args.month1, args[2], 'month')
    local day1   = date_component(args.day1  , args[3], 'day'  )
    local year2  = date_component(args.year2 , args[4], 'year' )
    local month2 = date_component(args.month2, args[5], 'month')
    local day2   = date_component(args.day2  , args[6], 'day'  )
    local gsd1 = gsd(year1, month1, day1)
    local gsd2 = gsd(year2, month2, day2)
    if gsd1 and gsd2 then
        local sign = ''
        local result = gsd2 - gsd1
        if result < 0 then
            sign = MINUS
            result = -result
        end
        return sign .. tostring(result)
    end
    return error_wikitext('Cannot handle dates before the year 1 AD')
end

local function age_ym(frame)
    -- Return age in years and months between two given dates, or
    -- between given date and current date.
    local args = frame:getParent().args
    local fields = {}
    for i = 1, 6 do
        fields[i] = strip_to_nil(args[i])
    end
    local date1, date2
    if fields[1] and fields[2] and fields[3] then
        date1 = Date:new():set_ymd(fields[1], fields[2], fields[3])
    end
    if not (date1 and date1.isvalid) then
        return error_wikitext('Need date: year, month, day')
    end
    if fields[4] and fields[5] and fields[6] then
        date2 = Date:new():set_ymd(fields[4], fields[5], fields[6])
        if not date2.isvalid then
            return error_wikitext('Second date should be year, month, day')
        end
    else
        date2 = Date:new():set_current()
    end
    return DateDiff:new():set(date1, date2):age_ym()
end

local function gsd_ymd(frame)
    -- Return Gregorian serial day of the given date, or the current date.
    -- Like {{Gregorian serial date}}, a missing argument is replaced from the
    -- current date, so can get a bizarre mixture of specified/current y/m/d.
    -- This accepts positional arguments, although the original template does not.
    local args = frame:getParent().args
    local year  = date_component(args.year , args[1], 'year' )
    local month = date_component(args.month, args[2], 'month')
    local day   = date_component(args.day  , args[3], 'day'  )
    local result = gsd(year, month, day)
    if result then
        return tostring(result)
    end
    return error_wikitext('Cannot handle dates before the year 1 AD')
end

return { age_days = age_days, age_ym = age_ym, gsd = gsd_ymd }