Module:SCurve

From Fallen London Wiki

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

local p = {}

local function get_arg_or_nil(args, name)
	local arg = args[name]
	if arg == '' then
		return nil
	end
	return arg
end

local function get_arg(args, parent_args, name, default)
	return get_arg_or_nil(args, name) or get_arg_or_nil(parent_args, name) or default
end

--[[
Round a number.

Input parameters:
    @num: the number to round
    @power: the number of decimal digits to round to

If power is non-zero, this is the same as {{#expr: num round power}}.
If power is zero, this uses round-half-to-even, as is typically used in-game.
]]
local function round(num, power)
	if num == 0 then
		return 0
	end
	if power ~= 0 then
  		return mw.ext.ParserFunctions.expr(tostring(num) .. 'round' .. tostring(power))
	end
	local anum = math.abs(num)
	local sign = math.floor(num / anum)
	local frac = math.fmod(anum, 1)
	if (frac == 0.5 and math.ceil(anum) % 2 == 0) or frac > .5 then
		return sign * math.ceil(anum)
	end
	if math.floor(anum) == 0 then
		-- silly, but needed to work around -0
		return 0
	end
	return sign * math.floor(anum)
end

local function make_scurve(height, k, midpoint, y_offset)
	return function(x, round_power)
  		return round(y_offset + height / (1 + math.exp(-k * (x - midpoint))), round_power)
	end
end

function p.eval_scurve(frame)
	local args = frame.args
	local parent_args = frame:getParent().args
	local height = tonumber(get_arg(args, parent_args, 'height', 1))
	local k = tonumber(get_arg(args, parent_args, 'k', 1))
	local midpoint = tonumber(get_arg(args, parent_args, 'midpoint', 0))
	local y_offset = tonumber(get_arg(args, parent_args, 'y_offset', 0))
	local x = tonumber(get_arg(args, parent_args, 'x', nil))
	local round_power = 3
	if get_arg(args, parent_args, 'Round', nil) == 'yes' then
  	  round_power = 0
	end

	return make_scurve(height, k, midpoint, y_offset)(x, round_power)
end

local function add_column(table_rows, table_data)
	table_rows[1]:tag('th')
		:wikitext(table_data[1])
	for row = 2, #(table_rows) do
		table_rows[row]:tag('td')
			:wikitext(table_data[row])
	end
end

function p.scurve_table(frame)
	local args = frame.args
	local parent_args = frame:getParent().args
	local height = tonumber(get_arg(args, parent_args, 'height', 1))
	local k = tonumber(get_arg(args, parent_args, 'k', 1))
	local midpoint = tonumber(get_arg(args, parent_args, 'midpoint', 0))
	local y_offset = tonumber(get_arg(args, parent_args, 'y_offset', 0))
	local xleft = tonumber(get_arg(args, parent_args, 'xleft', nil) or
		                   get_arg(args, parent_args, 'xmin', 0))
	local xright = tonumber(get_arg(args, parent_args, 'xright', nil) or
		                    get_arg(args, parent_args, 'xmax', xleft))
	local low_cap = get_arg(args, parent_args, 'lowcap', 'no') == 'yes'
	local high_cap = get_arg(args, parent_args, 'highcap', 'no') == 'yes'
	local compact = get_arg(args, parent_args, 'compact', 'no') == 'yes'
	local condition = args.condition
	local show_ending_condition = get_arg(args, parent_args, 'show_ending_condition', 'no') == 'yes'
	local condition_increase = tonumber(get_arg(args, parent_args, 'condition_increase', 1))
	local effect = get_arg(args, parent_args, 'Effect', nil)
	local max_columns = tonumber(get_arg(args, parent_args, 'max_columns', nil))
	local round_power = 3
	if get_arg(args, parent_args, 'Round', 'no') == 'yes' then
  		round_power = 0
	end

	local tbl = mw.html.create('table')
		:addClass('article-table')
	local function make_rows(tbl, table_rows, show_ending_condition)
		table.insert( table_rows, tbl:tag('tr') )
		table.insert( table_rows, tbl:tag('tr') )
		if show_ending_condition then
			table.insert( table_rows, tbl:tag('tr') )
			add_column(table_rows, { 'Starting ' .. condition, effect, 'Ending ' .. condition } )
		else
			add_column(table_rows, { condition, effect } )
		end
		return table_rows
	end
	
	local table_rows = make_rows(tbl, {}, show_ending_condition)

	local scurve = make_scurve(height, k, midpoint, y_offset)

	local function make_compact_condition_data(start, last, val)
		local key
		if start == last then
			key = tostring(start)
		else
			key = start .. '–' .. last
		end
		if not low_cap and
				((k > 0 and val == y_offset) or
				 (k < 0 and val == y_offset + height)) then
			key = '≤' .. key
		elseif not high_cap and
				((k < 0 and val == y_offset) or
				 (k > 0 and val == y_offset + height)) then
			key = '≥' .. key
		end
		return key
	end

	local start = nil
	local last = nil
	local val = nil
	local num_columns = 0
	local overflow = false
	local step = math.floor((xright - xleft) / math.abs(xright - xleft))
	for x=xleft,xright,step do
		local result = scurve(x, round_power)
		if compact then
			if val ~= nil and result ~= val then
				local heading = make_compact_condition_data(start, last, val)
				local table_data = { heading, val }
				if show_ending_condition then
					local ending_condition = make_compact_condition_data(start + condition_increase, last + condition_increase, val)
					table.insert( table_data, ending_condition )
				end
				add_column(table_rows, table_data)
				num_columns = num_columns + 1
				start = nil
			end
			if x == xright then
				local heading = make_compact_condition_data(start or x, x, result)
				local table_data = { heading, result }
				if show_ending_condition then
					local ending_condition = make_compact_condition_data((start or x) + condition_increase, x + condition_increase, result)
					table.insert( table_data, ending_condition )
				end
				add_column(table_rows, table_data)
				num_columns = num_columns + 1
			else
    			last = x
				if start == nil then
					if max_columns ~= nil and num_columns >= max_columns then
						table_rows = make_rows(tbl, {}, show_ending_condition)
						num_columns = 0
						overflow = true
					end
					start = x
					val = result
				end
			end
		else
			if max_columns ~= nil and num_columns >= max_columns then
				table_rows = make_rows(tbl, {}, show_ending_condition)
				num_columns = 0
				overflow = true
			end
			local table_data = { x, result }
			if show_ending_condition then
				local ending_condition = x + condition_increase
				table.insert( table_data, ending_condition )
			end
			add_column(table_rows, table_data)
			num_columns = num_columns + 1
		end
	end
	if overflow and num_columns < max_columns then
		table_rows[1]:tag('th')
			:attr('colspan', max_columns - num_columns)
		for row = 2, #(table_rows) do
			table_rows[row]:tag('td')
				:attr('colspan', max_columns - num_columns)
		end
	end
	return tostring(tbl)
end

return p