diff --git a/rsconcept/frontend/src/models/TMGraph.test.ts b/rsconcept/frontend/src/models/TMGraph.test.ts new file mode 100644 index 00000000..d6ab181f --- /dev/null +++ b/rsconcept/frontend/src/models/TMGraph.test.ts @@ -0,0 +1,72 @@ +import { TMGraph } from './TMGraph'; + +const typificationData = [ + ['', ''], + ['X1', 'X1'], + ['Z', 'Z'], + ['R1', 'R1'], + ['C1', 'C1'], + ['C1×X1', 'C1 X1 C1×X1'], + ['X1×X1', 'X1 X1×X1'], + ['X1×X1×X1', 'X1 X1×X1×X1'], + ['ℬ(X1)', 'X1 ℬ(X1)'], + ['ℬℬ(X1)', 'X1 ℬ(X1) ℬℬ(X1)'], + ['ℬℬ(X1×X2)', 'X1 X2 X1×X2 ℬ(X1×X2) ℬℬ(X1×X2)'], + ['ℬ((X1×X1)×X2)', 'X1 X1×X1 X2 (X1×X1)×X2 ℬ((X1×X1)×X2)'], + ['ℬ(ℬ(X1)×ℬ(X1))', 'X1 ℬ(X1) ℬ(X1)×ℬ(X1) ℬ(ℬ(X1)×ℬ(X1))'], + [ + 'ℬ(ℬ((X1×ℬ(X1))×X1)×X2)', + 'X1 ℬ(X1) X1×ℬ(X1) (X1×ℬ(X1))×X1 ℬ((X1×ℬ(X1))×X1) X2 ℬ((X1×ℬ(X1))×X1)×X2 ℬ(ℬ((X1×ℬ(X1))×X1)×X2)' + ] +]; +describe('Testing parsing typifications', () => { + it.each(typificationData)('Typification parsing %p', (input: string, expected: string) => { + const graph = new TMGraph(); + graph.addConstituenta('X1', input, []); + const nodeText = graph.nodes.map(node => node.text).join(' '); + expect(nodeText).toBe(expected); + }); +}); + +describe('Testing constituents parsing', () => { + test('simple expression no arguments', () => { + const graph = new TMGraph(); + graph.addConstituenta('X1', 'ℬ(X1)', []); + + expect(graph.nodes.length).toBe(2); + expect(graph.nodes.at(-1)?.annotations).toStrictEqual(['X1']); + }); + + test('no expression with single argument', () => { + const graph = new TMGraph(); + graph.addConstituenta('X1', '', [{ alias: 'a', typification: 'X1' }]); + const nodeText = graph.nodes.map(node => node.text).join(' '); + + expect(nodeText).toBe('X1 ℬ(X1)'); + expect(graph.nodes.at(-1)?.annotations).toStrictEqual(['X1']); + }); + + test('no expression with multiple arguments', () => { + const graph = new TMGraph(); + graph.addConstituenta('X1', '', [ + { alias: 'a', typification: 'X1' }, + { alias: 'b', typification: 'R1×X1' } + ]); + const nodeText = graph.nodes.map(node => node.text).join(' '); + + expect(nodeText).toBe('X1 R1 R1×X1 X1×(R1×X1) ℬ(X1×(R1×X1))'); + expect(graph.nodes.at(-1)?.annotations).toStrictEqual(['X1']); + }); + + test('expression with multiple arguments', () => { + const graph = new TMGraph(); + graph.addConstituenta('X1', 'ℬ(X2×Z)', [ + { alias: 'a', typification: 'X1' }, + { alias: 'b', typification: 'R1×X1' } + ]); + const nodeText = graph.nodes.map(node => node.text).join(' '); + + expect(nodeText).toBe('X1 R1 R1×X1 X1×(R1×X1) X2 Z X2×Z ℬ(X2×Z) (X1×(R1×X1))×ℬ(X2×Z) ℬ((X1×(R1×X1))×ℬ(X2×Z))'); + expect(graph.nodes.at(-1)?.annotations).toStrictEqual(['X1']); + }); +}); diff --git a/rsconcept/frontend/src/models/TMGraph.ts b/rsconcept/frontend/src/models/TMGraph.ts new file mode 100644 index 00000000..b14a1265 --- /dev/null +++ b/rsconcept/frontend/src/models/TMGraph.ts @@ -0,0 +1,213 @@ +/** + * Module: Multi-graph for typifications. + */ + +import { IArgumentInfo } from './rslang'; + +/** + * Represents a single node of a {@link TMGraph}. + */ +export interface TMGraphNode { + id: number; + rank: number; + text: string; + parents: number[]; + annotations: string[]; +} + +/** + * Represents a typification multi-graph. + */ +export class TMGraph { + /** List of nodes. */ + nodes: TMGraphNode[] = []; + /** Map of nodes by ID. */ + nodeById = new Map(); + /** Map of nodes by alias. */ + nodeByAlias = new Map(); + + /** + * Adds a constituent to the graph. + * + * @param alias - The alias of the constituent. + * @param result - typification of the formal definition. + * @param args - arguments for term or predicate function. + */ + addConstituenta(alias: string, result: string, args: IArgumentInfo[]): void { + const argsNode = this.processArguments(args); + const resultNode = this.processResult(result); + const combinedNode = this.combineResults(resultNode, argsNode); + if (!combinedNode) { + return; + } + this.addAliasAnnotation(combinedNode.id, alias); + } + + addBaseNode(baseAlias: string): TMGraphNode { + const existingNode = this.nodes.find(node => node.text === baseAlias); + if (existingNode) { + return existingNode; + } + + const node: TMGraphNode = { + id: this.nodes.length, + text: baseAlias, + rank: 0, + parents: [], + annotations: [] + }; + this.nodes.push(node); + this.nodeById.set(node.id, node); + return node; + } + + addBooleanNode(parent: number): TMGraphNode { + const existingNode = this.nodes.find(node => node.parents.length === 1 && node.parents[0] === parent); + if (existingNode) { + return existingNode; + } + + const parentNode = this.nodeById.get(parent); + if (!parentNode) { + throw new Error(`Parent node ${parent} not found`); + } + + const text = parentNode.parents.length === 1 ? `ℬ${parentNode.text}` : `ℬ(${parentNode.text})`; + const node: TMGraphNode = { + id: this.nodes.length, + rank: parentNode.rank, + text: text, + parents: [parent], + annotations: [] + }; + this.nodes.push(node); + this.nodeById.set(node.id, node); + return node; + } + + addCartesianNode(parents: number[]): TMGraphNode { + const existingNode = this.nodes.find( + node => node.parents.length === parents.length && node.parents.every((p, i) => p === parents[i]) + ); + if (existingNode) { + return existingNode; + } + + const parentNodes = parents.map(parent => this.nodeById.get(parent)); + if (parentNodes.some(parent => !parent) || parents.length < 2) { + throw new Error(`Parent nodes ${parents.join(', ')} not found`); + } + + const text = parentNodes.map(node => (node!.parents.length > 1 ? `(${node!.text})` : node!.text)).join('×'); + const node: TMGraphNode = { + id: this.nodes.length, + text: text, + rank: Math.max(...parentNodes.map(parent => parent!.rank)) + 1, + parents: parents, + annotations: [] + }; + this.nodes.push(node); + this.nodeById.set(node.id, node); + return node; + } + + addAliasAnnotation(node: number, alias: string): void { + const nodeToAnnotate = this.nodeById.get(node); + if (!nodeToAnnotate) { + throw new Error(`Node ${node} not found`); + } + nodeToAnnotate.annotations.push(alias); + this.nodeByAlias.set(alias, nodeToAnnotate); + } + + private processArguments(args: IArgumentInfo[]): TMGraphNode | undefined { + if (args.length === 0) { + return undefined; + } + const argsNodes = args.map(argument => this.parseToNode(argument.typification)); + if (args.length === 1) { + return argsNodes[0]; + } + return this.addCartesianNode(argsNodes.map(node => node.id)); + } + + private processResult(result: string): TMGraphNode | undefined { + if (!result) { + return undefined; + } + return this.parseToNode(result); + } + + private combineResults(result: TMGraphNode | undefined, args: TMGraphNode | undefined): TMGraphNode | undefined { + if (!result && !args) { + return undefined; + } + if (!result) { + return this.addBooleanNode(args!.id); + } + if (!args) { + return result; + } + const argsAndResult = this.addCartesianNode([args.id, result.id]); + return this.addBooleanNode(argsAndResult.id); + } + + private parseToNode(typification: string): TMGraphNode { + const tokens = this.tokenize(typification); + return this.parseTokens(tokens); + } + + private tokenize(expression: string): string[] { + const tokens = []; + let currentToken = ''; + for (const char of expression) { + if (['(', ')', '×', 'ℬ'].includes(char)) { + if (currentToken) { + tokens.push(currentToken); + currentToken = ''; + } + tokens.push(char); + } else { + currentToken += char; + } + } + if (currentToken) { + tokens.push(currentToken); + } + return tokens; + } + + private parseTokens(tokens: string[], isBoolean: boolean = false): TMGraphNode { + const stack: TMGraphNode[] = []; + let isCartesian = false; + while (tokens.length > 0) { + const token = tokens.shift(); + if (!token) { + throw new Error('Unexpected end of expression'); + } + + if (isBoolean && token === '(') { + return this.parseTokens(tokens); + } + + if (token === ')') { + break; + } else if (token === 'ℬ') { + const innerNode = this.parseTokens(tokens, true); + stack.push(this.addBooleanNode(innerNode.id)); + } else if (token === '×') { + isCartesian = true; + } else if (token === '(') { + stack.push(this.parseTokens(tokens)); + } else { + stack.push(this.addBaseNode(token)); + } + } + + if (isCartesian) { + return this.addCartesianNode(stack.map(node => node.id)); + } else { + return stack.pop()!; + } + } +}