Module:Grind/util

From Fallen London Wiki (Staging)

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

local p = {}

-- == Conversion utilities ==

-- Float to string conversion.
function p.f2s(f)
	if f == f + 1 then
		-- either inf or -inf
		return '' .. f
	end
	if ('' .. f):find('%.') == nil then
		-- an integer
		return '' .. f
	end
	if math.abs(f) >= 1 then
		return string.format('%.2f', f)
	elseif math.abs(f) >= 0.01 then
		return string.format('%.4f', f)
	else
		return string.format('%.8f', f)
	end
end

-- String to boolean conversion.
-- Recognised boolean strings:
-- * yes/no
-- * true/false
-- * on/off
-- * 1/0
function p.s2b(s, fallback)
	fallback = fallback or false
	if type(s) ~= 'string' then
		return fallback
	end
	local yes = {'yes', 'true', 'on', '1'}
	local no = {'no', 'false', 'off', '0'}
	for _, v in ipairs(yes) do
		if s:lower() == v then
			return true
		end
	end
	for _, v in ipairs(no) do
		if s:lower() == v then
			return false
		end
	end
	return fallback
end

-- Boolean to number conversion.
function p.b2n(b)
	return b and 1 or 0
end

-- == General parsing utilities ==

-- Returns the next position and the next character.
function p.advance(s, pos)
	return pos + 1, s:sub(pos + 1, pos + 1)
end

-- Matching token finder.
-- Supported token pairs:
-- * ()
-- * ""
-- Escape sequences in strings are taken into account.
-- `initial_pos` is the location of the starting token.
-- Location of the matching token is returned.
function p.find_match(s, initial_pos)
	local pos = initial_pos
	local context = {s:sub(pos, pos)}
	local last_escape = nil
	while #context > 0 do
		pos = pos + 1
		if pos > #s then
			return nil
		end
		local last = context[#context]
		local current_char = s:sub(pos, pos)
		if current_char == '\\' and last_escape ~= pos - 1 then
			last_escape = pos
		end
		if current_char == '"' then
			if last == '"' and last_escape ~= pos - 1 then
				table.remove(context)
			elseif last ~= '"' then
				table.insert(context, '"')
			end
		end
		if last == '(' and current_char == '(' then
			table.insert(context, '(')
		end
		if last == '(' and current_char == ')' then
			table.remove(context)
		end
	end
	return pos
end

-- Whitespace stripper.
-- Starting and ending whitespace characters are removed.
function p.strip_whitespaces(str)
	local s = '' .. str
	while s:sub(1, 1):match('%s') do
		s = s:sub(2)
	end
	while s:sub(-1, -1):match('%s') do
		s = s:sub(1, -2)
	end
	return s
end

-- Whitespace skipping.
-- Finds the first non-whitespace character starting with `initial_pos`.
-- Its position is returned.
-- `#s + 1` is returned if no such character is found.
function p.skip_whitespaces(s, initial_pos)
	local pos = initial_pos
	while s:sub(pos, pos):match('%s') do
		pos = pos + 1
	end
	return pos
end

-- Range parsing.
-- The following formats are supported:
-- * `X` -> exact value (for non-negative X);
-- * `X-Y` -> exact range (X, Y may be negative);
-- * `X-` -> range from X to positive infinity;
-- * `-Y` -> range from 0 to Y (for non-negative Y);
-- * `-` -> range from 0 to positive infinity.
-- Returns a tuple of range boundaries.
-- Returns nil, nil is parsing fails.
function p.parse_range(range)
	if type(range) ~= 'string' then
		return nil, nil
	end
	local _, n = range:gsub('%-', '')
	if n == 0 then
		-- exact value
		local exact = tonumber(range)
		return exact, exact
	elseif n == 1 then
		-- non-negative range; could be a negative number, but this case is not supported
		local min, max = range:gmatch('(%d*)%-(%d*)')()
		if min ~= nil and max ~= nil then
			min = tonumber(min) or 0
			max = tonumber(max) or 1/0
			return min, max
		end
		return nil, nil
	elseif n == 2 then
		-- one number is negative; assuming the first one is
		local min, max = range:gmatch('%-(%d+)%-(%d*)')()
		if min ~= nil and max ~= nil then
			min = -tonumber(min)
			max = tonumber(max) or 1/0
			return min, max
		end
		return nil, nil
	elseif n == 3 then
		-- both numbers are negative
		local min, max = range:gmatch('%-(%d+)%-%-(%d+)')()
		if min ~= nil and max ~= nil then
			min = -tonumber(min)
			max = -tonumber(max)
			return min, max
		end
		return nil, nil
	else
		-- ???
		return nil, nil
	end
end

-- == Misc. == 

-- Checks if two objects are equal.
-- Objects are considered to be equal iff
-- * they are non-table structures that are equal
-- OR all of the following conditions are true:
-- * they are both tables;
-- * values corresponding to the keys defined in t1 and t2 are equal.
function p.tables_equal(t1, t2)
	if type(t1) ~= type(t2) then
		return false
	end
	if type(t1) ~= 'table' then
		return t1 == t2
	end
	local keys = {}
	for k, _ in pairs(t1) do
		keys[k] = true
	end
	for k, _ in pairs(t2) do
		keys[k] = true
	end
	for k, _ in pairs(keys) do
		if not p.tables_equal(t1[k], t2[k]) then
			return false
		end
	end
	return true
end

-- Multiplies a range by a constant.
function p.multiply_range(range, m)
	local rmin, rmax = p.parse_range(range)
	if rmin == nil or rmax == nil then
		return range
	end
	if rmax ~= rmax + 1 then
		return (rmin * m) .. '-' .. (rmax * m)
	else
		return (rmin * m) .. '-'
	end
end

function p.normalise_input(name)
	if name:sub(-5) == ':GRON' then 
		return name:sub(1, -6), 'gron'
	end
	if name:sub(-8) == ':Boolean' then
		return name:sub(1, -9), 'boolean'
	end
	if name:sub(-7) == ':Number' then
		return name:sub(1, -8), 'number'
	end
	if name:sub(-7) == ':String' then
		return name:sub(1, -8), 'string'
	end
	return name, 'unknown'
end

function p.input_suffix(itype)
	if itype == 'gron' then
		return ':GRON'
	elseif itype == 'boolean' then
		return ':Boolean'
	elseif itype == 'number' then
		return ':Number'
	elseif itype == 'string' then
		return ':String'
	else
		return ''
	end
end

-- Packs the effect and the action cost into a table.
function p.complete_effect(a, effect)
	return {
		a=a,
		effect=effect
	}
end

return p