Module:MOS degrees: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
No edit summary
ArrowHead294 (talk | contribs)
mNo edit summary
 
(112 intermediate revisions by 3 users not shown)
Line 1: Line 1:
local mos = require('Module:MOS')
local mosg = require('Module:MOS gamut')
local et = require('Module:ET')
local rat = require('Module:Rational')
local utils = require('Module:Utils')
--local mosnot = require('Module:MOS notation')
local p = {}
local p = {}


-- Helper function for parsing a step ratio entered as a string "p/q"
local et = require("Module:ET")
-- TODO: separate this into a helper module called "MOS notation"
--local jiraf = require("Module:JI ratio finder")
function p.parse_step_ratio(step_ratio_unparsed)
local mos = require("Module:MOS")
local mosnot = require("Module:MOS notation")
local rat = require("Module:Rational")
local tamnams = require("Module:TAMNAMS")
local utils = require("Module:Utils")
local yesno = require("Module:Yesno")
 
-- TODO:
-- Rewrite "main function" into a underscore-prefixed function to be called by Lua code and a wrapper to be called by templates. (HIGH PRIORITY!!!)
-- Adopt MOS arithmetic function (MEDIUM-PRIORITY!!!)
-- Add support for double accidentals (low-priority)
-- Move certain helper functions to helper modules (low-priority)
 
-- Helper function
-- Parses entries from a semicolon-delimited string and returns them in an array
-- TODO: Separate this and related functions (parse_pair and parse_kv_pairs) into its own module, as they're included
-- in various modules at this point, such as: scale tree, MOS modes
function p.parse_entries(unparsed)
local parsed = {}
local parsed = {}
for entry in string.gmatch(step_ratio_unparsed, '([^/]+)') do
for entry in string.gmatch(unparsed, "([^;]+)") do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
table.insert(parsed, trimmed) -- Add to array
end
end
return parsed
local ratio = { tonumber(parsed[1]), tonumber(parsed[2]) }
return ratio
end
end


-- Helper function for parsing a UDP entered as a string "up,dp"
-- Helper function
-- To avoid potential issues, the "," character is used instead of "|"
-- Parses pairs of elements separated by a colon
function p.parse_udp(step_ratio_unparsed)
-- A pair must be two elements or it will be returned as an empty array
function p.parse_pair(unparsed)
local parsed = {}
local parsed = {}
for entry in string.gmatch(step_ratio_unparsed, '([^,]+)') do
for entry in string.gmatch(unparsed, "([^:]+)") do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
table.insert(parsed, trimmed) -- Add to array
end
end
if #parsed == 2 then
local udp = { tonumber(parsed[1]), tonumber(parsed[2]) }
return parsed
return udp
else
return {}
end
end
end


-- Helper function that converts a note name given as a quantity of mossteps
-- Helper function
-- and chromas (see gamut function) into a name, such as "C#"
-- Takes a list of semicolon-delimited pairs and returns a map
-- To be used in conjunction with the genchain function
-- (or dictionary or associative array) of key-value pairs
function p.mosstep_and_chroma_to_note_name(mossteps, chromas, note_symbol, chroma_symbol)
-- Each entry is colon-delimited as key : pair
local note_name = note_symbol
function p.parse_kv_pairs(unparsed)
if chromas < 0 then
-- Tokenize the string of unparsed pairs
note_name = note_name .. string.rep(chroma_symbol, chromas)
local parsed = p.parse_entries(unparsed)
elseif chromas > 0 then
-- Then tokenize the tokens into key-value pairs
note_name = note_name .. string.rep(chroma_symbol, -1 * chromas)
local pairs_ = {}
for i = 1, #parsed do
local pair = p.parse_pair(parsed[i])
if #pair == 2 then
pairs_[pair[1]] = pair[2]
end
end
end
return note_name
return pairs_
end
end


-- Helper function that converts a scale degree given as a quantity of mossteps
-- Helper function
-- and a numeric quality (0=perf, 1=maj, -1=min, 2=aug, -2=dim, etc) into a
-- Parses up to 5 step ratios entered as text in a semicolon-delimited string,
-- scale degree
-- where each step ratio is separated with a slash
-- To be used in conjunction with the degrees function
-- EG, "2/1; 3/1; 3/2" becomes {{2, 1}, {3, 1}, {3, 2}}
-- TODO: add ability to change naming from k-mosstep to mos-(k+1)th, since
-- NOTE: module relies on mosnot (mos notation) to parse step ratios
-- there are cases where that's favored instead of tamnams
function p.parse_step_ratio(unparsed)
function p.mosstep_and_quality_to_degree(mossteps, quality)
local parsed = {}
local degree_name = mossteps .. "-mosstep"
for entry in string.gmatch(unparsed, "([^;]+)") do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
end
if quality == 1 then
-- Parse up to 5 step ratios (hardcoded)
degree_name = "Major " .. degree_name
local max_ratios = 5
elseif quality == 2 then
local loop_limit = math.min(max_ratios, #parsed)
degree_name = "Augmented " .. degree_name
local step_ratios = {}
elseif quality > 2 then
for i = 1, loop_limit do
degree_name = (quality - 1) .. "× augmented " .. degree_name
local ratio = mosnot.parse_step_ratio(parsed[i])
elseif quality == -1 then
table.insert(step_ratios, ratio)
degree_name = "Minor " .. degree_name
elseif quality == -2 then
degree_name = "Diminished " .. degree_name
elseif quality < -2 then
degree_name = (math.abs(quality) - 1) .. "× diminished" .. degree_name
end
end
-- Return nil if the size is zero (meaning nothing was entered or parsable)
if loop_limit == 0 then
return nil
else
return step_ratios
end
end
-- Helper function
-- Takes in a step pattern and a quantity of mossteps and calculates the number
-- of large and small steps in that interval (or substring), returned as an
-- associative array containing the large and small step counts.
-- It's an associative array b/c that's how the brightgen function in the mos
-- module works.
function p.mosstep_pattern_to_vector(mosstep_pattern, mossteps)
local large_step_count = 0
local small_step_count = 0
return degree_name
for i = 1, mossteps do
local step = string.sub(mosstep_pattern, i, i)
if step == "L" then
large_step_count = large_step_count + 1
elseif step == "s" then
small_step_count = small_step_count + 1
end
end
 
local mosstep_vector = { ["L"] = large_step_count, ["s"] = small_step_count }
return mosstep_vector
end
 
-- Helper function
-- Takes in a mosstep (as an assoc. array containing the number of L's and s's),
-- and a step ratio (as 2-element array containing the sizes of L and s) and
-- calculates number of et-steps.
function p.interval_to_etsteps(mosstep_vector, step_ratios)
return mosstep_vector["L"] * step_ratios[1] + mosstep_vector["s"] * step_ratios[2]
end
end


-- Helper function to simplify step ratio
-- Helper function
-- TODO: separate this into a helper module called "MOS notation"
-- For producing row highlighting for the table
function p.simplify_step_ratio(step_ratio_unsimplified)
-- Alterations are highlighted, except for singy augmented/diminished intervals for generators
function p.calculate_row_colors(input_mos, number_of_alterations)
-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
local input_mos = input_mos or mos.new(4, 4, 2)
local number_of_alterations = number_of_alterations or 1
-- Get and simplify the step ratio
-- Get the number of mossteps per period and equave
local kp = step_ratio_unsimplified[1]
local mossteps_per_equave = input_mos.nL + input_mos.ns
local kq = step_ratio_unsimplified[2]
local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
local k = rat.gcd(kp, kq)
local mossteps_per_period = mossteps_per_equave / periods_per_equave
local num = kp / k
local den = kq / k
local row_colors = {}
for i = 1, mossteps_per_equave + 1 do
local mosstep = i - 1
local is_period = mosstep % mossteps_per_period == 0
local is_root = mosstep == 0
local is_equave = mosstep == mossteps_per_equave
-- Row colors for pre-alterations
-- If this is the root, don't add rows before it
if not is_root then
for i = 1, number_of_alterations do
table.insert(row_colors, "#eaeaff")
end
end
-- Row colors for main mossetps (default row color)
if is_period then
table.insert(row_colors, "none")
else
table.insert(row_colors, "none")
table.insert(row_colors, "none")
end
-- Row colors for post-alterations
-- If this is the equave, don't add rows after it
if not is_equave then
for i = 1, number_of_alterations do
table.insert(row_colors, "#eaeaff")
end
end
end
return { num, den }
return row_colors
end
end


-- Function that produces a chain of scale degrees. What scale degrees are
-- Helper function
-- reached by stacking a generator?
-- Calculates note names and stores it in an associative array
-- (EG, major 2nd, augmented 2nd, etc)
-- Default notation is diamond-mos, unless it's 5L 2s, then it's standard notation
-- This function only works one direction at a time, so it's necessary to call
function p.calculate_note_names(input_mos, udp, note_symbols, chroma_plus_symbol, chroma_minus_symbol, number_of_alterations)
-- it twice, one for each direction.
-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
-- Quality encodes maj/min/aug/perf/dim numerically:
local input_mos = input_mos or mos.new(5, 2)
-- -  3 = 2x augmented
local udp = udp or {5,2}
-- -  2 = 1x augmented
local note_symbols = note_symbols or "CDEFGAB"
-- -  1 = major
local chroma_plus_symbol = chroma_plus_symbol or "#"
-- -  0 = perfect (used for generators and root)
local chroma_minus_symbol = chroma_minus_symbol or "b"
-- - -1 = minor
local number_of_alterations = number_of_alterations or 0
-- - -2 = 1x diminished
-- - -3 = 2x diminished
-- TODO: part of a rewrite for the mos degrees function
function p.mos_degrees(input_mos, genchain_length_per_period, going_up)
-- Default parameters for testing
--[[
local input_mos = input_mos or mos.new(5, 2, 2)
local genchain_length_per_period = genchain_length_per_period or 10
local going_up = true
]]--
-- Get the number of mossteps per period and equave
-- Get the number of mossteps per period and equave
local mossteps_per_equave = input_mos.nL + input_mos.ns
local mossteps_per_equave = input_mos.nL + input_mos.ns
local periods_per_equave = rat.gcd(input_mos.nL, input_mos.ns)
local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
local mossteps_per_period = mossteps_per_equave / periods_per_equave
local mossteps_per_period = mossteps_per_equave / periods_per_equave
-- Get the number of mossteps for the generators
-- Get the number of generators going up and down from the UDP
local bright_gen = mos.bright_gen(input_mos)
local generators_up = udp[1]
local mossteps_per_bright_gen = bright_gen['L'] + bright_gen['s']
local generators_down = udp[2]
local mossteps_per_dark_gen = mossteps_per_period - mossteps_per_bright_gen
-- How long is the inital genchain for notes without accidentals?
local gens_up_per_period = generators_up / periods_per_equave
local gens_down_per_period = generators_down / periods_per_equave
-- How long should the genchain extend after the initial genchain?
-- The initial genchain lengths are determined by the U and D in the UDP
-- The final genchain length is the following: (x + y) * (alterations + 1)
local ascending_genchain_length = (mossteps_per_period) * (number_of_alterations + 1)
local descending_genchain_length = (mossteps_per_period) * (number_of_alterations + 1)
-- Get the ascending and descending genchains
-- The genchains are notationally agnostic so notation needs to be applied to them
local ascending_genchain = mosnot.mos_nomacc_chain(input_mos, gens_up_per_period, ascending_genchain_length, true)
local descending_genchain = mosnot.mos_nomacc_chain(input_mos, gens_down_per_period, descending_genchain_length, false)
-- Also get the ascending and descending degreechains
-- These chains are encoded in a numeric form and must be converted into actual names
local ascending_degchain = mosnot.mos_degree_chain(input_mos, ascending_genchain_length, true)
local descending_degchain = mosnot.mos_degree_chain(input_mos, descending_genchain_length, false)
-- Create an empty asoociative array
local note_names = {}
local degreechain = {}
-- Add the notes to the array
for j = 1, periods_per_equave do
for j = 1, periods_per_equave do
local chain_for_period = {}
for i = 1, #ascending_genchain[j] do
-- Convert the notationally agnostic form into a form that uses given notation
local note = ascending_genchain[j][i]
local note_symbol = string.sub(note_symbols, note["Mossteps"] + 1, note["Mossteps"] + 1)
local chroma_count = note["Chromas"]
local note_name = note_symbol .. string.rep(chroma_plus_symbol, chroma_count)
-- Convert the encoded degree into text
local degree_encoded = ascending_degchain[j][i]
local degree_decoded = mosnot.decode_mosstep_quality(degree_encoded, "m", "mosdegree", "abbreviated")
-- Add to note names
note_names[degree_decoded] = note_name
end
for i = 1, #descending_genchain[j] do
-- Convert the notationally agnostic form into a form that uses given notation
local note = descending_genchain[j][i]
local note_symbol = string.sub(note_symbols, note["Mossteps"] + 1, note["Mossteps"] + 1)
local chroma_count = note["Chromas"] * -1
local note_name = note_symbol .. string.rep(chroma_minus_symbol, chroma_count)
-- Convert the encoded degree into text
local degree_encoded = descending_degchain[j][i]
-- For the descending chain, any mossteps that correspond to the root of
-- a period should correspond instead to the root one period up (EG, if
-- the root refers to the unison for a single-period mos, it should be
-- the degree one octave up)
if degree_encoded["Mossteps"] % mossteps_per_period == 0 then
-- Transpose the mosstep by one period
degree_encoded["Mossteps"] = degree_encoded["Mossteps"] + mossteps_per_period
end
-- Correct the note name based on whether it should be a note that is
-- one period up. If the mos is single-period, then do not transpose.
if degree_encoded["Mossteps"] % mossteps_per_period == 0 and degree_encoded["Mossteps"] == 0 then
-- Correct the note name
note_symbol = string.sub(note_symbols, 1, 1)
note_name = note_symbol .. string.rep(chroma_minus_symbol, chroma_count)
elseif degree_encoded["Mossteps"] % mossteps_per_period == 0 and degree_encoded["Mossteps"] == 0 then
-- Correct the note name
note_symbol = string.sub(note_symbols, degree_encoded["Mossteps"] + 1, degree_encoded["Mossteps"] + 1)
note_name = note_symbol .. string.rep(chroma_minus_symbol, chroma_count)
end
-- Pass the encoded degree, along with the other args
local degree_decoded = mosnot.decode_mosstep_quality(degree_encoded, "m", "mosdegree", "abbreviated")
-- Add to note names
note_names[degree_decoded] = note_name
end
end
return note_names
end


for i = 1, genchain_length_per_period do
-- Helper function; generate the step vectors for every interval required for the table
function p.calculate_mosstep_vectors(input_mos, number_of_alterations)
-- Default params
local input_mos = input_mos or mos.new(5, 2)
local number_of_alterations = number_of_alterations or 0
-- Get the brightest mode
local brightest_mode = mos.brightest_mode(input_mos)
-- Get the number of mossteps per period and equave
local mossteps_per_equave = (input_mos.nL + input_mos.ns)
local mossteps_per_period = mossteps_per_equave / utils._gcd(input_mos.nL, input_mos.ns)
-- Add intervals and their alterations, using the large interval size as the zero point for alterations
local mosstep_vectors = {}
for i = 1, mossteps_per_equave + 1 do
local mossteps = i - 1
-- Consecutive alterations are always one chroma apart
-- With a perfect non-generator interval, alterations are added by going down and up the same amount
-- With all other intervals, since there are two sizes and the large interval size is treated as the zero point,
-- alterations are instead by going down n+1 chromas, then going up n chromas
-- With the unison, don't go down (only up), and with the equave, don't go up (only down).
local min_alterations = 0
local max_alterations = 0
if mossteps == 0 then
-- Unison; the min number of alterations is 0
min_alterations = 0
max_alterations = number_of_alterations
elseif mossteps == mossteps_per_equave then
-- Equave; the max number of alterations is 0
min_alterations = -number_of_alterations
max_alterations = 0
elseif mossteps % mossteps_per_period == 0 then
-- Non-unison non-equave periods; the max and min have the "distance" from the zero point
min_alterations = -number_of_alterations
max_alterations = number_of_alterations
else
-- All other intervals; the min's distance is one more than the max's distance
min_alterations = -number_of_alterations - 1
max_alterations = number_of_alterations
end
-- Get the current mosstep vector based on the brightest mode
local current_mosstep_vector = p.mosstep_pattern_to_vector(brightest_mode, mossteps)
for j = min_alterations, max_alterations do
-- j is the number of chromas to add or subtract from the base vector
-- Since a chroma is defined as (L-s), add j large steps and subtract j small steps from the current mosstep vector
local L_count = current_mosstep_vector["L"] + j
local s_count = current_mosstep_vector["s"] - j
-- Calculate mossteps
local current_mosstep_vector = { ["L"] = L_count, ["s"] = s_count }
local mossteps = 0
table.insert(mosstep_vectors, current_mosstep_vector)
if going_up then
end
mossteps = (i - 1) * mossteps_per_bright_gen % mossteps_per_period + (j - 1) * mossteps_per_period
end
else
mossteps = (i - 1) * mossteps_per_dark_gen % mossteps_per_period + (j - 1) * mossteps_per_period
return mosstep_vectors
end
 
-- Helper function; generate the mosdegree names and their abbreviations for the mos
function p.calculate_mosdegree_names_and_abbrevs(input_mos, mos_prefix, number_of_alterations)
-- Default params
local input_mos = input_mos or mos.new(5, 2)
local number_of_alterations = number_of_alterations or 0
local mos_prefix = mos_prefix or "mos"
-- Get the number of mossteps per period and equave
local mossteps_per_equave = (input_mos.nL + input_mos.ns)
local mossteps_per_period = mossteps_per_equave / utils._gcd(input_mos.nL, input_mos.ns)
-- Get the step counts for the bright and dark generators
local bright_gen = mos.bright_gen(input_mos)
local mossteps_per_bright_gen = bright_gen["L"] + bright_gen["s"]
local mossteps_per_dark_gen = mossteps_per_period - mossteps_per_bright_gen
-- Main loop
-- Interval qualities depend on whether the intervals are generators or if there is only one size.
-- Cases for which there are alterations either below or above the main interval sizes, but not both:
-- - If the interval class is the unison, there are no extensions before it and there is only one size (perfect).
-- - If the interval class is the equave, there are no extensions after it and there is only one size (perfect).
-- Cases for which there are alterations above and below the main interval sizes:
-- - If the interval class is a non-unison non-equave period, there are extensions before and after and there is only one size (perfect).
-- - If the interval class is a non-generator interval, or is the generator for an nL ns mos, there are extensions before and after and the sizes are major and minor.
-- - If the interval class is the bright generator, there are extensions before and after and the sizes are perfect (large) and diminished (small).
-- - If the interval class is the dark generator, there are extensions before and after and the sizes are augmented (large) and perfect (small).
local mosdegree_names = {}
local mosdegree_abbrevs = {}
for i = 1, mossteps_per_equave + 1 do
-- For calculating mossteps
local mossteps = i - 1
-- For bright and dark gens
local is_nL_ns = input_mos.nL == input_mos.ns
local is_bright_gen = mossteps % mossteps_per_period == mossteps_per_bright_gen and not is_nL_ns
local is_dark_gen = mossteps % mossteps_per_period == mossteps_per_dark_gen and not is_nL_ns
if mossteps % mossteps_per_period == 0 then
-- For perfect intervals
-- Operation for pre-alterations (diminshed degrees)
if number_of_alterations > 0 and mossteps ~= 0 then
for j = number_of_alterations, 1, -1 do
-- Diminished degree is formatted as "Diminished degree"; more than 1 augmentation is "2× Diminished", "3× Diminished", and so on
local dim_degree = ""
if j == 1 then dim_degree = string.format("Diminished %d-%sdegree", mossteps, mos_prefix)
else dim_degree = string.format("%d× Diminished %d-%sdegree", j, mossteps, mos_prefix)
end
-- Format abbreviation as repetitions of the letter "d", followed by the mosdegree
local dim_abbrev = string.rep("d", j) .. string.format("%dmd", mossteps)
-- Insert
table.insert(mosdegree_names, dim_degree)
table.insert(mosdegree_abbrevs, dim_abbrev)
end
end
end
 
-- Calculate quality
-- Calculate the main degree name and abbreviation
-- The first two elements in the chain are always perfect
local degree_name = string.format("Perfect %d-%sdegree", mossteps, mos_prefix)
-- All intervals after that are major (or minor if going down)
local abbrev_name = string.format("P%dmd", mossteps)
-- After the major intervals are augmented intervals, which starts
-- with the augmented dark generator, which comes before the
-- Main operation
-- augmented unison. (or minor and dim bright gen if going down)
table.insert(mosdegree_names, degree_name)
-- For nL ns mosses, generators are major and minor instead, so only
table.insert(mosdegree_abbrevs, abbrev_name)
-- the root is perfect
local quality = 0
-- Operation for post-alterations (augmented degrees)
if input_mos.nL ~= input_mos.ns then
if number_of_alterations > 0 and mossteps ~= mossteps_per_equave then
if i == 1 or i == 2 then
for j = 1, number_of_alterations do
quality = 0
-- Augmented degree is formatted as "Augmented degree"; more than 1 augmentation is "2× Augmented", "3× Augmented", and so on
else
local aug_degree = ""
-- Offsetting i by +1 will make it so the dark generator
if j == 1 then aug_degree = string.format("Augmented %d-%sdegree", mossteps, mos_prefix)
-- before the augmented unison is denoted as augmented,
else aug_degree = string.format("%d× Augmented %d-%sdegree", j, mossteps, mos_prefix)
-- but lua's start-from-1 indexing offsets it by 1 already.
quality = math.floor(i / mossteps_per_period) + 1
if not going_up then
quality = quality * -1
end
end
-- Format abbreviation as repetitions of the letter "A", followed by the mosdegree
local aug_abbrev = string.rep("A", j) .. string.format("%dmd", mossteps)
-- Insert
table.insert(mosdegree_names, aug_degree)
table.insert(mosdegree_abbrevs, aug_abbrev)
end
end
else
end
if i == 1 then
else  
quality = 0
-- For intervals with two sizes
else
-- Operation for pre-alterations (diminshed degrees)
quality = math.floor((i + 1) / mossteps_per_period)
if number_of_alterations > 0 and mossteps ~= 0 then
if not going_up then
for j = number_of_alterations, 1, -1 do
quality = quality * -1
-- The number of diminishings depends on whether the interval class is the bright gen; if so,
-- then one interval will already be diminished so intervals below that already start at 2xdim.
local dim_amount = 0
if is_bright_gen then dim_amount = 1 + j
else dim_amount = j
end
-- Diminished degree is formatted as "Diminished degree"; more than 1 augmentation is "2× Diminished", "3× Diminished", and so on
local dim_degree = ""
if dim_amount == 1 then dim_degree = string.format("Diminished %d-%sdegree", mossteps, mos_prefix)
else dim_degree = string.format("%d× Diminished %d-%sdegree", dim_amount, mossteps, mos_prefix)
end
end
-- Format abbreviation as repetitions of the letter "d", followed by the mosdegree
local dim_abbrev = string.rep("d", dim_amount) .. string.format("%dmd", mossteps)
-- Insert
table.insert(mosdegree_names, dim_degree)
table.insert(mosdegree_abbrevs, dim_abbrev)
end
end
end
end
-- Put together the name
-- Calculate the small and large names and abbreviations
local degree = { ['mossteps'] = mossteps, ['quality'] = quality }
-- Non-generator intervals for non-nL-ns mosses are minor (small) and major (large)
table.insert(chain_for_period, degree)
local small_degree_label = "Minor"
local large_degree_label = "Major"
local small_degree_abbrev = "m"
local large_degree_abbrev = "M"
if is_bright_gen then
-- Bright gen: diminished (small) and perfect (large)
small_degree_label = "Diminished"
large_degree_label = "Perfect"
small_degree_abbrev = "d"
large_degree_abbrev = "P"
elseif is_dark_gen then
-- Dark gen: perfect (small) and augmentd (large)
small_degree_label = "Perfect"
large_degree_label = "Augmented"
small_degree_abbrev = "P"
large_degree_abbrev = "A"
end
-- Main operation
local small_degree_name = string.format("%s %d-%sdegree", small_degree_label, mossteps, mos_prefix)
local large_degree_name = string.format("%s %d-%sdegree", large_degree_label, mossteps, mos_prefix)
local small_abbrev_name = string.format("%s%dmd", small_degree_abbrev, mossteps)
local large_abbrev_name = string.format("%s%dmd", large_degree_abbrev, mossteps)
table.insert(mosdegree_names, small_degree_name)
table.insert(mosdegree_names, large_degree_name)
table.insert(mosdegree_abbrevs, small_abbrev_name)
table.insert(mosdegree_abbrevs, large_abbrev_name)
-- Operation for post-alterations (augmented degrees)
if number_of_alterations > 0 and mossteps ~= mossteps_per_equave then
for j = 1, number_of_alterations do
-- The number of augmentings depends on whether the interval class is the dark gen; if so,
-- then one interval will already be augmented so intervals above that already start at 2xaug.
local aug_amount = 0
if is_dark_gen then aug_amount = 1 + j
else aug_amount = j
end
-- Augmented degree is formatted as "Augmented degree"; more than 1 augmentation is "2× Augmented", "3× Augmented", and so on
local aug_degree = ""
if aug_amount == 1 then aug_degree = string.format("Augmented %d-%sdegree", mossteps, mos_prefix)
else aug_degree = string.format("%d× Augmented %d-%sdegree", aug_amount, mossteps, mos_prefix)
end
-- Format abbreviation as repetitions of the letter "A", followed by the mosdegree
local aug_abbrev = string.rep("A", aug_amount) .. string.format("%dmd", mossteps)
-- Insert
table.insert(mosdegree_names, aug_degree)
table.insert(mosdegree_abbrevs, aug_abbrev)
end
end
end
end
table.insert(degreechain, chain_for_period)
end
end
return degreechain
return mosdegree_names, mosdegree_abbrevs
end
end


-- Algorithm:
-- Separate function for testing; the main "frame" function will call this
-- Use the input mos, udp, and step ratio to find the genchains
function p.mos_degrees(input_mos, step_ratios, mos_prefix, show_abbreviations, number_of_alterations, ji_ratios, udp, notation, show_notation)
-- Using the genchains and UDP, find the mos's intervals/degrees
-- Default params; all parameters are already parsed
-- Format the result as a table
local input_mos = input_mos or mos.new(5, 2)
function p.mos_degrees_frame(frame)
local step_ratios = step_ratios or {{2,1}, {3,1}, {3,2}}
-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
local mos_prefix = mos_prefix or "mos"
local input_mos_unparsed = frame.args['Scale Signature']
local show_abbrevs = show_abbreviations == 1
local input_mos = mos.parse(input_mos_unparsed) or mos.new(2, 5, 2)
local number_of_alterations = number_of_alterations or 1
local ji_ratios = ji_ratios or {["P0md"]="1/1"}
local udp = udp or {5,1}
local notation = notation or mosnot.parse_notation("CDEFGAB; #; b")
local show_notation = show_notation == 1
-- Get the scale sig
local scale_sig = mos.as_string(input_mos)
-- Step ratio
-- Get the brightest and darkest modes for the mos
local step_ratio = { 2, 1 }
local brightest_mode = mos.brightest_mode(input_mos)
if string.len(frame.args['Step Ratio']) > 0 then
local darkest_mode = string.reverse(brightest_mode)
step_ratio = p.parse_step_ratio(frame.args['Step Ratio'])
end
-- Get the number of mossteps per period and equave
-- Get the number of mossteps per period and equave, and periods per equave
local mossteps_per_equave = input_mos.nL + input_mos.ns
local mossteps_per_equave = (input_mos.nL + input_mos.ns)
local periods_per_equave = rat.gcd(input_mos.nL, input_mos.ns)
local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
local mossteps_per_period = mossteps_per_equave / periods_per_equave
local mossteps_per_period = mossteps_per_equave / periods_per_equave
-- If certain params were left blank and the scalesig is 5L 2s, the default
-- Get the step counts for the bright and dark generators
-- params will be for standard notation
local bright_gen = mos.bright_gen(input_mos)
local scale_sig = mos.as_string(input_mos)
local steps_per_bright_gen = bright_gen["L"] + bright_gen["s"]
local steps_per_dark_gen = mossteps_per_period - steps_per_bright_gen
-- Get the step counts as a vector (or associative array, rather)
local input_mos_step_vector = {["L"] = input_mos.nL, ["s"] = input_mos.ns}
-- What's the equave in cents?
local equave_in_cents = rat.cents(input_mos.equave)
 
-- How many decimal places to round to? (hardcoded)
local round = 1
-- The default UDP corresponds to the brightest mode
-- Precalculate row colors
-- If it's 5L 2s, default to the second-brightest mode
local row_colors = p.calculate_row_colors(input_mos, number_of_alterations)
local udp = { mossteps_per_equave - periods_per_equave, 0 }
 
if scale_sig == "5L 2s" then
-- Precalculate the ets for each step ratio
udp = { 5, 1 }
-- Each et is used to calculate a scale degree's cent value
end
local ets_for_mos = {}
if string.len(frame.args['UDP']) > 0 then
for i = 1, #step_ratios do
udp = p.parse_udp(frame.args['UDP'])
local etsteps = p.interval_to_etsteps(input_mos_step_vector, step_ratios[i])
local et_for_mos = et.new(etsteps, input_mos.equave)
table.insert(ets_for_mos, et_for_mos)
end
end
-- Get note symbols
-- Precalculate degree names, degree abbreviations, and mosstep vectors
-- If this param was blank, default to diamond-mos; limited to 17 note names
local degree_names, degree_abbrevs = p.calculate_mosdegree_names_and_abbrevs(input_mos, mos_prefix, number_of_alterations)
-- But if it's blank and the scalesig is 5L 2s, default to standard notation
local mosstep_vectors = p.calculate_mosstep_vectors(input_mos, number_of_alterations)
-- This order of operations allows for overriding standard notation for 5L 2s
local note_symbols_main = "JKLMNOPQRSTUVWXYZ"
-- Precalculate default comments for JI ratios; there's only two entries here
local note_symbols = string.sub(note_symbols_main, 1, mossteps_per_equave)
local default_ji_comments = {}
if scale_sig == "5L 2s" then
default_ji_comments["P0md"] = "1/1 (exact)"
note_symbols = "CDEFGAB"
default_ji_comments[string.format("P%dmd", mossteps_per_equave)] = string.format("%s (exact)", rat.as_ratio(input_mos.equave))
end
-- If a value was entered, override the default value
if string.len(frame.args['Note Symbols']) > 0 then
note_symbols = frame.args['Note Symbols']
end
-- Get accidental symbols
-- Then, using the UDP, get the notation
-- If this param was blank, default to diamond-mos symbols & and @
-- The default notation is either standard notation (for 5L 2s) or diamond-mos (most other mosses)
-- unless the mos is 5L 2s, then it's sharp and flat # and b
-- If notation is passed in, use that instead
-- This order of operations allows for overriding standard notation for 5L 2s
-- If no notation is passed in, notation will not be displayed
local chroma_plus_symbol = "&"
local note_names = {}
local chroma_minus_symbol = "@"
local root_note = ""
if scale_sig == "5L 2s" then
if show_notation then
chroma_plus_symbol = "#"
note_names = p.calculate_note_names(input_mos, udp, notation["Naturals"], notation["Sharp"], notation["Flat"], number_of_alterations)
chroma_minus_symbol = "b"
root_note = string.sub(notation["Naturals"], 1, 1)
end
-- If value(s) were entered, override the default values
if string.len(frame.args['Sharp Symbol']) > 0 then
chroma_plus_symbol = frame.args['Sharp Symbol']
end
if string.len(frame.args['Flat Symbol']) > 0 then
chroma_minus_symbol = frame.args['Flat Symbol']
end
end
-- How long are the initial genchain lengths? (These correspond to the UDP)
-- Create the table, starting with the headers
local gens_up_per_period = udp[1] / periods_per_equave
local result = "{| class=\"wikitable sortable mw-collapsible mw-collapsed\"\n"
local gens_dn_per_period = udp[2] / periods_per_equave
-- How long should the genchains be?
-- First row
-- The length should be such that:
result = result
-- - Every non-root interval is shown in its small, large, augmented, and
.. "|+ style=\"font-size: 105%%; white-space: nowrap;\" | " .. string.format("Scale degree of %s\n", scale_sig)
--  diminished size.
.. "|-\n"
-- - The root and equave are shown in their perfect sizes, followed by their
.. "! rowspan=\"2\" class=\"unsortable\" | Scale degree\n"
--  augmented and diminished sizes respectively.
-- - Any non-root non-equave periods are shown in their perfect, augmented,
--  and diminished sizes.
-- To do this requires going up 2x+2y generators, and down the same amount.
-- Going up x+y gens from the root reaches every scale degree's large size,
-- plus the augmented root, then going up x+y-1 more gens reaches each
-- augmented degree. Same is true for going down to get minor/dim degrees.
local asc_chain_length = (input_mos.nL + input_mos.ns) * 2
local des_chain_length = (input_mos.nL + input_mos.ns) * 2
-- Get the genchains
-- Add column for abbreviations
local asc_genchain = mosg.mos_genchain(input_mos, gens_up_per_period, asc_chain_length, true)
-- Abbreviations do not use a mos-prefix or mos-name
local des_genchain = mosg.mos_genchain(input_mos, gens_dn_per_period, des_chain_length, false)
if show_abbrevs then
result = result .. "! rowspan=\"2\" class=\"unsortable\" | Abbrev.\n"
end
-- Get the degrees
-- Add column for note names
local asc_degrees = p.mos_degrees(input_mos, asc_chain_length, true)
if show_notation then
local des_degrees = p.mos_degrees(input_mos, des_chain_length, true)
result = result .. string.format("! rowspan=\"2\" class=\"unsortable\" | On %s\n", root_note)
end
-- Format the output as a table, starting with the header row
-- Add column headers for up to 5 different step ratios
local result = '{| class="wikitable"\n'
for i = 1, #step_ratios do
 
-- Step ratio names, for reference
-- Produce the headers
local tamnams_step_ratios = {
local steps_in_et = input_mos.nL * step_ratio[1] + input_mos.ns * step_ratio[2]
["1:1"] = "Equalized",
local et_for_mos = et.new(steps_in_et, input_mos.equave)
["4:3"] = "Supersoft",
result = result .. "! Scale degree !! Steps !! Cents !! Note name on ".. string.sub(note_symbols, 1, 1) .. "\n"
["3:2"] = "Soft",
["5:3"] = "Semisoft",
["2:1"] = "Basic",
["5:2"] = "Semihard",
["3:1"] = "Hard",
["4:1"] = "Superhard",
["1:0"] = "Collapsed",
}
-- Get name for step ratio
local step_ratio_simplified = mosnot.simplify_step_ratio(step_ratios[i])
local step_ratio_key = step_ratio_simplified[1] .. ":" .. step_ratio_simplified[2]
local step_ratio_name = tamnams_step_ratios[step_ratio_key]
-- Calculate the et for the mos with a given step ratio; this is needed to produce
-- the name for the et/edo
local et_for_mos = et.new(ets_for_mos[i].size, input_mos.equave)
local et_as_string = et.as_string(et_for_mos)
-- Add the step ratio name if there is one
if step_ratio_name == nil then
result = result .. "! colspan=\"2\" | " .. et_as_string .. " (L:s = " .. step_ratio_key .. ")\n"
else
result = result .. "! colspan=\"2\" | " .. et_as_string .. " (" .. step_ratio_name .. ", L:s = " .. step_ratio_key .. ")\n"
end
end
-- How many esteps per period? Per bright/dark gen?
-- Add JI ratio column header
local esteps_per_period = steps_in_et / periods_per_equave
result = result .. "! rowspan=\"2\" class=\"unsortable\" | Approx. JI Ratios\n"
local bright_gen = mos.bright_gen(input_mos)
local esteps_per_bright_gen = bright_gen['L'] * step_ratio[1] + bright_gen['s'] * step_ratio[2]
local esteps_per_dark_gen = esteps_per_period - esteps_per_bright_gen
-- Add the rows
-- Second row
local step_ratio_gcd = rat.gcd(step_ratio[1], step_ratio[2]) -- GCD of the sizes of L and s, in case L:s isn't simplified
result = result .. "|-\n"
local cents_per_equave = rat.cents(input_mos.equave) -- Equave in cents
-- Add headers for the steps and cents up to 5 times
for i = 1, #step_ratios do
result = result .. "! Steps\n"
result = result .. "! Cents\n"
end
-- For each period, add a row containing a scale degree, step count, cent
-- Add in successive rows, containing the degree name, abbreviation (if applicable),
-- value, and note name from the ascending genchain, then do the same with
-- note names (if applicable), step size (in steps and cents), and JI ratio
-- the descending genchain, in reverse and skipping the perfect root and
for i = 1, #degree_names do
-- raising any other root by one period. Repeat for all other periods.
-- Start new row
-- For the last period, add the perfect root as the perfect equave.
-- Add row highlighting if provided
for i = 1, periods_per_equave do
local row_color = row_colors[i]
-- Add degrees from ascending chain
if row_color == "" then
for j = 1, #asc_genchain[i] do
result = result .. "|-\n"
local note = asc_genchain[i][j]
else
local mossteps = note['mossteps']
result = result .. string.format("|- style=\"background: %s\"\n", row_color)
local chromas = note['chromas']
end
local quality = asc_degrees['quality']
-- Add degree name
-- Find the note name
-- Make the text bold if the interval is a perfect interval
local note_name = p.mosstep_and_chroma_to_note_name(mossteps, chromas, string.sub(note_symbols, mossteps, mossteps), chroma_plus_symbol)
local degree_name = degree_names[i]
if string.find(degree_name, "Perfect") then
-- Find the degree name
if i == 1 then
-- If the degree is the 0-mosdegree, say it's the unison instead
result = result .. string.format("| '''%s (unison)'''\n", degree_names[i])
local degree_name = p.mosstep_and_quality_to_degree(mossteps, quality)
elseif i == #degree_names and equave_in_cents == 1200 then
degree_name = string:gsub("0-mosstep", "unison")
result = result .. string.format("| '''%s (octave)'''\n", degree_names[i])
elseif i == #degree_names and equave_in_cents ~= 1200 then
-- Find the estep count
result = result .. string.format("| '''%s (equave)'''\n", degree_names[i])
local estep_count = ((j - 1) * esteps_per_bright_gen) % esteps_per_period + (j - 1) * esteps_per_period
else
result = result .. string.format("| '''%s'''\n", degree_names[i])
-- Find the cent value
end
local cent_value = et.cents(et_for_mos, estep_count)
else
result = result .. string.format("| %s\n", degree_names[i])
-- Add the row
end
result = result .. "|- " .. degree_name .. " || " .. estep_count .. " || " .. cent_value .. " || " .. note_name .. "\n"
-- Add abbrev if allowed
local degree_abbrev = degree_abbrevs[i]
if show_abbrevs then
result = result .. string.format("| %s\n", degree_abbrev)
end
end
-- Calculate the stop value for the for loop as being 1 or 2, depending
-- Add note names if allowed
-- on whether this is the last period or not
-- Use the degree_abbrev as the key when accessing key-value pairs
local stop_value = 1
if show_notation then
if i == periods_per_equave then
result = result .. string.format("| %s\n", note_names[degree_abbrev])
stop_value = stop_value + 1
end
end
-- Add degrees from descending chain
-- Add mossteps and cent values
for j = #des_genchain[i], stop_value, -1 do
-- Rounding is hardcoded to one decimal place
local note = des_genchain[i][j]
-- Also record the cent value for JI ratio search
local mossteps = note['mossteps']
local round = 1
local chromas = note['chromas']
local average_cents = 0
local quality = asc_degrees['quality']
for j = 1, #ets_for_mos do
local etsteps = mosstep_vectors[i]["L"] * step_ratios[j][1] + mosstep_vectors[i]["s"] * step_ratios[j][2]
local cents = utils._round_dec(et.cents(ets_for_mos[j], etsteps), round)
-- Find the note name
result = result .. string.format("| %s\n", etsteps)
-- If the mosstep is the root of the period, add a period to it
result = result .. string.format("| %s\n", cents)
if mossteps % mossteps_per_period == 0 then
mossteps = mossteps + mossteps_per_period
end
local note_name = p.mosstep_and_chroma_to_note_name(mossteps, chromas, string.sub(note_symbols, mossteps, mossteps), chroma_plus_symbol)
-- Find the degree name
average_cents = average_cents + cents / #ets_for_mos
-- If the degree corresponds to the equave, say it's the equave
end
local degree_name = p.mosstep_and_quality_to_degree(mossteps, quality)
local equave_degree_name = (input_mos.nL + input_mos.ns) .. "-mosstep"
-- Calculate JI ratio approximations using jiraf module
degree_name = string:gsub(equave_degree_name, "equave")
-- For now:
-- - Cent value is the average of the sizes given the step ratios
-- Find the estep count
-- - Tolerance is hardcoded to +/-15 cents
local estep_count = ((j - 1) * esteps_per_dark_gen) % esteps_per_period + (j - 1) * esteps_per_period
-- - Prime limit is hardocoded to 19
-- - Odd limit hardcoded to 49
-- Find the cent value
--local approx_ratios = jiraf.find_ratios_for_cents(average_cents, 15, 19, 39)
local cent_value = et.cents(et_for_mos, estep_count)
--local ratios_as_text = jiraf.ratios_to_text(approx_ratios);
-- Add the row
-- Add JI ratios if any
result = result .. "|- " .. degree_name .. " || " .. estep_count .. " || " .. cent_value .. " || " .. note_name .. "\n"
local ji_comment_entry = ""
local default_ji_comment = default_ji_comments[degree_abbrev]
-- Add ratios found using jiraf
local entered_ji_comment = ji_ratios[degree_abbrev]
--local default_ji_comment = nil
--local entered_ji_comment = ratios_as_text
if default_ji_comment == nil and entered_ji_comment == nil then
-- No comments
result = result .. "|\n"
elseif default_ji_comment ~= nil and entered_ji_comment == nil then
-- Default comments but no entered comments
result = result .. string.format("| %s\n", default_ji_comment)
elseif default_ji_comment == nil and entered_ji_comment ~= nil then
-- Entered comments but no default comments
result = result .. string.format("| %s\n", entered_ji_comment)
else
-- Both comments present; default comments take precedence
result = result .. string.format("| %s, %s\n", default_ji_comment, entered_ji_comment)
end
end
end
end
-- End of table
result = result .. "|}"
result = result .. "|}"
return result
return result
end
end
-- This function is to be called by a template, with parameters
function p.mos_degrees_frame(frame)
-- Default param for input mos is 5L 2s
local input_mos = mos.parse(frame.args["Scale Signature"]) or mos.new(2, 5, 2)
-- Get the scale sig; for calculating the mos prefix
local scale_sig = mos.as_string(input_mos)
-- Get the step ratio
local step_ratios = p.parse_step_ratio(frame.args["Step Ratio"]) or p.parse_step_ratio("2/1")
-- Default param for mos prefix
-- If "NONE" is given, no prefix will be used
-- If left blank, try to find the appropriate mos prefix, or else defualt to "mos"
-- If not left blank, use the prefix passed in instead
local mos_prefix = "mos"
if frame.args["MOS Prefix"] == "NONE" then
mos_prefix = ""
elseif string.len(frame.args["MOS Prefix"]) == 0 then
mos_prefix_lookup = tamnams.lookup_prefix(input_mos) or ""
if string.len(mos_prefix_lookup) ~= 0 then
mos_prefix = mos_prefix_lookup
end
else
mos_prefix = frame.args["MOS Prefix"]
end
-- Get whether to display abbreviations
local show_abbreviations = 0
if frame.args["Show Abbreviations"] == "1" or frame.args["Show Abbreviations"] == 1 then
show_abbreviations = 1
end
-- Get the number of alterations
local number_of_alterations = 0
if string.len(frame.args["Number of Alterations"]) ~= 0 then
number_of_alterations = tonumber(frame.args["Number of Alterations"])
end
-- Get JI ratios
local ji_ratios_parsed = {}
if #frame.args["JI Ratios"] > 0 then
-- If the comments can't be parsed, default to an empty table
ji_ratios_parsed = p.parse_kv_pairs(frame.args["JI Ratios"]) or {}
end
-- Get the number of mossteps per period and equave, and periods per equave
-- Needed for calculating default UDP and notation
local mossteps_per_equave = (input_mos.nL + input_mos.ns)
local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
local mossteps_per_period = mossteps_per_equave / periods_per_equave
-- Get UDP
-- If no UDP is found, a default will be calculated as the middle mode, or the
-- brighter of two middle modes (as with an even number of modes in a mos)
local udp_parsed = { periods_per_equave * math.ceil((mossteps_per_period - 1)/ 2), periods_per_equave * math.floor((mossteps_per_period - 1) / 2) }
if scale_sig == "5L 2s" then
udp_parsed = { 5, 1 }
end
if #frame.args["UDP"] > 0 then
udp_parsed = mosnot.parse_udp(frame.args["UDP"])
end
-- Get notation
-- This also determines whether to show notation
-- Typing in "Default" is a shortcut to default notation, wherein standard notation (for 5L 2s) or diamond-mos (for other mosses) is used
local notation_parsed = {}
local show_notation = 0
if #frame.args["Notation"] > 0 then
if frame.args["Notation"] == "Default" and scale_sig == "5L 2s" then
notation_parsed = { ["Naturals"] = "CDEFGAB", ["Sharp"] = "#", ["Flat"] = "b" }
show_notation = 1
elseif frame.args["Notation"] == "Default" and scale_sig ~= "5L 2s" then
local default_nominals = "JKLMNOPQRSTUVWXYZ"
notation_parsed = { ["Naturals"] = string.sub(default_nominals, 1, mossteps_per_equave), ["Sharp"] = "&", ["Flat"] = "@" }
show_notation = 1
else
notation_parsed = mosnot.parse_notation(frame.args["Notation"])
if notation_parsed ~= nil then
show_notation = 1
end
end
end
result = p.mos_degrees(input_mos, step_ratios, mos_prefix, show_abbreviations, number_of_alterations, ji_ratios_parsed, udp_parsed, notation_parsed, show_notation)
-- Debugger
local debugg = yesno(frame.args["debug"])
if debugg == true then
result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
end
return frame:preprocess(result)
end
return p
return p

Latest revision as of 12:25, 1 June 2025

Module documentation[view] [edit] [history] [purge]
This module should not be invoked directly; use its corresponding template instead: Template:MOS degrees.
Module:MOS degrees is deprecated and has been replaced by Module:MOS tunings. Further use of this module is not advised. This module is kept for historical purposes and should not be deleted.
Introspection summary for Module:MOS degrees 
Functions provided (12)
Line Function Params
22 parse_entries (unparsed)
34 parse_pair (unparsed)
51 parse_kv_pairs (unparsed)
70 parse_step_ratio (unparsed)
99 mosstep_pattern_to_vector (mosstep_pattern, mossteps)
120 interval_to_etsteps (mosstep_vector, step_ratios)
127 calculate_row_colors (input_mos, number_of_alterations)
175 calculate_note_names (input_mos, udp, note_symbols, chroma_plus_symbol, chroma_minus_symbol, number_of_alterations)
276 calculate_mosstep_vectors (input_mos, number_of_alterations)
336 calculate_mosdegree_names_and_abbrevs (input_mos, mos_prefix, number_of_alterations)
505 mos_degrees (input_mos, step_ratios, mos_prefix, show_abbreviations, number_of_alterations, ji_ratios, udp, notation, show_notation)
735 mos_degrees_frame (invokable) (frame)
Lua modules required (7)
Variable Module Functions used
et Module:ET new
as_string
cents
mos Module:MOS new
brightest_mode
bright_gen
as_string
parse
mosnot Module:MOS notation parse_step_ratio
mos_nomacc_chain
mos_degree_chain
decode_mosstep_quality
parse_notation
simplify_step_ratio
parse_udp
rat Module:Rational cents
as_ratio
tamnams Module:TAMNAMS lookup_prefix
utils Module:Utils _gcd
_round_dec
yesno Module:Yesno yesno

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


local p = {}

local et = require("Module:ET")
--local jiraf = require("Module:JI ratio finder")
local mos = require("Module:MOS")
local mosnot = require("Module:MOS notation")
local rat = require("Module:Rational")
local tamnams = require("Module:TAMNAMS")
local utils = require("Module:Utils")
local yesno = require("Module:Yesno")

-- TODO:
-- Rewrite "main function" into a underscore-prefixed function to be called by Lua code and a wrapper to be called by templates. (HIGH PRIORITY!!!)
-- Adopt MOS arithmetic function (MEDIUM-PRIORITY!!!)
-- Add support for double accidentals (low-priority)
-- Move certain helper functions to helper modules (low-priority)

-- Helper function
-- Parses entries from a semicolon-delimited string and returns them in an array
-- TODO: Separate this and related functions (parse_pair and parse_kv_pairs) into its own module, as they're included
-- in various modules at this point, such as: scale tree, MOS modes
function p.parse_entries(unparsed)
	local parsed = {}
	for entry in string.gmatch(unparsed, "([^;]+)") do
		local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
		table.insert(parsed, trimmed)		-- Add to array
	end
	return parsed
end

-- Helper function
-- Parses pairs of elements separated by a colon
-- A pair must be two elements or it will be returned as an empty array
function p.parse_pair(unparsed)
	local parsed = {}
	for entry in string.gmatch(unparsed, "([^:]+)") do
		local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
		table.insert(parsed, trimmed)		-- Add to array
	end
	if #parsed == 2 then
		return parsed
	else
		return {}
	end
end

-- Helper function
-- Takes a list of semicolon-delimited pairs and returns a map
-- (or dictionary or associative array) of key-value pairs
-- Each entry is colon-delimited as key : pair
function p.parse_kv_pairs(unparsed)
	-- Tokenize the string of unparsed pairs
	local parsed = p.parse_entries(unparsed)
	-- Then tokenize the tokens into key-value pairs
	local pairs_ = {}
	for i = 1, #parsed do
		local pair = p.parse_pair(parsed[i])
		if #pair == 2 then
			pairs_[pair[1]] = pair[2]
		end
	end
	return pairs_
end

-- Helper function
-- Parses up to 5 step ratios entered as text in a semicolon-delimited string,
-- where each step ratio is separated with a slash
-- EG, "2/1; 3/1; 3/2" becomes {{2, 1}, {3, 1}, {3, 2}}
-- NOTE: module relies on mosnot (mos notation) to parse step ratios
function p.parse_step_ratio(unparsed)
	local parsed = {}
	for entry in string.gmatch(unparsed, "([^;]+)") do
		local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
		table.insert(parsed, trimmed)		-- Add to array
	end
	
	-- Parse up to 5 step ratios (hardcoded)
	local max_ratios = 5
	local loop_limit = math.min(max_ratios, #parsed)
	local step_ratios = {}
	for i = 1, loop_limit do
		local ratio = mosnot.parse_step_ratio(parsed[i])
		table.insert(step_ratios, ratio)
	end
	-- Return nil if the size is zero (meaning nothing was entered or parsable)
	if loop_limit == 0 then
		return nil
	else
		return step_ratios
	end
end

-- Helper function
-- Takes in a step pattern and a quantity of mossteps and calculates the number
-- of large and small steps in that interval (or substring), returned as an
-- associative array containing the large and small step counts.
-- It's an associative array b/c that's how the brightgen function in the mos
-- module works.
function p.mosstep_pattern_to_vector(mosstep_pattern, mossteps)
	local large_step_count = 0
	local small_step_count = 0
	
	for i = 1, mossteps do
		local step = string.sub(mosstep_pattern, i, i)
		if step == "L" then
			large_step_count = large_step_count + 1
		elseif step == "s" then
			small_step_count = small_step_count + 1
		end
	end

	local mosstep_vector = { ["L"] = large_step_count, ["s"] = small_step_count }
	return mosstep_vector
end

-- Helper function
-- Takes in a mosstep (as an assoc. array containing the number of L's and s's),
-- and a step ratio (as 2-element array containing the sizes of L and s) and
-- calculates number of et-steps.
function p.interval_to_etsteps(mosstep_vector, step_ratios)
	return mosstep_vector["L"] * step_ratios[1] + mosstep_vector["s"] * step_ratios[2]
end

-- Helper function
-- For producing row highlighting for the table
-- Alterations are highlighted, except for singy augmented/diminished intervals for generators
function p.calculate_row_colors(input_mos, number_of_alterations)
	-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
	local input_mos = input_mos or mos.new(4, 4, 2)
	local number_of_alterations = number_of_alterations or 1
	
	-- Get the number of mossteps per period and equave
	local mossteps_per_equave = input_mos.nL + input_mos.ns
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	local row_colors = {}
	for i = 1, mossteps_per_equave + 1 do
		local mosstep = i - 1
		local is_period = mosstep % mossteps_per_period == 0
		local is_root = mosstep == 0
		local is_equave = mosstep == mossteps_per_equave
		
		-- Row colors for pre-alterations
		-- If this is the root, don't add rows before it
		if not is_root then
			for i = 1, number_of_alterations do
				table.insert(row_colors, "#eaeaff")
			end
		end
		
		-- Row colors for main mossetps (default row color)
		if is_period then
			table.insert(row_colors, "none")
		else
			table.insert(row_colors, "none")
			table.insert(row_colors, "none")
		end
		
		-- Row colors for post-alterations
		-- If this is the equave, don't add rows after it
		if not is_equave then
			for i = 1, number_of_alterations do
				table.insert(row_colors, "#eaeaff")
			end
		end
	end
	
	return row_colors
end

-- Helper function
-- Calculates note names and stores it in an associative array
-- Default notation is diamond-mos, unless it's 5L 2s, then it's standard notation
function p.calculate_note_names(input_mos, udp, note_symbols, chroma_plus_symbol, chroma_minus_symbol, number_of_alterations)
	-- Default parameters for input mos and step ratio (5L 2s and 2:1 step ratio)
	local input_mos = input_mos or mos.new(5, 2)
	local udp = udp or {5,2}
	local note_symbols = note_symbols or "CDEFGAB"
	local chroma_plus_symbol = chroma_plus_symbol or "#"
	local chroma_minus_symbol = chroma_minus_symbol or "b"
	local number_of_alterations = number_of_alterations or 0
	
	-- Get the number of mossteps per period and equave
	local mossteps_per_equave = input_mos.nL + input_mos.ns
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the number of generators going up and down from the UDP
	local generators_up = udp[1]
	local generators_down = udp[2]
	
	-- How long is the inital genchain for notes without accidentals?
	local gens_up_per_period = generators_up / periods_per_equave
	local gens_down_per_period = generators_down / periods_per_equave
	
	-- How long should the genchain extend after the initial genchain?
	-- The initial genchain lengths are determined by the U and D in the UDP
	-- The final genchain length is the following: (x + y) * (alterations + 1)
	local ascending_genchain_length = (mossteps_per_period) * (number_of_alterations + 1)
	local descending_genchain_length = (mossteps_per_period) * (number_of_alterations + 1)
	
	-- Get the ascending and descending genchains
	-- The genchains are notationally agnostic so notation needs to be applied to them
	local ascending_genchain = mosnot.mos_nomacc_chain(input_mos, gens_up_per_period, ascending_genchain_length, true)
	local descending_genchain = mosnot.mos_nomacc_chain(input_mos, gens_down_per_period, descending_genchain_length, false)
	
	-- Also get the ascending and descending degreechains
	-- These chains are encoded in a numeric form and must be converted into actual names
	local ascending_degchain = mosnot.mos_degree_chain(input_mos, ascending_genchain_length, true)
	local descending_degchain = mosnot.mos_degree_chain(input_mos, descending_genchain_length, false)
	
	-- Create an empty asoociative array
	local note_names = {}
	
	-- Add the notes to the array
	for j = 1, periods_per_equave do
		for i = 1, #ascending_genchain[j] do
			-- Convert the notationally agnostic form into a form that uses given notation
			local note = ascending_genchain[j][i]
			local note_symbol = string.sub(note_symbols, note["Mossteps"] + 1, note["Mossteps"] + 1)
			local chroma_count = note["Chromas"]
			local note_name = note_symbol .. string.rep(chroma_plus_symbol, chroma_count)
			
			-- Convert the encoded degree into text
			local degree_encoded = ascending_degchain[j][i]
			local degree_decoded = mosnot.decode_mosstep_quality(degree_encoded, "m", "mosdegree", "abbreviated")
			
			-- Add to note names
			note_names[degree_decoded] = note_name
		end
		
		for i = 1, #descending_genchain[j] do
			-- Convert the notationally agnostic form into a form that uses given notation
			local note = descending_genchain[j][i]
			local note_symbol = string.sub(note_symbols, note["Mossteps"] + 1, note["Mossteps"] + 1)
			local chroma_count = note["Chromas"] * -1
			local note_name = note_symbol .. string.rep(chroma_minus_symbol, chroma_count)
			
			-- Convert the encoded degree into text
			local degree_encoded = descending_degchain[j][i]
			
			-- For the descending chain, any mossteps that correspond to the root of
			-- a period should correspond instead to the root one period up (EG, if
			-- the root refers to the unison for a single-period mos, it should be
			-- the degree one octave up)
			if degree_encoded["Mossteps"] % mossteps_per_period == 0 then
				-- Transpose the mosstep by one period
				degree_encoded["Mossteps"] = degree_encoded["Mossteps"] + mossteps_per_period
			end
			
			-- Correct the note name based on whether it should be a note that is
			-- one period up. If the mos is single-period, then do not transpose.
			if degree_encoded["Mossteps"] % mossteps_per_period == 0 and degree_encoded["Mossteps"] == 0 then
				-- Correct the note name
				note_symbol = string.sub(note_symbols, 1, 1)
				note_name = note_symbol .. string.rep(chroma_minus_symbol, chroma_count)
			elseif degree_encoded["Mossteps"] % mossteps_per_period == 0 and degree_encoded["Mossteps"] == 0 then
				-- Correct the note name
				note_symbol = string.sub(note_symbols, degree_encoded["Mossteps"] + 1, degree_encoded["Mossteps"] + 1)
				note_name = note_symbol .. string.rep(chroma_minus_symbol, chroma_count)
			end
			
			-- Pass the encoded degree, along with the other args
			local degree_decoded = mosnot.decode_mosstep_quality(degree_encoded, "m", "mosdegree", "abbreviated")
			
			-- Add to note names
			note_names[degree_decoded] = note_name
		end
	end
	
	return note_names
end

-- Helper function; generate the step vectors for every interval required for the table
function p.calculate_mosstep_vectors(input_mos, number_of_alterations)
	-- Default params
	local input_mos = input_mos or mos.new(5, 2)
	local number_of_alterations = number_of_alterations or 0
	
	-- Get the brightest mode
	local brightest_mode = mos.brightest_mode(input_mos)
	
	-- Get the number of mossteps per period and equave
	local mossteps_per_equave = (input_mos.nL + input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / utils._gcd(input_mos.nL, input_mos.ns)
	
	-- Add intervals and their alterations, using the large interval size as the zero point for alterations
	local mosstep_vectors = {}
	for i = 1, mossteps_per_equave + 1 do
		local mossteps = i - 1
		
		-- Consecutive alterations are always one chroma apart
		-- With a perfect non-generator interval, alterations are added by going down and up the same amount
		-- With all other intervals, since there are two sizes and the large interval size is treated as the zero point,
		-- alterations are instead by going down n+1 chromas, then going up n chromas
		-- With the unison, don't go down (only up), and with the equave, don't go up (only down).
		local min_alterations = 0
		local max_alterations = 0
	
		if mossteps == 0 then
			-- Unison; the min number of alterations is 0
			min_alterations = 0
			max_alterations = number_of_alterations
		elseif mossteps == mossteps_per_equave then
			-- Equave; the max number of alterations is 0
			min_alterations = -number_of_alterations
			max_alterations = 0
		elseif mossteps % mossteps_per_period == 0 then
			-- Non-unison non-equave periods; the max and min have the "distance" from the zero point
			min_alterations = -number_of_alterations
			max_alterations = number_of_alterations
		else
			-- All other intervals; the min's distance is one more than the max's distance
			min_alterations = -number_of_alterations - 1
			max_alterations = number_of_alterations
		end
		
		-- Get the current mosstep vector based on the brightest mode
		local current_mosstep_vector = p.mosstep_pattern_to_vector(brightest_mode, mossteps)
		for j = min_alterations, max_alterations do
			-- j is the number of chromas to add or subtract from the base vector
			-- Since a chroma is defined as (L-s), add j large steps and subtract j small steps from the current mosstep vector
			local L_count = current_mosstep_vector["L"] + j
			local s_count = current_mosstep_vector["s"] - j
			
			local current_mosstep_vector = { ["L"] = L_count, ["s"] = s_count }
			table.insert(mosstep_vectors, current_mosstep_vector)
		end
	end
	
	return mosstep_vectors
end

-- Helper function; generate the mosdegree names and their abbreviations for the mos
function p.calculate_mosdegree_names_and_abbrevs(input_mos, mos_prefix, number_of_alterations)
	-- Default params
	local input_mos = input_mos or mos.new(5, 2)
	local number_of_alterations = number_of_alterations or 0
	local mos_prefix = mos_prefix or "mos"
	
	-- Get the number of mossteps per period and equave
	local mossteps_per_equave = (input_mos.nL + input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / utils._gcd(input_mos.nL, input_mos.ns)
	
	-- Get the step counts for the bright and dark generators
	local bright_gen = mos.bright_gen(input_mos)
	local mossteps_per_bright_gen = bright_gen["L"] + bright_gen["s"]
	local mossteps_per_dark_gen = mossteps_per_period - mossteps_per_bright_gen
	
	-- Main loop
	-- Interval qualities depend on whether the intervals are generators or if there is only one size.
	-- Cases for which there are alterations either below or above the main interval sizes, but not both:
	-- - If the interval class is the unison, there are no extensions before it and there is only one size (perfect).
	-- - If the interval class is the equave, there are no extensions after it and there is only one size (perfect).
	-- Cases for which there are alterations above and below the main interval sizes:
	-- - If the interval class is a non-unison non-equave period, there are extensions before and after and there is only one size (perfect).
	-- - If the interval class is a non-generator interval, or is the generator for an nL ns mos, there are extensions before and after and the sizes are major and minor.
	-- - If the interval class is the bright generator, there are extensions before and after and the sizes are perfect (large) and diminished (small).
	-- - If the interval class is the dark generator, there are extensions before and after and the sizes are augmented (large) and perfect (small).
	local mosdegree_names = {}
	local mosdegree_abbrevs = {}
	for i = 1, mossteps_per_equave + 1 do
		-- For calculating mossteps
		local mossteps = i - 1
		
		-- For bright and dark gens
		local is_nL_ns = input_mos.nL == input_mos.ns
		local is_bright_gen = mossteps % mossteps_per_period == mossteps_per_bright_gen and not is_nL_ns
		local is_dark_gen = mossteps % mossteps_per_period == mossteps_per_dark_gen and not is_nL_ns
		
		if mossteps % mossteps_per_period == 0 then
			-- For perfect intervals
			-- Operation for pre-alterations (diminshed degrees)
			if number_of_alterations > 0 and mossteps ~= 0 then
				for j = number_of_alterations, 1, -1 do
					-- Diminished degree is formatted as "Diminished degree"; more than 1 augmentation is "2× Diminished", "3× Diminished", and so on
					local dim_degree = ""
					if j == 1 then dim_degree = string.format("Diminished %d-%sdegree", mossteps, mos_prefix)
					else dim_degree = string.format("%d× Diminished %d-%sdegree", j, mossteps, mos_prefix)
					end
					
					-- Format abbreviation as repetitions of the letter "d", followed by the mosdegree
					local dim_abbrev = string.rep("d", j) .. string.format("%dmd", mossteps)
					
					-- Insert
					table.insert(mosdegree_names, dim_degree)
					table.insert(mosdegree_abbrevs, dim_abbrev)
				end
			end
			
			-- Calculate the main degree name and abbreviation
			local degree_name = string.format("Perfect %d-%sdegree", mossteps, mos_prefix)
			local abbrev_name = string.format("P%dmd", mossteps)
			
			-- Main operation
			table.insert(mosdegree_names, degree_name)
			table.insert(mosdegree_abbrevs, abbrev_name)
			
			-- Operation for post-alterations (augmented degrees)
			if number_of_alterations > 0 and mossteps ~= mossteps_per_equave then
				for j = 1, number_of_alterations do
					-- Augmented degree is formatted as "Augmented degree"; more than 1 augmentation is "2× Augmented", "3× Augmented", and so on
					local aug_degree = ""
					if j == 1 then aug_degree = string.format("Augmented %d-%sdegree", mossteps, mos_prefix)
					else aug_degree = string.format("%d× Augmented %d-%sdegree", j, mossteps, mos_prefix)
					end
					
					-- Format abbreviation as repetitions of the letter "A", followed by the mosdegree
					local aug_abbrev = string.rep("A", j) .. string.format("%dmd", mossteps)
					
					-- Insert
					table.insert(mosdegree_names, aug_degree)
					table.insert(mosdegree_abbrevs, aug_abbrev)
				end
			end
		else 
			-- For intervals with two sizes
			-- Operation for pre-alterations (diminshed degrees)
			if number_of_alterations > 0 and mossteps ~= 0 then
				for j = number_of_alterations, 1, -1 do
					-- The number of diminishings depends on whether the interval class is the bright gen; if so,
					-- then one interval will already be diminished so intervals below that already start at 2xdim.
					local dim_amount = 0
					if is_bright_gen then dim_amount = 1 + j
					else dim_amount = j
					end
					
					-- Diminished degree is formatted as "Diminished degree"; more than 1 augmentation is "2× Diminished", "3× Diminished", and so on
					local dim_degree = ""
					if dim_amount == 1 then dim_degree = string.format("Diminished %d-%sdegree", mossteps, mos_prefix)
					else dim_degree = string.format("%d× Diminished %d-%sdegree", dim_amount, mossteps, mos_prefix)
					end
					
					-- Format abbreviation as repetitions of the letter "d", followed by the mosdegree
					local dim_abbrev = string.rep("d", dim_amount) .. string.format("%dmd", mossteps)
					
					-- Insert
					table.insert(mosdegree_names, dim_degree)
					table.insert(mosdegree_abbrevs, dim_abbrev)
				end
			end
			
			-- Calculate the small and large names and abbreviations
			-- Non-generator intervals for non-nL-ns mosses are minor (small) and major (large)
			local small_degree_label = "Minor"
			local large_degree_label = "Major"
			local small_degree_abbrev = "m"
			local large_degree_abbrev = "M"
			if is_bright_gen then
				-- Bright gen: diminished (small) and perfect (large)
				small_degree_label = "Diminished"
				large_degree_label = "Perfect"
				small_degree_abbrev = "d"
				large_degree_abbrev = "P"
			elseif is_dark_gen then
				-- Dark gen: perfect (small) and augmentd (large)
				small_degree_label = "Perfect"
				large_degree_label = "Augmented"
				small_degree_abbrev = "P"
				large_degree_abbrev = "A"
			end
			
			-- Main operation
			local small_degree_name = string.format("%s %d-%sdegree", small_degree_label, mossteps, mos_prefix)
			local large_degree_name = string.format("%s %d-%sdegree", large_degree_label, mossteps, mos_prefix)
			local small_abbrev_name = string.format("%s%dmd", small_degree_abbrev, mossteps)
			local large_abbrev_name = string.format("%s%dmd", large_degree_abbrev, mossteps)
			table.insert(mosdegree_names, small_degree_name)
			table.insert(mosdegree_names, large_degree_name)
			table.insert(mosdegree_abbrevs, small_abbrev_name)
			table.insert(mosdegree_abbrevs, large_abbrev_name)
			
			-- Operation for post-alterations (augmented degrees)
			if number_of_alterations > 0 and mossteps ~= mossteps_per_equave then
				for j = 1, number_of_alterations do
					-- The number of augmentings depends on whether the interval class is the dark gen; if so,
					-- then one interval will already be augmented so intervals above that already start at 2xaug.
					local aug_amount = 0
					if is_dark_gen then aug_amount = 1 + j
					else aug_amount = j
					end
					
					-- Augmented degree is formatted as "Augmented degree"; more than 1 augmentation is "2× Augmented", "3× Augmented", and so on
					local aug_degree = ""
					if aug_amount == 1 then aug_degree = string.format("Augmented %d-%sdegree", mossteps, mos_prefix)
					else aug_degree = string.format("%d× Augmented %d-%sdegree", aug_amount, mossteps, mos_prefix)
					end
					
					-- Format abbreviation as repetitions of the letter "A", followed by the mosdegree
					local aug_abbrev = string.rep("A", aug_amount) .. string.format("%dmd", mossteps)
					
					-- Insert
					table.insert(mosdegree_names, aug_degree)
					table.insert(mosdegree_abbrevs, aug_abbrev)
				end
			end
		end
	end
	
	return mosdegree_names, mosdegree_abbrevs
end

-- Separate function for testing; the main "frame" function will call this
function p.mos_degrees(input_mos, step_ratios, mos_prefix, show_abbreviations, number_of_alterations, ji_ratios, udp, notation, show_notation)
	-- Default params; all parameters are already parsed
	local input_mos = input_mos or mos.new(5, 2)
	local step_ratios = step_ratios or {{2,1}, {3,1}, {3,2}}
	local mos_prefix = mos_prefix or "mos"
	local show_abbrevs = show_abbreviations == 1
	local number_of_alterations = number_of_alterations or 1
	local ji_ratios = ji_ratios or {["P0md"]="1/1"}
	local udp = udp or {5,1}
	local notation = notation or mosnot.parse_notation("CDEFGAB; #; b")
	local show_notation = show_notation == 1
	
	-- Get the scale sig
	local scale_sig = mos.as_string(input_mos)
	
	-- Get the brightest and darkest modes for the mos
	local brightest_mode = mos.brightest_mode(input_mos)
	local darkest_mode = string.reverse(brightest_mode)
	
	-- Get the number of mossteps per period and equave, and periods per equave
	local mossteps_per_equave = (input_mos.nL + input_mos.ns)
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get the step counts for the bright and dark generators
	local bright_gen = mos.bright_gen(input_mos)
	local steps_per_bright_gen = bright_gen["L"] + bright_gen["s"]
	local steps_per_dark_gen = mossteps_per_period - steps_per_bright_gen
	
	-- Get the step counts as a vector (or associative array, rather)
	local input_mos_step_vector = {["L"] = input_mos.nL, ["s"] = input_mos.ns}
	
	-- What's the equave in cents?
	local equave_in_cents = rat.cents(input_mos.equave)

	-- How many decimal places to round to? (hardcoded)
	local round = 1
	
	-- Precalculate row colors
	local row_colors = p.calculate_row_colors(input_mos, number_of_alterations)

	-- Precalculate the ets for each step ratio
	-- Each et is used to calculate a scale degree's cent value
	local ets_for_mos = {}
	for i = 1, #step_ratios do
		local etsteps = p.interval_to_etsteps(input_mos_step_vector, step_ratios[i])
		local et_for_mos = et.new(etsteps, input_mos.equave)
		table.insert(ets_for_mos, et_for_mos)
	end
	
	-- Precalculate degree names, degree abbreviations, and mosstep vectors
	local degree_names, degree_abbrevs = p.calculate_mosdegree_names_and_abbrevs(input_mos, mos_prefix, number_of_alterations)
	local mosstep_vectors = p.calculate_mosstep_vectors(input_mos, number_of_alterations)
	
	-- Precalculate default comments for JI ratios; there's only two entries here
	local default_ji_comments = {}
	default_ji_comments["P0md"] = "1/1 (exact)"
	default_ji_comments[string.format("P%dmd", mossteps_per_equave)] = string.format("%s (exact)", rat.as_ratio(input_mos.equave))
	
	-- Then, using the UDP, get the notation
	-- The default notation is either standard notation (for 5L 2s) or diamond-mos (most other mosses)
	-- If notation is passed in, use that instead
	-- If no notation is passed in, notation will not be displayed
	local note_names = {}
	local root_note = ""
	if show_notation then
		note_names = p.calculate_note_names(input_mos, udp, notation["Naturals"], notation["Sharp"], notation["Flat"], number_of_alterations)
		root_note = string.sub(notation["Naturals"], 1, 1)
	end
	
	-- Create the table, starting with the headers
	local result = "{| class=\"wikitable sortable mw-collapsible mw-collapsed\"\n"
	
	-- First row
	result = result
		.. "|+ style=\"font-size: 105%%; white-space: nowrap;\" | " .. string.format("Scale degree of %s\n", scale_sig)
		.. "|-\n"
		.. "! rowspan=\"2\" class=\"unsortable\" | Scale degree\n"
	
	-- Add column for abbreviations
	-- Abbreviations do not use a mos-prefix or mos-name
	if show_abbrevs then
		result = result .. "! rowspan=\"2\" class=\"unsortable\" | Abbrev.\n"
	end
	
	-- Add column for note names
	if show_notation then
		result = result .. string.format("! rowspan=\"2\" class=\"unsortable\" | On %s\n", root_note)
	end
	
	-- Add column headers for up to 5 different step ratios
	for i = 1, #step_ratios do
		-- Step ratio names, for reference
		local tamnams_step_ratios = {
			["1:1"] = "Equalized",
			["4:3"] = "Supersoft",
			["3:2"] = "Soft",
			["5:3"] = "Semisoft",
			["2:1"] = "Basic",
			["5:2"] = "Semihard",
			["3:1"] = "Hard",
			["4:1"] = "Superhard",
			["1:0"] = "Collapsed",
		}
		
		-- Get name for step ratio
		local step_ratio_simplified = mosnot.simplify_step_ratio(step_ratios[i])
		local step_ratio_key = step_ratio_simplified[1] .. ":" .. step_ratio_simplified[2]
		local step_ratio_name = tamnams_step_ratios[step_ratio_key]
		
		-- Calculate the et for the mos with a given step ratio; this is needed to produce
		-- the name for the et/edo
		local et_for_mos = et.new(ets_for_mos[i].size, input_mos.equave)
		local et_as_string = et.as_string(et_for_mos)
		
		-- Add the step ratio name if there is one
		if step_ratio_name == nil then
			result = result .. "! colspan=\"2\" | " .. et_as_string .. " (L:s = " .. step_ratio_key .. ")\n"
		else
			result = result .. "! colspan=\"2\" | " .. et_as_string .. " (" .. step_ratio_name .. ", L:s = " .. step_ratio_key .. ")\n"
		end
	end
	
	-- Add JI ratio column header
	result = result .. "! rowspan=\"2\" class=\"unsortable\" | Approx. JI Ratios\n"
	
	-- Second row
	result = result .. "|-\n"
	-- Add headers for the steps and cents up to 5 times
	for i = 1, #step_ratios do
		result = result .. "! Steps\n"
		result = result .. "! Cents\n"
	end
	
	-- Add in successive rows, containing the degree name, abbreviation (if applicable),
	-- note names (if applicable), step size (in steps and cents), and JI ratio
	for i = 1, #degree_names do
		-- Start new row
		-- Add row highlighting if provided
		local row_color = row_colors[i]
		if row_color == "" then
			result = result .. "|-\n"
		else
			result = result .. string.format("|- style=\"background: %s\"\n", row_color)
		end
		
		-- Add degree name
		-- Make the text bold if the interval is a perfect interval
		local degree_name = degree_names[i]
		if string.find(degree_name, "Perfect") then
			if i == 1 then
				result = result .. string.format("| '''%s (unison)'''\n", degree_names[i])
			elseif i == #degree_names and equave_in_cents == 1200 then
				result = result .. string.format("| '''%s (octave)'''\n", degree_names[i])
			elseif i == #degree_names and equave_in_cents ~= 1200 then
				result = result .. string.format("| '''%s (equave)'''\n", degree_names[i])
			else
				result = result .. string.format("| '''%s'''\n", degree_names[i])
			end
		else
			result = result .. string.format("| %s\n", degree_names[i])
		end
		
		-- Add abbrev if allowed
		local degree_abbrev = degree_abbrevs[i]
		if show_abbrevs then
			result = result .. string.format("| %s\n", degree_abbrev)
		end
		
		-- Add note names if allowed
		-- Use the degree_abbrev as the key when accessing key-value pairs
		if show_notation then
			result = result .. string.format("| %s\n", note_names[degree_abbrev])
		end
		
		-- Add mossteps and cent values
		-- Rounding is hardcoded to one decimal place
		-- Also record the cent value for JI ratio search
		local round = 1
		local average_cents = 0
		for j = 1, #ets_for_mos do
			local etsteps = mosstep_vectors[i]["L"] * step_ratios[j][1] + mosstep_vectors[i]["s"] * step_ratios[j][2]
			local cents = utils._round_dec(et.cents(ets_for_mos[j], etsteps), round)
			
			result = result .. string.format("| %s\n", etsteps)
			result = result .. string.format("| %s\n", cents)
			
			average_cents = average_cents + cents / #ets_for_mos
		end
		
		-- Calculate JI ratio approximations using jiraf module
		-- For now:
		-- - Cent value is the average of the sizes given the step ratios
		-- - Tolerance is hardcoded to +/-15 cents
		-- - Prime limit is hardocoded to 19
		-- - Odd limit hardcoded to 49
		--local approx_ratios = jiraf.find_ratios_for_cents(average_cents, 15, 19, 39)
		--local ratios_as_text = jiraf.ratios_to_text(approx_ratios);
		
		-- Add JI ratios if any
		local ji_comment_entry = ""
		local default_ji_comment = default_ji_comments[degree_abbrev]
		
		-- Add ratios found using jiraf
		local entered_ji_comment = ji_ratios[degree_abbrev]
		--local default_ji_comment = nil
		--local entered_ji_comment = ratios_as_text
		
		if default_ji_comment == nil and entered_ji_comment == nil then
			-- No comments
			result = result .. "|\n"
		elseif default_ji_comment ~= nil and entered_ji_comment == nil then
			-- Default comments but no entered comments
			result = result .. string.format("| %s\n", default_ji_comment)
		elseif default_ji_comment == nil and entered_ji_comment ~= nil then
			-- Entered comments but no default comments
			result = result .. string.format("| %s\n", entered_ji_comment)
		else
			-- Both comments present; default comments take precedence
			result = result .. string.format("| %s, %s\n", default_ji_comment, entered_ji_comment)
		end
	end
	
	-- End of table
	result = result .. "|}"
	
	return result
end

-- This function is to be called by a template, with parameters
function p.mos_degrees_frame(frame)
	-- Default param for input mos is 5L 2s
	local input_mos = mos.parse(frame.args["Scale Signature"]) or mos.new(2, 5, 2)
	
	-- Get the scale sig; for calculating the mos prefix
	local scale_sig = mos.as_string(input_mos)
	
	-- Get the step ratio
	local step_ratios = p.parse_step_ratio(frame.args["Step Ratio"]) or p.parse_step_ratio("2/1")
	
	-- Default param for mos prefix
	-- If "NONE" is given, no prefix will be used
	-- If left blank, try to find the appropriate mos prefix, or else defualt to "mos"
	-- If not left blank, use the prefix passed in instead
	local mos_prefix = "mos"
	if frame.args["MOS Prefix"] == "NONE" then
		mos_prefix = ""
	elseif string.len(frame.args["MOS Prefix"]) == 0 then
		mos_prefix_lookup = tamnams.lookup_prefix(input_mos) or ""
		if string.len(mos_prefix_lookup) ~= 0 then
			mos_prefix = mos_prefix_lookup
		end
	else
		mos_prefix = frame.args["MOS Prefix"]
	end
	
	-- Get whether to display abbreviations
	local show_abbreviations = 0
	if frame.args["Show Abbreviations"] == "1" or frame.args["Show Abbreviations"] == 1 then
		show_abbreviations = 1
	end
	
	-- Get the number of alterations
	local number_of_alterations = 0
	if string.len(frame.args["Number of Alterations"]) ~= 0 then
		number_of_alterations = tonumber(frame.args["Number of Alterations"])
	end
	
	-- Get JI ratios
	local ji_ratios_parsed = {}
	if #frame.args["JI Ratios"] > 0 then
		-- If the comments can't be parsed, default to an empty table
		ji_ratios_parsed = p.parse_kv_pairs(frame.args["JI Ratios"]) or {}
	end
	
	-- Get the number of mossteps per period and equave, and periods per equave
	-- Needed for calculating default UDP and notation
	local mossteps_per_equave = (input_mos.nL + input_mos.ns)
	local periods_per_equave = utils._gcd(input_mos.nL, input_mos.ns)
	local mossteps_per_period = mossteps_per_equave / periods_per_equave
	
	-- Get UDP
	-- If no UDP is found, a default will be calculated as the middle mode, or the
	-- brighter of two middle modes (as with an even number of modes in a mos)
	local udp_parsed = { periods_per_equave * math.ceil((mossteps_per_period - 1)/ 2), periods_per_equave * math.floor((mossteps_per_period - 1) / 2) }
	if scale_sig == "5L 2s" then
		udp_parsed = { 5, 1 }
	end
	if #frame.args["UDP"] > 0 then
		udp_parsed = mosnot.parse_udp(frame.args["UDP"])
	end
	
	-- Get notation
	-- This also determines whether to show notation
	-- Typing in "Default" is a shortcut to default notation, wherein standard notation (for 5L 2s) or diamond-mos (for other mosses) is used
	local notation_parsed = {}
	local show_notation = 0
	if #frame.args["Notation"] > 0 then
		if frame.args["Notation"] == "Default" and scale_sig == "5L 2s" then
			notation_parsed = { ["Naturals"] = "CDEFGAB", ["Sharp"] = "#", ["Flat"] = "b" }
			show_notation = 1
		elseif frame.args["Notation"] == "Default" and scale_sig ~= "5L 2s" then
			local default_nominals = "JKLMNOPQRSTUVWXYZ"
			notation_parsed = { ["Naturals"] = string.sub(default_nominals, 1, mossteps_per_equave), ["Sharp"] = "&", ["Flat"] = "@" }
			show_notation = 1
		else
			notation_parsed = mosnot.parse_notation(frame.args["Notation"])
			if notation_parsed ~= nil then
				show_notation = 1
			end
		end
	end
	
	result = p.mos_degrees(input_mos, step_ratios, mos_prefix, show_abbreviations, number_of_alterations, ji_ratios_parsed, udp_parsed, notation_parsed, show_notation)
	
	-- Debugger
	local debugg = yesno(frame.args["debug"])
	if debugg == true then
		result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
	end
	
	return frame:preprocess(result)
end

return p