bit = require("bit")
+local generator = require("setclass.generator")
local M = {}
local function load_csv()
local f = io.open(csvPath, "r")
- if not f then error("Could not open setclasses.csv at " .. csvPath) end
+ if not f then
+ return false
+ end
for line in f:lines() do
- local dec, sc, icv = line:match("^(%d+),%[?([0-9AB ]+)%]?%,%<([0-9 ]+)%>$")
- if dec and sc then
+ local dec, set, sc, icv = line:match("^(%d+),([0-9AB]+),([0-9AB]+),(<[0-9AB]+>)")
+ if dec then
lookup[tonumber(dec)] = {
- sc = "[" .. sc .. "]",
- icv = "<" .. icv .. ">"
+ set = set,
+ sc = sc,
+ icv = icv
}
end
end
f:close()
+ csvLoaded = true
+ return true
end
+
function M.get_visual_selection()
local start_pos = vim.fn.getpos("'<")
local end_pos = vim.fn.getpos("'>")
end
function M.analyze_set(inputStr)
+
if not csvLoaded then
- load_csv()
- csvLoaded = true
+ local success = load_csv()
+ if not success then
+ print("[SetClass] CSV not found — generating now...")
+ generator.generate_csv(csvPath)
+ assert(load_csv(), "Failed to load generated set-classes.csv")
+ end
end
local function parse_set(input)
local mask = parse_set(inputStr)
return {
- input = inputStr,
+ input = lookup[mask].set,
decimal = mask,
set_class = lookup[mask].sc,
interval_class_vector = lookup[mask].icv,
+++ /dev/null
-local bit32 = require("bit32") or require("bit")\r
-\r
--- Convert pitch class to A/B notation\r
-local function pc_to_char(pc)\r
- if pc < 10 then return tostring(pc)\r
- elseif pc == 10 then return 'A'\r
- elseif pc == 11 then return 'B' end\r
-end\r
-\r
--- Generate a string for a PC set\r
-local function pcs_to_string(pcs)\r
- local out = {}\r
- for _, pc in ipairs(pcs) do table.insert(out, pc_to_char(pc)) end\r
- return "[" .. table.concat(out, "") .. "]"\r
-end\r
-\r
--- Get all rotations of a sorted set\r
-local function rotations(pcs)\r
- local out = {}\r
- local n = #pcs\r
- for i = 1, n do\r
- local rot = {}\r
- local offset = pcs[i]\r
- for j = 0, n - 1 do\r
- local val = (pcs[(i + j - 1) % n + 1] - offset + 12) % 12\r
- table.insert(rot, val)\r
- end\r
- table.insert(out, rot)\r
- end\r
- return out\r
-end\r
-\r
--- Get normal form of a set (most packed rotation)\r
-local function normal_form(pcs)\r
- table.sort(pcs)\r
- local all_rots = rotations(pcs)\r
-\r
- -- Find the most compact (smallest span)\r
- table.sort(all_rots, function(a, b)\r
- local span_a = (a[#a] - a[1]) % 12\r
- local span_b = (b[#b] - b[1]) % 12\r
- if span_a ~= span_b then return span_a < span_b end\r
-\r
- -- If equal span, choose lex smallest\r
- for i = 1, #a do\r
- if a[i] ~= b[i] then return a[i] < b[i] end\r
- end\r
- return false\r
- end)\r
-\r
- return all_rots[1]\r
-end\r
-\r
--- Invert a set mod 12\r
-local function invert(pcs)\r
- local inv = {}\r
- for _, pc in ipairs(pcs) do\r
- table.insert(inv, (12 - pc) % 12)\r
- end\r
- return inv\r
-end\r
-\r
--- Get prime form of a set\r
-local function prime_form(pcs)\r
- local nf1 = normal_form(pcs)\r
- local nf2 = normal_form(invert(pcs))\r
-\r
- -- Lexical comparison\r
- for i = 1, #nf1 do\r
- if nf1[i] < nf2[i] then return nf1\r
- elseif nf1[i] > nf2[i] then return nf2 end\r
- end\r
- return nf1\r
-end\r
-\r
--- Convert bitmask to PC set\r
-local function bitmask_to_pcs(n)\r
- local pcs = {}\r
- for i = 0, 11 do\r
- if bit32.band(n, bit32.lshift(1, i)) ~= 0 then\r
- table.insert(pcs, i)\r
- end\r
- end\r
- return pcs\r
-end\r
-\r
-local function interval_class_vector(pcs)\r
- local icv = {0, 0, 0, 0, 0, 0}\r
- table.sort(pcs)\r
- for i = 1, #pcs do\r
- for j = i + 1, #pcs do\r
- local interval = (pcs[j] - pcs[i]) % 12\r
- local ic = math.min(interval, 12 - interval)\r
- if ic > 0 and ic <= 6 then\r
- icv[ic] = icv[ic] +1\r
- end\r
- end\r
- end\r
- return table.concat(icv)\r
-end\r
-\r
--- Write the CSV\r
-local output = io.open("set-classes.csv", "w")\r
-for i = 1, 4095 do -- skip 0 (empty set)\r
- local pcs = bitmask_to_pcs(i)\r
- local pf = prime_form(pcs)\r
- local icv = interval_class_vector(pcs)\r
- output:write(i .. "," .. pcs_to_string(pf) .. ",<" .. icv .. ">\n")\r
-end\r
-output:close()\r
-\r
-print("✅ setclasses.csv generated with correct prime forms.")\r
--- /dev/null
+local M = {}
+
+local bit = require("bit")
+
+-- Convert bitmask to PC list (integers)
+local function mask_to_pcs(mask)
+ local pcs = {}
+ for i = 0, 11 do
+ if bit.band(mask, bit.lshift(1, i)) ~= 0 then
+ table.insert(pcs, i)
+ end
+ end
+ return pcs
+end
+
+-- Convert PC list to hex string (e.g., 10 -> A, 11 -> B)
+local function pcs_to_hex(pcs)
+ local out = {}
+ for _, pc in ipairs(pcs) do
+ table.insert(out, string.format("%X", pc))
+ end
+ return table.concat(out)
+end
+
+-- Transpose bitmask left by n
+local function transpose(mask, n)
+ return bit.band(bit.lshift(mask, n) + bit.rshift(mask, 12 - n), 0xFFF)
+end
+
+-- Invert a bitmask (I-transform)
+local function invert(mask)
+ local result = 0
+ for i = 0, 11 do
+ if bit.band(mask, bit.lshift(1, i)) ~= 0 then
+ local inv = (12 - i) % 12
+ result = bit.bor(result, bit.lshift(1, inv))
+ end
+ end
+ return result
+end
+
+-- Find prime form of bitmask
+local function prime_form_bitmask(mask)
+ local min = 0xFFF + 1
+ for i = 0, 11 do
+ local t = transpose(mask, i)
+ local ti = transpose(invert(mask), i)
+ if t < min then min = t end
+ if ti < min then min = ti end
+ end
+ return min
+end
+
+-- Generate ICV from bitmask
+local function icv_from_mask(mask)
+ local pcs = mask_to_pcs(mask)
+ local icv = {0, 0, 0, 0, 0, 0}
+ for i = 1, #pcs - 1 do
+ for j = i + 1, #pcs do
+ local a, b = pcs[i], pcs[j]
+ local ic = math.min((a - b) % 12, (b - a) % 12)
+ if ic >= 1 and ic <= 6 then
+ icv[ic] = icv[ic] + 1
+ end
+ end
+ end
+ local out = {}
+ for _, v in ipairs(icv) do
+ table.insert(out, string.format("%X", v))
+ end
+ return "<" .. table.concat(out) .. ">"
+end
+
+-- Generate CSV with format: decimal,pc_order,prime_form,icv
+function M.generate_csv(path)
+ local out = io.open(path, "w")
+ local total = 4096
+ for mask = 0, total - 1 do
+ local pcs = mask_to_pcs(mask)
+ if #pcs > 0 then
+ local pc_order = pcs_to_hex(pcs)
+ local prime_mask = prime_form_bitmask(mask)
+ local prime_form = pcs_to_hex(mask_to_pcs(prime_mask))
+ local icv = icv_from_mask(mask)
+ out:write(string.format("%d,%s,%s,%s\n", mask, pc_order, prime_form, icv))
+ end
+ end
+ out:close()
+end
+
+return M
local input = core.get_visual_selection()
local result = core.analyze_set(input)
if result then
- vim.notify("input: {" .. result.input .. "} (" .. result.decimal .. ")")
- vim.notify("set class: " .. result.set_class .. " " .. result.interval_class_vector)
- vim.notify("abs. complement: " .. result.complement_class)
- vim.notify("M5: " .. result.m5_class)
+ vim.notify(" input: " .. result.input .. " (" .. result.decimal .. ")")
+ vim.notify(" set class: " .. result.set_class .. " " .. result.interval_class_vector)
+ vim.notify("abs. complement: " .. result.complement_class)
+ vim.notify(" m5 class: " .. result.m5_class)
else
vim.notify("unable to parse collection")
end
+++ /dev/null
--- load csv
-local bit = require("bit")
-local generator = require("setclass.generator")
-
-local M = {}
-
-local pluginDir = debug.getinfo(1, "S").source:sub(2):match("(.*/)")
-local csvPath = pluginDir .. "set-classes.csv"
-
-local csvLoaded = false
-local lookup = {}
-
-local function load_csv()
- local f = io.open(csvPath, "r")
- if not f then
- return false
- end
- for line in f:lines() do
- local dec, norm, prime, icv = line:match("^(%d+),([0-9AB]+),([0-9AB]+),(<[0-9AB]+>)")
- if dec then
- lookup[tonumber(dec)] = {
- normal = norm,
- prime = prime,
- icv = icv
- }
- end
- end
- f:close()
- csvLoaded = true
- return true
-end
-
--- lazy load in analyze
-function M.analyze_set(inputStr)
- -- Try to load the CSV, generate if needed
- if not csvLoaded then
- local success = load_csv()
- if not success then
- print("[SetClass] CSV not found — generating now...")
- generator.generate_csv(csvPath)
- assert(load_csv(), "Failed to load generated set-classes.csv")
- end
- end
-
- -- Now safely proceed with your parse/lookup logic...
-end
-
--- csv generator (generator.lua)
-
-
--- Progress bar helper
-local function show_progress(current, total, width)
- width = width or 30
- local pct = current / total
- local filled = math.floor(pct * width)
- local bar = string.rep("#", filled) .. string.rep("-", width - filled)
- io.write(string.format("\r[%s] %3d%%", bar, pct * 100))
- io.flush()
-end
-
--- Main CSV generation
-function M.generate_csv(path)
- local out = io.open(path, "w")
- local total = 4095
- for mask = 1, total do
- local pcs = bitmask_to_pcs(mask)
- if #pcs > 0 then
- -- compute norm, prime, icv...
- out:write(string.format("%d,%s,%s,%s\n", mask, norm_hex, prime_hex, icv))
- end
- show_progress(mask, total)
- end
- out:close()
- io.write("\nCSV generated: " .. path .. "\n")
-end
-
-
--- setclass/generator.lua
-local M = {}
-
-function M.generate_csv(path)
- local bit = require("bit")
-
- local function bitmask_to_pcs(mask)
- local pcs = {}
- for i = 0, 11 do
- if bit.band(mask, bit.lshift(1, i)) ~= 0 then
- table.insert(pcs, i)
- end
- end
- return pcs
- end
-
- local function pcs_to_hex(pcs)
- local out = {}
- for _, pc in ipairs(pcs) do
- if pc < 10 then
- table.insert(out, tostring(pc))
- elseif pc == 10 then
- table.insert(out, "A")
- else
- table.insert(out, "B")
- end
- end
- return table.concat(out)
- end
-
- local function rotate(t, n)
- local len, out = #t, {}
- for i = 1, len do
- out[i] = t[((i + n - 2) % len) + 1]
- end
- return out
- end
-
- local function transpose_to_zero(set)
- local out, root = {}, set[1]
- for i = 1, #set do out[i] = (set[i] - root) % 12 end
- table.sort(out)
- return out
- end
-
- local function normal_form(pcs)
- local best
- for i = 0, #pcs - 1 do
- local rot = rotate(pcs, i)
- local dist = rot[#rot] - rot[1]
- if not best or dist < (best[#best] - best[1]) or
- (dist == (best[#best] - best[1]) and table.concat(rot) < table.concat(best)) then
- best = rot
- end
- end
- return transpose_to_zero(best)
- end
-
- local function invert(pcs)
- local inv = {}
- for i = 1, #pcs do inv[i] = (12 - pcs[i]) % 12 end
- table.sort(inv)
- return inv
- end
-
- local function prime_form(pcs)
- local nf = normal_form(pcs)
- local inv = normal_form(invert(nf))
- local nf_str = table.concat(transpose_to_zero(nf))
- local inv_str = table.concat(transpose_to_zero(inv))
- if nf_str < inv_str then
- return transpose_to_zero(nf)
- else
- return transpose_to_zero(inv)
- end
- end
-
- local function interval_class_vector(pcs)
- local icv = {0, 0, 0, 0, 0, 0}
- for i = 1, #pcs - 1 do
- for j = i + 1, #pcs do
- local ic = math.min((pcs[j] - pcs[i]) % 12, (pcs[i] - pcs[j]) % 12)
- if ic >= 1 and ic <= 6 then
- icv[ic] = icv[ic] + 1
- end
- end
- end
- local function hex(n)
- if n < 10 then return tostring(n)
- elseif n == 10 then return "A"
- elseif n == 11 then return "B" end
- end
- return "<" .. table.concat(vim.tbl_map(hex, icv)) .. ">"
- end
-
- local out = io.open(path, "w")
- for mask = 0, 4095 do
- local pcs = bitmask_to_pcs(mask)
- if #pcs > 0 then
- local nf = normal_form(pcs)
- local pf = prime_form(pcs)
- local icv = interval_class_vector(pcs)
- out:write(string.format("%d,%s,%s,%s\n",
- mask, pcs_to_hex(nf), pcs_to_hex(pf), icv
- ))
- end
- end
- out:close()
- print("Set-class CSV generated at: " .. path)
-end
-
-return M
-
-
--- standalone script:
-
-
-local bit = require("bit")
-
-local function bitmask_to_pcs(mask)
- local pcs = {}
- for i = 0, 11 do
- if bit.band(mask, bit.lshift(1, i)) ~= 0 then
- table.insert(pcs, i)
- end
- end
- return pcs
-end
-
--- Converts a PC list to hex string like "014A9"
-local function pcs_to_hex(pcs)
- local symbols = {}
- for _, pc in ipairs(pcs) do
- if pc < 10 then
- table.insert(symbols, tostring(pc))
- elseif pc == 10 then
- table.insert(symbols, "A")
- else
- table.insert(symbols, "B")
- end
- end
- return table.concat(symbols)
-end
-
--- Rotate a set
-local function rotate(tbl, n)
- local out = {}
- local len = #tbl
- for i = 1, len do
- out[i] = tbl[((i + n - 2) % len) + 1]
- end
- return out
-end
-
--- Transpose set so first element is 0
-local function transpose_to_zero(set)
- local transposed = {}
- local root = set[1]
- for i = 1, #set do
- table.insert(transposed, (set[i] - root) % 12)
- end
- table.sort(transposed)
- return transposed
-end
-
--- Normal form (compact rotation)
-local function normal_form(pcs)
- local best = nil
- for i = 0, #pcs - 1 do
- local rotated = rotate(pcs, i)
- local dist = rotated[#rotated] - rotated[1]
- if not best or dist < (best[#best] - best[1]) or
- (dist == (best[#best] - best[1]) and table.concat(rotated) < table.concat(best)) then
- best = rotated
- end
- end
- return transpose_to_zero(best)
-end
-
--- Prime form (lowest between normal and inversion)
-local function invert(pcs)
- local inv = {}
- for i = 1, #pcs do
- inv[i] = (12 - pcs[i]) % 12
- end
- table.sort(inv)
- return inv
-end
-
-local function prime_form(pcs)
- local nf = normal_form(pcs)
- local inv = invert(nf)
- local nif = normal_form(inv)
-
- local nf_str = table.concat(transpose_to_zero(nf))
- local nif_str = table.concat(transpose_to_zero(nif))
-
- if nf_str < nif_str then
- return transpose_to_zero(nf)
- else
- return transpose_to_zero(nif)
- end
-end
-
--- ICV as hex
-local function interval_class_vector(pcs)
- local icv = {0, 0, 0, 0, 0, 0}
- for i = 1, #pcs - 1 do
- for j = i + 1, #pcs do
- local ic = math.min((pcs[j] - pcs[i]) % 12, (pcs[i] - pcs[j]) % 12)
- if ic >= 1 and ic <= 6 then
- icv[ic] = icv[ic] + 1
- end
- end
- end
-
- local function hex(n)
- if n < 10 then return tostring(n)
- elseif n == 10 then return "A"
- elseif n == 11 then return "B" end
- end
-
- return "<" .. table.concat(vim.tbl_map(hex, icv)) .. ">"
-end
-
--- Write CSV
-local out = io.open("set-classes.csv", "w")
-for mask = 0, 4095 do
- local pcs = bitmask_to_pcs(mask)
- if #pcs > 0 then
- local norm = normal_form(pcs)
- local prime = prime_form(pcs)
- local icv = interval_class_vector(pcs)
- out:write(string.format("%d,%s,%s,%s\n",
- mask,
- pcs_to_hex(norm),
- pcs_to_hex(prime),
- icv
- ))
- end
-end
-out:close()
-print("CSV generated.")