Module:Sandbox/KickahaOta/QueryDistances: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
(More progress)
(Lots more progress)
Line 3: Line 3:
local m_cargo = require('Module:Cargo')
local m_cargo = require('Module:Cargo')


local m_tablecalc = {}
local m_calculate = {}


function m_tablecalc.computedistance(query_results, args)
function m_calculate.calculate_distances(query_results, args)
-- Takes 2 arguments:
--[[
--  query_results - table returned from m_cargo.query
Computes and inserts a new column into a cargo query results table. This column will contain the distance between each
--   args
   row in the table and a specified origin row. (The distance value will be an integer. The specified origin row
--    idfield - string; the name of the field containing a row's id for connection purposes (for example, "passive_skills.id")
  will have a distance value of 0; the rows connected to that row will have a distance value of 1; the rows
--    connectionidsfield - string: the name of the field containing the ids of the rows that a given row connects to
  connected to those rows will have a distance value of 2; and so on.)
--        (for example, "passive_skills.connections")
--    connectionidsdelimiter - string; the delimeter separating ids in the connectiond id field (for example, ",")
--    originrowid - string; the id of a row that should be used as the starting point of the calculation. (For example,
--      "AscendancyBerserkerStart" to start at the starting node in the Berserker's Ascendancy tree.)
--    distancefield - string; the name of the field that should hold each row's calculated distance from the starting
--      point. (This will be an integer. The row identified by originrowid will have a distance value of 0; the rows
--      connected to that row will have a distance value of 1; the rows connected to those rows will have a distance value
--      of 2; and so on.)
--    maxdistance - integer: The longest route we should calculate. For instance, if maxdistance = 10, then we will only
--      calculate distances to rows that can be reached from the origin row through 10 or fewer connections. Any nodes
--      that can't be reached within this limit will wind up with the distance field set to nil.
--    test - boolean; if true, does logging
-- Returns: A table that is a copy of the table in query_results, except that each row will have the field identified by
--  distancefield set to the calculated distance. The field will be created if it does not already exist in each row.


query_results = query_results or error("No results table supplied")
Takes 2 arguments:
local paramtest = args or error("No args supplied")
    query_results: table; required. Table returned from m_cargo.query
paramtest = args.idfield or error("no idfield supplied")
    args - table; required. Association table of arguments:
paramtest = args.connectionidsfield or error("no connectionidsfield supplied")
        id_field - string; required. The name of the field containing a row's id for connection purposes.
paramtest = args.distancefield or error("no distancefield supplied")
            Example: "passive_skills.id".
paramtest = args.originrowid or error("no originidrow supplied")
        connection_ids_field - string; required. The name of the field containing the ids of the rows that a given row connects to.
args.maxdistance = args.maxdistance or 99
            Example: "passive_skills.connections".
test = args.test or false
            The contents of this field can either be an array of ids or a string containing a delimited list of ids.
-- Start by creating three tables:
        connection_ids_delimiter - string; the delimeter separating ids in the connection_ids_field field.
--  calc_results: A copy of query_results, where we'll eventually insert the computed distances into each row.
            Example: ",".
--  calc_results_id_map: A table that maps each connection ID to the index of the row in calc_results with that ID.  
            This parameter is required if the connection_ids_field field contains a string. It is optional and ignored
--  calculables: A queue table containing the indexes of rows in calcResults where we now know the distance for that row, but we
              if the field contains an array.
--    haven't yet used that knowledge to set the distances of the other rows connected to it. At first, this should contain
        origin_row_id - string; required. The id of a row that should be used as the starting point of the calculation.
--    a single index -- the index of the row identified by originrowid.
            Example: "AscendancyBerserkerStart" to start at the starting node in the Berserker's Ascendancy tree.
        distance_field - string; required. the name of the field that should be set to each row's calculated distance from
local calc_results = {}
              the starting point.
local calc_results_id_map = {}
            Examples: "distance"; "passive_skills.connection_distance".
local calculables = {}
        max_distance - integer; optional. The longest route we should calculate.
            For instance, if max_distance = 10, then we will only calculate distances to rows that can be reached from the
for i, row in ipairs(query_results) do
              origin row through 10 or fewer connections. Any nodes that can't be reached within this limit will wind up with
calc_results[i] = row
              the distance field set to nil.
local rowid = calc_results[i][args.idfield]
            By default, max_distance is 99. This typically means that any rows that have any connection at all to the
if rowid == nil then
              origin row will have their distance fields set to non-nil.
error("No idfield")
            Example: 10
end
      test - boolean; optional. If true, does logging. Defaults to false.
if calc_results_id_map[rowid] ~= nil then
      Any additional arguments accepted by remove_unwanted_rows.
if test then mw.log(string.format("Warning: idfield not unique: idfield='%s', rowid='%s', previous rowid='%s'", args.idfield, rowid, calc_results_id_map[rowid])) end
Returns: A table that is a copy of the table in query_results, except that each row will have the field identified by
else
  distance_field set to the calculated distance. The field will be created if it does not already exist in each row.
calc_results_id_map[rowid] = i
]]
end
 
if rowid == args.originrowid then
    query_results = query_results or error("No results table supplied")
calc_results[i][args.distancefield] = 0
    local param_test = args or error("No args supplied")
table.insert(calculables, i)
    param_test = args.id_field or error("no id_field supplied")
else
    param_test = args.connection_ids_field or error("no connection_ids_field supplied")
calc_results[i][args.distancefield] = nil
    param_test = args.distance_field or error("no distance_field supplied")
end
    param_test = args.origin_row_id or error("no originidrow supplied")
end
    args.max_distance = args.max_distance or 99
    test = args.test or false
if #calculables == 0 then
 
error("Origin row not found")
   
elseif #calculables > 1 then
-- Start by creating three tables:
error("Multiple origin rows found")
--  calc_results: A copy of query_results, where we'll eventually insert the computed distances into each row.
end
--  calc_results_id_map: A table that maps each connection ID to the index of the row in calc_results with that ID.  
--  calculables: A queue table containing the indexes of rows in calcResults where we now know the distance for that row, but we
-- Now we start processing by grabbing the next row from the queue
--    haven't yet used that knowledge to set the distances of the other rows connected to it. At first, this should contain
while #calculables > 0 do
--    a single index -- the index of the row identified by origin_row_id.
local idx = table.remove(calculables, 1)
   
local id = calc_results[idx][args.idfield]
    local calc_results = {}
local distance_to_this=calc_results[idx][args.distancefield]
    local calc_results_id_map = {}
    local calculables = {}
if test then mw.log(string.format("Processing node %s with distance %d", id, distance_to_this)) end
   
-- Check each connected node to see if our current node is the most efficient route we've found to that other node
    for i, row in ipairs(query_results) do
local outgoing_ids = calc_results[idx][args.connectionidsfield]
        calc_results[i] = row
if type(outgoing_ids) == "string" then
        local rowid = calc_results[i][args.id_field]
paramtest = args.connectionidsdelimiter or error("no connectionidsdelimiter supplied")
        if rowid == nil then
outgoing_ids = m_util.string.split(outgoing_ids, args.connectionidsdelimiter)
            error("No id_field")
end
        end
if type(outgoing_ids) ~= "table" then
        if calc_results_id_map[rowid] ~= nil then
error(string.format("Unexpected outgoing id data type '%s' in field '%s' for id '%s'", type(outgoing_ids), args.connectionidsfield, calc_results[idx][args.idfield]))
            if test then mw.log(string.format("Warning: id_field not unique: id_field='%s', rowid='%s', previous rowid='%s'", args.id_field, rowid, calc_results_id_map[rowid])) end
end
        else
            calc_results_id_map[rowid] = i
for _,outgoing_id in ipairs(outgoing_ids) do
        end
local idx_connected_row = calc_results_id_map[outgoing_id]
        if rowid == args.origin_row_id then
if idx_connected_row == nil then
            calc_results[i][args.distance_field] = 0
if test then mw.log(string.format("Outgoing connection %s for row %s not found, ignoring", outgoing_id, calc_results[idx][args.idfield])) end
            table.insert(calculables, i)
else
        else
connected_row_current_distance = calc_results[idx_connected_row][args.distancefield]
            calc_results[i][args.distance_field] = nil
-- Does the connected node have no distance computed yet? Then the route through the current node is the first
        end
--  route we've found to the connected node; so by definition it's the most efficient we've found.
    end
-- Does the connected node have a distance computed that's higher than our current node's distance + 1? Then
   
--  we've found a more efficient route than the previous one(s) we found, and we should update it.
    if #calculables == 0 then
-- Does the connected node have a distance computed that's lower than that? Then we already found a more
        error("Origin row not found")
--  efficient route to that node, and we should leave it alone.
    elseif #calculables > 1 then
local isbetter = false
        error("Multiple origin rows found")
if (connected_row_current_distance == nil) then
    end
isbetter = true
   
if test then mw.log(string.format("Found first route to %s with distance %d", outgoing_id, distance_to_this + 1)) end
-- Now we start processing by grabbing the next row from the queue
elseif (connected_row_current_distance > distance_to_this + 1) then
    while #calculables > 0 do
isbetter = true
        local idx = table.remove(calculables, 1)
if test then mw.log(string.format("Found better route to %s, reducing distance from %d to %d", outgoing_id, connected_row_current_distance, distance_to_this + 1)) end
        local id = calc_results[idx][args.id_field]
else
        local distance_to_this=calc_results[idx][args.distance_field]
if test then mw.log(string.format("Found route to %s with distance %d, but prior route has better distance %d", outgoing_id, distance_to_this + 1, connected_row_current_distance)) end
       
end
        if test then mw.log(string.format("Processing node %s with distance %d", id, distance_to_this)) end
if isbetter then
 
calc_results[idx_connected_row][args.distancefield] = distance_to_this + 1
        -- Check each connected node to see if our current node is the most efficient route we've found to that other node
-- Add the connected node to the calculables queue, so that we (re)check the nodes connected to it
        local outgoing_ids = calc_results[idx][args.connection_ids_field]
if (distance_to_this + 1) < args.maxdistance then
        if type(outgoing_ids) == "string" then
table.insert(calculables, idx_connected_row)
            param_test = args.connection_ids_delimiter or error("no connection_ids_delimiter supplied")
else
            outgoing_ids = m_util.string.split(outgoing_ids, args.connection_ids_delimiter)
if test then mw.log(string.format("Not checking %s because maxdistance reached", outgoing_id)) end
        end
end
        if type(outgoing_ids) ~= "table" then
end
            error(string.format("Unexpected outgoing id data type '%s' in field '%s' for id '%s'", type(outgoing_ids), args.connection_ids_field, calc_results[idx][args.id_field]))
end
        end
end
       
end
        for _,outgoing_id in ipairs(outgoing_ids) do
return m_tablecalc.removeunneededrows(calc_results,args)
            local idx_connected_row = calc_results_id_map[outgoing_id]
            if idx_connected_row == nil then
                if test then mw.log(string.format("Outgoing connection %s for row %s not found, ignoring", outgoing_id, calc_results[idx][args.id_field])) end
            else
                connected_row_current_distance = calc_results[idx_connected_row][args.distance_field]
                -- Does the connected node have no distance computed yet? Then the route through the current node is the first
                --  route we've found to the connected node; so by definition it's the most efficient we've found.
                -- Does the connected node have a distance computed that's higher than our current node's distance + 1? Then
                --  we've found a more efficient route than the previous one(s) we found, and we should update it.
                -- Does the connected node have a distance computed that's lower than that? Then we already found a more
                --  efficient route to that node, and we should leave it alone.
                local is_better = false
                if (connected_row_current_distance == nil) then
                    is_better = true
                    if test then mw.log(string.format("Found first route to %s with distance %d", outgoing_id, distance_to_this + 1)) end
                elseif (connected_row_current_distance > distance_to_this + 1) then
                    is_better = true
                    if test then mw.log(string.format("Found better route to %s, reducing distance from %d to %d", outgoing_id, connected_row_current_distance, distance_to_this + 1)) end
                else
                    if test then mw.log(string.format("Found route to %s with distance %d, but prior route has better distance %d", outgoing_id, distance_to_this + 1, connected_row_current_distance)) end
                end
                if is_better then
                    calc_results[idx_connected_row][args.distance_field] = distance_to_this + 1
                -- Add the connected node to the calculables queue, so that we (re)check the nodes connected to it
                    if (distance_to_this + 1) < args.max_distance then
                        table.insert(calculables, idx_connected_row)
                    else
                        if test then mw.log(string.format("Not checking %s because max_distance reached", outgoing_id)) end
                    end
                end
            end
        end
    end
    return m_calculate.remove_unwanted_rows(calc_results,args)
end
end


function m_tablecalc.removeunneededrows(calc_results,args)
function m_calculate.remove_unwanted_rows(calc_results,args)
calc_results = calc_results or error("No results table supplied")
    calc_results = calc_results or error("No results table supplied")
local paramtest = args or error("No args supplied")
    local param_test = args or error("No args supplied")
paramtest = args.distancefield or error("no distancefield supplied")
    param_test = args.distance_field or error("no distance_field supplied")
paramtest = args.idfield or error("no idfield specified")
    param_test = args.id_field or error("no id_field specified")
test = args.test or false
    test = args.test_remove or false


local stripped_calc_results = {}
    local stripped_calc_results = {}
for i, row in ipairs(calc_results) do
    for i, row in ipairs(calc_results) do
if args.removenildistances and row[args.distancefield] == nil then
        local rowid = row[args.id_field] or tostring(i)
-- if test then mw.log(string.format("Removing row %s with nil distance", row[args.idfield] or tostring(i))) end
        local keep = true
elseif (args.removeorigin or args.removezero) and row[args.distancefield] == 0 then
        if args.remove_nil_distances and row[args.distance_field] == nil then
-- if test then mw.log(string.format("Removing row %s with zero distance", row[args.idfield] or tostring(i))) end
            keep = false
else
            if test then mw.log(string.format("Removing row %s with nil distance", rowid)) end
stripped_calc_results[#stripped_calc_results+1] = row
        end
-- if test then mw.log(string.format("Keeping row %s with distance %d", row[args.idfield] or tostring(i), row[args.distancefield])) end
        if keep and (args.remove_origin or args.remove_zero) and row[args.distance_field] == 0 then
end
            keep = false
end
            if test then mw.log(string.format("Removing row %s with zero distance", rowid)) end
return stripped_calc_results
        end
        if keep and args.field_required_values then
            local field_required_values = {}
            if type(args.field_required_values) == 'table' then
                field_required_values = args.field_required_values
            elseif type(args.field.required_values) == 'string' then
                field_required_values = m_util.string.splitargs(args.field_required_values)
            else
                error(string.format("Unexpected field_required_values type '%s'", tostring(type(args.field_required_values))))
            end
            for key, val in pairs(field_required_values) do
                if tostring(row[key]) ~= tostring(val) then
                    if test and keep then
                        mw.log(string.format("Removing row %s with %s value %s (not %s)",
                            rowid, key, tostring(row[key]), tostring(val)))
                    end
                    keep = false
                    break
                end
            end
        end
        if keep and args.target_ids then
            keep = false
            for _, target_id in ipairs(args.target_ids) do
                if tostring(rowid) == tostring(target_id) then
                    keep = true
                    break
                end
            end
            if test and not keep then mw.log(string.format("Removing row %s not in target ID list", tostring(rowid))) end
        end
        if keep then
            stripped_calc_results[#stripped_calc_results+1] = row
            if test then mw.log(string.format("Keeping row %s with distance %s", tostring(rowid), tostring(row[args.distance_field]))) end
        end
    end
    return stripped_calc_results
end
end


function m_calculate.testascendancy(test_calc, test_remove)
    local results = m_cargo.query(
        {'passive_skills'},
        {'passive_skills.name', 'passive_skills.id', 'passive_skills.connections', 'passive_skills.ascendancy_class', 'CONCAT("")=distance'},
        {where='passive_skills.ascendancy_class="Berserker"', order_by='passive_skills.id'}
    )
    local calc_args = {
        id_field = "passive_skills.id",
        connection_ids_field="passive_skills.connections",
        connection_ids_delimiter=",",
        origin_row_id="AscendancyBerserkerStart",
        distance_field="distance",
        test = test_calc or false,
        test_remove = test_remove or false,
    }
    local calc_results = m_calculate.calculate_distances(results, calc_args)
    return calc_results
end
function m_calculate.testwitch(test_calc, test_remove)
    local results = m_cargo.query(
        {'passive_skills'},
        {'passive_skills.name', 'passive_skills.id', 'passive_skills.connections', 'passive_skills.ascendancy_class', 'CONCAT("")=distance'},
        {where='id NOT LIKE "%jewel_slot%" AND connections HOLDS LIKE "%" AND (ascendancy_class="" OR ascendancy_class IS NULL)', order_by='passive_skills.id'}
    )
    local calc_args = {
        id_field = "passive_skills.id",
        connection_ids_field="passive_skills.connections",
        connection_ids_delimiter=",",
        origin_row_id="witch595",
        distance_field="distance",
        test = test_calc or false,
        test_remove = test_remove or false,
        max_distance = 10,
        remove_nil_distances=true,
        remove_origin=true,
    }
    local calc_results = m_calculate.calculate_distances(results, calc_args)
    return calc_results
end


function m_tablecalc.testascendancy(test)
function m_calculate.testmarauder(test_calc, test_remove)
local results = m_cargo.query(
    local results = m_cargo.query(
{'passive_skills'},
        {'passive_skills'},
{'passive_skills.name', 'passive_skills.id', 'passive_skills.connections', 'passive_skills.ascendancy_class', 'CONCAT("")=distance'},
        {'passive_skills.name', 'passive_skills.id', 'passive_skills.connections', 'passive_skills.ascendancy_class', 'passive_skills.is_notable', 'CONCAT("")=distance'},
{where='passive_skills.ascendancy_class="Berserker"', order_by='passive_skills.id'}
        {where='id NOT LIKE "%jewel_slot%" AND connections HOLDS LIKE "%" AND (ascendancy_class="" OR ascendancy_class IS NULL)', order_by='passive_skills.id'}
)
    )
local calc_args = {
    local calc_args = {
idfield = "passive_skills.id",
        id_field = "passive_skills.id",
connectionidsfield="passive_skills.connections",
        connection_ids_field="passive_skills.connections",
connectionidsdelimiter=",",
        connection_ids_delimiter=",",
originrowid="AscendancyBerserkerStart",
        origin_row_id="marauder594",
distancefield="distance",
        distance_field="distance",
test = test or false
        test = test_calc or false,
}
        test_remove = test_remove or false,
local calc_results = m_tablecalc.computedistance(results, calc_args)
        max_distance = 30,
return calc_results
        field_required_values = {
            ["passive_skills.is_notable"] = 1,
        }
    }
    local calc_results = m_calculate.calculate_distances(results, calc_args)
    return calc_results
end
end


function m_tablecalc.testwitch(test)
function m_calculate.testtargets(test_calc, test_remove)
local results = m_cargo.query(
    local results = m_cargo.query(
{'passive_skills'},
        {'passive_skills'},
{'passive_skills.name', 'passive_skills.id', 'passive_skills.connections', 'passive_skills.ascendancy_class', 'CONCAT("")=distance'},
        {'passive_skills.name', 'passive_skills.id', 'passive_skills.connections', 'passive_skills.ascendancy_class', 'passive_skills.is_notable', 'CONCAT("")=distance'},
{where='id NOT LIKE "%jewel_slot%" AND connections HOLDS LIKE "%" AND (ascendancy_class="" OR ascendancy_class IS NULL)', order_by='passive_skills.id'}
        {where='id NOT LIKE "%jewel_slot%" AND connections HOLDS LIKE "%" AND (ascendancy_class="" OR ascendancy_class IS NULL)', order_by='passive_skills.id'}
)
    )
local calc_args = {
    local calc_args = {
idfield = "passive_skills.id",
        id_field = "passive_skills.id",
connectionidsfield="passive_skills.connections",
        connection_ids_field="passive_skills.connections",
connectionidsdelimiter=",",
        connection_ids_delimiter=",",
originrowid="witch595",
        origin_row_id="melee_damage687",
distancefield="distance",
        distance_field="distance",
test = test or false,
        test = test_calc or false,
maxdistance=10,
        test_remove = test_remove or false,
removenildistances=true,
        max_distance = 30,
removeorigin=true,
        target_ids = { "marauder594", "witch595", "ranger596", "duelist597", "templar598", "six704", "seven1490" },
}
    }
local calc_results = m_tablecalc.computedistance(results, calc_args)
    local calc_results = m_calculate.calculate_distances(results, calc_args)
return calc_results
    return calc_results
end
end


return m_tablecalc
return m_calculate

Revision as of 19:50, 19 December 2021

Module documentation[create] [purge]
local getArgs = require('Module:Arguments').getArgs
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')

local m_calculate = {}

function m_calculate.calculate_distances(query_results, args)
--[[
Computes and inserts a new column into a cargo query results table. This column will contain the distance between each
  row in the table and a specified origin row. (The distance value will be an integer. The specified origin row
  will have a distance value of 0; the rows connected to that row will have a distance value of 1; the rows
  connected to those rows will have a distance value of 2; and so on.)

Takes 2 arguments:
    query_results: table; required. Table returned from m_cargo.query
    args - table; required. Association table of arguments:
        id_field - string; required. The name of the field containing a row's id for connection purposes.
            Example: "passive_skills.id".
        connection_ids_field - string; required. The name of the field containing the ids of the rows that a given row connects to.
            Example: "passive_skills.connections".
            The contents of this field can either be an array of ids or a string containing a delimited list of ids.
        connection_ids_delimiter - string; the delimeter separating ids in the connection_ids_field field.
            Example: ",".
            This parameter is required if the connection_ids_field field contains a string. It is optional and ignored
              if the field contains an array.
        origin_row_id - string; required. The id of a row that should be used as the starting point of the calculation.
            Example: "AscendancyBerserkerStart" to start at the starting node in the Berserker's Ascendancy tree.
        distance_field - string; required. the name of the field that should be set to each row's calculated distance from
              the starting point. 
            Examples: "distance"; "passive_skills.connection_distance".
        max_distance - integer; optional. The longest route we should calculate.
            For instance, if max_distance = 10, then we will only calculate distances to rows that can be reached from the
              origin row through 10 or fewer connections. Any nodes that can't be reached within this limit will wind up with
              the distance field set to nil.
            By default, max_distance is 99. This typically means that any rows that have any connection at all to the
              origin row will have their distance fields set to non-nil.
            Example: 10
       test - boolean; optional. If true, does logging. Defaults to false.
       Any additional arguments accepted by remove_unwanted_rows.
Returns: A table that is a copy of the table in query_results, except that each row will have the field identified by
  distance_field set to the calculated distance. The field will be created if it does not already exist in each row.
]]

    query_results = query_results or error("No results table supplied")
    local param_test = args or error("No args supplied")
    param_test = args.id_field or error("no id_field supplied")
    param_test = args.connection_ids_field or error("no connection_ids_field supplied")
    param_test = args.distance_field or error("no distance_field supplied")
    param_test = args.origin_row_id or error("no originidrow supplied")
    args.max_distance = args.max_distance or 99
    test = args.test or false

    
-- Start by creating three tables:
--   calc_results: A copy of query_results, where we'll eventually insert the computed distances into each row.
--   calc_results_id_map: A table that maps each connection ID to the index of the row in calc_results with that ID. 
--   calculables: A queue table containing the indexes of rows in calcResults where we now know the distance for that row, but we
--     haven't yet used that knowledge to set the distances of the other rows connected to it. At first, this should contain
--     a single index -- the index of the row identified by origin_row_id.
    
    local calc_results = {}
    local calc_results_id_map = {}
    local calculables = {}
    
    for i, row in ipairs(query_results) do
        calc_results[i] = row
        local rowid = calc_results[i][args.id_field]
        if rowid == nil then
            error("No id_field")
        end
        if calc_results_id_map[rowid] ~= nil then
            if test then mw.log(string.format("Warning: id_field not unique: id_field='%s', rowid='%s', previous rowid='%s'", args.id_field, rowid, calc_results_id_map[rowid])) end
        else
            calc_results_id_map[rowid] = i
        end
        if rowid == args.origin_row_id then
            calc_results[i][args.distance_field] = 0
            table.insert(calculables, i)
        else
            calc_results[i][args.distance_field] = nil
        end
    end
    
    if #calculables == 0 then
        error("Origin row not found")
    elseif #calculables > 1 then
        error("Multiple origin rows found")
    end
    
-- Now we start processing by grabbing the next row from the queue
    while #calculables > 0 do
        local idx = table.remove(calculables, 1)
        local id = calc_results[idx][args.id_field]
        local distance_to_this=calc_results[idx][args.distance_field]
        
        if test then mw.log(string.format("Processing node %s with distance %d", id, distance_to_this)) end

        -- Check each connected node to see if our current node is the most efficient route we've found to that other node
        local outgoing_ids = calc_results[idx][args.connection_ids_field]
        if type(outgoing_ids) == "string" then
            param_test = args.connection_ids_delimiter or error("no connection_ids_delimiter supplied")
            outgoing_ids = m_util.string.split(outgoing_ids, args.connection_ids_delimiter)
        end
        if type(outgoing_ids) ~= "table" then
            error(string.format("Unexpected outgoing id data type '%s' in field '%s' for id '%s'", type(outgoing_ids), args.connection_ids_field, calc_results[idx][args.id_field]))
        end
        
        for _,outgoing_id in ipairs(outgoing_ids) do
            local idx_connected_row = calc_results_id_map[outgoing_id]
            if idx_connected_row == nil then
                if test then mw.log(string.format("Outgoing connection %s for row %s not found, ignoring", outgoing_id, calc_results[idx][args.id_field])) end
            else
                connected_row_current_distance = calc_results[idx_connected_row][args.distance_field]
                -- Does the connected node have no distance computed yet? Then the route through the current node is the first
                --   route we've found to the connected node; so by definition it's the most efficient we've found.
                -- Does the connected node have a distance computed that's higher than our current node's distance + 1? Then
                --   we've found a more efficient route than the previous one(s) we found, and we should update it.
                -- Does the connected node have a distance computed that's lower than that? Then we already found a more
                --   efficient route to that node, and we should leave it alone.
                local is_better = false
                if (connected_row_current_distance == nil) then
                    is_better = true
                    if test then mw.log(string.format("Found first route to %s with distance %d", outgoing_id, distance_to_this + 1)) end
                elseif (connected_row_current_distance > distance_to_this + 1) then
                    is_better = true
                    if test then mw.log(string.format("Found better route to %s, reducing distance from %d to %d", outgoing_id, connected_row_current_distance, distance_to_this + 1)) end
                else
                    if test then mw.log(string.format("Found route to %s with distance %d, but prior route has better distance %d", outgoing_id, distance_to_this + 1, connected_row_current_distance)) end
                end
                if is_better then
                    calc_results[idx_connected_row][args.distance_field] = distance_to_this + 1
                -- Add the connected node to the calculables queue, so that we (re)check the nodes connected to it
                    if (distance_to_this + 1) < args.max_distance then
                        table.insert(calculables, idx_connected_row)
                    else
                        if test then mw.log(string.format("Not checking %s because max_distance reached", outgoing_id)) end
                    end
                end
            end
        end
    end
    return m_calculate.remove_unwanted_rows(calc_results,args)	
end

function m_calculate.remove_unwanted_rows(calc_results,args)
    calc_results = calc_results or error("No results table supplied")
    local param_test = args or error("No args supplied")
    param_test = args.distance_field or error("no distance_field supplied")
    param_test = args.id_field or error("no id_field specified")
    test = args.test_remove or false

    local stripped_calc_results = {}
    for i, row in ipairs(calc_results) do
        local rowid = row[args.id_field] or tostring(i)
        local keep = true
        if args.remove_nil_distances and row[args.distance_field] == nil then
            keep = false
            if test then mw.log(string.format("Removing row %s with nil distance", rowid)) end
        end
        if keep and (args.remove_origin or args.remove_zero) and row[args.distance_field] == 0 then
            keep = false
            if test then mw.log(string.format("Removing row %s with zero distance", rowid)) end
        end
        if keep and args.field_required_values then
            local field_required_values = {}
            if type(args.field_required_values) == 'table' then
                field_required_values = args.field_required_values
            elseif type(args.field.required_values) == 'string' then
                field_required_values = m_util.string.splitargs(args.field_required_values)
            else
                error(string.format("Unexpected field_required_values type '%s'", tostring(type(args.field_required_values))))
            end
            for key, val in pairs(field_required_values) do
                if tostring(row[key]) ~= tostring(val) then
                    if test and keep then 
                        mw.log(string.format("Removing row %s with %s value %s (not %s)", 
                            rowid, key, tostring(row[key]), tostring(val)))
                    end
                    keep = false
                    break
                end
            end
        end
        if keep and args.target_ids then
            keep = false
            for _, target_id in ipairs(args.target_ids) do
                if tostring(rowid) == tostring(target_id) then
                    keep = true
                    break
                end
            end
            if test and not keep then mw.log(string.format("Removing row %s not in target ID list", tostring(rowid))) end
        end	
        if keep then
            stripped_calc_results[#stripped_calc_results+1] = row
            if test then mw.log(string.format("Keeping row %s with distance %s", tostring(rowid), tostring(row[args.distance_field]))) end
        end
    end
    return stripped_calc_results
end	

function m_calculate.testascendancy(test_calc, test_remove)
    local results = m_cargo.query(
        {'passive_skills'},
        {'passive_skills.name', 'passive_skills.id', 'passive_skills.connections', 'passive_skills.ascendancy_class', 'CONCAT("")=distance'},
        {where='passive_skills.ascendancy_class="Berserker"', order_by='passive_skills.id'}
    )
    local calc_args = {
        id_field = "passive_skills.id",
        connection_ids_field="passive_skills.connections",
        connection_ids_delimiter=",",
        origin_row_id="AscendancyBerserkerStart",
        distance_field="distance",
        test = test_calc or false,
        test_remove = test_remove or false,
    }
    local calc_results = m_calculate.calculate_distances(results, calc_args)
    return calc_results
end

function m_calculate.testwitch(test_calc, test_remove)
    local results = m_cargo.query(
        {'passive_skills'},
        {'passive_skills.name', 'passive_skills.id', 'passive_skills.connections', 'passive_skills.ascendancy_class', 'CONCAT("")=distance'},
        {where='id NOT LIKE "%jewel_slot%" AND connections HOLDS LIKE "%" AND (ascendancy_class="" OR ascendancy_class IS NULL)', order_by='passive_skills.id'}
    )
    local calc_args = {
        id_field = "passive_skills.id",
        connection_ids_field="passive_skills.connections",
        connection_ids_delimiter=",",
        origin_row_id="witch595",
        distance_field="distance",
        test = test_calc or false,
        test_remove = test_remove or false,
        max_distance = 10,
        remove_nil_distances=true,
        remove_origin=true,
    }
    local calc_results = m_calculate.calculate_distances(results, calc_args)
    return calc_results
end

function m_calculate.testmarauder(test_calc, test_remove)
    local results = m_cargo.query(
        {'passive_skills'},
        {'passive_skills.name', 'passive_skills.id', 'passive_skills.connections', 'passive_skills.ascendancy_class', 'passive_skills.is_notable', 'CONCAT("")=distance'},
        {where='id NOT LIKE "%jewel_slot%" AND connections HOLDS LIKE "%" AND (ascendancy_class="" OR ascendancy_class IS NULL)', order_by='passive_skills.id'}
    )
    local calc_args = {
        id_field = "passive_skills.id",
        connection_ids_field="passive_skills.connections",
        connection_ids_delimiter=",",
        origin_row_id="marauder594",
        distance_field="distance",
        test = test_calc or false,
        test_remove = test_remove or false,
        max_distance = 30,
        field_required_values = {
            ["passive_skills.is_notable"] = 1,
        }
    }
    local calc_results = m_calculate.calculate_distances(results, calc_args)
    return calc_results
end

function m_calculate.testtargets(test_calc, test_remove)
    local results = m_cargo.query(
        {'passive_skills'},
        {'passive_skills.name', 'passive_skills.id', 'passive_skills.connections', 'passive_skills.ascendancy_class', 'passive_skills.is_notable', 'CONCAT("")=distance'},
        {where='id NOT LIKE "%jewel_slot%" AND connections HOLDS LIKE "%" AND (ascendancy_class="" OR ascendancy_class IS NULL)', order_by='passive_skills.id'}
    )
    local calc_args = {
        id_field = "passive_skills.id",
        connection_ids_field="passive_skills.connections",
        connection_ids_delimiter=",",
        origin_row_id="melee_damage687",
        distance_field="distance",
        test = test_calc or false,
        test_remove = test_remove or false,
        max_distance = 30,
        target_ids = { "marauder594", "witch595", "ranger596", "duelist597", "templar598", "six704", "seven1490" },
    }
    local calc_results = m_calculate.calculate_distances(results, calc_args)
    return calc_results
end

return m_calculate