Module:BassaridEmployment: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
No edit summary
Tag: Manual revert
NewZimiaGov (talk | contribs)
No edit summary
Line 1: Line 1:
local p = {}
local p = {}
---------------------------------------------------------
-- Module:BassaridEmployment (Optimized)
--
-- What this module does
--  • Computes a daily "previous vs current" employment estimate per company
--  • Uses deterministic per-company variation so:
--      - results are stable for a given day
--      - results do NOT depend on loop order / table order
--  • Builds wiki output using buffers (table.concat) for speed
--
-- Usage:
--  {{#invoke:BassaridEmployment|generateTable}}
--  Optional args:
--    |population=55499301
--    |unemployment=0.04
--    |maxChange=0.04
---------------------------------------------------------


local ag = require("Module:AutoUpdateAgPrices")
---------------------------------------------------------
local manf = require("Module:AutoUpdateManfPrices")
-- 1) getCurrentDate
local serv = require("Module:AutoUpdateServPrices")
--  Returns a date string in PSSC format, plus day/month/year/dayOfYear.
local market = require("Module:BassaridMarketActivity")
---------------------------------------------------------
 
local CITY_POP = {
["Vaeringheim"] = 20887650.86,
["Lunalis Sancta"] = 7832869.07,
["Luminaria"] = 6382335.99,
["Serena"] = 6672442.61,
["Symphonara"] = 4351592.87,
["Aurelia"] = 3771379.63,
["Sylvapolis"] = 3481276.21,
["Delphica"] = 2030743.12,
["Catonis Atrium"] = 1682617.10,
["Ferrum Citadel"] = 928339.26,
["Pyralis"] = 12965917.88,
["Nexa"] = 7979025.10,
["Saluria"] = 7729680.74,
["Acheron"] = 4238856.91,
["Koinonía"] = 5485581.50,
["Aureum"] = 4488201.27,
["Aetherium"] = 3740168.19,
["Erythros"] = 3241479.47,
["Somniumpolis"] = 16035032.55,
["Hafaan"] = 3568579.65,
["Jeseri"] = 2430778.52,
["Skýrophos"] = 5132630.96,
["Bjornopolis"] = 4016840.25,
["Aegerheim"] = 3347368.98,
["Norsolyra"] = 2901052.70,
["Thorsalon"] = 2454736.41,
["Myrene"] = 1338945.70,
["Ephyra"] = 1115790.71,
["Thyrea"] = 892632.57,
["Pelagia"] = 669474.43,
["Halicarn"] = 446316.28,
["Keybir-Aviv"] = 4592562.31,
["Tel-Amin"] = 4155174.90,
["Jogi"] = 3936483.03,
["Lewisburg"] = 3280401.91,
["Diamandis"] = 2843014.50,
["Thermosalem"] = 1312160.40,
["Akróstadium"] = 1093466.69,
["Lykopolis"] = 437387.41,
["Sufriya"] = 218693.71,
["Riddersborg"] = 11757190.77,
["Ardclach"] = 5426395.09,
["Ourid"] = 31826487.08,
["Bashkim"] = 23046766.50
}
 
local function getCurrentDate()
local function getCurrentDate()
local startDate = os.time({ year = 1999, month = 8, day = 6 })
local startDate = os.time({ year = 1999, month = 8, day = 6 })
local secondsInDay = 86400
local secondsInDay = 86400
local daysPerYear = 183
local daysPerYear = 183
local now = os.time()
local currentDate = os.time()
local totalDaysElapsed = math.floor((now - startDate) / secondsInDay)
local totalDaysElapsed = math.floor((currentDate - startDate) / secondsInDay)
local yearFraction = totalDaysElapsed / daysPerYear
local yearFraction = totalDaysElapsed / daysPerYear
local psscYear = math.floor(yearFraction)
local psscYear     = math.floor(yearFraction)
local dayOfYear = math.floor((yearFraction - psscYear) * daysPerYear) + 1
local dayOfYear   = math.floor((yearFraction - psscYear) * daysPerYear) + 1
local month, dayOfMonth
local month, dayOfMonth
if dayOfYear <= 61 then
if dayOfYear <= 61 then
month = 1
month = 1; dayOfMonth = dayOfYear
dayOfMonth = dayOfYear
elseif dayOfYear <= 122 then
elseif dayOfYear <= 122 then
month = 2
month = 2; dayOfMonth = dayOfYear - 61
dayOfMonth = dayOfYear - 61
else
else
month = 3
month = 3; dayOfMonth = dayOfYear - 122
dayOfMonth = dayOfYear - 122
end
end
return string.format("%d/%d/%d PSSC", dayOfMonth, month, psscYear)
 
return string.format("%d/%d/%d [[PSSC]]", dayOfMonth, month, psscYear), dayOfMonth, month, psscYear, dayOfYear
end
end


local MOD = 2147483647
---------------------------------------------------------
-- 2) Company table (same data as current module)
---------------------------------------------------------
local companies = {
{name = "Lake Morovia Blockade Fund", employmentPercentage = 0.0300},
{name = "Gladeseed Farmers' Union", employmentPercentage = 0.1205},
{name = "Atterian Cattle Drivers", employmentPercentage = 0.0502},
{name = "Roving Wind Farm Corporation", employmentPercentage = 0.0210},
{name = "Baratar Corporation", employmentPercentage = 0.0401},
{name = "Noctic Fleet", employmentPercentage = 0.0603},
{name = "Keepers of the Bloodbaths of Laprind", employmentPercentage = 0.0105},
{name = "Jogiani Merchants of Manwine", employmentPercentage = 0.0208},
{name = "Keepers of the Grove of Zoe Elm", employmentPercentage = 0.0112},
{name = "Plains of Jogi", employmentPercentage = 0.0807},
{name = "Ale of the Night Central Brewery", employmentPercentage = 0.0201},
{name = "Camel Herders of the Jangsong Province", employmentPercentage = 0.0306},
{name = "Thalassian Temple Granite Export Cooperative", employmentPercentage = 0.0310},
{name = "East Keltian Iron Company", employmentPercentage = 0.0415},
{name = "Mylecian Coal Ports", employmentPercentage = 0.0317},
{name = "Merchants of the Valley of Diamonds", employmentPercentage = 0.0102},
{name = "Maritime Guild of the Cult of Maskmakers", employmentPercentage = 0.0109},
{name = "Hatch Ministry", employmentPercentage = 0.0203},
{name = "Anterran Imports and Services", employmentPercentage = 0.0113},
{name = "East Keltian Timber Company", employmentPercentage = 0.0312},
{name = "Grand Cave Bee Meadery", employmentPercentage = 0.0215},
{name = "Merchant Guild of the Poppy Goddess", employmentPercentage = 0.0111},
{name = "Cinnamon Plains of Rouge", employmentPercentage = 0.0218},
{name = "Seamstresses of Rouge", employmentPercentage = 0.0114},
{name = "Salurian Temple of Sacred Horehound", employmentPercentage = 0.0212},
{name = "Suncliff Fisheries", employmentPercentage = 0.0309},
{name = "Bird Keepers of Saluria", employmentPercentage = 0.0108},
{name = "Velvet Homestead", employmentPercentage = 0.0110},
{name = "Hunters of the Giant Fanged Penguin", employmentPercentage = 0.0116},
{name = "Professional Pillarion League", employmentPercentage = 0.0052},
{name = "La Sái Ebile", employmentPercentage = 0.0104},
{name = "Temple Bank of the Reformed Stripping Path", employmentPercentage = 0.0118},
{name = "Market Hanavusu", employmentPercentage = 0.0157},
{name = "Couriers of the Lizard Queen", employmentPercentage = 0.0117},
{name = "Temple University of Delphica", employmentPercentage = 0.0209},
{name = "Trans-Morovian Railway Company", employmentPercentage = 0.0155},
}
 
---------------------------------------------------------
-- 2.5) Normalize employment percentages into weights
---------------------------------------------------------
local totalEmploymentWeight = 0
for _, company in ipairs(companies) do
totalEmploymentWeight = totalEmploymentWeight + company.employmentPercentage
end
 
---------------------------------------------------------
-- 3) Deterministic RNG helpers (fast; no global math.random)
---------------------------------------------------------
local MOD = 2147483647 -- 2^31 - 1
local A  = 1103515245
local A  = 1103515245
local C  = 12345
local C  = 12345
Line 97: Line 117:
end
end


local function randUnit(nextInt)
local function randIntInclusive(nextInt, lo, hi)
return (nextInt() % 100000) / 100000
if hi < lo then lo, hi = hi, lo end
local span = (hi - lo) + 1
local r = (nextInt() + nextInt()) % MOD
return lo + (r % span)
end
end


local function clamp(x, lo, hi)
---------------------------------------------------------
if x < lo then return lo end
-- 4) Per-company calculation
if x > hi then return hi end
---------------------------------------------------------
return x
local function calculateEmployees(company, employedPopulation, maxChangeFrac, dailySeed)
end
local normalizedShare = company.employmentPercentage / totalEmploymentWeight
local base = math.floor(employedPopulation * normalizedShare)


local function round(num)
local h = hashString(company.name)
return math.floor(num + 0.5)
local nextInt = lcg(dailySeed + h)
end
local maxChange = math.floor(base * maxChangeFrac)
if maxChange < 0 then maxChange = 0 end


local function getAllCompanies()
local changeAmount = 0
local out = {}
if maxChange > 0 then
local function append(list)
changeAmount = randIntInclusive(nextInt, -maxChange, maxChange)
for _, row in ipairs(list or {}) do
table.insert(out, row)
end
end
end
append(ag.getCompanyEmploymentInputs())
append(manf.getCompanyEmploymentInputs())
append(serv.getCompanyEmploymentInputs())
return out
end
local function computeWeights(companies)
local weights = {}
local total = 0
for _, co in ipairs(companies) do
local scale = math.max(1, co.count or 1)
local priceMass = math.max(0.01, co.avgBase or 0.01)
local cityPop = CITY_POP[co.city] or 1000000
-- soft city-size weighting
local cityFactor = math.max(0.60, math.min(1.50, cityPop / 5000000))


local w = scale * priceMass * cityFactor
local prevEmployees = base
weights[co.name] = w
local currEmployees = base + changeAmount
total = total + w
if currEmployees < 1 then currEmployees = 1 end
end


for _, co in ipairs(companies) do
return prevEmployees, currEmployees, normalizedShare
co.employmentWeight = weights[co.name] / total
end
end
 
local function computeEmploymentRow(co, employedPopulation, dailySeed)
local baseEmployees = round(employedPopulation * co.employmentWeight)
 
local sig = market.getEmploymentSignals and market.getEmploymentSignals(co.city or "Vaeringheim") or {
marketScore = 0,
volatility = 0
}
 
local h = hashString((co.name or "") .. "|" .. (co.city or ""))
local nextInt = lcg(dailySeed + h)
 
-- company-specific deterministic jitter, small
local jitter = (randUnit(nextInt) - 0.5) * 0.04  -- about ±2%
 
-- price effect: every +1% price change nudges labor demand slightly
local priceEffect = (tonumber(co.pctChange) or 0) * 0.35 / 100
 
-- market effect: local/global ops conditions
local marketEffect = sig.marketScore or 0
 
-- volatility slightly damps hiring
local volDrag = (sig.volatility or 0) * 0.03
 
local netEffect = clamp(priceEffect + marketEffect + jitter - volDrag, -0.18, 0.18)
 
local currentEmployees = round(baseEmployees * (1 + netEffect))
if currentEmployees < 1 then currentEmployees = 1 end
 
local changeText = "Same"
if currentEmployees > baseEmployees then
changeText = "Increased"
elseif currentEmployees < baseEmployees then
changeText = "Decreased"
end
 
return {
prevEmployees = baseEmployees,
currEmployees = currentEmployees,
changeText = changeText,
netEffectPct = netEffect * 100,
marketScore = sig.marketScore or 0
}
end
end


---------------------------------------------------------
-- 5) generateTable
---------------------------------------------------------
function p.generateTable(frame)
function p.generateTable(frame)
frame = frame or {}
frame = frame or {}
local args = (frame.getParent and frame:getParent() and frame:getParent().args) or {}
local args = (frame.getParent and frame:getParent() and frame:getParent().args) or {}
local fargs = frame.args or {}
local fargs = frame.args or {}
 
local population = tonumber(fargs.population or args.population) or 55499301
local population = tonumber(fargs.population or args.population) or 293833483.50
local unemploymentRate = tonumber(fargs.unemployment or args.unemployment) or 0.04
local unemploymentRate = tonumber(fargs.unemployment or args.unemployment) or 0.04
local maxChangeFrac = tonumber(fargs.maxChange or args.maxChange) or 0.04


if unemploymentRate < 0 then unemploymentRate = 0 end
if unemploymentRate < 0 then unemploymentRate = 0 end
if unemploymentRate > 1 then unemploymentRate = 1 end
if unemploymentRate > 1 then unemploymentRate = 1 end
if maxChangeFrac < 0 then maxChangeFrac = 0 end
if maxChangeFrac > 1 then maxChangeFrac = 1 end
local employedPopulation = math.floor(population * (1 - unemploymentRate))


local employedPopulation = math.floor(population * (1 - unemploymentRate))
local localTime = os.date("*t")
local localTime = os.date("*t")
local dailySeed = localTime.year * 1000 + localTime.yday
local dailySeed = localTime.year * 1000 + localTime.yday
local dateString = getCurrentDate()
local dateString = getCurrentDate()
local companies = getAllCompanies()
computeWeights(companies)
table.sort(companies, function(a, b)
if a.sector ~= b.sector then return a.sector < b.sector end
return a.name < b.name
end)


local out = {}
local out = {}
out[#out + 1] = '{| class="wikitable sortable"\n'
out[#out + 1] = "{| class=\"wikitable\"\n"
out[#out + 1] = '! Date !! Sector !! Company !! City !! Price Change !! Previous Employees !! Current Employees !! Change\n'
out[#out + 1] = "! Date !! Company !! Employment % !! Previous Employees !! Current Employees !! Change\n"


local totalEmployed = 0
local totalEmployed = 0
for _, company in ipairs(companies) do
local prevE, currE, normalizedShare = calculateEmployees(company, employedPopulation, maxChangeFrac, dailySeed)


for _, co in ipairs(companies) do
local changeType
local r = computeEmploymentRow(co, employedPopulation, dailySeed)
if currE > prevE then
totalEmployed = totalEmployed + r.currEmployees
changeType = "Increased"
elseif currE < prevE then
changeType = "Decreased"
else
changeType = "Same"
end


local rowStyle = ''
local rowStyle
if r.changeText == "Increased" then
if changeType == "Increased" then
rowStyle = ' style="background-color:#d8f0d2;"'
rowStyle = ' style="background-color:lightblue;"'
elseif r.changeText == "Decreased" then
elseif changeType == "Same" then
rowStyle = ' style="background-color:#f8d7d7;"'
rowStyle = ' style="background-color:lightgreen;"'
else
else
rowStyle = ' style="background-color:#f3f3f3;"'
rowStyle = ' style="background-color:#ffcccb;"'
end
end


totalEmployed = totalEmployed + currE
out[#out + 1] = string.format(
out[#out + 1] = string.format(
'|-%s\n| %s || %s || %s || %s || %+0.2f%% || %d || %d || %s\n',
"|-%s\n| %s || %s || %.2f%% || %d || %d || %s\n",
rowStyle,
rowStyle,
dateString,
dateString,
co.sector,
company.name,
co.name,
normalizedShare * 100,
co.city or "",
prevE,
co.pctChange or 0,
currE,
r.prevEmployees,
changeType
r.currEmployees,
r.changeText
)
)
end
end


local totalUnemployed = math.max(0, math.floor(population - totalEmployed))
local totalUnemployed = population - totalEmployed
if totalUnemployed < 0 then totalUnemployed = 0 end


out[#out + 1] = string.format('|-\n! colspan="6" | Total Employed || %d || \n', totalEmployed)
out[#out + 1] = string.format("|-\n! colspan=\"4\" | Total Employed || %d || \n", totalEmployed)
out[#out + 1] = string.format('|-\n! colspan="6" | Total Unemployed || %d || \n', totalUnemployed)
out[#out + 1] = string.format("|-\n! colspan=\"4\" | Total Unemployed || %d || \n", totalUnemployed)
out[#out + 1] = '|}'
out[#out + 1] = "|}"


return table.concat(out)
return table.concat(out)

Revision as of 18:27, 20 March 2026

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

local p = {}
---------------------------------------------------------
-- Module:BassaridEmployment (Optimized)
--
-- What this module does
--  • Computes a daily "previous vs current" employment estimate per company
--  • Uses deterministic per-company variation so:
--      - results are stable for a given day
--      - results do NOT depend on loop order / table order
--  • Builds wiki output using buffers (table.concat) for speed
--
-- Usage:
--   {{#invoke:BassaridEmployment|generateTable}}
--   Optional args:
--     |population=55499301
--     |unemployment=0.04
--     |maxChange=0.04
---------------------------------------------------------

---------------------------------------------------------
-- 1) getCurrentDate
--   Returns a date string in PSSC format, plus day/month/year/dayOfYear.
---------------------------------------------------------
local function getCurrentDate()
	local startDate = os.time({ year = 1999, month = 8, day = 6 })
	local secondsInDay = 86400
	local daysPerYear = 183
	local currentDate = os.time()
	local totalDaysElapsed = math.floor((currentDate - startDate) / secondsInDay)
	local yearFraction = totalDaysElapsed / daysPerYear
	local psscYear     = math.floor(yearFraction)
	local dayOfYear    = math.floor((yearFraction - psscYear) * daysPerYear) + 1
	local month, dayOfMonth
	if dayOfYear <= 61 then
		month = 1; dayOfMonth = dayOfYear
	elseif dayOfYear <= 122 then
		month = 2; dayOfMonth = dayOfYear - 61
	else
		month = 3; dayOfMonth = dayOfYear - 122
	end

	return string.format("%d/%d/%d [[PSSC]]", dayOfMonth, month, psscYear), dayOfMonth, month, psscYear, dayOfYear
end

---------------------------------------------------------
-- 2) Company table (same data as current module)
---------------------------------------------------------
local companies = {
	{name = "Lake Morovia Blockade Fund", employmentPercentage = 0.0300},
	{name = "Gladeseed Farmers' Union", employmentPercentage = 0.1205},
	{name = "Atterian Cattle Drivers", employmentPercentage = 0.0502},
	{name = "Roving Wind Farm Corporation", employmentPercentage = 0.0210},
	{name = "Baratar Corporation", employmentPercentage = 0.0401},
	{name = "Noctic Fleet", employmentPercentage = 0.0603},
	{name = "Keepers of the Bloodbaths of Laprind", employmentPercentage = 0.0105},
	{name = "Jogiani Merchants of Manwine", employmentPercentage = 0.0208},
	{name = "Keepers of the Grove of Zoe Elm", employmentPercentage = 0.0112},
	{name = "Plains of Jogi", employmentPercentage = 0.0807},
	{name = "Ale of the Night Central Brewery", employmentPercentage = 0.0201},
	{name = "Camel Herders of the Jangsong Province", employmentPercentage = 0.0306},
	{name = "Thalassian Temple Granite Export Cooperative", employmentPercentage = 0.0310},
	{name = "East Keltian Iron Company", employmentPercentage = 0.0415},
	{name = "Mylecian Coal Ports", employmentPercentage = 0.0317},
	{name = "Merchants of the Valley of Diamonds", employmentPercentage = 0.0102},
	{name = "Maritime Guild of the Cult of Maskmakers", employmentPercentage = 0.0109},
	{name = "Hatch Ministry", employmentPercentage = 0.0203},
	{name = "Anterran Imports and Services", employmentPercentage = 0.0113},
	{name = "East Keltian Timber Company", employmentPercentage = 0.0312},
	{name = "Grand Cave Bee Meadery", employmentPercentage = 0.0215},
	{name = "Merchant Guild of the Poppy Goddess", employmentPercentage = 0.0111},
	{name = "Cinnamon Plains of Rouge", employmentPercentage = 0.0218},
	{name = "Seamstresses of Rouge", employmentPercentage = 0.0114},
	{name = "Salurian Temple of Sacred Horehound", employmentPercentage = 0.0212},
	{name = "Suncliff Fisheries", employmentPercentage = 0.0309},
	{name = "Bird Keepers of Saluria", employmentPercentage = 0.0108},
	{name = "Velvet Homestead", employmentPercentage = 0.0110},
	{name = "Hunters of the Giant Fanged Penguin", employmentPercentage = 0.0116},
	{name = "Professional Pillarion League", employmentPercentage = 0.0052},
	{name = "La Sái Ebile", employmentPercentage = 0.0104},
	{name = "Temple Bank of the Reformed Stripping Path", employmentPercentage = 0.0118},
	{name = "Market Hanavusu", employmentPercentage = 0.0157},
	{name = "Couriers of the Lizard Queen", employmentPercentage = 0.0117},
	{name = "Temple University of Delphica", employmentPercentage = 0.0209},
	{name = "Trans-Morovian Railway Company", employmentPercentage = 0.0155},
}

---------------------------------------------------------
-- 2.5) Normalize employment percentages into weights
---------------------------------------------------------
local totalEmploymentWeight = 0
for _, company in ipairs(companies) do
	totalEmploymentWeight = totalEmploymentWeight + company.employmentPercentage
end

---------------------------------------------------------
-- 3) Deterministic RNG helpers (fast; no global math.random)
---------------------------------------------------------
local MOD = 2147483647 -- 2^31 - 1
local A   = 1103515245
local C   = 12345

local function hashString(s)
	s = tostring(s or "")
	local h = 0
	for i = 1, #s do
		h = (h * 31 + s:byte(i)) % MOD
	end
	return h
end

local function lcg(seed)
	seed = seed % MOD
	return function()
		seed = (A * seed + C) % MOD
		return seed
	end
end

local function randIntInclusive(nextInt, lo, hi)
	if hi < lo then lo, hi = hi, lo end
	local span = (hi - lo) + 1
	local r = (nextInt() + nextInt()) % MOD
	return lo + (r % span)
end

---------------------------------------------------------
-- 4) Per-company calculation
---------------------------------------------------------
local function calculateEmployees(company, employedPopulation, maxChangeFrac, dailySeed)
	local normalizedShare = company.employmentPercentage / totalEmploymentWeight
	local base = math.floor(employedPopulation * normalizedShare)

	local h = hashString(company.name)
	local nextInt = lcg(dailySeed + h)
	local maxChange = math.floor(base * maxChangeFrac)
	if maxChange < 0 then maxChange = 0 end

	local changeAmount = 0
	if maxChange > 0 then
		changeAmount = randIntInclusive(nextInt, -maxChange, maxChange)
	end

	local prevEmployees = base
	local currEmployees = base + changeAmount
	if currEmployees < 1 then currEmployees = 1 end

	return prevEmployees, currEmployees, normalizedShare
end

---------------------------------------------------------
-- 5) generateTable
---------------------------------------------------------
function p.generateTable(frame)
	frame = frame or {}
	local args = (frame.getParent and frame:getParent() and frame:getParent().args) or {}
	local fargs = frame.args or {}
	local population = tonumber(fargs.population or args.population) or 55499301
	local unemploymentRate = tonumber(fargs.unemployment or args.unemployment) or 0.04
	local maxChangeFrac = tonumber(fargs.maxChange or args.maxChange) or 0.04

	if unemploymentRate < 0 then unemploymentRate = 0 end
	if unemploymentRate > 1 then unemploymentRate = 1 end
	if maxChangeFrac < 0 then maxChangeFrac = 0 end
	if maxChangeFrac > 1 then maxChangeFrac = 1 end
	local employedPopulation = math.floor(population * (1 - unemploymentRate))

	local localTime = os.date("*t")
	local dailySeed = localTime.year * 1000 + localTime.yday

	local dateString = getCurrentDate()

	local out = {}
	out[#out + 1] = "{| class=\"wikitable\"\n"
	out[#out + 1] = "! Date !! Company !! Employment % !! Previous Employees !! Current Employees !! Change\n"

	local totalEmployed = 0
	for _, company in ipairs(companies) do
		local prevE, currE, normalizedShare = calculateEmployees(company, employedPopulation, maxChangeFrac, dailySeed)

		local changeType
		if currE > prevE then
			changeType = "Increased"
		elseif currE < prevE then
			changeType = "Decreased"
		else
			changeType = "Same"
		end

		local rowStyle
		if changeType == "Increased" then
			rowStyle = ' style="background-color:lightblue;"'
		elseif changeType == "Same" then
			rowStyle = ' style="background-color:lightgreen;"'
		else
			rowStyle = ' style="background-color:#ffcccb;"'
		end

		totalEmployed = totalEmployed + currE
		out[#out + 1] = string.format(
			"|-%s\n| %s || %s || %.2f%% || %d || %d || %s\n",
			rowStyle,
			dateString,
			company.name,
			normalizedShare * 100,
			prevE,
			currE,
			changeType
		)
	end

	local totalUnemployed = population - totalEmployed
	if totalUnemployed < 0 then totalUnemployed = 0 end

	out[#out + 1] = string.format("|-\n! colspan=\"4\" | Total Employed || %d || \n", totalEmployed)
	out[#out + 1] = string.format("|-\n! colspan=\"4\" | Total Unemployed || %d || \n", totalUnemployed)
	out[#out + 1] = "|}"

	return table.concat(out)
end

return p