new
This commit is contained in:
325
src/utils/symbolUtil.ts
Normal file
325
src/utils/symbolUtil.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* Transform flattened path data into a tree structure
|
||||
* @param data Raw array returned by the backend
|
||||
*/
|
||||
export const transformToSymbolTree = (
|
||||
data: API.SymbolDataItem[],
|
||||
): API.SymbolTreeNode[] => {
|
||||
const tree: API.SymbolTreeNode[] = [];
|
||||
const map = new Map<string, API.SymbolTreeNode>();
|
||||
|
||||
data.forEach((item) => {
|
||||
const parts = item.path.split('\\');
|
||||
let currentPath = '';
|
||||
|
||||
parts.forEach((part, index) => {
|
||||
const isLast = index === parts.length - 1;
|
||||
const parentPath = currentPath;
|
||||
currentPath = parentPath ? `${parentPath}\\${part}` : part;
|
||||
|
||||
if (!map.has(currentPath)) {
|
||||
const newNode: API.SymbolTreeNode = {
|
||||
title: part,
|
||||
// Leaf nodes use symbol, directory nodes use path to prevent name conflicts
|
||||
key: isLast ? item.symbol : currentPath,
|
||||
};
|
||||
|
||||
if (isLast) {
|
||||
newNode.value = item.symbol;
|
||||
newNode.isLeaf = true;
|
||||
newNode.meta = item;
|
||||
} else {
|
||||
newNode.children = [];
|
||||
newNode.selectable = false; // Directory nodes are not selectable
|
||||
}
|
||||
|
||||
map.set(currentPath, newNode);
|
||||
|
||||
// If there is no parent path, it is a root node
|
||||
if (!parentPath) {
|
||||
tree.push(newNode);
|
||||
} else {
|
||||
// Otherwise, add it to the parent's children
|
||||
map.get(parentPath)?.children?.push(newNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return tree;
|
||||
};
|
||||
|
||||
type SymbolTreeNode = {
|
||||
title: string;
|
||||
key: string;
|
||||
value?: string;
|
||||
path?: string;
|
||||
children?: SymbolTreeNode[];
|
||||
isLeaf?: boolean;
|
||||
selectable?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a Pattern string based on the checkedKeys of Ant Design Tree
|
||||
* @param treeData The tree structure of symbols (must contain complete symbol and key information)
|
||||
* @param checkedKeys The checked keys of Ant Design Tree
|
||||
* @param options Configuration items
|
||||
*/
|
||||
export function generatePattern(
|
||||
treeData: SymbolTreeNode[],
|
||||
checkedKeys: string[],
|
||||
options: { useShortMode?: boolean; separator?: string } = {},
|
||||
) {
|
||||
const { useShortMode = true, separator = '\\' } = options;
|
||||
|
||||
const excludes: string[] = [];
|
||||
const includes: string[] = [];
|
||||
|
||||
// --- Utility function: Get all leaf keys under a node ---
|
||||
const getAllLeafKeys = (node: SymbolTreeNode): string[] => {
|
||||
if (!node.children || node.children.length === 0) return [node.key];
|
||||
return node.children.flatMap(getAllLeafKeys);
|
||||
};
|
||||
|
||||
// --- Formatting function: process paths and wildcards ---
|
||||
const format = (node: SymbolTreeNode, isExclude: boolean) => {
|
||||
const isLeaf = !node.children || node.children.length === 0;
|
||||
|
||||
// Short mode (Logic from screenshot two): !*SYMBOL or path\*
|
||||
if (useShortMode && isLeaf && node.value) {
|
||||
return isExclude ? `!*${node.value}` : `*${node.value}`;
|
||||
}
|
||||
|
||||
// Short mode still uses wildcards to match categories, but we want to keep the path structure for clarity
|
||||
const path = node.key.split('/').join(separator); // Adapt to backslashes
|
||||
const suffix = isLeaf ? '' : `${separator}*`;
|
||||
const prefix = isExclude ? '!' : '';
|
||||
|
||||
// Special handling for exclusion logic in short mode:
|
||||
// If it is short mode and we want to exclude a specific product, it should be !*SYMBOL according to screenshot two
|
||||
if (useShortMode && isExclude && isLeaf && node.value) {
|
||||
return `!*${node.value}`;
|
||||
}
|
||||
|
||||
return `${prefix}${path}${suffix}`;
|
||||
};
|
||||
|
||||
// --- Core recursive logic ---
|
||||
const process = (node: SymbolTreeNode) => {
|
||||
const allLeaves = getAllLeafKeys(node);
|
||||
const selectedLeaves = allLeaves.filter((k) => checkedKeys.includes(k));
|
||||
|
||||
if (selectedLeaves.length === 0) return; // This branch is not selected at all
|
||||
|
||||
// 1. If the current level is fully selected
|
||||
if (selectedLeaves.length === allLeaves.length) {
|
||||
includes.push(format(node, false));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. If it is a leaf node (and is selected)
|
||||
if (!node.children || node.children.length === 0) {
|
||||
includes.push(format(node, false));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedLeaves.length > allLeaves.length / 2) {
|
||||
includes.push(format(node, false));
|
||||
|
||||
node.children.forEach((child) => {
|
||||
const childLeaves = getAllLeafKeys(child);
|
||||
const childSelected = childLeaves.filter((k) =>
|
||||
checkedKeys.includes(k),
|
||||
);
|
||||
|
||||
if (childSelected.length === 0) {
|
||||
excludes.push(format(child, true));
|
||||
} else if (childSelected.length < childLeaves.length) {
|
||||
findExcludesOnly(child);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
node.children.forEach(process);
|
||||
}
|
||||
};
|
||||
|
||||
const findExcludesOnly = (node: SymbolTreeNode) => {
|
||||
const allLeaves = getAllLeafKeys(node);
|
||||
const selectedLeaves = allLeaves.filter((k) => checkedKeys.includes(k));
|
||||
|
||||
if (selectedLeaves.length === 0) {
|
||||
excludes.push(format(node, true));
|
||||
return;
|
||||
}
|
||||
|
||||
node.children?.forEach((child) => {
|
||||
const childLeaves = getAllLeafKeys(child);
|
||||
const childSelected = childLeaves.filter((k) => checkedKeys.includes(k));
|
||||
|
||||
if (childSelected.length === 0) {
|
||||
excludes.push(format(child, true));
|
||||
} else if (childSelected.length < childLeaves.length) {
|
||||
findExcludesOnly(child);
|
||||
}
|
||||
});
|
||||
};
|
||||
treeData.forEach(process);
|
||||
|
||||
// 3. Final concatenation: Exclude items must be at the front
|
||||
return [...excludes, ...includes].join(',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the background Pattern string to Ant Design Tree's checkedKeys
|
||||
* @param patternString The string returned by the background, such as "!forex\exotic\*,forex\*"
|
||||
* @param treeData Original tree structure (must contain complete symbol and key information)
|
||||
* @param options Configuration items
|
||||
*/
|
||||
export function parsePatternToKeys(
|
||||
patternString: string,
|
||||
treeData: SymbolTreeNode[],
|
||||
options: { separator?: string } = {},
|
||||
): string[] {
|
||||
if (!patternString) return [];
|
||||
|
||||
const { separator = '\\' } = options;
|
||||
// 1. Split the string into rule array
|
||||
const rules = patternString.split(',').map((s) => s.trim());
|
||||
|
||||
// 2. Get all leaf nodes (final products), because the selection state finally appears on the leaf nodes
|
||||
const allLeafNodes: SymbolTreeNode[] = [];
|
||||
const collectLeaves = (nodes: SymbolTreeNode[], parentPath: string = '') => {
|
||||
nodes.forEach((node) => {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
node.path = `${parentPath}${separator}${node.key}`;
|
||||
allLeafNodes.push(node);
|
||||
} else {
|
||||
collectLeaves(node.children, node.key);
|
||||
}
|
||||
});
|
||||
};
|
||||
collectLeaves(treeData);
|
||||
|
||||
// 3. Define the matching logic (simulate the background matching process)
|
||||
const isMatched = (
|
||||
node: SymbolTreeNode,
|
||||
rule: string,
|
||||
): { matched: boolean; isExclude: boolean } => {
|
||||
let tempRule = rule;
|
||||
const isExclude = tempRule.startsWith('!');
|
||||
if (isExclude) tempRule = tempRule.substring(1); // Remove !
|
||||
|
||||
let matched = false;
|
||||
|
||||
// Case A: Abbreviated pattern wildcard *BITCOIN
|
||||
if (tempRule.startsWith('*')) {
|
||||
const symbol = tempRule.substring(1);
|
||||
matched = node.value === symbol;
|
||||
}
|
||||
// Case B: Path wildcard forex/general/*
|
||||
else if (tempRule.endsWith('*')) {
|
||||
const prefix = tempRule.substring(0, tempRule.length - 1);
|
||||
// Check if the node's key starts with the prefix path
|
||||
// Note: We need to handle separator consistency
|
||||
const nodePath = (node?.path || node.key).split('/').join(separator);
|
||||
matched = nodePath.startsWith(prefix);
|
||||
}
|
||||
// Case C: Exact path match forex/general/XAUUSD
|
||||
else {
|
||||
const nodePath = (node?.path || node.key).split('/').join(separator);
|
||||
matched = nodePath === tempRule;
|
||||
}
|
||||
|
||||
return { matched, isExclude };
|
||||
};
|
||||
|
||||
// 4. Execute rule matching for each leaf node
|
||||
const selectedKeys: string[] = [];
|
||||
|
||||
for (const node of allLeafNodes) {
|
||||
// Simulate left-to-right matching, stop when hit
|
||||
for (const rule of rules) {
|
||||
const { matched, isExclude } = isMatched(node, rule);
|
||||
if (matched) {
|
||||
if (!isExclude && node.value) {
|
||||
selectedKeys.push(node.value);
|
||||
}
|
||||
// Once a rule is hit (whether it's inclusion or exclusion), immediately break out of the current node's rule loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selectedKeys;
|
||||
}
|
||||
|
||||
export interface AntdTreeNode {
|
||||
title: string;
|
||||
value: string;
|
||||
key?: string;
|
||||
selectable?: boolean;
|
||||
children?: AntdTreeNode[];
|
||||
}
|
||||
const CALC_MODE_MAP: Record<number, string> = {
|
||||
0: 'Forex',
|
||||
1: 'Futures',
|
||||
2: 'CFD',
|
||||
3: 'CFD Index',
|
||||
4: 'CFD Leverage',
|
||||
5: 'Forex No Leverage',
|
||||
32: 'Exchange Stocks',
|
||||
33: 'Exchange Futures',
|
||||
34: 'Exchange FORTS Futures',
|
||||
35: 'Exchange Options',
|
||||
36: 'Exchange Options Margin',
|
||||
37: 'Exchange Bonds',
|
||||
38: 'Exchange MOEX Stocks',
|
||||
39: 'Exchange MOEX Bonds',
|
||||
64: 'Collateral',
|
||||
};
|
||||
|
||||
export function transformToAssetsTree(
|
||||
sourceData: API.SymbolDataItem[],
|
||||
): AntdTreeNode[] {
|
||||
const groupedData: Record<string, Set<string>> = {};
|
||||
|
||||
sourceData.forEach((item) => {
|
||||
const { calcMode, currencyBase, currencyProfit, symbol } = item;
|
||||
|
||||
// Determine the category name, with 0 and 5 both classified as 'Forex'
|
||||
let categoryName = CALC_MODE_MAP[calcMode] || 'Other';
|
||||
if (calcMode === 0 || calcMode === 5) {
|
||||
categoryName = 'Forex';
|
||||
}
|
||||
|
||||
if (!groupedData[categoryName]) {
|
||||
groupedData[categoryName] = new Set<string>();
|
||||
}
|
||||
|
||||
if (calcMode === 0 || calcMode === 5) {
|
||||
if (currencyBase) groupedData[categoryName].add(currencyBase);
|
||||
if (currencyProfit) groupedData[categoryName].add(currencyProfit);
|
||||
} else {
|
||||
// Other cases extract the symbol directly to remove duplicates
|
||||
if (symbol) groupedData[categoryName].add(symbol);
|
||||
}
|
||||
});
|
||||
|
||||
const treeData: AntdTreeNode[] = Object.keys(groupedData).map((category) => {
|
||||
const uniqueValues = Array.from(groupedData[category]);
|
||||
|
||||
return {
|
||||
title: category,
|
||||
value: `GROUP_${category}`,
|
||||
key: `GROUP_${category}`,
|
||||
selectable: false,
|
||||
children: uniqueValues.map((val) => ({
|
||||
title: val,
|
||||
value: val,
|
||||
// key: `${category}_${val}`,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
return treeData;
|
||||
}
|
||||
Reference in New Issue
Block a user