-- EDO Approximations Module -- Calculates EDO approximations for just intervals -- Usage: Script error: No such module "EDO_Approximations".

local p = {}

-- Convert a frequency ratio to cents local function cents(ratio)

   return 1200 * math.log(ratio, 2)

end

-- Parse a ratio string like '3/2' into a number local function parse_ratio(ratio_str)

   local num, denom = ratio_str:match("^(%d+)/(%d+)$")
   if not num or not denom then
       return nil
   end
   return tonumber(num) / tonumber(denom)

end

-- Find the best approximation of an interval in a given EDO local function find_best_approximation(ratio_cents, edo)

   local edostep = 1200 / edo
   -- Find the nearest step
   local best_step = math.floor(ratio_cents / edostep + 0.5)
   local approximation_cents = best_step * edostep
   local absolute_error = approximation_cents - ratio_cents
   local relative_error = (absolute_error / edostep) * 100
   return best_step, absolute_error, relative_error

end

-- Calculate all EDO approximations within tolerance for a given ratio local function calculate_edo_approximations(ratio, tolerance, min_edo, max_edo)

   local ratio_cents = cents(ratio)
   local results = {}
   for edo = min_edo, max_edo do
       local steps, abs_error, rel_error = find_best_approximation(ratio_cents, edo)
       if math.abs(rel_error) <= tolerance then
           table.insert(results, {
               edo = edo,
               steps = steps,
               abs_error = abs_error,
               rel_error = rel_error
           })
       end
   end
   return results

end

-- Format a number with sign and 2 decimal places local function format_error(value)

   if value >= 0 then
       return string.format("+%.2f", value)
   else
       return string.format("%.2f", value)
   end

end

-- Main function to generate the wikitable function p.main(frame)

   -- Get parameters from template invocation
   local args = frame.args
   local interval_str = args.interval or args[1]
   local tolerance = tonumber(args.tolerance) or 9.0
   local min_edo = tonumber(args.min_edo) or 10
   local max_edo = tonumber(args.max_edo) or 60
   -- Validate and parse interval
   if not interval_str then
       return "Error: No interval specified"
   end
   local ratio = parse_ratio(interval_str)
   if not ratio then
       return "Error: Invalid interval format (use format like '3/2')"
   end
   -- Calculate approximations
   local results = calculate_edo_approximations(ratio, tolerance, min_edo, max_edo)
   if #results == 0 then
       return "No EDOs found within tolerance of " .. tolerance .. "%"
   end
   -- Build the wikitable
   local output = {}
   table.insert(output, '{| class="wikitable"')
   table.insert(output, '|+ EDO Approximations for ' .. interval_str)
   table.insert(output, '|-')
   table.insert(output, '! EDO !! Step size !! Absolute Error (¢) !! Relative Error (%)')
   for _, result in ipairs(results) do
       local edo_link = string.format("[[%dedo|%d]]", result.edo, result.edo)
       local step_size = string.format("%d\\%d", result.steps, result.edo)
       local abs_err = format_error(result.abs_error)
       local rel_err = format_error(result.rel_error)
       table.insert(output, '|-')
       table.insert(output, string.format('| %s || %s || %s || %s', edo_link, step_size, abs_err, rel_err))
   end
   table.insert(output, '|}')
   return table.concat(output, '\n')

end

return p