Module:Infobox interval

Revision as of 14:07, 16 January 2024 by FloraC (talk | contribs) (Fix categorizing irregular intervals as rational)
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
19 infobox_interval (invokable) (frame)
Lua modules required (5)
Variable Module Functions used
ET Module:ET dependency not used
infobox Module:Infobox build
i Module:Interval harmonic_entropy
encode
rat Module:Rational parse
cents
as_ket
as_ratio
from_ket
eq
max_prime
is_superparticular
is_square_superparticular
is_reduced
is_harmonic
is_subharmonic
as_subgroup_ket
factorisation
as_FJS
tenney_height
weil_height
wilson_height
is_power
find_S_expression
u Module:Utils _round

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


local p = {}
local rat = require('Module:Rational')
local ET = require('Module:ET')
local u = require('Module:Utils')
local i = require('Module:Interval')
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

local function gen_id()
	math.randomseed(math.floor((os.clock() % 1) * 1000000) % 1000000)
	local id = math.random(0, 100000)
	return id
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
			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
			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 and small then
		if 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
	end
	if rational then
		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]]')
			cats = cats .. '[[Category:Harmonics]]'
		elseif rat.is_harmonic(ratio, true, not small) then
			table.insert(special_properties, '[[Harmonic|reduced harmonic]]')
			cats = cats .. '[[Category:Octave-reduced harmonics]]'
		end
		if rat.is_subharmonic(ratio) then
			table.insert(special_properties, '[[subharmonic]]')
			cats = cats .. '[[Category:Subharmonics]]'
		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
		table.insert(infobox_data, {
			'Size in [[cent]]s',
			u._round(cents, 8) .. '¢'
		})
	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 = 0
			name, matches = name:gsub('<br/?>', '')
			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 = 0
		FJS_name = FJS_name:gsub('%s', '')
		FJS_name, matches = FJS_name:gsub('<br/?>', '')
		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\'\')",
			u._round(rat.tenney_height(ratio), 6)
		})
		table.insert(infobox_data, {
			"[[Weil height]] (log<sub>2</sub> max(\'\'n\'\', \'\'d\'\'))",
			u._round(rat.weil_height(ratio), 6)
		})
		table.insert(infobox_data, {
			"[[Wilson height]] (sopfr (\'\'nd\'\'))",
			u._round(rat.wilson_height(ratio), 6)
		})
	end

	if regular then
		table.insert(infobox_data, {
			frame:preprocess('[[Harmonic entropy]]<br/>(Shannon, <math>\\sqrt{n\\cdot d}</math>)'),
			'~' .. u._round(i.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
		if rational and rat.is_power(ratio) then
			-- rational powers are not considered commas
		elseif 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
	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/><small>[[:File:' .. sound .. '|[sound info]]]</small>'
		})
	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_' .. gen_id()
			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, {
			'<small>[https://www.yacavone.net/xen-calc/?q=' .. query .. ' open this interval in \'\'xen-calc\'\']</small>'
		})
	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