FANDOM


--Copied then built on http://en.wikipedia.org/wiki/Module:Age
 
--Here's where you adjust for language differences. Hopefully the rest of the code can remain unchanged.
local SECONDword = 'second'
local SECONDSword = 'seconds'
local MINUTEword = 'minute'
local MINUTESword = 'minutes'
local DAYword = 'day'
local DAYSword = 'days'
local WEEKword = 'week'
local WEEKSword = 'weeks'
local MONTHword1 = 'month' --For some reason it was having issues with plain MONTHword that are fixed when it's something else?
local MONTHSword = 'months'
local YEARword = 'year'
local YEARSword = 'years'
 
local DECIMALword = '.'
local THOUSANDSSEPARATOR = ','
 
--local BIRTHDAYphrase = '#MONTH# #DAY#, #YEAR# (age #AGE#)'
local BIRTHDAYphrase = '#YEAR#年#MONTH#月#DAY#日(#AGE#歳)'
 
local MONTHword = {}
MONTHword[1] = 'January'
MONTHword[2] = 'February'
MONTHword[3] = 'March'
MONTHword[4] = 'April'
MONTHword[5] = 'May'
MONTHword[6] = 'June'
MONTHword[7] = 'July'
MONTHword[8] = 'August'
MONTHword[9] = 'September'
MONTHword[10] = 'October'
MONTHword[11] = 'November'
MONTHword[12] = 'December'
 
 
--[[ 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 decimalfloor(somenumber, decimals)
    return math.floor(somenumber * 10^decimals) / 10^decimals
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)
    -- 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 default year/month/day (UTC), but only if needed.
local current = setmetatable({}, {
        __index = function (self, key)
            --local d = os.date('!*t')
            --local d = os.date('!*t', os.time()+32400) --Additional seconds changes it from UTC to JST current.
            self.year = 1
            self.month = 1
            self.day = 1
            self.hour = 0
            self.min = 0
            self.sec = 0
            return rawget(self, key)
        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')
            local d = os.date('!*t', os.time()+32400) --Additional seconds changes it from UTC to JST current.
            self.year = d.year
            self.month = d.month
            self.day = d.day
            self.hour = d.hour
            self.min = d.min
            self.sec = d.sec
            return rawget(self, key)
        end
    })
 
local function date_componentdefault(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 default[component]
    end
    return nil
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 function gss(year, month, day, hour, min, sec) --Gregorian Serial Second
    return gsd(year, month, day)*86400 + hour*3600 + min*60 + sec
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
    hour = 0,   -- 0 to 23
    min = 0,    -- 0 to 59
    sec = 0,    -- 0 to 59
    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
            if self.day < rhs.day then
                return true
            elseif self.day == rhs.day then
                if self.hour < rhs.hour then
                    return true
                elseif self.hour == rhs.hour then
                    if self.min < rhs.min then
                        return true
                    elseif self.minute == rhs.minute then
                        return self.sec < rhs.sec
                    end
                end
            end
        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_default()
    -- Assume midnight January 1, 1 AD for "working" date.
    self.year = 1
    self.month = 1
    self.day = 1
    self.hour = 0
    self.min = 0
    self.sec = 0
    self.isvalid = true
    return self
end
 
function Date:set_current()
    -- Set date from current time (UTC) and return self.
    -- Usually for the "now" date.
    self.year = current.year
    self.month = current.month
    self.day = current.day
    self.hour = current.hour
    self.min = current.min
    self.sec = current.sec
    self.isvalid = true
    return self
end
 
function Date:set_ymd(y, m, d, hour, min, sec)
    -- 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)
    hour = tonumber(hour)
    min = tonumber(min)
    sec = tonumber(sec)
 
    if type(y) == 'number' and type(m) == 'number' and type(d) == 'number' and type(hour) == 'number' and type(min) == 'number' and type(sec) == 'number' then
        self.year = y
        self.month = m
        self.day = d
        self.hour = hour
        self.min = min
        self.sec = sec
        self.isvalid = (1 <= y and y <= 9999 and 1 <= m and m <= 12 and
                        1 <= d and d <= days_in_month(y, m) and
                        0 <= hour and hour <=23 and
                        0 <= min and min <= 59 and
                        0 <= sec and sec <= 59)
    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,
    hours = 0,
    minutes = 0,
    seconds = 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, h1, min1, s1 = date1.year, date1.month, date1.day, date1.hour, date1.min, date1.sec
    local y2, m2, d2, h2, min2, s2 = date2.year, date2.month, date2.day, date2.hour, date2.min, date2.sec
    local years, months, days, hours, minutes, seconds = y2 - y1, m2 - m1, d2 - d1, h2-h1, min2-min1, s2-s1
 
    --local gss1 = gss(date1.year, date1.month, date1.day, date1.hour, date1.min, date1.sec)
    --local gss2 = gss(date2.year, date2.month, date2.day, date2.hour, date2.min, date2.sec)
 
    if seconds < 0 then
        seconds = seconds + 60
        minutes = minutes - 1
    end
    if minutes < 0 then
        minutes = minutes + 60
        hours = hours - 1
    end
    if hours < 0 then
        hours = hours + 24
        days = days - 1
    end
    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
 
    --finalyear and finalmonth needed for fractional month/year things
    endyear = date1.year + years
    endmonth = date1.month + months
    if endmonth > 12 then
        endmonth = endmonth - 12
        endyear = endyear + 1
    end
 
    self.years, self.months, self.days, self.hours, self.minutes, self.seconds, self.isnegative, self.endyear, self.endmonth, self.date1, self.date2 = years, months, days, hours, minutes, seconds, isnegative, endyear, endmonth, date1, date2
    return self
end
 
function DateDiff:age_ym(decimals)
    -- Return text specifying difference in years, months.
    local sign = self.isnegative and MINUS or ''
    decimals = decimals or 0
 
    --if 0 < decimals then
        extraseconds = self.days*86400 + self.hours*3600 + self.minutes*60 + self.seconds
        --To know the length of the month, we need to know the year and month--which DateDiff doesn't currently hold.
        monthseconds = days_in_month(self.endyear, self.endmonth)*86400
        --monthseconds = 3000000
        decimalpart = extraseconds / monthseconds
        self.months = self.months + decimalpart
        self.months = decimalfloor(self.months,decimals)
    --end
 
    local mtext = number_name(self.months, MONTHword1, MONTHSword)
    local result
    if self.years > 0 then
        local ytext = number_name(self.years, YEARword, YEARSword)
        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
 
function DateDiff:age_ymd(decimals)
    -- Return text specifying difference in years, months, days.
    local sign = self.isnegative and MINUS or ''
    decimals = decimals or 0
 
    --if decimals > 0 then
        extraseconds = self.hours*3600 + self.minutes*60 + self.seconds
        decimalpart = extraseconds/86400
        self.days = self.days + decimalpart
        self.days = decimalfloor(self.days, decimals)
    --end
 
    local result = number_name(self.years, YEARword, YEARSword) .. ', ' .. number_name(self.months, MONTHword1, MONTHSword) .. ', ' .. number_name(self.days, DAYword, DAYSword) --If we want all three displayed regardless, no need to check if any of them are 0.
 
    return sign .. result
end
 
function DateDiff:age_months(decimals)
    -- Return text specifying difference in years, months, days.
    local sign = self.isnegative and MINUS or ''
    decimals = decimals or 0
 
    self.months = self.months + self.years*12
 
    --if decimals > 0 then
        extraseconds = self.hours*3600 + self.minutes*60 + self.seconds
        decimalpart = extraseconds/86400
        self.days = self.days + decimalpart
        self.days = decimalfloor(self.days, decimals)
    --end
 
    local result = number_name(self.months, MONTHword1, MONTHSword)
 
    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_seconds(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 hour1  = date_component(args.hour1 , args[4], 'hour' )
    local min1   = date_component(args.min1  , args[5], 'min'  )
    local sec1   = date_component(args.sec1  , args[6], 'sec'  )
 
    local year2  = date_component(args.year2 , args[7], 'year' )
    local month2 = date_component(args.month2, args[8], 'month')
    local day2   = date_component(args.day2  , args[9], 'day'  )
    local hour2  = date_component(args.hour2 , args[10],'hour' )
    local min2   = date_component(args.min2  , args[11],'min'  )
    local sec2   = date_component(args.sec2  , args[12],'sec'  )
 
 
    local gss1 = gss(year1, month1, day1, hour1, min1, sec1)
    local gss2 = gss(year2, month2, day2, hour2, min2, sec2)
    if gss1 and gss2 then
        local sign = ''
        local result = gss2 - gss1
        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_years_internal(year1, month1, day1,  year2, month2, day2, decimals)
    year1 = year1 or current.year
    month1 = month1 or current.month
    day1 = day1 or current.day
    year2 = year2 or current.year
    month2 = month2 or current.month
    day2 = day2 or current.day
    decimals = decimals or 0
 
    local yearinteger = year2 - year1
    if (month2<month1) or (month2==month1 and day2<day1) then yearinteger = yearinteger - 1 end
 
    local yeardecimal = 0
 
    if (month2==month1 and day2==day1) or decimals==0 then
 
    else
        local gsd1 = gsd(year1 + yearinteger, month1, day1)
        local gsd2 = gsd(year2, month2, day2)
        local gsd3 = gsd(year1 + yearinteger + 1, month1, day1)
        yeardecimal = (gsd2-gsd1)/(gsd3-gsd1) --Days in part year / days in full year
        yeardecimal = math.floor(yeardecimal * 10^decimals) / 10^decimals --So it cuts off excess decimal info rather than rounding in the next step.
    end
 
    years = string.format("%."..decimals.."f", yearinteger+yeardecimal )
    years = string.gsub(years, "%.", DECIMALword) --In case it's for a language where commas or something are used.
 
    return years
end
 
local function age_years(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 decimals = args.dec or 0
 
    return age_years_internal(year1, month1, day1, year2, month2, day2, decimals)
    --return 'age_years_internal('..year1..', '..month1..', '..day1..', '..year2..', '..month2..', '..day2..', '..decimals..')'
    --return gsd2(year1, month1, day1)
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 decimals = args.dec or 0
    local fields = {}
    for i = 1, 12 do
        fields[i] = strip_to_nil(args[i])
    end
    local date1, date2
    if fields[1] and fields[2] and fields[3] and fields[4] and fields[5] and fields[6] then
        date1 = Date:new():set_ymd(fields[1], fields[2], fields[3], fields[4], fields[5], fields[6])
    end
    if not (date1 and date1.isvalid) then
        return error_wikitext('Need date: year, month, day, hour, min, sec')
    end
    if fields[7] and fields[8] and fields[9] and fields[10] and fields[11] and fields[12] then
        date2 = Date:new():set_ymd(fields[7], fields[8], fields[9], fields[10], fields[11], fields[12])
        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(decimals)
end
 
local function age_ymd(frame)
    -- Return age in years, months, and days between two given dates, or
    -- between given date and current date.
    local args = frame:getParent().args
    local decimals = args.dec or 0
    local fields = {}
    for i = 1, 12 do
        fields[i] = strip_to_nil(args[i])
    end
    local date1, date2
    if fields[1] and fields[2] and fields[3] and fields[4] and fields[5] and fields[6] then
        date1 = Date:new():set_ymd(fields[1], fields[2], fields[3], fields[4], fields[5], fields[6])
    end
    if not (date1 and date1.isvalid) then
        return error_wikitext('Need date: year, month, day, hour, min, sec')
    end
    if fields[7] and fields[8] and fields[9] and fields[10] and fields[11] and fields[12] then
        date2 = Date:new():set_ymd(fields[7], fields[8], fields[9], fields[10], fields[11], fields[12])
        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_ymd(decimals)
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
 
local function gss_template(frame)
    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 hour  = date_component(args.hour , args[4], 'hour' )
    local min   = date_component(args.min  , args[5], 'min'  )
    local sec   = date_component(args.sec  , args[6], 'sec'  )
 
    local result = gss(year, month, day, hour, min, sec)
    if result then
        return tostring(result)
    end
end
 
local function birthday(year, month, day, decimals)
    year = year + 0
    month = month + 0
    day = day + 0
    decimals = decimals or 0
 
    local displayphrase = BIRTHDAYphrase
    displayphrase = string.gsub(displayphrase, '#YEAR#', year)
    displayphrase = string.gsub(displayphrase, '#MONTH#', MONTHword[month])
    displayphrase = string.gsub(displayphrase, '#DAY#', day)
    displayage = age_years_internal(year, month, day, false, false, false, decimals)
    displayphrase = string.gsub(displayphrase, '#AGE#', displayage)
 
    return displayphrase
end
 
 
local function birthday_template(frame)
    local args = frame:getParent().args
    local year = args.year or args[1]
    local month = args.month or args[2]
    local day = args.day or args[3]
    local decimals = args.dec or args[4] or 0
 
    --return "FISH"
    --return year .. " " .. month .. " " .. day .. " " .. decimals
    return birthday(year, month, day, decimals)
end
 
return { birthday_template = birthday_template, age_days = age_days, age_seconds = age_seconds, age_years = age_years, age_ym = age_ym, age_ymd = age_ymd, gsd = gsd_ymd, gss=gss_template }