Module:Random

-- This module contains a number of functions that make use of random numbers.

local cfg = {}

-- -- Configuration --

-- Set this to true if your wiki has a traffic rate of less than one edit every two minutes or so. -- This will prevent the same "random" number being generated many times in a row until a new edit is made -- to the wiki. This setting is only relevant if the |same= parameter is set. cfg.lowTraffic = false

-- If cfg.lowTraffic is set to true, and the |same= parameter is set, this value is used for the refresh rate of the random seed. -- This is the number of seconds until the seed is changed. Getting this right is tricky. If you set it too high, the same number -- will be returned many times in a row. If you set it too low, you may get different random numbers appearing on the same page, -- particularly for pages that take many seconds to process. cfg.seedRefreshRate = 60

-- -- End configuration --

local p = {} -- For functions available from other Lua modules. local l = {} -- For functions not available from other Lua modules, but that need to be accessed using table keys.

local yesno = require('Dev:Yesno') local makeList = require('Dev:List').makeList

-- -- Helper functions --

local function raiseError(msg) -- This helps to generate a wikitext error. It is the calling function's responsibility as to how to include it in the output. return mw.ustring.format(' Module:Random error: %s.', msg) end

-- -- random number function --

local function getBigRandom(l, u) -- Gets a random integer between l and u, and is not limited to RAND_MAX. local r = 0 local n = 2^math.random(30) -- Any power of 2. local limit = math.ceil(53 / (math.log(n) / math.log(2))) for i = 1, limit do		r = r + math.random(0, n - 1) / (n^i) end return math.floor(r * (u - l + 1)) + l end

function l.number(args) -- Gets a random number. first = tonumber(args[1]) second = tonumber(args[2]) -- This needs to use if statements as math.random won't accept explicit nil values as arguments. if first then if second then if first > second then -- Second number cannot be less than the first, or it causes an error. first, second = second, first end return getBigRandom(first, second) else return getBigRandom(1, first) end else return math.random end end

-- -- Date function --

function l.date(args) -- This function gets random dates, and takes timestamps as positional arguments. -- With no arguments specified, it outputs a random date in the current year. -- With two arguments specified, it outputs a random date between the timestamps. -- With one argument specified, the date is a random date between the unix epoch (1 Jan 1970) and the timestamp. -- The output can be formatted using the "format" argument, which works in the same way as the #time parser function. -- The default format is the standard Wikipedia timestamp. local lang = mw.language.getContentLanguage

local function getDate(format, ts) local success, date = pcall(lang.formatDate, lang, format, ts) if success then return date end end

local function getUnixTimestamp(ts) local unixts = getDate('U', ts) if unixts then return tonumber(unixts) end end

local t1 = args[1] local t2 = args[2] -- Find the start timestamp and the end timestamp. local startTimestamp, endTimestamp if not t1 then -- Find the first and last second in the current year. local currentYear = tonumber(getDate('Y')) local currentYearStartUnix = tonumber(getUnixTimestamp('1 Jan ' .. tostring(currentYear))) local currentYearEndUnix = tonumber(getUnixTimestamp('1 Jan ' .. tostring(currentYear + 1))) - 1 startTimestamp = '@' .. tostring(currentYearStartUnix) -- @ is used to denote Unix timestamps with lang:formatDate. endTimestamp = '@' .. tostring(currentYearEndUnix) elseif t1 and not t2 then startTimestamp = '@0' -- the Unix epoch, 1 January 1970 endTimestamp = t1	elseif t1 and t2 then startTimestamp = t1		endTimestamp = t2	end

-- Get Unix timestamps and return errors for bad input (or for bugs in the underlying PHP library, of which there are unfortunately a few) local startTimestampUnix = getUnixTimestamp(startTimestamp) local endTimestampUnix = getUnixTimestamp(endTimestamp) if not startTimestampUnix then return raiseError('"' .. tostring(startTimestamp) .. '" was not recognised as a valid timestamp') elseif not endTimestampUnix then return raiseError('"' .. tostring(endTimestamp) .. '" was not recognised as a valid timestamp') elseif startTimestampUnix > endTimestampUnix then return raiseError('the start date must not be later than the end date (start date: "' .. startTimestamp .. '", end date: "' .. endTimestamp .. '")') end

-- Get a random number between the two Unix timestamps and return it using the specified format. local randomTimestamp = getBigRandom(startTimestampUnix, endTimestampUnix) local dateFormat = args.format or 'H:i, d F Y (T)' local result = getDate(dateFormat, '@' .. tostring(randomTimestamp)) if result then return result else return raiseError('"' .. dateFormat .. '" is not a valid date format') end end

-- -- List functions --

local function randomizeArray(t, limit) -- Randomizes an array. It works by iterating through the list backwards, each time swapping the entry -- "i" with a random entry. Courtesy of Xinhuan at http://forums.wowace.com/showthread.php?p=279756 -- If the limit parameter is set, the array is shortened to that many elements after being randomized. -- The lowest possible value is 0, and the highest possible is the length of the array. local len = #t for i = len, 2, -1 do		local r = math.random(i) t[i], t[r] = t[r], t[i] end if limit and limit < len then local ret = {} for i, v in ipairs(t) do			if i > limit then break end ret[i] = v		end return ret else return t	end end

local function removeBlanks(t) -- Removes blank entries from an array so that it can be used with ipairs. local ret = {} for k, v in pairs(t) do		if type(k) == 'number' then table.insert(ret, k)		end end table.sort(ret) for i, v in ipairs(ret) do		ret[i] = t[v] end return ret end

local function makeSeparator(sep) if sep == 'space' then -- Include an easy way to use spaces as separators. return ' ' elseif sep == 'newline' then -- Ditto for newlines return '\n' elseif type(sep) == 'string' then -- If the separator is a recognised MediaWiki separator, use that. Otherwise use the value of sep if it is a string. local mwseparators = {'dot', 'pipe', 'comma', 'tpt-languages'} for _, mwsep in ipairs(mwseparators) do			if sep == mwsep then return mw.message.new( sep .. '-separator' ):plain end end return sep end end

local function makeRandomList(args) local list = removeBlanks(args) list = randomizeArray(list, tonumber(args.limit)) return list end

function l.item(args) -- Returns a random item from a numbered list. local list = removeBlanks(args) local len = #list if len >= 1 then return list[math.random(len)] end end

function l.list(args) -- Randomizes a list and concatenates the result with a separator. local list = makeRandomList(args) local sep = makeSeparator(args.sep or args.separator) return table.concat(list, sep) end

function l.text_list(args) -- Randomizes a list and concatenates the result, text-style. Accepts separator and conjunction arguments. local list = makeRandomList(args) local sep = makeSeparator(args.sep or args.separator) local conj = makeSeparator(args.conj or args.conjunction) return mw.text.listToText(list, sep, conj) end

function l.array(args) -- Returns a Lua array, randomized. For use from other Lua modules. return randomizeArray(args.t, args.limit) end

-- -- HTML list function --

function l.html_list(args, listType) -- Randomizes a list and turns it into an HTML list. Uses Module:List. listType = listType or 'bulleted' local listArgs = makeRandomList(args) -- Arguments for Module:List. for k, v in pairs(args) do		if type(k) == 'string' then listArgs[k] = v		end end return makeList(listType, listArgs) end

-- -- The main function. Called from other Lua modules. --

function p.main(funcName, args, listType) -- Sets the seed for the random number generator and passes control over to the other functions. local same = yesno(args.same) if not same then -- Generates a different number every time the module is called, even from the same page. -- This is because of the variability of os.clock (the time in seconds that the Lua script has been running for). math.randomseed(mw.site.stats.edits + mw.site.stats.pages + os.time + math.floor(os.clock * 1000000000)) else if not cfg.lowTraffic then -- Make the seed as random as possible without using anything time-based. This means that the same random number -- will be generated for the same input from the same page - necessary behaviour for some wikicode templates that -- assume bad pseudo-random-number generation. local stats = mw.site.stats local views = stats.views or 0 -- This is not always available, so we need a backup. local seed = views + stats.pages + stats.articles + stats.files + stats.edits + stats.users + stats.activeUsers + stats.admins -- Make this as random as possible without using os.time or os.clock math.randomseed(seed) else -- Make the random seed change every n seconds, where n is set by cfg.seedRefreshRate. -- This is useful for low-traffic wikis where new edits may not happen very often. math.randomseed(math.floor(os.time / cfg.seedRefreshRate)) end end if type(args) ~= 'table' then error('the second argument to p.main must be a table') end return l[funcName](args, listType) end -- -- Process arguments from #invoke --

local function makeWrapper(funcName, listType) -- This function provides a wrapper for argument-processing from #invoke. -- listType is only used with p.html_list, and is nil the rest of the time. return function (frame) -- If called via #invoke, use the args passed into the invoking template, or the args passed to #invoke if any exist. -- Otherwise assume args are being passed directly in from the debug console or from another Lua module. local origArgs if frame == mw.getCurrentFrame then origArgs = frame:getParent.args for k, v in pairs(frame.args) do				origArgs = frame.args break end else origArgs = frame end -- Trim whitespace and remove blank arguments. local args = {} for k, v in pairs(origArgs) do			if type(v) == 'string' then v = mw.text.trim(v) end if v ~= '' then args[k] = v			end end return p.main(funcName, args, listType) end end

-- Process arguments for HTML list functions. local htmlListFuncs = { bulleted_list          = 'bulleted', unbulleted_list        = 'unbulleted', horizontal_list        = 'horizontal', ordered_list           = 'ordered', horizontal_ordered_list = 'horizontal_ordered' } for funcName, listType in pairs(htmlListFuncs) do	p[funcName] = makeWrapper('html_list', listType) end

-- Process arguments for other functions. local otherFuncs = {'number', 'date', 'item', 'list', 'text_list'} for _, funcName in ipairs(otherFuncs) do	p[funcName] = makeWrapper(funcName) end

return p