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
-- what does this do?
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| ? ]]</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