Module:JI ratios: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
Implement new new subgroup-search; only finds products so far
Ganaram inukshuk (talk | contribs)
m comments
 
(46 intermediate revisions by 2 users not shown)
Line 1: Line 1:
-- This module follows [[User:Ganaram inukshuk/Provisional style guide for Lua]]
local getArgs = require("Module:Arguments").getArgs
local med = require("Module:Mediants")
local rat = require("Module:Rational")
local rat = require("Module:Rational")
local tip = require("Module:Template input parse")
local utils = require("Module:Utils")
local utils = require("Module:Utils")
local tip = require("Module:Template input parse")
local med = require("Module:Mediants")
local yesno = require("Module:Yesno")
local yesno = require("Module:Yesno")
p = {}


-- TODO:
local p = {}


-- Template for handling multiple entry of JI ratios into a template, and for
-- Template for handling multiple entry of JI ratios into a template, and for
Line 12: Line 13:
-- This is a successor/replacement for JI ratio finder.
-- This is a successor/replacement for JI ratio finder.


-- JI ratios are searched by the following params in a hierarchy:
-- TODO: Refactor code such that:
-- - Search by prime limit. Int limit is used to limit the num/den of ratios.
-- - For int-limit search, int limit is the first arg, and equave and min/max
--  Prime limit takes precedence over subgroup.
--  cents default to 2/1, 0c, and 1200c respectively.
-- - Search by subgroup. (Subgroup may contain nonprime numbers, but ratios are
--   (int_limit, equave)
--  currently not supported.) Int limit is used to limit the num/den of ratios.
--  (int_limit, min_cents, max_cents)
-- - If neither prime limit or subgroup is present, search by int limit. This
-- - For odd-limit search, odd limit is the first arg, int limit defaults to
--  is considered the absolute minimum requirement for ratio searching.
--  twice the odd limit, and equave and min/max cents default to 2/1, 0c, and
-- NOTES:
--  1200c respectively.
-- - Prime limits are infinite sets, so int limit is used to restrain the set
--  (odd_limit, int_limit, equave)
--   to a finite size. The same is true for subgroup.
--  (odd_limit, int_limit, min_cents, max_cents)
-- - Tenney height is used for further filtering of ratios, and is considered
-- - For prime-limit search, prime-limit is the first arg, int limit defaults to
--  optional. If omitted, tenney height defaults to infinity.
--  twice the largest prime, and equave and min/max cents default to 2/1, 0c,
--  and 1200c respectively.
--  (prime_limit, int_limit, equave)
--  (prime_limit, int_limit, min_cents, max_cents)
-- - For subgroup search, subgroup is the first arg, there's no default value
--  for int limit (due to complexity of subgroups), and equave and min/max
--  cents default to 2/1, 0c, and 1200c respectively.
--   (subgroup, int_limit, equave)
--  (subgroup, int_limit, min_cents, max_cents)
-- - Filter ratios function is split into two:
--   - Filter ratios by complement removes ratios from a table if its complement
--    is missing. Complements are octave-complements by default.
--  - Filter ratios by tenney height removes ratios from a table if its tenney
--    height exceeds a passed-in value.
 
-- TODO: write filter function for cent range
 
-- Module searches for ratios that are, at the minimum, up to an equave and are
-- up to some integer limit. Search hierarchy is as follows:
-- - Search by subgroup (subgroup elements may be nonprime or rational)
-- - Then search by prime limit
-- - Then search by odd limit
-- - Then search by int limit
 
-- Optional args omit ratios that don't meet certain conditions, and are used
-- to further limit the number of ratios found. Current options include:
-- - Tenney Height: omits ratios that exceed some max Tenney height. Has no
--  effect if no Tenney height is passed in.
-- - Complements Only: omits ratios and their equave complements if either would
--  be omitted by Tenney height, or if no Tenney height is entered, omits
--  ratios whose complements are missing.


local DEFAULT_FINE_SEARCH_ARGS = {
local DEFAULT_EQUAVE = rat.new(2)
["Int Limit"]        = 50,
local DEFAULT_INT_LIMIT = 30
["Tenney Height"]    = 1/0,
["Complements Only"] = false
}


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
----------------------------- HELPER FUNCTIONS -------- ------------------------
------------------------------- FILTER FUNCTIONS -------------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


-- Preprocess fine-search args
-- Filter function removes certain ratios that don't meet some requirement.
function p.preprocess_fine_search_args(fine_search_args)
-- Filters currently include:
local fine_search_args = fine_search_args or DEFAULT_FINE_SEARCH_ARGS
-- - Removing ratios that exceed a max Tenney height.
local tenney_height = fine_search_args["Tenney Height"] or 1/0
-- - Removing ratios whose complement would exceed a max Tenney height or int limit
local comps_only    = fine_search_args["Complements Only"] or false
function p.filter_ratios(ratios, equave, int_limit, tenney_height, complements_only)
local int_limit     = fine_search_args["Int Limit"] or DEFAULT_INT_LIMIT
return {
local filtered_ratios = {}
["Tenney Height"]    = tenney_height,
for i = 1, #ratios do
["Complements Only"] = comps_only,
local complement = rat.mul(rat.inv(ratios[i]), equave)
["Int Limit"]       = int_limit
local ratio_th  = rat.tenney_height(ratios[i])
}
local compl_th  = rat.tenney_height(complement)
end
 
-- Are the ratios within the Tenney height?
-- Given a ratio, determine whether to include it based on:
-- Has no effect (defaults to TRUE) if Tenney height is infinity.
-- - Whether it's between 1/1 and the equave
local ratio_within_th = ratio_th <= tenney_height
-- - Whether it's within an int limit (required)
local compl_within_th = compl_th <= tenney_height
-- - Whether it's below a tenney height (default height is infinity)
-- NOTE: ratio is not of the form defined by module:rational, but equave is!
-- Is the ratio's complement within the int limit?
function p.ratio_within_search(ratio, equave, fine_search_args)
local compl_within_int_limit = rat.is_within_int_limit(complement, int_limit)
-- Ratio, complement, and equave as float
if complements_only then
local ratio_as_float = ratio[1] / ratio[2]
if ratio_within_th and compl_within_th and compl_within_int_limit then
local equave_as_float = rat.as_float(equave)
table.insert(filtered_ratios, ratios[i])
end
-- Fine search params for ease of access
else
local int_limit = fine_search_args["Int Limit"]
if ratio_within_th then
local tenney_height = fine_search_args["Tenney Height"]
table.insert(filtered_ratios, ratios[i])
local comps_only = fine_search_args["Complements Only"]
end
end
local ratio_within_int_limit = math.max(ratio[1], ratio[2]) <= int_limit
end
local ratio_within_tenney_height = math.log(ratio[1] * ratio[2]) / math.log(2) <= tenney_height
local ratio_within_equave = ratio_as_float <= equave_as_float
return ratio_within_int_limit and ratio_within_tenney_height and ratio_within_equave
return filtered_ratios
end
end


-- Remove ratios whose complements exceed the int limit
-- Filters ratios from a table of ratios, returning an array of ratios within
-- Ratios should already of the form defined by module:rational
-- the cent range and preserving the original table. Meant for searching for
-- This function modifies the table of ratios directly, as it has no return
-- multiple ranges. TODO: write
-- value.
function p.filter_ratios_within_cent_range(ratios, min_cents, max_cents)
function p.remove_non_complementing_ratios(ratios, equave, fine_search_args)
if fine_search_args["Complements Only"] then
local filtered_ratios = {}
for i = 1, #ratios do
local complement = rat.mul(rat.inv(ratios[i]), equave)
local a, b
a, b = rat.as_pair(complement)
if math.max(a,b) <= fine_search_args["Int Limit"] and rat.tenney_height(complement) <= fine_search_args["Tenney Height"] then
table.insert(filtered_ratios, ratios[i])
end
end
return filtered_ratios
else
return ratios
end
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
----------------------- INT-LIMIT-BASED SEARCH FUNCTION ------------------------
-------------------------- INT-LIMIT SEARCH FUNCTION ---------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


-- Int-limit-based search; finds ratios between 1/1 and an equave, within an int
-- Int limit search finds ratios from 1/1 to an equave, where each ratio's
-- limit. An optional tenney height can be passed in.
-- numerator or denominator don't exceed the int limit.
-- Int limit is hardcoded to a max size to restrict the size of output, to avoid
function p.search_by_int_limit(equave, int_limit)
-- risk of out-of-memory operations or the like.
return p.search_by_int_limit_within_cents(0, rat.cents(equave), int_limit)
function p.search_within_equave(equave, fine_search_args)
end
local equave = equave or rat.new(2,1) -- Defualt equave is 2/1.
 
local fine_search_args = p.preprocess_fine_search_args(fine_search_args)
-- Cent range search finds ratios within a cent range. Meant for searching for
-- ratios within a single interval range. If searching for ratios within many
-- interval ranges, then try a broad search first.
function p.search_by_int_limit_within_cents(min_cents, max_cents, int_limit)
local init_ratios = {{1,1}, {1,0}}
local init_ratios = {{1,1}, {1,0}}
local search_func = p.int_limit_mediant_search
local ratios = med.find_only_mediants(init_ratios, 2)
local search_args = {
for i = 3, int_limit do
["Equave"] = equave,
ratios = med.find_mediants_by_int_limit(ratios, i)
["Int Limit"]     = fine_search_args["Int Limit"],
["Tenney Height"] = fine_search_args["Tenney Height"],
-- Purge ratios from the beginning.
["Complements Only"] = fine_search_args["Complements Only"]
-- If the first and second ratio are smaller than min_cents, and smaller
}
-- than max_cents, then remove the first ratio. Keeping the first ratio
local ratios = med.find_only_mediants_by_search_func(init_ratios, search_func, search_args)
-- would add mediants outside the cent range.
local cents_1 = utils.log2(ratios[1][1] / ratios[1][2]) * 1200
local cents_2 = utils.log2(ratios[2][1] / ratios[2][2]) * 1200
if cents_1 < min_cents and cents_2 <= min_cents and cents_1 < max_cents and cents_2 < max_cents then
table.remove(ratios, 1)
end
-- Purge ratios from the end.
-- If the 2nd-last ratio and last ratio are greater than max_cents, and
-- larger than min_cents, then remove the last ratio. Keeping the last
-- ratio would add mediants outside the cent range.
local cents_3 = utils.log2(ratios[#ratios-1][1] / ratios[#ratios-1][2]) * 1200
local cents_4 = utils.log2(ratios[#ratios  ][1] / ratios[#ratios ][2]) * 1200
if cents_3 > max_cents and cents_4 >= max_cents and cents_3 > min_cents and cents_4 > min_cents then
table.remove(ratios, #ratios)
end
end
-- Convert to ratios that Module:Rational can work with
-- Convert to ratios that Module:Rational can work with
Line 121: Line 150:
end
end
-- Remove ratios that exceed the equave.
-- Remove any remaining ratios that fall outside the cent range.
-- Note that mediant search results in sorted ratios, so remove them from
while rat.cents(ratios[1]) < min_cents do
-- the end until there's no more to remove.
table.remove(ratios, 1)
while rat.gt(ratios[#ratios], equave) do
end
while rat.cents(ratios[#ratios]) > max_cents do
table.remove(ratios, #ratios)
table.remove(ratios, #ratios)
end
end
-- Filter out ratios whose complements exceed the int limit
ratios = p.remove_non_complementing_ratios(ratios, equave, fine_search_args)
return ratios
return ratios
end
end


-- Int limit search function, with equave and tenney height cutoffs.
--------------------------------------------------------------------------------
-- If nil is passed in for the tenney height, it will defualt to infinity.
-------------------------- ODD-LIMIT SEARCH FUNCTION ---------------------------
-- To be passed into mediant-search function, as part of int-limit-search
--------------------------------------------------------------------------------
-- function call.
 
function p.int_limit_mediant_search(mediant_data, search_args)
-- Convert odd limit into equivalent subgroup.
local mediant  = mediant_data["mediant"]
-- EG, 11-odd-limit becomes 2.3.5.7.9.11
local ratio_1  = mediant_data["ratio_1"]
-- 2 is part of the subgroup by definition.
local equave        = search_args["Equave"]
function p.odd_limit_to_subgroup(odd_limit)
local subgroup = { rat.new(2) }
local equave_as_float = rat.as_float(equave)
for i = 3, odd_limit, 2 do
local rat_1_as_float = ratio_1[1] / ratio_1[2]
table.insert(subgroup, rat.new(i))
local mediant_th = math.log(mediant[1] * mediant[2]) / math.log(2)
end
return subgroup
-- When the ratio is added, is ratio 1 within the equave? If so, add the
end
-- new ratio.
 
local within_equave = rat_1_as_float < equave_as_float
function p.search_by_odd_limit(equave, int_limit, odd_limit)
local subgroup = p.odd_limit_to_subgroup(odd_limit)
-- Is the mediant within the int limit and tenney height?
return p.search_by_subgroup_within_cents(0, rat.cents(equave), int_limit, subgroup)
local within_int_limit = math.max(mediant[1], mediant[2]) <= search_args["Int Limit"]
end
local within_tenney_height = mediant_th <= search_args["Tenney Height"]
 
function p.search_by_odd_limit_within_cents(min_cents, max_cents, odd_limit)
return within_equave and within_int_limit and within_tenney_height
local subgroup = p.odd_limit_to_subgroup(odd_limit)
return p.search_by_subgroup_within_cents(min_cents, max_cents, int_limit, subgroup)
end
 
--------------------------------------------------------------------------------
------------------------- PRIME-LIMIT SEARCH FUNCTION --------------------------
--------------------------------------------------------------------------------
 
-- Convert prime limit into equivalent subgroup.
-- EG, 11-prime-limit becomes 2.3.5.7.11
function p.prime_limit_to_subgroup(prime_limit)
local subgroup = {}
for i = 3, prime_limit do
local is_prime = true
for j = 2, math.floor(math.sqrt(i)) do
if i % j == 0 then
is_prime = false
break
end
end
if is_prime then
table.insert(subgroup, rat.new(i))
end
end
return subgroup
end
 
-- Prime limit search finds ratios with prime factors that don't exceed some
-- prime limit.
-- Upper bounds for searching is the equave and int limit.
function p.search_by_prime_limit(equave, int_limit, prime_limit)
local subgroup = p.prime_limit_to_subgroup(prime_limit)
return p.search_by_subgroup_within_cents(0, rat.cents(equave), int_limit, subgroup)
end
 
-- Prime limit search finds ratios with prime factors that don't exceed some
-- prime limit. Searches within a cent range.
function p.search_by_prime_limit_within_cents(min_cents, max_cents, int_limit, prime_limit)
local subgroup = p.prime_limit_to_subgroup(prime_limit)
local ratios = p.search_by_subgroup_within_cents(min_cents, max_cents, int_limit, subgroup)
while rat.cents(ratios[1]) < min_cents do
table.remove(ratios, 1)
end
return ratios
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
---------------- WORK-IN-PROGERSS SUBGROUP-BASED SEARCH FUNCTION ---------------
---------------------------- SUBGROUP SEARCH FUNCTION --------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


-- WORK-IN-PROGRESS!!
-- Subgroup search find ratios that are products of at least two non-unique
function p.search_by_subgroup_new(subgroup, equave, fine_search_args)
-- elements from the subgroup.
local subgroup = {rat.new(2), rat.new(5), rat.new(7,6), rat.new(11,6)}
function p.search_by_subgroup(equave, int_limit, subgroup)
local equave = rat.new(2,1)
local ratios = p.search_by_subgroup_within_cents(0, rat.cents(equave), int_limit, subgroup)
local fine_search_args = p.preprocess_fine_search_args(fine_search_args)
return ratios
end
-- Fine search params for ease of access
 
local int_limit = fine_search_args["Int Limit"]
function p.search_by_subgroup_within_cents(min_cents, max_cents, int_limit, subgroup)
local tenney_height = fine_search_args["Tenney Height"]
--local equave   = equave or rat.new(2,1) -- Defualt equave is 2/1.
local comps_only = fine_search_args["Complements Only"]
--local int_limit = int_limit or 50 -- Default is 50
--local subgroup  = subgroup or {rat.new(2), rat.new(3), rat.new(7)} -- Default is 2.3.7 subgroup
-- Find all possible ways to multiply subgroup elements with one another
-- using breadth-first-search. Products found this way should not exceed the
-- int limit, and if a subgroup element is rational, neither its numerator
-- nor denominator should exceed the int limit.
local products = { rat.new(1) }
local products = { rat.new(1) }
local i = 1
-- Perform breadth-first-search to find all ratios greater than 1 within
while i <= #products do
-- the int limit.
-- Multiply each subgroup element by the current ratio. The table of
local index_counter = 1
-- product ratios created this way is merged with the running table of
while index_counter <= #products do
-- ratios. This is the Cartesian product of the single ratio as a set,
local new_products = p.multiply_ratio_by_subgroup_members(products[index_counter], subgroup, int_limit)
-- with the subgroup elements as a set, or {p/q} X subgroup.
local new_products = {}
for i = 1, #new_products do
for j = 1, #subgroup do
if not p.find_ratio_in_table(products, new_products[i]) then
local new_ratio = rat.mul(products[i], subgroup[j])
table.insert(products, new_products[i])
if rat.is_within_int_limit(new_ratio, int_limit) and not p.find_ratio_in_table(new_products, new_ratio) then
table.insert(new_products, new_ratio)
end
end
end
end
index_counter = index_counter + 1
-- Merge new products with the table of products, omitting duplicates.
p.merge_tables(products, new_products)
i = i + 1
end
end
-- Sort
-- Sort for next step
table.sort(products, rat.lt)
table.sort(products, rat.lt)
-- Use the products found to find all ratios between 1 and the equave
-- Use the products found to find all ratios between 1 and the equave.
-- For each ratio in the table of products, create a set of new ratios by
-- having that ratio be the numerator and all successive ratios be possible
-- denominators. Store these new ratios in a table, and repeat with all
-- successive products, omitting duplicats. From earlier testing, this is
-- faster than performing BFS on each ratio, and yields the same results.
local ratios = {}
local ratios = {}
for i = 1, #products do
for i = 1, #products do
local new_ratios = {}
for j = i, #products do
local new_ratio = rat.div(products[j], products[i])
if rat.cents(new_ratio) > max_cents then break end
if not p.find_ratio_in_table(new_ratios, new_ratio) and rat.is_within_int_limit(new_ratio, int_limit) then
table.insert(new_ratios, new_ratio)
end
end
-- Merge new ratios with the table of ratios, omitting duplicates.
p.merge_tables(ratios, new_ratios)
end
end
-- Return as a string for testing purposes
-- Sort
return p.ratios_as_string(products)
table.sort(ratios, rat.lt)
end
 
function p.multiply_ratio_by_subgroup_members(ratio, subgroup, int_limit)
local new_products = {}
for i = 1, #subgroup do
-- Remove ratios less than minimum
local product = rat.mul(ratio, subgroup[i])
while rat.cents(ratios[1]) < min_cents do
local product_as_float = rat.as_float(product)
table.remove(ratios, 1)
if rat.int_limit(product) <= int_limit and not p.find_ratio_in_table(new_products, product) then
table.insert(new_products, product)
end
end
end
return new_products
return ratios
end
end


function p.divide_ratio_by_subgroup_members(ratio, subgroup, int_limit)
--------------------------------------------------------------------------------
local new_quotients = {}
------------------------------- HELPER FUNCTIONS -------------------------------
--------------------------------------------------------------------------------
for i = 1, #subgroup do
 
local quotient = rat.div(ratio, subgroup[i])
-- Heleper function; merges elements from source table with destination table
local quotient_as_float = rat.as_float(quotient)
-- while disallowing duplicates.
if rat.int_limit(quotient) <= int_limit and not p.find_ratio_in_table(new_quotients, quotient) then
function p.merge_tables(dest_table, source_table)
table.insert(new_quotients, product)
for i = 1, #source_table do
if not p.find_ratio_in_table(dest_table, source_table[i]) then
table.insert(dest_table, source_table[i])
end
end
end
end
return new_quotients
end
end


-- Helper function for merge function.
function p.find_ratio_in_table(table_, ratio)
function p.find_ratio_in_table(table_, ratio)
local found = false
local found = false
Line 240: Line 327:
end
end
end
end
return found
return found
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
------------------------ SUBGROUP-BASED SEARCH FUNCTION ------------------------
---------------------------- RATIO STRING FUNCTIONS ----------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


-- Subgroup-based search; finds ratios between 1/1 and an equave, within a sub-
-- Convert a table of ratios into a string, with options for links and delimiter
-- group. An int limit is passed in to limit the size of output, since subgroups
function p.ratios_as_string(ratios, add_links, delimiter)
-- are infinite sets. An optional tenney height can be passed in to further
local add_links = add_links == true
-- limit output.
local delimiter = delimiter or ", "
-- Unlike int limit search, subgroup search can allow for very high int limits,
-- as long as the subgroup is reasonably small and has reasonably small terms.
-- Note that members in a subgroup need not be prime, as long as the terms are,
-- for the most part, relatively prime.
function p.search_by_subgroup_within_equave(subgroup, equave, fine_search_args)
local subgroup = subgroup or { 2, 3, 7 }
local equave = equave or rat.new(2,1) -- Defualt equave is 2/1.
local fine_search_args = p.preprocess_fine_search_args(fine_search_args)
-- Be absolutely sure the subgroup's members are sorted!
local text = ""
table.sort(subgroup)
if #ratios ~= 0 then
text = add_links and string.format("[[%s]]", rat.as_ratio(ratios[1])) or rat.as_ratio(ratios[1])
-- Fine search params for ease of access
for i = 2, #ratios do
local int_limit = fine_search_args["Int Limit"]
text = text .. (add_links and string.format("%s[[%s]]", delimiter, rat.as_ratio(ratios[i])) or string.format("%s%s", delimiter, rat.as_ratio(ratios[i])))
local tenney_height = fine_search_args["Tenney Height"]
local comps_only = fine_search_args["Complements Only"]
-- Find all possible products given the factors in the subgroup.
-- These will be used to find all possible ratios.
local products = {{1}}
local new_products_found = true
while new_products_found do
local new_products = {}
for i = 1, #subgroup do
for j = 1, #products[#products] do
local new_product = products[#products][j] * subgroup[i]
if new_product <= int_limit then
local product_already_added = false
for k = 1, #new_products do
product_already_added = product_already_added or new_product == new_products[k]
if product_already_added then break end
end
if not product_already_added then
table.insert(new_products, new_product)
end
end
end
end
if #new_products == 0 then
new_products_found = false
else
table.insert(products, new_products)
end
end
end
end
return text
-- Consolidate and sort products
local consolidated_products = {}
for i = 1, #products do
for j = 1, #products[i] do
table.insert(consolidated_products, products[i][j])
end
end
products = consolidated_products
table.sort(products)
 
-- Using the products produced earlier, combine them to make all possible
-- ratios from 1/1 to the equave.
local ratios = {}
local equave_as_float = rat.as_float(equave)
for i = 1, #products do
local denominator = products[i]
for j = i, #products do
local numerator = products[j]
local gcd = utils._gcd(numerator, denominator)
local ratio = { numerator / gcd, denominator / gcd }
local ratio_added_alraedy = false
for k = 1, #ratios do
ratio_added_alraedy = ratios[k][1] == ratio[1] and ratios[k][2] == ratio[2]
if ratio_added_alraedy then
break
end
end
if not ratio_added_alraedy and p.ratio_within_search(ratio, equave, fine_search_args) then
table.insert(ratios, ratio)
end
end
end
 
-- Convert to ratios that Module:Rational can work with
for i = 1, #ratios do
ratios[i] = rat.new(ratios[i][1], ratios[i][2])
end
-- Sort ratios
table.sort(ratios, rat.lt)
-- Filter out ratios whose complements exceed the int limit
ratios = p.remove_non_complementing_ratios(ratios, equave, fine_search_args)
return ratios
end
end


--------------------------------------------------------------------------------
-- Convert a jagged array of ratios into an array of strings
---------------------- PRIME-LIMIT-BASED SEARCH FUNCTION -----------------------
function p.ratios_as_strings(ratios, add_links, delimiter)
--------------------------------------------------------------------------------
local add_links = add_links == true
 
local delimiter = delimiter or ", "
-- Prime-limit-based search; finds ratios between 1/1 and an equave, within a
-- prime limit. An int limit is passed in to limit the size of output, since
-- prime limits are inifinite sets. An optional tenney height can be passed in
-- to further limit output.
-- Like subgroup search, prime limit search can also allow for very high int
-- limits, as long as the prime is reasonably small.
function p.search_by_prime_limit_within_equave(prime_limit, equave, fine_search_args)
local prime_limit = prime_limit or 5
local equave = equave or rat.new(2,1) -- Defualt equave is 2/1.
local fine_search_args = p.preprocess_fine_search_args(fine_search_args)
-- Find all primes up to the prime limit.
local texts = {}
local primes = {}
for i = 1, #ratios do
for i = 2, prime_limit do
local text = p.ratios_as_string(ratios[i], add_links, delimiter)
local is_prime = true
table.insert(texts, text)
for j = 2, math.floor(math.sqrt(i)) do
if i % j == 0 then
is_prime = false
break
end
end
if is_prime then
table.insert(primes, i)
end
end
end
return texts
-- Perform subgroup search on the primes found, as subgroup-search code can
-- be reused for prime-limit search.
return p.search_by_subgroup_within_equave(primes, equave, fine_search_args)
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-------------------------- ARG-BASED SEARCH FUNCTIONS --------------------------
---------------------------- ARG-PARSING FUNCTION ------------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


-- Search for ratios based on an array of args passed in. Certain params have
-- Parse search args if entered as one string. Use is to be determined.
-- their own function calls.
function p.parse_args(search_args)
function p.search_by_args_within_equave(equave, search_args)
local parsed = tip.parse_kv_pairs(search_args)
local equave = equave or rat.new(2,1)
-- For each search method, check whether the corresponding search arg and
if parsed["Equave"] ~= nil then
-- int limit are both present and pass it and the equave to that function.
parsed["Equave"] = rat.parse(parsed["Equave"])
-- All other search args are used as finer search args.
-- Note that search by int limit alone just has the equave and search args
-- passed in.
local ratios = {}
if search_args["Prime Limit"] ~= nil and search_args["Int Limit"] ~= nil then
ratios = p.search_by_prime_limit_within_equave(search_args["Prime Limit"], equave, search_args)
elseif search_args["Subgroup"] ~= nil and search_args["Int Limit"] ~= nil then
ratios = p.search_by_subgroup_within_equave(search_args["Subgroup"], equave, search_args)
elseif search_args["Int Limit"] ~= nil then
ratios = p.search_within_equave(equave, search_args)
end
end
return ratios
end
-- Parse search args.
function p.parse_search_args(search_args)
local parsed = tip.parse_kv_pairs(search_args)
if parsed["Int Limit"] ~= nil then
if parsed["Int Limit"] ~= nil then
Line 423: Line 387:
if parsed["Subgroup"] ~= nil then
if parsed["Subgroup"] ~= nil then
parsed["Subgroup"] = tip.parse_numeric_entries(parsed["Subgroup"], ".")
local subgroup_elements = tip.parse_numeric_pairs(parsed["Subgroup"], ".", "/", true)
for i = 1, #subgroup_elements do
subgroup_elements[i] = rat.new(subgroup_elements[i][1], subgroup_elements[i][2])
end
parsed["Subgroup"] = subgroup_elements
end
end
Line 433: Line 401:
end
end


function p.search_footnotes(search_args)
--------------------------------------------------------------------------------
local result = "Other interpretations are possible."
----------------------------- INVOKABLE FUNCTIONS ------------------------------
local autosearch_text = "Automatic search may be inexact."
--------------------------------------------------------------------------------
local search_text = ""
 
-- Function callable by other modules
-- Ratios are returned as a table, for use with other modules.
function p._ji_ratios(args)
-- Args for ease of access
equave      = args["Equave"    ] or DEFAULT_EQUAVE
int_limit  = args["Int Limit"  ] or DEFAULT_INT_LIMIT
odd_limit  = args["Odd Limit"  ]
prime_limit = args["Prime Limit"]
subgroup    = args["Subgroup"   ]
-- Filtering args
tenney_height    = args["Tenney Height"   ] or 1/0 -- Default Tenney height is infinity
complements_only = args["Complements Only"] or false -- Default is to include all ratios
if search_args["Prime Limit"] ~= nil then
local ratios = {}
search_text = string.format("Ratios shown are within the %s-prime limit.", search_args["Prime Limit"])
if subgroup ~= nil then
.. " " .. autosearch_text
ratios = p.search_by_subgroup(equave, int_limit, subgroup)
elseif search_args["Subgroup"] ~= nil then
elseif prime_limit ~= nil then
search_text = string.format("Ratios shown are within the %s subgroup.", table.concat(search_args["Subgroup"], "."))
ratios = p.search_by_prime_limit(equave, int_limit, prime_limit)
.. " " .. autosearch_text
elseif int_limit ~= nil then
elseif search_args["Int Limit"] ~= nil then
ratios = p.search_by_int_limit(equave, int_limit)
search_text = string.format("Ratios shown are within the %s-integer limit.", search_args["Int Limit"])
.. " " .. autosearch_text
end
end
result = search_text .. " " .. result
-- Filter ratios
return result
ratios = p.filter_ratios(ratios, equave, int_limit, tenney_height, complements_only)
return ratios
end
 
-- Invokable function; for templates
-- Ratios are returned as a comma-delimited list. For finer control, it's
-- necessary to call the "main" function, then further process the results.
function p.ji_ratios(frame)
args = getArgs(frame)
-- Preprocess equave
-- Ratios are searched from 1/1 to some equave (default 2/1), so an equave
-- must be passed in.
args["Equave"] = args["Equave"] ~= nil and rat.parse(args["Equave"])
-- Preprocess int limit
-- Ratios are searched up to some int limit (default 50), so an int limit
-- must be passed in.
args["Int Limit"] = args["Int Limit"] ~= nil and tonumber(args["Int Limit"])
 
-- Preprocess Tenney height
if args["Tenney Height"] ~= nil then
args["Tenney Height"] = tonumber(args["Tenney Height"])
end
-- Preprocess prime limit
if args["Prime Limit"] ~= nil then
args["Prime Limit"] = tonumber(args["Prime Limit"])
end
-- Preprocess subgroup
if args["Subgroup"] ~= nil then
local subgroup_elements = tip.parse_numeric_pairs(args["Subgroup"], ".", "/", true)
for i = 1, #subgroup_elements do
subgroup_elements[i] = rat.new(subgroup_elements[i][1], subgroup_elements[i][2])
end
args["Subgroup"] = subgroup_elements
end
if args["Complements Only"] ~= nil then
args["Complements Only"] = yesno(args["Complements Only"], false)
end
-- Find and return ratios
local result = p.ratios_as_string(p._ji_ratios(args))
local debugg = yesno(frame.args["debug"])
if debugg == true then
result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
end
return frame:preprocess(result)
 
end
 
function p.tester()
--return p.ratios_as_string(p._ji_ratios(p.parse_args("Int Limit: 16; Equave: 3/1; Complements Only: 0")))
--return p.ratios_as_string(p.search_by_prime_limit_within_cents(372, 440, 17, 30))
return p.ratios_as_string(p.search_by_odd_limit(rat.new(2), 15, 15*2))
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--------------------------- RATIO SORTING FUNCTIONS ----------------------------
---------------------------- FUNCTIONS TO BE MOVED -----------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


-- Sorts ratios by closeness to cent values.
-- Parse a list of ratios from a string. String is formatted as follows:
-- "a/b; c/d; e/f; g/h"
function p.parse_ratios(unparsed)
local parsed = tip.parse_numeric_pairs(unparsed)
for i = 1, #parsed do
parsed[i] = rat.new(parsed[i][1], parsed[i][2])
end
return parsed
end
 
-- Sorts ratios by closeness to cent values. Move to new module?
function p.sort_by_closeness_to_cent_values(ratios, cent_values, tolerance)
function p.sort_by_closeness_to_cent_values(ratios, cent_values, tolerance)
local tolerance = tolerance or 30
local tolerance = tolerance or 30
Line 485: Line 533:
return sorted_ratios
return sorted_ratios
end
--------------------------------------------------------------------------------
------------------------ RATIO PARSING/INPUT FUNCTIONS -------------------------
--------------------------------------------------------------------------------
-- Parse a list of ratios from a string. String is formatted as follows:
-- "a/b; c/d; e/f; g/h"
function p.parse_ratios(unparsed)
local parsed = tip.parse_numeric_pairs(unparsed)
for i = 1, #parsed do
parsed[i] = rat.new(parsed[i][1], parsed[i][2])
end
return parsed
end
--------------------------------------------------------------------------------
---------------------------- RATIO STRING FUNCTIONS ----------------------------
--------------------------------------------------------------------------------
-- Convert a table of ratios into a string, with options for links and delimiter
function p.ratios_as_string(ratios, add_links, delimiter)
local add_links = add_links == true
local delimiter = delimiter or ", "
local text = ""
if #ratios ~= 0 then
text = add_links and string.format("[[%s]]", rat.as_ratio(ratios[1])) or rat.as_ratio(ratios[1])
for i = 2, #ratios do
text = text .. (add_links and string.format("%s[[%s]]", delimiter, rat.as_ratio(ratios[i])) or string.format("%s%s", delimiter, rat.as_ratio(ratios[i])))
end
end
return text
end
-- Convert a jagged array of ratios into an array of strings
function p.ratios_as_strings(ratios, add_links, delimiter)
local add_links = add_links == true
local delimiter = delimiter or ", "
local texts = {}
for i = 1, #ratios do
local text = p.ratios_as_text(ratios[i], add_links, delimiter)
table.insert(texts, text)
end
return texts
end
function p.tester()
local equave = rat.new(2,1)
local fine_search_args = p.parse_search_args("Subgroup: 2.5.9.21; Int Limit: 30; Complements Only: 1; Tenney Height: 100000000")
--return p.ratios_as_string(p.search_by_args_within_equave(equave, fine_search_args))
return p.multiply_ratio_by_subgroup_members(rat.new(1), {rat.new(2), rat.new(3), rat.new(7)}, 50)
--return p.ratio_within_search({16,13}, equave, fine_search_args) and p.ratio_within_search({8,13}, equave, fine_search_args)
end
end


return p
return p

Latest revision as of 09:21, 23 October 2025

Module documentation[view] [edit] [history] [purge]
This module may be invoked by templates using its corresponding template Template:JI ratios, or used directly from other modules.
Module:JI ratios is a draft module. It is incomplete and may not be in active development. If possible, editors are encouraged to help with its development. In the meantime, editors should avoid using this module across the Xenharmonic Wiki, except for testing.
Introspection summary for Module:JI ratios 
Functions provided (22)
Line Function Params
69 filter_ratios (ratios, equave, int_limit, tenney_height, complements_only)
102 filter_ratios_within_cent_range (ratios, min_cents, max_cents)
112 search_by_int_limit (equave, int_limit)
119 search_by_int_limit_within_cents (min_cents, max_cents, int_limit)
170 odd_limit_to_subgroup (odd_limit)
178 search_by_odd_limit (equave, int_limit, odd_limit)
183 search_by_odd_limit_within_cents (min_cents, max_cents, odd_limit)
194 prime_limit_to_subgroup (prime_limit)
214 search_by_prime_limit (equave, int_limit, prime_limit)
221 search_by_prime_limit_within_cents (min_cents, max_cents, int_limit, prime_limit)
236 search_by_subgroup (equave, int_limit, subgroup)
241 search_by_subgroup_within_cents (min_cents, max_cents, int_limit, subgroup)
312 merge_tables (dest_table, source_table)
321 find_ratio_in_table (table_, ratio)
337 ratios_as_string (ratios, add_links, delimiter)
352 ratios_as_strings (ratios, add_links, delimiter)
369 parse_args (search_args)
409 _ji_ratios (main) (args)
439 ji_ratios (invokable) (frame)
487 tester none
499 parse_ratios (unparsed)
508 sort_by_closeness_to_cent_values (ratios, cent_values, tolerance)
Lua modules required (6)
Variable Module Functions used
getArgs Module:Arguments getArgs
med Module:Mediants find_only_mediants
find_mediants_by_int_limit
rat Module:Rational new
mul
inv
tenney_height
is_within_int_limit
cents
div
as_float
as_ratio
parse
tip Module:Template input parse parse_kv_pairs
parse_numeric_pairs
utils Module:Utils log2
yesno Module:Yesno yesno

No function descriptions were provided. The Lua code may have further information.


-- This module follows [[User:Ganaram inukshuk/Provisional style guide for Lua]]
local getArgs = require("Module:Arguments").getArgs
local med = require("Module:Mediants")
local rat = require("Module:Rational")
local tip = require("Module:Template input parse")
local utils = require("Module:Utils")
local yesno = require("Module:Yesno")

local p = {}

-- Template for handling multiple entry of JI ratios into a template, and for
-- searching for JI ratios if automatic entry is desired.
-- This is a successor/replacement for JI ratio finder.

-- TODO: Refactor code such that:
-- - For int-limit search, int limit is the first arg, and equave and min/max
--   cents default to 2/1, 0c, and 1200c respectively.
--   (int_limit, equave)
--   (int_limit, min_cents, max_cents)
-- - For odd-limit search, odd limit is the first arg, int limit defaults to
--   twice the odd limit, and equave and min/max cents default to 2/1, 0c, and 
--   1200c respectively.
--   (odd_limit, int_limit, equave)
--   (odd_limit, int_limit, min_cents, max_cents)
-- - For prime-limit search, prime-limit is the first arg, int limit defaults to
--   twice the largest prime, and equave and min/max cents default to 2/1, 0c,
--   and 1200c respectively.
--   (prime_limit, int_limit, equave)
--   (prime_limit, int_limit, min_cents, max_cents)
-- - For subgroup search, subgroup is the first arg, there's no default value
--   for int limit (due to complexity of subgroups), and equave and min/max
--   cents default to 2/1, 0c, and 1200c respectively.
--   (subgroup, int_limit, equave)
--   (subgroup, int_limit, min_cents, max_cents)
-- - Filter ratios function is split into two:
--   - Filter ratios by complement removes ratios from a table if its complement
--     is missing. Complements are octave-complements by default.
--   - Filter ratios by tenney height removes ratios from a table if its tenney
--     height exceeds a passed-in value.

-- TODO: write filter function for cent range

-- Module searches for ratios that are, at the minimum, up to an equave and are
-- up to some integer limit. Search hierarchy is as follows:
-- - Search by subgroup (subgroup elements may be nonprime or rational)
-- - Then search by prime limit
-- - Then search by odd limit
-- - Then search by int limit

-- Optional args omit ratios that don't meet certain conditions, and are used
-- to further limit the number of ratios found. Current options include:
-- - Tenney Height: omits ratios that exceed some max Tenney height. Has no
--   effect if no Tenney height is passed in.
-- - Complements Only: omits ratios and their equave complements if either would
--   be omitted by Tenney height, or if no Tenney height is entered, omits
--   ratios whose complements are missing.

local DEFAULT_EQUAVE = rat.new(2)
local DEFAULT_INT_LIMIT = 30

--------------------------------------------------------------------------------
------------------------------- FILTER FUNCTIONS -------------------------------
--------------------------------------------------------------------------------

-- Filter function removes certain ratios that don't meet some requirement.
-- Filters currently include:
-- - Removing ratios that exceed a max Tenney height.
-- - Removing ratios whose complement would exceed a max Tenney height or int limit
function p.filter_ratios(ratios, equave, int_limit, tenney_height, complements_only)
	
	local filtered_ratios = {}
	for i = 1, #ratios do
		local complement = rat.mul(rat.inv(ratios[i]), equave)
		local ratio_th   = rat.tenney_height(ratios[i])
		local compl_th   = rat.tenney_height(complement)
		
		-- Are the ratios within the Tenney height?
		-- Has no effect (defaults to TRUE) if Tenney height is infinity.
		local ratio_within_th = ratio_th <= tenney_height
		local compl_within_th = compl_th <= tenney_height
		
		-- Is the ratio's complement within the int limit?
		local compl_within_int_limit = rat.is_within_int_limit(complement, int_limit)
		
		if complements_only then
			if ratio_within_th and compl_within_th and compl_within_int_limit then
				table.insert(filtered_ratios, ratios[i])
			end
		else
			if ratio_within_th then
				table.insert(filtered_ratios, ratios[i])
			end
		end
	end
	
	return filtered_ratios
end

-- Filters ratios from a table of ratios, returning an array of ratios within
-- the cent range and preserving the original table. Meant for searching for
-- multiple ranges. TODO: write
function p.filter_ratios_within_cent_range(ratios, min_cents, max_cents)
	
end

--------------------------------------------------------------------------------
-------------------------- INT-LIMIT SEARCH FUNCTION ---------------------------
--------------------------------------------------------------------------------

-- Int limit search finds ratios from 1/1 to an equave, where each ratio's
-- numerator or denominator don't exceed the int limit.
function p.search_by_int_limit(equave, int_limit)
	return p.search_by_int_limit_within_cents(0, rat.cents(equave), int_limit)
end

-- Cent range search finds ratios within a cent range. Meant for searching for
-- ratios within a single interval range. If searching for ratios within many
-- interval ranges, then try a broad search first.
function p.search_by_int_limit_within_cents(min_cents, max_cents, int_limit)
	
	local init_ratios = {{1,1}, {1,0}}
	local ratios = med.find_only_mediants(init_ratios, 2)
	for i = 3, int_limit do
		ratios = med.find_mediants_by_int_limit(ratios, i)
		
		-- Purge ratios from the beginning.
		-- If the first and second ratio are smaller than min_cents, and smaller
		-- than max_cents, then remove the first ratio. Keeping the first ratio
		-- would add mediants outside the cent range.
		local cents_1 = utils.log2(ratios[1][1] / ratios[1][2]) * 1200
		local cents_2 = utils.log2(ratios[2][1] / ratios[2][2]) * 1200
		if cents_1 < min_cents and cents_2 <= min_cents and cents_1 < max_cents and cents_2 < max_cents then
			table.remove(ratios, 1)
		end
		
		-- Purge ratios from the end.
		-- If the 2nd-last ratio and last ratio are greater than max_cents, and
		-- larger than min_cents, then remove the last ratio. Keeping the last
		-- ratio would add mediants outside the cent range.
		local cents_3 = utils.log2(ratios[#ratios-1][1] / ratios[#ratios-1][2]) * 1200
		local cents_4 = utils.log2(ratios[#ratios  ][1] / ratios[#ratios  ][2]) * 1200
		if cents_3 > max_cents and cents_4 >= max_cents and cents_3 > min_cents and cents_4 > min_cents then
			table.remove(ratios, #ratios)
		end
	end
	
	-- Convert to ratios that Module:Rational can work with
	for i = 1, #ratios do
		ratios[i] = rat.new(ratios[i][1], ratios[i][2])
	end
	
	-- Remove any remaining ratios that fall outside the cent range.
	while rat.cents(ratios[1]) < min_cents do
		table.remove(ratios, 1)
	end
	while rat.cents(ratios[#ratios]) > max_cents do
		table.remove(ratios, #ratios)
	end
	
	return ratios
end

--------------------------------------------------------------------------------
-------------------------- ODD-LIMIT SEARCH FUNCTION ---------------------------
--------------------------------------------------------------------------------

-- Convert odd limit into equivalent subgroup.
-- EG, 11-odd-limit becomes 2.3.5.7.9.11
-- 2 is part of the subgroup by definition.
function p.odd_limit_to_subgroup(odd_limit)
	local subgroup = { rat.new(2) }
	for i = 3, odd_limit, 2 do
		table.insert(subgroup, rat.new(i))
	end
	return subgroup
end

function p.search_by_odd_limit(equave, int_limit, odd_limit)
	local subgroup = p.odd_limit_to_subgroup(odd_limit)
	return p.search_by_subgroup_within_cents(0, rat.cents(equave), int_limit, subgroup)
end

function p.search_by_odd_limit_within_cents(min_cents, max_cents, odd_limit)
	local subgroup = p.odd_limit_to_subgroup(odd_limit)
	return p.search_by_subgroup_within_cents(min_cents, max_cents, int_limit, subgroup)
end

--------------------------------------------------------------------------------
------------------------- PRIME-LIMIT SEARCH FUNCTION --------------------------
--------------------------------------------------------------------------------

-- Convert prime limit into equivalent subgroup.
-- EG, 11-prime-limit becomes 2.3.5.7.11
function p.prime_limit_to_subgroup(prime_limit)
	local subgroup = {}
	for i = 3, prime_limit do
		local is_prime = true
		for j = 2, math.floor(math.sqrt(i)) do
			if i % j == 0 then
				is_prime = false
				break
			end
		end
		if is_prime then
			table.insert(subgroup, rat.new(i))
		end
	end
	return subgroup
end

-- Prime limit search finds ratios with prime factors that don't exceed some
-- prime limit.
-- Upper bounds for searching is the equave and int limit.
function p.search_by_prime_limit(equave, int_limit, prime_limit)
	local subgroup = p.prime_limit_to_subgroup(prime_limit)
	return p.search_by_subgroup_within_cents(0, rat.cents(equave), int_limit, subgroup)
end

-- Prime limit search finds ratios with prime factors that don't exceed some
-- prime limit. Searches within a cent range.
function p.search_by_prime_limit_within_cents(min_cents, max_cents, int_limit, prime_limit)
	local subgroup = p.prime_limit_to_subgroup(prime_limit)
	local ratios = p.search_by_subgroup_within_cents(min_cents, max_cents, int_limit, subgroup)
	while rat.cents(ratios[1]) < min_cents do
		table.remove(ratios, 1)
	end
	return ratios
end

--------------------------------------------------------------------------------
---------------------------- SUBGROUP SEARCH FUNCTION --------------------------
--------------------------------------------------------------------------------

-- Subgroup search find ratios that are products of at least two non-unique
-- elements from the subgroup.
function p.search_by_subgroup(equave, int_limit, subgroup)
	local ratios = p.search_by_subgroup_within_cents(0, rat.cents(equave), int_limit, subgroup)
	return ratios
end

function p.search_by_subgroup_within_cents(min_cents, max_cents, int_limit, subgroup)
	--local equave    = equave or rat.new(2,1)	-- Defualt equave is 2/1.
	--local int_limit = int_limit or 50			-- Default is 50
	--local subgroup  = subgroup or {rat.new(2), rat.new(3), rat.new(7)}		-- Default is 2.3.7 subgroup
	
	-- Find all possible ways to multiply subgroup elements with one another
	-- using breadth-first-search. Products found this way should not exceed the
	-- int limit, and if a subgroup element is rational, neither its numerator
	-- nor denominator should exceed the int limit.
	local products = { rat.new(1) }
	local i = 1
	while i <= #products do
		-- Multiply each subgroup element by the current ratio. The table of
		-- product ratios created this way is merged with the running table of
		-- ratios. This is the Cartesian product of the single ratio as a set,
		-- with the subgroup elements as a set, or {p/q} X subgroup.
		local new_products = {}
		for j = 1, #subgroup do
			local new_ratio = rat.mul(products[i], subgroup[j])
			if rat.is_within_int_limit(new_ratio, int_limit) and not p.find_ratio_in_table(new_products, new_ratio) then
				table.insert(new_products, new_ratio)
			end
		end
		
		-- Merge new products with the table of products, omitting duplicates.
		p.merge_tables(products, new_products)
		i = i + 1
	end
	
	-- Sort for next step
	table.sort(products, rat.lt)
	
	-- Use the products found to find all ratios between 1 and the equave.
	-- For each ratio in the table of products, create a set of new ratios by
	-- having that ratio be the numerator and all successive ratios be possible
	-- denominators. Store these new ratios in a table, and repeat with all
	-- successive products, omitting duplicats. From earlier testing, this is
	-- faster than performing BFS on each ratio, and yields the same results.
	local ratios = {}
	for i = 1, #products do
		local new_ratios = {}
		for j = i, #products do
			local new_ratio = rat.div(products[j], products[i])	
			if rat.cents(new_ratio) > max_cents then break end
			
			if not p.find_ratio_in_table(new_ratios, new_ratio) and rat.is_within_int_limit(new_ratio, int_limit) then
				table.insert(new_ratios, new_ratio)
			end
		end
		
		-- Merge new ratios with the table of ratios, omitting duplicates.
		p.merge_tables(ratios, new_ratios)
	end
	
	-- Sort
	table.sort(ratios, rat.lt)
	
	-- Remove ratios less than minimum
	while rat.cents(ratios[1]) < min_cents do
		table.remove(ratios, 1)
	end
	
	return ratios
end

--------------------------------------------------------------------------------
------------------------------- HELPER FUNCTIONS -------------------------------
--------------------------------------------------------------------------------

-- Heleper function; merges elements from source table with destination table
-- while disallowing duplicates.
function p.merge_tables(dest_table, source_table)
	for i = 1, #source_table do
		if not p.find_ratio_in_table(dest_table, source_table[i]) then
			table.insert(dest_table, source_table[i])
		end
	end
end

-- Helper function for merge function.
function p.find_ratio_in_table(table_, ratio)
	local found = false
	for i = 1, #table_ do
		if rat.as_float(table_[i]) == rat.as_float(ratio) then
			found = true
			break
		end
	end
	return found
end

--------------------------------------------------------------------------------
---------------------------- RATIO STRING FUNCTIONS ----------------------------
--------------------------------------------------------------------------------

-- Convert a table of ratios into a string, with options for links and delimiter
function p.ratios_as_string(ratios, add_links, delimiter)
	local add_links = add_links == true
	local delimiter = delimiter or ", "
	
	local text = ""
	if #ratios ~= 0 then
		text = add_links and string.format("[[%s]]", rat.as_ratio(ratios[1])) or rat.as_ratio(ratios[1])
		for i = 2, #ratios do
			text = text .. (add_links and string.format("%s[[%s]]", delimiter, rat.as_ratio(ratios[i])) or string.format("%s%s", delimiter, rat.as_ratio(ratios[i])))
		end
	end
	return text
end

-- Convert a jagged array of ratios into an array of strings
function p.ratios_as_strings(ratios, add_links, delimiter)
	local add_links = add_links == true
	local delimiter = delimiter or ", "
	
	local texts = {}
	for i = 1, #ratios do
		local text = p.ratios_as_string(ratios[i], add_links, delimiter)
		table.insert(texts, text)
	end
	return texts
end

--------------------------------------------------------------------------------
---------------------------- ARG-PARSING FUNCTION ------------------------------
--------------------------------------------------------------------------------

-- Parse search args if entered as one string. Use is to be determined.
function p.parse_args(search_args)
	local parsed = tip.parse_kv_pairs(search_args)
	
	if parsed["Equave"] ~= nil then
		parsed["Equave"] = rat.parse(parsed["Equave"])
	end
	
	if parsed["Int Limit"] ~= nil then
		parsed["Int Limit"] = tonumber(parsed["Int Limit"])
	end
	
	if parsed["Tenney Height"] ~= nil then
		parsed["Tenney Height"] = tonumber(parsed["Tenney Height"])
	end
	
	if parsed["Prime Limit"] ~= nil then
		parsed["Prime Limit"] = tonumber(parsed["Prime Limit"])
	end
	
	if parsed["Subgroup"] ~= nil then
		local subgroup_elements = tip.parse_numeric_pairs(parsed["Subgroup"], ".", "/", true)
		for i = 1, #subgroup_elements do
			subgroup_elements[i] = rat.new(subgroup_elements[i][1], subgroup_elements[i][2])
		end
		parsed["Subgroup"] = subgroup_elements
	end
	
	if parsed["Complements Only"] ~= nil then
		parsed["Complements Only"] = yesno(parsed["Complements Only"])
	end
	
	return parsed
end

--------------------------------------------------------------------------------
----------------------------- INVOKABLE FUNCTIONS ------------------------------
--------------------------------------------------------------------------------

-- Function callable by other modules
-- Ratios are returned as a table, for use with other modules.
function p._ji_ratios(args)
	-- Args for ease of access
	equave      = args["Equave"     ]	or DEFAULT_EQUAVE
	int_limit   = args["Int Limit"  ]	or DEFAULT_INT_LIMIT
	odd_limit   = args["Odd Limit"  ]
	prime_limit = args["Prime Limit"]
	subgroup    = args["Subgroup"   ]
	
	-- Filtering args
	tenney_height    = args["Tenney Height"   ] or 1/0		-- Default Tenney height is infinity
	complements_only = args["Complements Only"] or false	-- Default is to include all ratios
	
	local ratios = {}
	if subgroup ~= nil then
		ratios = p.search_by_subgroup(equave, int_limit, subgroup)
	elseif prime_limit ~= nil then
		ratios = p.search_by_prime_limit(equave, int_limit, prime_limit)
	elseif int_limit ~= nil then
		ratios = p.search_by_int_limit(equave, int_limit)
	end
	
	-- Filter ratios
	ratios = p.filter_ratios(ratios, equave, int_limit, tenney_height, complements_only)
	
	return ratios
end

-- Invokable function; for templates
-- Ratios are returned as a comma-delimited list. For finer control, it's
-- necessary to call the "main" function, then further process the results.
function p.ji_ratios(frame)
	args = getArgs(frame)
	
	-- Preprocess equave
	-- Ratios are searched from 1/1 to some equave (default 2/1), so an equave
	-- must be passed in.
	args["Equave"] = args["Equave"] ~= nil and rat.parse(args["Equave"])
	
	-- Preprocess int limit
	-- Ratios are searched up to some int limit (default 50), so an int limit
	-- must be passed in.
	args["Int Limit"] = args["Int Limit"] ~= nil and tonumber(args["Int Limit"])

	-- Preprocess Tenney height
	if args["Tenney Height"] ~= nil then
		args["Tenney Height"] = tonumber(args["Tenney Height"])
	end
	
	-- Preprocess prime limit
	if args["Prime Limit"] ~= nil then
		args["Prime Limit"] = tonumber(args["Prime Limit"])
	end
	
	-- Preprocess subgroup
	if args["Subgroup"] ~= nil then
		local subgroup_elements = tip.parse_numeric_pairs(args["Subgroup"], ".", "/", true)
		for i = 1, #subgroup_elements do
			subgroup_elements[i] = rat.new(subgroup_elements[i][1], subgroup_elements[i][2])
		end
		args["Subgroup"] = subgroup_elements
	end
	
	if args["Complements Only"] ~= nil then
		args["Complements Only"] = yesno(args["Complements Only"], false)
	end
	
	-- Find and return ratios
	local result = p.ratios_as_string(p._ji_ratios(args))
	local debugg = yesno(frame.args["debug"])
	
	if debugg == true then
		result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
	end
	
	return frame:preprocess(result)

end

function p.tester()
	--return p.ratios_as_string(p._ji_ratios(p.parse_args("Int Limit: 16; Equave: 3/1; Complements Only: 0")))
	--return p.ratios_as_string(p.search_by_prime_limit_within_cents(372, 440, 17, 30))
	return p.ratios_as_string(p.search_by_odd_limit(rat.new(2), 15, 15*2))
end

--------------------------------------------------------------------------------
---------------------------- FUNCTIONS TO BE MOVED -----------------------------
--------------------------------------------------------------------------------

-- Parse a list of ratios from a string. String is formatted as follows:
-- "a/b; c/d; e/f; g/h"
function p.parse_ratios(unparsed)
	local parsed = tip.parse_numeric_pairs(unparsed)
	for i = 1, #parsed do
		parsed[i] = rat.new(parsed[i][1], parsed[i][2])
	end
	return parsed
end

-- Sorts ratios by closeness to cent values. Move to new module?
function p.sort_by_closeness_to_cent_values(ratios, cent_values, tolerance)
	local tolerance = tolerance or 30
	
	local sorted_ratios = {}
	local curr_index = 1		-- Index of current_ratio
	for i = 1, #cent_values do
		local lower_bound = cent_values[i] - tolerance
		local upper_bound = cent_values[i] + tolerance
		local cents_within_range = true
		local curr_ratios = {}
		
		for j = curr_index, #ratios do
			local curr_ratio = ratios[j]
			local curr_cents = rat.cents(curr_ratio)
			
			if lower_bound < curr_cents and curr_cents < upper_bound then
				table.insert(curr_ratios, curr_ratio)
			--elseif curr_cents > upper_bound then
			--	curr_index = j
			--	break
			end
		end
		
		table.insert(sorted_ratios, curr_ratios)
	end
	
	return sorted_ratios
end

return p