Module:ServicesRecipesData

--- --- Module for compiling services recipes information from wiki data sources. --- Restructures the flat data tables that are produced from CsvUtils to make --- them more conducive to Lua methods that need to display the information. --- --- The standard way of using this module is a call like the following: --- --- goodID = ServicesRecipesData.getServiceGoodID(serviceName) --- --- This will return the ID of the good that is used to satisfy the service --- need. It is preferable to call getter methods with the ID of the service --- rather than retrieving the entire record for the recipe, so that your code --- stays protected from variations in this module. --- --- * serviceID = ServicesRecipesData.getServiceRecipeID(serviceName) --- * gradeStars = ServicesRecipesData.getServiceRecipeGrade(serviceName) --- * goodID = ServicesRecipesData.getServiceGoodID(serviceName) --- --- The most specific way of retrieving data that does not return a table is --- the preferred way of retrieving the data, because it is the most protected --- from changes in this module. --- --- As a last resort, you may use methods that begin with "getAll..." that --- return tables of data (that is, other than the ones that get lists of --- recipeIDs), but as mentioned they are less preferable, because the internal --- workings of this module may change, and these tables may suddenly change --- their structure, ruining your code. When in doubt, take the extra time to --- write code that uses one of the specific getter methods (that begin with --- "getRecipe..."). --- --- The data table for these services recipes has the following structure: --- --- recipesTable = { ---		["recipe1_ID"] = { --- 		["id"] = "recipe1_ID", --- 		["gradeID"] = 1, --- 		["serviceNeed"] = "Education" ---			["serviceGoods"] = { --- 			[1] = { --- 				["goodID"] = "good1_ID", --- 				["stackSize"] = 1 --- 			}, --- 			[2] = { ... } or missing if fewer --- 		} ---		}, ---		["recipe2_ID"] = { ---			... ---		}, --- 	["recipe3_ID"] = { --- 		... --- 	}, ---		... --- } --- --- @module ServicesRecipesData local ServicesRecipesData = {}

local CsvUtils = require("Module:CsvUtils")

--region Private member variables

--- Main data tables, like this: table[recipeID] = table containing recipe --- data local recipesTable

--- Lookup map. Built once and reused on subsequent calls within this session, --- like this: table[serviceName] = { recipeID, recipeID, ... } local mapNamesToIDs --- Lookup map. Built once and reused on subsequent calls within this session, --- like this: table[goodID] = { serviceNeed1, serviceNeed2, ... } local mapGoodIDsToRecipeIDs

--endregion

--region Private constants

local DATA_TEMPLATE_NAME = "Template:Institutions_Recipes_csv"

local INDEX_ID = "id" local INDEX_GRADE = "gradeID" local INDEX_SERVICE_NAME = "serviceNeed" local INDEX_GOODS = "serviceGoods"

local INDEX_GOODS_GOOD_ID = "goodID" local INDEX_GOODS_STACK_SIZE = "stackSize"

local PATTERN_SPLIT_STACK_AND_ID = "(%d+)%s([%[%]%s%a]+)" local PATTERN_CAPTURE_END_NUMBER = "Grade(%d)"

--endregion

--region Private methods

--- --- Transforms the originalRecipesTable returned from CSV processing to be more --- conducive to member functions looking up data. Converts text strings --- into subtables. Converts header row into field keys on every record and --- stores records by id rather than arbitrary integer. --- --- @param originalRecipesTable table of CSV-based data, with header row, data rows --- @param recipesHeaderLookup table lookup table of headers to get indexes --- @return table better structured with IDs as keys local function restructureRecipesTable(originalRecipesTable, recipesHeaderLookup)

-- A few constants we'll need only within this function. local DATA_ROWS = 2 local INDEX_ORIGINAL_ID = 1 local INDEX_ORIGINAL_GRADE = 2 local INDEX_ORIGINAL_SERVED_NEED = 3 local MAX_SERVICES_GOODS = 6 local LOOKUP_SERVICE_GOODS_BASE_STRING = "Good"

mapNamesToIDs = {} mapGoodIDsToRecipeIDs = {}

local newRecipeTable = {} for _, originalRecipe in ipairs(originalRecipesTable[DATA_ROWS]) do

local newRecipe = {}

newRecipe[INDEX_ID] = originalRecipe[INDEX_ORIGINAL_ID] newRecipe[INDEX_GRADE] = originalRecipe[INDEX_ORIGINAL_GRADE]:match(PATTERN_CAPTURE_END_NUMBER) newRecipe[INDEX_SERVICE_NAME] = originalRecipe[INDEX_ORIGINAL_SERVED_NEED]

local serviceGoods = {} for i = 1, MAX_SERVICES_GOODS do local goodIndex = recipesHeaderLookup[LOOKUP_SERVICE_GOODS_BASE_STRING .. i]			local stackSize, goodID = originalRecipe[goodIndex]:match(PATTERN_SPLIT_STACK_AND_ID)

if not goodID then break end

local newGood = { [INDEX_GOODS_GOOD_ID] = goodID, [INDEX_GOODS_STACK_SIZE] = stackSize }			table.insert(serviceGoods, newGood)

-- Populate the lookup map for goods. if not mapGoodIDsToRecipeIDs[goodID] then mapGoodIDsToRecipeIDs[goodID] = {} end table.insert(mapGoodIDsToRecipeIDs[goodID], newRecipe[INDEX_ID])

end

newRecipe[INDEX_GOODS] = serviceGoods

-- Populate the lookup map for services. if not mapNamesToIDs[ newRecipe[INDEX_SERVICE_NAME] ] then mapNamesToIDs[ newRecipe[INDEX_SERVICE_NAME] ] = {} end table.insert( mapNamesToIDs[ newRecipe[INDEX_SERVICE_NAME] ], newRecipe[INDEX_ID])

newRecipeTable[ newRecipe[INDEX_ID] ] = newRecipe end

return newRecipeTable end

--- --- Data loader function that uses the utility module and restructures the data --- to be easier to access for invoking methods and later method calls. This --- method is automatically called by all public member functions if the main --- data table has not yet been populated in the current session. local function loadData

-- Utility module retrieves the data as basic, flat lua tables. local originalRecipesTable, recipesHeaderLookup = CsvUtils.extractTables(DATA_TEMPLATE_NAME)

-- Now restructure to be more conducive. recipesTable = restructureRecipesTable(originalRecipesTable, recipesHeaderLookup) end

--endregion

--region Public methods

--- --- Returns the whole table of data for the specified recipe. Instead of this, --- you should probably be calling the individual getter methods. --- --- Throws an error if called with a nil or empty string. Returns nil if the --- specified recipe cannot be found. --- ---@param recipeID string the ID of the recipe ---@return table containing the data for the specified recipe, or nil if not found function ServicesRecipesData.getAllDataForRecipeByID(recipeID)

-- At runtime, this should never be nil or empty. if not recipeID or recipeID == "" then error("Parameter is nil or empty for recipe ID.") end

if not recipesTable then loadData end

return recipesTable[recipeID] end

--- --- Retrieve all IDs for recipes for the specified service. --- --- @param serviceName string the name of the service --- @return table the recipes that result in the product function ServicesRecipesData.getAllRecipeIDsForServiceName(serviceName)

-- At runtime, this should never be nil or empty. if not serviceName or serviceName == "" then error("Parameter is nil or empty for product ID.") end

if not recipesTable then loadData end

return mapNamesToIDs[serviceName] end

--- --- Retrieves all IDs for recipes for which the specified good is necessary. --- --- @param goodID string the ID of the ingredient (good) --- @return table the recipes that could use that ingredient function ServicesRecipesData.getAllRecipeIDsWithServiceGoodID(goodID)

-- At runtime, this should never be nil or empty. if not goodID or goodID == "" then error("Parameter is nil or empty for ingredient ID.") end

if not recipesTable then loadData end

return mapGoodIDsToRecipeIDs[goodID] end

--- --- Retrieves the grade code for the recipe specified by its ID. --- --- Returns nil if the recipe ID was not found. --- ---@param recipeID string the ID of the recipe ---@return string the code of the grade. function ServicesRecipesData.getRecipeGradeByID(recipeID)

local recipe = ServicesRecipesData.getAllDataForRecipeByID(recipeID)

if not recipe then return nil end

return recipe[INDEX_GRADE] end

--- --- Retrieves the service name for the recipe specified by its ID. --- --- Returns nil if the recipe ID was not found. --- ---@param recipeID string the ID of the recipe ---@return string the ID of the good produced by the recipe function ServicesRecipesData.getRecipeServiceNameByID(recipeID)

local recipe = ServicesRecipesData.getAllDataForRecipeByID(recipeID)

if not recipe then return nil end

return recipe[INDEX_SERVICE_NAME] end

--- --- Retrieves the subtable of data for ingredients for the recipe specified by --- its ID. --- --- Returns nil if the recipe ID was not found. --- ---@param recipeID string the ID of the recipe ---@return table of good IDs and stack sizes function ServicesRecipesData.getAllRecipeServiceGoodsByID(recipeID)

local recipe = ServicesRecipesData.getAllDataForRecipeByID(recipeID)

if not recipe then return nil end

return recipe[INDEX_GOODS] end

--- --- Retrieves the number of ingredients for the recipe specified by its ID. --- --- Returns nil if the recipe ID was not found. --- ---@param recipeID string the ID of the recipe ---@return number of ingredient groups function ServicesRecipesData.getRecipeNumberOfServiceGoodsByID(recipeID)

local recipe = ServicesRecipesData.getAllDataForRecipeByID(recipeID)

if not recipe then return nil end

return #recipe[INDEX_GOODS] end

--- --- Retrieves the specified service goods for the recipe specified by its ID. --- --- Returns nil if the recipe ID was not found. --- ---@param recipeID string the ID of the recipe ---@param goodIndex number which ingredient to retrieve ---@return string the ID of the good that is the specified option, or nil if none ---@return number the stack size of that good, or nil if none function ServicesRecipesData.getRecipeOptionByID(recipeID, goodIndex)

local recipe = ServicesRecipesData.getAllDataForRecipeByID(recipeID)

if not recipe then return nil end

local goods = ServicesRecipesData.getAllRecipeServiceGoodsByID(recipeID) if not goods then return nil end

local good = goods[goodIndex]

return good[INDEX_GOODS_GOOD_ID], good[INDEX_GOODS_STACK_SIZE] end

--endregion

return ServicesRecipesData