/**
* Template Decoder Module - Full List Iteration Method
*
* This module handles the decoding of template expressions using the Cartesian product approach.
* It depends on the global 'templates' variable defined in script.js
*
* Functions:
* - decodeTemplates(): Main function to decode templates
* - generateCombinations(): Generate all possible combinations using Cartesian product
* - displayDecodedResults(): Display the decoded results
* - searchResults(): Search through all decoded results
* - copySingleResult(): Copy a single result to clipboard
* - copyAllResults(): Copy all results to clipboard
* - downloadResults(): Download results as a text file
*/
// Global variable to store all decoded expressions for searching
let allDecodedExpressions = [];
let displayedExpressions = [];
const MAX_DISPLAY_RESULTS = 999;
let simulationOptions = {}; // Store valid simulation options
// Decode templates with full list approach
function decodeTemplates() {
const editor = document.getElementById('expressionEditor');
const expression = editor.value.trim();
const errorsDiv = document.getElementById('grammarErrors');
// Check if expression is empty
if (!expression) {
errorsDiv.innerHTML = '
ERROR: Please enter an expression to decode
';
return;
}
// First, detect all templates
const templateRegex = /<(\w+)\/>/g;
const matches = [...expression.matchAll(templateRegex)];
const uniqueTemplates = [...new Set(matches.map(match => match[1]))];
// Check if there are any templates to decode
if (uniqueTemplates.length === 0) {
errorsDiv.innerHTML = '
ERROR: No templates found in the expression to decode
';
return;
}
// Check if all templates have been configured
const unconfigured = [];
const templateValues = new Map();
uniqueTemplates.forEach(templateName => {
const template = templates.get(templateName);
if (!template || !template.variables || template.variables.length === 0) {
unconfigured.push(templateName);
} else {
templateValues.set(templateName, template.variables);
}
});
// Show error if any templates are not configured
if (unconfigured.length > 0) {
errorsDiv.innerHTML = `
ERROR: The following templates need to be configured before decoding:
${unconfigured.map(t => `<${t}/>`).join(', ')}
`;
return;
}
// Calculate total combinations
let totalCombinations = 1;
templateValues.forEach(values => {
totalCombinations *= values.length;
});
// Warn if too many combinations
if (totalCombinations > 1000) {
if (!confirm(`This will generate ${totalCombinations} expressions. This might take a while. Continue?`)) {
return;
}
}
// Generate all combinations (Cartesian product)
const combinations = generateCombinations(templateValues);
// Generate decoded expressions
const decodedExpressions = combinations.map(combination => {
let decodedExpression = expression;
combination.forEach(({template, value}) => {
const regex = new RegExp(`<${template}/>`, 'g');
decodedExpression = decodedExpression.replace(regex, value);
});
return decodedExpression;
});
// Store all expressions globally
allDecodedExpressions = decodedExpressions;
// Display results (limit to MAX_DISPLAY_RESULTS)
displayDecodedResults(decodedExpressions.slice(0, MAX_DISPLAY_RESULTS), decodedExpressions.length);
// Clear errors and show success
errorsDiv.innerHTML = `
✓ Successfully decoded ${decodedExpressions.length} expressions using full list approach
${decodedExpressions.length > MAX_DISPLAY_RESULTS ?
` ⚠️ Showing first ${MAX_DISPLAY_RESULTS} results. Use search to find specific expressions.` : ''}
`;
}
// Generate all combinations (Cartesian product) of template values
function generateCombinations(templateValues) {
const templates = Array.from(templateValues.keys());
if (templates.length === 0) return [];
const combinations = [];
function generate(index, current) {
if (index === templates.length) {
combinations.push([...current]);
return;
}
const template = templates[index];
const values = templateValues.get(template);
for (const value of values) {
current.push({template, value});
generate(index + 1, current);
current.pop();
}
}
generate(0, []);
return combinations;
}
// Display decoded results
function displayDecodedResults(expressions, totalCount = null, isRandom = false) {
const resultsList = document.getElementById('resultsList');
// Clear previous results
resultsList.innerHTML = '';
// Add search box if there are more results than displayed (only for full iteration)
if (!isRandom && totalCount && totalCount > MAX_DISPLAY_RESULTS) {
const searchContainer = document.createElement('div');
searchContainer.className = 'results-search-container';
searchContainer.innerHTML = `
`;
resultsList.appendChild(searchContainer);
// Add event listeners for search
document.getElementById('resultsSearchBtn').addEventListener('click', searchResults);
document.getElementById('resultsSearchInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') searchResults();
});
document.getElementById('resultsClearSearchBtn').addEventListener('click', clearSearch);
}
// Add info about the number of results
if (expressions.length > 0) {
const infoDiv = document.createElement('div');
infoDiv.className = 'results-info';
if (isRandom) {
// For random results, show the actual selected count vs total combinations
const actualSelectedCount = allDecodedExpressions.length;
if (actualSelectedCount > expressions.length) {
infoDiv.innerHTML = `Randomly selected ${actualSelectedCount} expressions from ${totalCount} total combinations
Displaying first ${expressions.length} results. Download will include all ${actualSelectedCount} expressions.`;
} else {
infoDiv.innerHTML = `Randomly selected ${expressions.length} expressions from ${totalCount} total combinations`;
}
} else if (totalCount && totalCount > expressions.length) {
infoDiv.innerHTML = `Generated ${totalCount} expressions total.
Displaying ${expressions.length} results
${expressions.length === MAX_DISPLAY_RESULTS ? '(first 999)' : '(filtered)'}.`;
} else {
infoDiv.textContent = `Generated ${expressions.length} expressions using full list iteration`;
}
resultsList.appendChild(infoDiv);
}
// Store displayed expressions globally
displayedExpressions = expressions;
// Add each expression
expressions.forEach((expr, index) => {
const resultItem = document.createElement('div');
resultItem.className = 'result-item';
const number = document.createElement('span');
number.className = 'result-number';
number.textContent = `${index + 1}.`;
const expression = document.createElement('span');
expression.className = 'result-expression';
expression.textContent = expr;
resultItem.appendChild(number);
resultItem.appendChild(expression);
// Copy button disabled
// resultItem.appendChild(copyBtn);
resultsList.appendChild(resultItem);
});
// Show the results tab and update badge
const resultsTab = document.getElementById('resultsTab');
const resultsBadge = document.getElementById('resultsBadge');
resultsTab.style.display = 'flex';
resultsBadge.textContent = totalCount || expressions.length;
// Navigate to results page
navigateToPage('results');
}
// Search through all results
function searchResults() {
const searchInput = document.getElementById('resultsSearchInput');
const searchTerm = searchInput.value.trim().toLowerCase();
if (!searchTerm) {
// If empty search, show first 1000 again
displayDecodedResults(allDecodedExpressions.slice(0, MAX_DISPLAY_RESULTS), allDecodedExpressions.length);
return;
}
// Filter all expressions based on search term
const filteredExpressions = allDecodedExpressions.filter(expr =>
expr.toLowerCase().includes(searchTerm)
);
// Display filtered results (still limit to MAX_DISPLAY_RESULTS)
displayDecodedResults(filteredExpressions.slice(0, MAX_DISPLAY_RESULTS), allDecodedExpressions.length);
// Show clear button
document.getElementById('resultsClearSearchBtn').style.display = 'inline-block';
// Update info message
const errorsDiv = document.getElementById('grammarErrors');
if (filteredExpressions.length === 0) {
errorsDiv.innerHTML = `
ERROR: Failed to prepare data for clipboard: ${error.message}
`;
}
}
// Copy all results
function copyAllResults() {
// Copy ALL generated expressions
try {
// Check if the data is too large for clipboard (rough estimate: 1MB limit)
const expressions = allDecodedExpressions.join('\n');
const dataSize = new Blob([expressions]).size;
if (dataSize > 1024 * 1024) { // 1MB limit
const errorsDiv = document.getElementById('grammarErrors');
errorsDiv.innerHTML = `
ERROR: Data too large for clipboard (${(dataSize / 1024 / 1024).toFixed(1)}MB).
Please use the Download All button instead.
`;
}
}
// Random iteration - generate all then randomly pick
function randomIteration() {
const editor = document.getElementById('expressionEditor');
const expression = editor.value.trim();
const errorsDiv = document.getElementById('grammarErrors');
const randomCountInput = document.getElementById('randomCount');
const randomCount = parseInt(randomCountInput.value) || 10;
// Check if expression is empty
if (!expression) {
errorsDiv.innerHTML = '
ERROR: Please enter an expression to decode
';
return;
}
// First, detect all templates
const templateRegex = /<(\w+)\/>/g;
const matches = [...expression.matchAll(templateRegex)];
const uniqueTemplates = [...new Set(matches.map(match => match[1]))];
// Check if there are any templates to decode
if (uniqueTemplates.length === 0) {
errorsDiv.innerHTML = '
ERROR: No templates found in the expression to decode
';
return;
}
// Check if all templates have been configured
const unconfigured = [];
const templateValues = new Map();
uniqueTemplates.forEach(templateName => {
const template = templates.get(templateName);
if (!template || !template.variables || template.variables.length === 0) {
unconfigured.push(templateName);
} else {
templateValues.set(templateName, template.variables);
}
});
// Show error if any templates are not configured
if (unconfigured.length > 0) {
errorsDiv.innerHTML = `
ERROR: The following templates need to be configured before decoding:
${unconfigured.map(t => `<${t}/>`).join(', ')}
`;
return;
}
// Calculate total combinations
let totalCombinations = 1;
templateValues.forEach(values => {
totalCombinations *= values.length;
});
// Validate random count
if (randomCount > totalCombinations) {
errorsDiv.innerHTML = `
⚠️ Requested ${randomCount} random expressions, but only ${totalCombinations} unique combinations exist.
Generating all ${totalCombinations} expressions instead.
`;
}
// Generate all combinations (Cartesian product)
const combinations = generateCombinations(templateValues);
// Generate all decoded expressions
const allExpressions = combinations.map(combination => {
let decodedExpression = expression;
combination.forEach(({template, value}) => {
const regex = new RegExp(`<${template}/>`, 'g');
decodedExpression = decodedExpression.replace(regex, value);
});
return decodedExpression;
});
// Randomly select the requested number of expressions
const selectedExpressions = [];
const actualCount = Math.min(randomCount, allExpressions.length);
if (actualCount === allExpressions.length) {
// If requesting all or more, just return all
selectedExpressions.push(...allExpressions);
} else {
// Randomly select without replacement
const indices = new Set();
while (indices.size < actualCount) {
indices.add(Math.floor(Math.random() * allExpressions.length));
}
indices.forEach(index => {
selectedExpressions.push(allExpressions[index]);
});
}
// Store ALL selected expressions globally for download (not limited by display)
allDecodedExpressions = selectedExpressions;
// For display, limit to MAX_DISPLAY_RESULTS but keep full set for download
const displayExpressions = selectedExpressions.slice(0, MAX_DISPLAY_RESULTS);
// Display results (limited for display, but full count for download)
displayDecodedResults(displayExpressions, allExpressions.length, true);
// Clear errors and show success with clear indication about display vs download
if (selectedExpressions.length > MAX_DISPLAY_RESULTS) {
errorsDiv.innerHTML = `
✓ Randomly selected ${selectedExpressions.length} expressions from ${allExpressions.length} total combinations
📺 Displaying first ${MAX_DISPLAY_RESULTS} results. Download will include all ${selectedExpressions.length} expressions.
`;
} else {
errorsDiv.innerHTML = `
✓ Randomly selected ${selectedExpressions.length} expressions from ${allExpressions.length} total combinations
`;
}
}
// Open settings modal for Next Move
function openSettingsModal() {
const modal = document.getElementById('settingsModal');
modal.style.display = 'block';
// Check if we have simulation options and update UI
if (typeof simulationOptions !== 'undefined' && Object.keys(simulationOptions).length > 0) {
updateSettingsUIWithOptions();
}
updateTotalCombinations();
// Add event listeners for setting inputs (inputs/selects/checkbox groups)
document.querySelectorAll('.setting-value-input').forEach(el => {
el.addEventListener('input', updateTotalCombinations);
el.addEventListener('change', updateTotalCombinations);
if (el.classList && el.classList.contains('setting-checkbox-group')) {
el.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.addEventListener('change', updateTotalCombinations);
});
}
});
// Add event listener for add setting button
document.getElementById('addSettingBtn').addEventListener('click', addCustomSetting);
// Add event listener for test period slider
const testPeriodSlider = document.querySelector('.test-period-slider');
if (testPeriodSlider) {
testPeriodSlider.addEventListener('input', updateTestPeriodValue);
// Initialize the display value
updateTestPeriodValue();
}
}
function updateSettingsUIWithOptions() {
// Assume EQUITY for now as it's the default
const instType = 'EQUITY';
const options = simulationOptions[instType];
if (!options) return;
// 1. Update Region Input to Select
const regionInput = document.querySelector('input[data-setting="region"]');
if (regionInput && regionInput.tagName === 'INPUT') {
const select = document.createElement('select');
select.className = 'setting-value-input';
select.setAttribute('data-setting', 'region');
// Add empty option
const emptyOpt = document.createElement('option');
emptyOpt.value = '';
emptyOpt.text = 'Select Region...';
select.appendChild(emptyOpt);
// Add regions
Object.keys(options).forEach(r => {
const opt = document.createElement('option');
opt.value = r;
opt.text = r;
select.appendChild(opt);
});
// Replace input
regionInput.parentNode.replaceChild(select, regionInput);
// Add change listener
select.addEventListener('change', function() {
updateDependentSettings(this.value);
updateTotalCombinations();
});
}
// 2. Convert Universe and Neutralization to CHECKBOX groups if they are inputs/selects
convertInputToCheckboxGroup('universe', { placeholder: 'Select Region first...' });
convertInputToCheckboxGroup('neutralization', { placeholder: 'Select Region first...' });
// Delay is already a select, but we might want to update it based on region
// For now, we'll leave it as is or update it in updateDependentSettings
}
function convertInputToCheckboxGroup(settingName, { placeholder = 'Select Region first...' } = {}) {
const current = document.querySelector(`[data-setting="${settingName}"]`);
if (!current) return;
// If it's already a checkbox group, do nothing
if (current.tagName === 'DIV' && current.classList.contains('setting-checkbox-group')) return;
const container = document.createElement('div');
container.className = 'setting-value-input setting-checkbox-group';
container.setAttribute('data-setting', settingName);
container.setAttribute('role', 'group');
container.setAttribute('aria-label', settingName);
// Placeholder text (shown until region populates)
const placeholderEl = document.createElement('div');
placeholderEl.className = 'setting-checkbox-placeholder';
placeholderEl.textContent = placeholder;
container.appendChild(placeholderEl);
current.parentNode.replaceChild(container, current);
}
function setCheckboxGroupOptions(settingName, optionValues) {
const container = document.querySelector(`.setting-checkbox-group[data-setting="${settingName}"]`);
if (!container) return;
const previousSelected = new Set(
Array.from(container.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value)
);
container.innerHTML = '';
(optionValues || []).forEach((val, idx) => {
const label = document.createElement('label');
label.className = 'setting-checkbox-item';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.value = val;
// Keep previous selections when possible; otherwise select first item by default
if (previousSelected.has(val) || (previousSelected.size === 0 && idx === 0)) {
cb.checked = true;
}
cb.addEventListener('change', updateTotalCombinations);
const text = document.createElement('span');
text.textContent = val;
label.appendChild(cb);
label.appendChild(text);
container.appendChild(label);
});
updateTotalCombinations();
}
function updateDependentSettings(region) {
if (!region) return;
const instType = 'EQUITY';
const settings = simulationOptions[instType][region];
if (!settings) return;
// Update Universe (checkbox group preferred)
if (document.querySelector('.setting-checkbox-group[data-setting="universe"]')) {
setCheckboxGroupOptions('universe', settings.universes);
} else {
// Back-compat if still a select
const univSelect = document.querySelector('select[data-setting="universe"]');
if (univSelect) {
univSelect.innerHTML = '';
settings.universes.forEach(u => {
const opt = document.createElement('option');
opt.value = u;
opt.text = u;
univSelect.appendChild(opt);
});
if (settings.universes.length > 0) univSelect.value = settings.universes[0];
}
}
// Update Neutralization (checkbox group preferred)
if (document.querySelector('.setting-checkbox-group[data-setting="neutralization"]')) {
setCheckboxGroupOptions('neutralization', settings.neutralizations);
} else {
// Back-compat if still a select
const neutSelect = document.querySelector('select[data-setting="neutralization"]');
if (neutSelect) {
neutSelect.innerHTML = '';
settings.neutralizations.forEach(n => {
const opt = document.createElement('option');
opt.value = n;
opt.text = n;
neutSelect.appendChild(opt);
});
if (settings.neutralizations.length > 0) neutSelect.value = settings.neutralizations[0];
}
}
// Update Delay
const delaySelect = document.querySelector('select[data-setting="delay"]');
if (delaySelect && settings.delays) {
delaySelect.innerHTML = '';
settings.delays.forEach(d => {
const opt = document.createElement('option');
opt.value = d;
opt.text = d;
delaySelect.appendChild(opt);
});
// Select first
if (settings.delays.length > 0) delaySelect.value = settings.delays[0];
}
}
// Close settings modal
function closeSettingsModal() {
const modal = document.getElementById('settingsModal');
modal.style.display = 'none';
}
// Update test period value display
function updateTestPeriodValue() {
const slider = document.querySelector('.test-period-slider');
const valueDisplay = document.getElementById('testPeriodValue');
if (slider && valueDisplay) {
const totalMonths = parseInt(slider.value);
const years = Math.floor(totalMonths / 12);
const months = totalMonths % 12;
const periodValue = `P${years}Y${months}M`;
valueDisplay.textContent = periodValue;
// Update the slider's value attribute so it can be read by parseSettingValues
slider.setAttribute('data-period-value', periodValue);
}
}
// Add custom setting row
function addCustomSetting() {
const tbody = document.getElementById('settingsTableBody');
const row = document.createElement('tr');
row.className = 'custom-setting-row';
row.innerHTML = `
`;
tbody.appendChild(row);
// Add event listener to new input
row.querySelector('.setting-value-input').addEventListener('input', updateTotalCombinations);
row.querySelector('.setting-name-input').addEventListener('input', updateTotalCombinations);
}
// Remove custom setting row
function removeCustomSetting(button) {
button.closest('tr').remove();
updateTotalCombinations();
}
// Calculate total combinations
function updateTotalCombinations() {
let totalCombinations = allDecodedExpressions.length;
// Get all settings and their values
const settingInputs = document.querySelectorAll('.setting-value-input');
settingInputs.forEach(input => {
let values = [];
if (input.tagName === 'DIV' && input.classList.contains('setting-checkbox-group')) {
values = Array.from(input.querySelectorAll('input[type="checkbox"]:checked'))
.map(cb => cb.value)
.filter(v => v !== '');
} else if (input.tagName === 'SELECT') {
if (input.multiple) {
values = Array.from(input.selectedOptions)
.map(o => o.value)
.filter(v => v !== '');
} else {
values = input.value ? [input.value] : [];
}
} else if (input.type === 'range' && input.getAttribute('data-period-value')) {
values = [input.getAttribute('data-period-value')];
} else {
values = (input.value || '').split(',').map(v => v.trim()).filter(v => v !== '');
}
if (values.length > 1) totalCombinations *= values.length;
});
document.getElementById('totalCombinations').textContent = totalCombinations.toLocaleString();
}
// Parse settings values (handle comma-separated values)
function parseSettingValues() {
const settings = {};
const variations = {};
const types = {};
// Get predefined settings
const settingRows = document.querySelectorAll('#settingsTableBody tr');
settingRows.forEach(row => {
const nameCell = row.cells[0];
// Use select or input for value
let input = row.querySelector('.setting-value-input');
if (input) {
let settingName;
// Check if it's a custom setting
const nameInput = row.querySelector('.setting-name-input');
if (nameInput) {
settingName = nameInput.value.trim();
if (!settingName) return; // Skip if no name
} else {
settingName = nameCell.textContent.trim();
}
// Get the type
const typeSelect = row.querySelector('.setting-type-select');
const type = typeSelect ? typeSelect.value : 'string';
types[settingName] = type;
// For select dropdowns, get value differently
let values = [];
if (input.tagName === 'DIV' && input.classList.contains('setting-checkbox-group')) {
values = Array.from(input.querySelectorAll('input[type="checkbox"]:checked'))
.map(cb => cb.value)
.filter(v => v !== '');
} else if (input.tagName === 'SELECT') {
if (input.multiple) {
values = Array.from(input.selectedOptions)
.map(o => o.value)
.filter(v => v !== '');
} else {
values = input.value ? [input.value] : [];
}
} else if (input.type === 'range' && settingName === 'testPeriod') {
// Special handling for test period slider
const periodValue = input.getAttribute('data-period-value') || 'P0Y0M';
values = [periodValue];
} else {
values = input.value.split(',').map(v => v.trim()).filter(v => v !== '');
}
// Convert values based on type
const convertedValues = values.map(v => {
if (type === 'number') {
const num = parseFloat(v);
return isNaN(num) ? v : num;
} else if (type === 'boolean') {
if (typeof v === 'boolean') return v;
if (typeof v === 'string') {
if (v.toLowerCase() === 'true') return true;
if (v.toLowerCase() === 'false') return false;
}
return false;
} else {
return v;
}
});
if (convertedValues.length === 0) {
// Use empty string if no value
settings[settingName] = '';
} else if (convertedValues.length === 1) {
// Single value
settings[settingName] = convertedValues[0];
} else {
// Multiple values - store for iteration
variations[settingName] = convertedValues;
settings[settingName] = convertedValues[0]; // Default to first value
}
}
});
return { settings, variations, types };
}
// Generate all setting combinations
function generateSettingCombinations(baseSettings, variations) {
const variationKeys = Object.keys(variations);
if (variationKeys.length === 0) {
return [baseSettings];
}
const combinations = [];
function generate(index, current) {
if (index === variationKeys.length) {
combinations.push({...current});
return;
}
const key = variationKeys[index];
const values = variations[key];
for (const value of values) {
current[key] = value;
generate(index + 1, current);
}
}
generate(0, {...baseSettings});
return combinations;
}
// Confirm and apply settings with shuffle option
function confirmAndApplySettings() {
const { settings, variations, types } = parseSettingValues();
const settingCombinations = generateSettingCombinations(settings, variations);
const totalCombinations = allDecodedExpressions.length * settingCombinations.length;
if (totalCombinations > 1000) {
// Show confirmation dialog for large datasets
const shouldShuffle = confirm(`即将生成 ${totalCombinations.toLocaleString()} 个表达式配置。\n\n是否需要随机打乱表达式顺序?\n\n点击"确定"进行随机打乱\n点击"取消"保持原始顺序`);
applySettings(shouldShuffle);
} else {
// For small datasets, ask if user wants to shuffle
const shouldShuffle = confirm(`即将生成 ${totalCombinations.toLocaleString()} 个表达式配置。\n\n是否需要随机打乱表达式顺序?\n\n点击"确定"进行随机打乱\n点击"取消"保持原始顺序`);
applySettings(shouldShuffle);
}
}
// Apply settings to expressions
async function applySettings(shouldShuffle = false) {
const { settings, variations, types } = parseSettingValues();
// Always include instrumentType and language
settings.instrumentType = settings.instrumentType || "EQUITY";
settings.language = settings.language || "FASTEXPR";
// Generate all setting combinations
const settingCombinations = generateSettingCombinations(settings, variations);
// Calculate total combinations for progress tracking
const totalCombinations = allDecodedExpressions.length * settingCombinations.length;
// Get the button and show progress
const button = document.getElementById('generateDownloadBtn');
const btnText = button.querySelector('.btn-text');
const btnProgress = button.querySelector('.btn-progress');
const progressBarFill = button.querySelector('.progress-bar-fill');
const progressText = button.querySelector('.progress-text');
// Disable button and show progress
button.disabled = true;
btnText.style.display = 'none';
btnProgress.style.display = 'flex';
// Show progress to user
const errorsDiv = document.getElementById('grammarErrors');
errorsDiv.innerHTML = `