Module:Infobox interval

From Xenharmonic Wiki
Revision as of 23:41, 14 December 2024 by Lériendil (talk | contribs) (am pushing a change without discussing, admittedly; however it greatly benefits me in particular to directly compare how a specific interval is tuned in some tuning, to that interval in JI, at precision higher than a single cent, by looking at that interval's wiki page; intervals used as generators and stacked many times require much higher precision. Therefore I'm unilaterally adjusting the precision to at least one decimal place.)
Jump to navigation Jump to search
Module documentation[view] [edit] [history] [purge]
This module should not be invoked directly; use its corresponding template instead: Template:Infobox interval.

This module generates an infobox providing information about a given interval.

Introspection summary for Module:Infobox interval 
Functions provided (1)
Line Function Params
12 infobox_interval (invokable) (frame)
Lua modules required (4)
Variable Module Functions used
he Module:Harmonic entropy harmonic_entropy
infobox Module:Infobox build
rat Module:Rational parse
cents
as_ket
as_ratio
from_ket
eq
max_prime
is_superparticular
is_square_superparticular
is_reduced
is_harmonic
as_pair
is_subharmonic
as_subgroup_ket
factorisation
as_FJS
tenney_height
weil_height
wilson_height
is_power
find_S_expression
utils Module:Utils _round

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


local p = {}
local rat = require("Module:Rational")
local utils = require("Module:Utils")
local he = require("Module:Harmonic entropy")
local infobox = require("Module:Infobox")

-- check whether the input is a non-empty string
local function value_provided(s)
	return type(s) == "string" and #s > 0
end

function p.infobox_interval(frame)
	local debug_mode = frame.args["debug"]

	local page_name = frame:preprocess("{{PAGENAME}}")

	local rational = false
	local small = false -- numerator and denominator can be represented as Lua numbers, or irrational
	local regular = false -- finite and greater than zero

	local ratio = nil
	local cents = nil
	local ket = nil
	local ratio_string = nil

	local infobox_data = {}
	local cats = ""

	-- intervals with relatively small powers
	if value_provided(frame.args["Ratio"]) then
		ratio = rat.parse(frame.args["Ratio"])
		if ratio ~= nil then
			rational = true
			small = true
			regular = not ratio.nan and not ratio.inf and not ratio.zero and ratio.sign > 0
			cents = rat.cents(ratio)
			ket = rat.as_ket(ratio, frame)
			ratio_string = rat.as_ratio(ratio)
		end
	end

	-- intervals with large powers
	if ratio == nil and value_provided(frame.args["Ket"]) then
		ratio = rat.from_ket(frame.args["Ket"])
		if ratio ~= nil then
			rational = true
			small = false
			regular = true
			cents = rat.cents(ratio)
			ket = rat.as_ket(ratio, frame)
			-- display Ratio unless it is page name, in which case it is probably a fallback -- what does this mean?
			if frame.args["Ratio"] ~= page_name then
				ratio_string = frame.args["Ratio"] or ""
			end
		end
	elseif ratio ~= nil and value_provided(frame.args["Ket"]) then
		cats = cats .. "[[Category:Todo:remove explicit ket notation]]"
	end

	-- irrational intervals
	if ratio == nil and value_provided(frame.args["Cents"]) then
		cents = tonumber(frame.args["Cents"])
		if cents ~= nil then
			rational = false
			small = true
			regular = true
			if value_provided(frame.args["Ket"]) then
				ket = frame.args["Ket"]
			end
			-- Ratio is LaTeX unless it is page name, in which case it is probably a fallback -- what does this mean?
			if frame.args["Ratio"] ~= page_name then
				ratio_string = frame.args["Ratio"] or ""
			else
				cats = cats .. "[[Category:Todo:add interval ratio]]"
			end
		end
	elseif ratio ~= nil and value_provided(frame.args["Cents"]) then
		cats = cats .. "[[Category:Todo:remove explicit cents]]"
	end

	if not (regular or rational) then
		cats = cats .. "[[Category:Todo:initialise interval]]"
	end

	-- categorize by rationality and prime limit
	if regular then
		if rational then
			local prime_limit = 2
			if not rat.eq(ratio, 1) then
				prime_limit = rat.max_prime(ratio)
			end
			cats = cats .. "[[Category:Rational intervals]]" .. "[[Category:" .. prime_limit .. "-limit intervals]]"
		else
			cats = cats .. "[[Category:Irrational intervals]]"
		end
	end

	local special_properties = {}
	if rational then
		if small and rat.is_superparticular(ratio) then
			if rat.is_square_superparticular(ratio) then
				table.insert(special_properties, "[[Square superparticular|square superparticular]]")
			else
				table.insert(special_properties, "[[Superparticular interval|superparticular]]")
			end
			cats = cats .. "[[Category:Superparticular ratios]]"
		end
		if rat.is_reduced(ratio, 2, not small) then
			table.insert(special_properties, "[[Octave reduction|reduced]]")
		end
		if rat.is_harmonic(ratio) then
			table.insert(special_properties, "[[harmonic]]")
			num, den = rat.as_pair (ratio)
			cats = cats .. "[[Category:Harmonics|" .. string.rep("#", string.len(num)) .. "]]"
		elseif rat.is_harmonic(ratio, true, not small) then
			table.insert(special_properties, "[[Harmonic|reduced harmonic]]")
			cats = cats .. "[[Category:Octave-reduced harmonics]]"
		elseif rat.is_subharmonic(ratio, true, not small) then
			table.insert(special_properties, "[[Subharmonic|reduced subharmonic]]")
			cats = cats .. "[[Category:Octave-reduced subharmonics]]"
		end
	elseif regular then
		if cents >= 0 and cents < 1200 then
			table.insert(special_properties, "[[Octave reduction|reduced]]")
		end
	end

	if value_provided(ratio_string) then
		if rational then
			table.insert(infobox_data, {
				"Ratio",
				ratio_string,
			})
		else
			table.insert(infobox_data, {
				"Expression",
				frame:preprocess("<math>" .. ratio_string .. "</math>"),
			})
		end
	end
	if regular and rational then
		if ket:match("<sup>") then
			-- there was a subsequence of 4+ zeros
			table.insert(infobox_data, {
				"[[Smonzos and svals|Subgroup monzo]]",
				rat.as_subgroup_ket(ratio, frame),
			})
		else
			table.insert(infobox_data, {
				"Factorization",
				rat.factorisation(ratio),
			})
			table.insert(infobox_data, {
				"[[Monzo]]",
				ket,
			})
		end
	elseif rational then
		table.insert(infobox_data, {
			"Factorization",
			rat.factorisation(ratio),
		})
	elseif value_provided(ket) then
		-- irrational ket is provided:
		table.insert(infobox_data, {
			"[[Monzo]]",
			frame:expandTemplate({
				title = "Monzo",
				args = { ket },
			}),
		})
	end
	if regular then
		-- it's really hard to hear a tenth of a cent, but 3 sig figs seems possibly useful
		local sigFigs = 3
		if cents >= 100 then sigFigs = 4 end
		if cents >= 1000 then sigFigs = 5 end
		table.insert(infobox_data, {
			"Size in [[cent]]s",
			utils._round(cents, sigFigs) .. "¢",
		})
	end

	local name = frame.args["Name"]
	if value_provided(name) then
		local caption = "Name"
		if name:match(",") then
			caption = "Names"
			-- removing manual line breaks
			local matches
			name, matches = name:gsub("<br%s*/?>", "")
			if matches > 0 then
				cats = cats .. "[[Category:Todo:remove manual line breaks]]"
			end
			-- removing whitespaces after commas
			name = name:gsub(",%s+", ",")
			-- placing line breaks after commas
			name = name:gsub(",", ",<br />")
		end
		table.insert(infobox_data, {
			caption,
			name,
		})
	else
		cats = cats .. "[[Category:Todo:add interval name]]"
		table.insert(infobox_data, {
			"Name(s)",
			"<abbr title=\"missing value for parameter 'Name'\">''missing''</abbr><sup>[[Template:Infobox Interval| ?&nbsp;]]</sup>",
		})
	end

	local colour_name = frame.args["Color name"]
	if value_provided(colour_name) then
		table.insert(infobox_data, {
			"[[Color notation|Color name]]",
			colour_name,
		})
	elseif regular and rational then
		cats = cats .. "[[Category:Todo:add color name]]"
	end

	local FJS_name = frame.args["FJS name"]
	if not value_provided(FJS_name) and rational and regular then
		FJS_name = rat.as_FJS(ratio)
	elseif value_provided(FJS_name) then
		local matches
		FJS_name = FJS_name:gsub("%s", "")
		FJS_name, matches = FJS_name:gsub("<br%s*/?>", "")
		if matches > 0 then
			cats = cats .. "[[Category:Todo:remove manual line breaks]]"
		end
		FJS_name, matches = FJS_name:gsub("<sup>(.*)</sup>", "^{%1}")
		if matches > 0 then
			cats = cats .. "[[Category:Todo:replace sup and sub with LaTeX]]"
		end
		FJS_name, matches = FJS_name:gsub("<sub>(.*)</sub>", "_{%1}")
		if matches > 0 then
			cats = cats .. "[[Category:Todo:replace sup and sub with LaTeX]]"
		end
	end
	if value_provided(FJS_name) then
		FJS_name = FJS_name:gsub("^(%w+)", "\\text{%1}")
		FJS_name = FJS_name:gsub("(%-%d+)", "{%1}")
		if #FJS_name <= 200 then
			table.insert(infobox_data, {
				"[[Functional Just System|FJS name]]",
				frame:preprocess("<math>" .. FJS_name .. "</math>"),
			})
		end
	end

	if #special_properties > 0 then
		table.insert(infobox_data, {
			"Special properties",
			table.concat(special_properties, ",<br />"),
		})
	end

	-- interval complexity
	if rational and regular then
		table.insert(infobox_data, {
			"[[Tenney height]] (log<sub>2</sub> ''nd'')",
			utils._round(rat.tenney_height(ratio), 6),
		})
		table.insert(infobox_data, {
			"[[Weil height]] (log<sub>2</sub> max(''n'', ''d''))",
			utils._round(rat.weil_height(ratio), 6),
		})
		table.insert(infobox_data, {
			"[[Wilson height]] (sopfr(''nd''))",
			utils._round(rat.wilson_height(ratio), 6),
		})
	end

	if regular then
		table.insert(infobox_data, {
			frame:preprocess("[[Harmonic entropy]]<br />(Shannon, <math>\\sqrt{nd}</math>)"),
			"~" .. utils._round(he.harmonic_entropy(cents), 6) .. " bits",
		})
	end

	local is_comma = value_provided(frame.args["Comma"])
	local comma = nil
	if is_comma and regular and cents > 0 then
		-- rational powers are not considered commas
		if not (rational and rat.is_power(ratio)) then
			if cents <= 3.5 then
				comma = "[[Unnoticeable comma|unnoticeable]]"
				cats = cats .. "[[Category:Unnoticeable commas]]"
			elseif cents <= 30 then
				comma = "[[Small comma|small]]"
				cats = cats .. "[[Category:Small commas]]"
			elseif cents <= 100 then
				comma = "[[Medium comma|medium]]"
				cats = cats .. "[[Category:Medium commas]]"
			else
				comma = "[[Large comma|large]]"
				cats = cats .. "[[Category:Large commas]]"
			end
		end
	end
	if comma then
		table.insert(infobox_data, {
			"[[Comma|Comma size]]",
			comma,
		})
	end
	if comma and rational then
		local S_expressions = rat.find_S_expression(ratio)
		if #S_expressions > 0 then
			local caption = "[[Square superparticular|S-expression]]"
			if #S_expressions > 1 then
				caption = caption .. "s"
			end
			table.insert(infobox_data, {
				caption,
				table.concat(S_expressions, ",<br />"),
			})
		end
	end

	local sound = frame.args["Sound"]
	if value_provided(sound) then
		cats = cats .. "[[Category:Pages with internal sound examples]]"
		table.insert(infobox_data, {
			"[[File:" .. sound .. "|270px]]<br /><span style=\"font-size: 75%;\">[[:File:" .. sound .. "|[sound info]]]</span>",
		})
	elseif debug_mode and debug_mode ~= "hide" and regular then
		local hz = 2 ^ (math.log(440) / math.log(2) + cents / 1200)
		-- is it within hearing range?
		if hz >= 20 and hz <= 20000 then
			local html_id = "interval_" .. tostring(math.floor(cents))
			table.insert(infobox_data, {
				"<div style=\"display: flex; justify-content: space-around;\"><div style=\"width: 270px; text-align: center;\">"
					.. frame:expandTemplate({
						title = "User:Plumtree/Interval Sound",
						args = {
							Frequency = tostring(hz),
							Center = "true",
							Label = "Audio demonstration",
							Attributes = "id=\"" .. html_id .. "\"",
						},
					})
					.. "<div class=\"sequence-audio-timbre-selector\" data-target=\""
					.. html_id
					.. "\" data-key=\"interval-audio\" data-default=\"semisine\"></div>"
					.. "</div></div>",
			})
		end
	end

	if value_provided(frame.args["Calc"]) or (regular and rational) then
		local query = frame.args["Calc"] or ""
		if not value_provided(query) then
			if small then
				query = ratio_string
			else
				query = "|" .. rat.as_ket(ratio, nil, false, true) .. ">"
			end
		end
		query = mw.uri.encode(query)
		table.insert(infobox_data, {
			"<span style=\"font-size: 75%;\">[https://www.yacavone.net/xen-calc/?q=" .. query .. " open this interval in ''xen-calc'']</span>",
		})
	end

	local s = infobox.build("<u>Interval information</u>", infobox_data)
	if not debug_mode then
		s = s .. cats
	end
	return s
end

return p