nerdamer-prime
Version:
javascript light-weight symbolic math library
672 lines (565 loc) • 31 kB
text/typescript
// Load complete nerdamer with all modules
import nerdamerRuntime from '../all';
import tsMorph from 'ts-morph';
// Type alias for dynamic property access in reflection tests
type Indexable = Record<string, any>;
const nerdamer = nerdamerRuntime as typeof nerdamerRuntime & Indexable;
describe('Nerdamer TypeScript Interface Reflection', () => {
let project: tsMorph.Project;
let sourceFile: tsMorph.SourceFile;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let typeChecker: tsMorph.TypeChecker;
/**
* Helper to get method-like members from an interface. The index.d.ts uses property signatures with function types
* (e.g., `add: (x: T) => R`) instead of method signatures (e.g., `add(x: T): R`), so we need to check properties
* whose type is a function type.
*/
const getMethodLikeMembers = (iface: tsMorph.InterfaceDeclaration | undefined): string[] => {
if (!iface) {
return [];
}
const methodNames: string[] = [];
// Check actual method signatures
iface.getMethods().forEach(m => {
methodNames.push(m.getName());
});
// Check property signatures with function types
iface.getProperties().forEach(p => {
const typeNode = p.getTypeNode();
if (typeNode?.getKind() === tsMorph.SyntaxKind.FunctionType) {
methodNames.push(p.getName());
}
});
return methodNames;
};
/** Helper to get a method-like member by name (either method signature or property with function type) */
const getMethodLikeMember = (
iface: tsMorph.InterfaceDeclaration | undefined,
name: string
):
| {
getReturnTypeNode: () => tsMorph.TypeNode | undefined;
getParameters: () => { getTypeNode: () => tsMorph.TypeNode | undefined }[];
}
| undefined => {
if (!iface) {
return undefined;
}
// Try method signature first
const method = iface.getMethod(name);
if (method) {
return {
getReturnTypeNode: () => method.getReturnTypeNode(),
getParameters: () => method.getParameters(),
};
}
// Try property with function type
const prop = iface.getProperty(name);
if (prop) {
const typeNode = prop.getTypeNode();
if (typeNode?.getKind() === tsMorph.SyntaxKind.FunctionType) {
const funcType = typeNode as tsMorph.FunctionTypeNode;
return {
getReturnTypeNode: () => funcType.getReturnTypeNode(),
getParameters: () =>
funcType.getParameters().map(p => ({
getTypeNode: () => p.getTypeNode(),
})),
};
}
}
return undefined;
};
beforeAll(() => {
project = new tsMorph.Project({
tsConfigFilePath: 'tsconfig.json',
});
sourceFile = project.getSourceFileOrThrow('index.d.ts');
typeChecker = project.getTypeChecker();
});
describe('Core Type Aliases', () => {
const expectedTypeAliases = [
'OutputType',
'ParseOption',
'ExpressionHistoryIndex',
'int',
'ExpressionParam',
'SolveResult',
];
it('should have all expected type aliases defined', () => {
const typeAliases = sourceFile.getTypeAliases().map((ta: tsMorph.TypeAliasDeclaration) => ta.getName());
for (const expectedAlias of expectedTypeAliases) {
expect(typeAliases).toContain(expectedAlias);
}
});
it('should have OutputType with correct union members', () => {
const outputType = sourceFile.getTypeAlias('OutputType');
expect(outputType).toBeDefined();
const typeNode = outputType?.getTypeNode();
if (typeNode?.getKind() === tsMorph.SyntaxKind.UnionType) {
const unionType = typeNode.asKindOrThrow(tsMorph.SyntaxKind.UnionType);
const members = unionType
.getTypeNodes()
.map((tn: tsMorph.TypeNode) => {
const text = tn.getText();
// Remove quotes from string literals
return text.startsWith("'") && text.endsWith("'") ? text.slice(1, -1) : text;
})
.filter(Boolean);
expect(members).toContain('decimals');
expect(members).toContain('fractions');
expect(members).toContain('scientific');
expect(members).toContain('mixed');
expect(members).toContain('recurring');
}
});
it('should have ExpressionParam with correct union structure', () => {
const expressionParam = sourceFile.getTypeAlias('ExpressionParam');
expect(expressionParam).toBeDefined();
const typeText = expressionParam?.getTypeNode()?.getText();
expect(typeText).toContain('string');
expect(typeText).toContain('number');
expect(typeText).toContain('NerdamerExpression');
expect(typeText).toContain('NerdamerEquation');
});
});
describe('Core Expression Interfaces', () => {
it('should have NerdamerExpression interface with essential output methods', () => {
const nerdamerExpression = sourceFile.getInterface('NerdamerExpression');
expect(nerdamerExpression).toBeDefined();
const methodNames = getMethodLikeMembers(nerdamerExpression);
// These methods were previously in CoreExpressionBase but are now directly on NerdamerExpression
const expectedMethods = ['toString', 'text', 'latex', 'valueOf'];
for (const method of expectedMethods) {
expect(methodNames).toContain(method);
}
});
it('should have NerdamerExpression interface as standalone (not extending CoreExpressionBase)', () => {
const nerdamerExpression = sourceFile.getInterface('NerdamerExpression');
expect(nerdamerExpression).toBeDefined();
const extendsClause = nerdamerExpression?.getExtends();
// NerdamerExpression no longer extends CoreExpressionBase - methods are directly defined
expect(extendsClause).toHaveLength(0);
});
it('should have NerdamerExpression with essential arithmetic methods', () => {
const nerdamerExpression = sourceFile.getInterface('NerdamerExpression');
const methodNames = getMethodLikeMembers(nerdamerExpression);
const arithmeticMethods = ['add', 'subtract', 'multiply', 'divide', 'pow'];
for (const method of arithmeticMethods) {
expect(methodNames).toContain(method);
}
});
it('should have NerdamerExpression with comparison methods', () => {
const nerdamerExpression = sourceFile.getInterface('NerdamerExpression');
const methodNames = getMethodLikeMembers(nerdamerExpression);
const comparisonMethods = ['eq', 'lt', 'gt', 'lte', 'gte'];
for (const method of comparisonMethods) {
expect(methodNames).toContain(method);
}
});
it('should have NerdamerExpression with manipulation methods', () => {
const nerdamerExpression = sourceFile.getInterface('NerdamerExpression');
const methodNames = getMethodLikeMembers(nerdamerExpression);
// Note: 'factor' is a static method, not an instance method
const manipulationMethods = ['expand', 'simplify', 'evaluate', 'sub', 'solveFor'];
for (const method of manipulationMethods) {
expect(methodNames).toContain(method);
}
});
});
describe('JavaScript Runtime vs TypeScript Interface Comparison', () => {
it('should validate NerdamerExpression methods exist on runtime instances', () => {
// Create a runtime expression
const runtimeExpr = nerdamerRuntime('x + 1') as Indexable;
// Get TypeScript interface methods (including property signatures with function types)
const nerdamerExpression = sourceFile.getInterface('NerdamerExpression');
const tsMethodNames = getMethodLikeMembers(nerdamerExpression);
// Test that each TS method exists on the runtime object
const methodTestResults = [];
for (const methodName of tsMethodNames) {
const exists = typeof runtimeExpr[methodName] === 'function';
const actualType = typeof runtimeExpr[methodName];
methodTestResults.push({ method: methodName, exists, type: actualType });
}
const missingMethods = methodTestResults.filter(r => !r.exists);
if (missingMethods.length > 0) {
console.log(
'Missing methods in runtime:',
missingMethods.map(m => m.method)
);
}
// Allow some methods to be missing since some might be inherited or added dynamically
expect(missingMethods.length).toBeLessThan(tsMethodNames.length * 0.3); // Allow up to 30% missing
});
it('should validate runtime expression behavior matches TypeScript expectations', () => {
const runtimeExpr = nerdamerRuntime('x^2 + 2*x + 1');
// Test basic operations that should exist per TypeScript
expect(typeof runtimeExpr.toString).toBe('function');
expect(typeof runtimeExpr.text).toBe('function');
expect(typeof runtimeExpr.evaluate).toBe('function');
expect(typeof runtimeExpr.expand).toBe('function');
expect(typeof runtimeExpr.simplify).toBe('function');
// Test that operations return expected types
const expanded = runtimeExpr.expand();
expect(expanded).toBeDefined();
expect(typeof expanded.toString).toBe('function');
// Test factor as static method (not instance method)
expect(typeof nerdamerRuntime.factor).toBe('function');
const factored = nerdamerRuntime.factor(runtimeExpr);
expect(factored).toBeDefined();
expect(typeof factored.toString).toBe('function');
});
it('should validate arithmetic operations work as TypeScript suggests', () => {
const expr1 = nerdamerRuntime('x + 1');
const expr2 = nerdamerRuntime('y + 2');
// Test that arithmetic methods exist and work
if (typeof expr1.add === 'function') {
const sum = expr1.add(expr2);
expect(sum).toBeDefined();
expect(typeof sum.toString).toBe('function');
}
if (typeof expr1.multiply === 'function') {
const product = expr1.multiply(2);
expect(product).toBeDefined();
expect(typeof product.toString).toBe('function');
}
if (typeof expr1.pow === 'function') {
const power = expr1.pow(2);
expect(power).toBeDefined();
expect(typeof power.toString).toBe('function');
}
});
it('should validate comparison operations work as TypeScript suggests', () => {
const expr1 = nerdamerRuntime('5');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const expr2 = nerdamerRuntime('3');
// Test comparison methods if they exist
if (typeof expr1.eq === 'function') {
const isEqual = expr1.eq(5);
expect(typeof isEqual).toBe('boolean');
}
if (typeof expr1.gt === 'function') {
const isGreater = expr1.gt(3);
expect(typeof isGreater).toBe('boolean');
}
if (typeof expr1.lt === 'function') {
const isLess = expr1.lt(10);
expect(typeof isLess).toBe('boolean');
}
});
it('should validate buildFunction method works as TypeScript suggests', () => {
const expr = nerdamerRuntime('x^2 + y');
if (typeof expr.buildFunction === 'function') {
try {
const fn = expr.buildFunction(['x', 'y']);
expect(typeof fn).toBe('function');
// Test that the generated function works
const result = fn(2, 3);
expect(typeof result).toBe('number');
expect(result).toBe(7); // 2^2 + 3 = 7
} catch (error) {
// If buildFunction fails, just log it but don't fail the test
console.log('buildFunction test failed:', (error as Error).message);
}
}
});
it('should validate equation handling matches TypeScript definitions', () => {
const equation = nerdamerRuntime('x^2 = 4') as Indexable;
// Check if it's recognized as an equation (has LHS and RHS properties)
if (equation['LHS'] && equation['RHS']) {
expect(typeof equation['LHS'].toString).toBe('function');
expect(typeof equation['RHS'].toString).toBe('function');
if (typeof equation['toLHS'] === 'function') {
const lhsForm = equation['toLHS']();
expect(lhsForm).toBeDefined();
expect(typeof lhsForm.toString).toBe('function');
}
}
// Test solve functionality if available
if (typeof equation['solveFor'] === 'function') {
try {
const solutions = equation['solveFor']('x');
expect(Array.isArray(solutions)).toBe(true);
} catch (error) {
// Some equations might not be solvable, that's ok
console.log('Equation solving test failed (expected for some cases):', (error as Error).message);
}
}
});
});
describe('NerdamerCore Namespace Interfaces', () => {
let nerdamerCoreNamespace: tsMorph.ModuleDeclaration | undefined;
beforeAll(() => {
const primeNamespace = sourceFile.getModules().find(m => m.getName() === 'nerdamerPrime');
nerdamerCoreNamespace = primeNamespace?.getModules().find(m => m.getName() === 'NerdamerCore');
expect(nerdamerCoreNamespace).toBeDefined();
});
it('should have core data structure interfaces', () => {
// These are the main data structure interfaces (not constructors)
const expectedInterfaces = ['Frac', 'NerdamerSymbol', 'Vector', 'Matrix', 'NerdamerSet', 'Collection'];
for (const ifaceName of expectedInterfaces) {
const iface = nerdamerCoreNamespace?.getInterface(ifaceName);
expect(iface).toBeDefined();
}
});
it('should have Core interface with essential properties', () => {
const coreInterface = nerdamerCoreNamespace?.getInterface('Core');
expect(coreInterface).toBeDefined();
// Core should have essential types like PARSER, NerdamerSymbol constructor, etc.
const memberNames =
coreInterface?.getMembers().map(m => {
// TypeElementTypes can be PropertySignature, MethodSignature, etc.
// Check if it has getName method (most do)
if ('getName' in m && typeof m.getName === 'function') {
return m.getName();
}
return undefined;
}) || [];
expect(memberNames.length).toBeGreaterThan(0);
});
it('should validate Vector/Matrix are properly wrapped Expressions', () => {
// Test vector creation and base expression functionality
if (typeof nerdamerRuntime.vector === 'function') {
try {
const vec = nerdamerRuntime.vector([1, 2, 3]) as Indexable;
expect(vec).toBeDefined();
// Test that vector IS an expression (has base expression methods)
const baseExpressionMethods = [
'toString',
'text',
'evaluate',
'add',
'multiply',
'expand',
// Note: 'clone' removed due to Expression prototype inconsistency
];
const existingBaseMethods = baseExpressionMethods.filter(
(method: string) => typeof vec[method] === 'function'
);
// Vector should have most base expression methods since it extends NerdamerExpression
expect(existingBaseMethods.length).toBeGreaterThan(4);
// Test vector-specific properties or behavior (if any)
// Vectors use static functions like nerdamer.vecget(), not instance methods
expect(typeof nerdamerRuntime.vecget).toBe('function');
expect(typeof nerdamerRuntime.dot).toBe('function');
expect(typeof nerdamerRuntime.cross).toBe('function');
} catch (error) {
console.log('Vector test failed:', (error as Error).message);
}
}
// Test matrix creation and base expression functionality
if (typeof nerdamerRuntime.matrix === 'function') {
try {
const mat = nerdamerRuntime.matrix([
[1, 2],
[3, 4],
]) as Indexable;
expect(mat).toBeDefined();
// Test that matrix IS an expression (has base expression methods)
const baseExpressionMethods = [
'toString',
'text',
'evaluate',
'add',
'multiply',
'expand',
// Note: 'clone' removed due to Expression prototype inconsistency
];
const existingBaseMethods = baseExpressionMethods.filter(
(method: string) => typeof mat[method] === 'function'
);
// Matrix should have most base expression methods since it extends NerdamerExpression
expect(existingBaseMethods.length).toBeGreaterThan(4);
// Test matrix-specific properties or behavior (if any)
// Matrices use static functions like nerdamer.matget(), not instance methods
expect(typeof nerdamerRuntime.matget).toBe('function');
expect(typeof nerdamerRuntime.determinant).toBe('function');
expect(typeof nerdamerRuntime.transpose).toBe('function');
} catch (error) {
console.log('Matrix test failed:', (error as Error).message);
}
}
});
});
describe('Main Function Interface vs Runtime', () => {
it('should have overloaded nerdamer function declarations matching runtime behavior', () => {
const functionDeclarations = sourceFile.getFunctions();
expect(functionDeclarations.length).toBeGreaterThan(0);
// Test that runtime nerdamer function works with different parameter types
expect(typeof nerdamerRuntime).toBe('function');
// Test string input
const expr1 = nerdamerRuntime('x + 1');
expect(expr1).toBeDefined();
// Test with substitutions
const expr2 = nerdamerRuntime('x + 1', { x: 2 });
expect(expr2).toBeDefined();
// Test with options
const expr3 = nerdamerRuntime('x^2 + 2*x + 1', {}, 'expand');
expect(expr3).toBeDefined();
});
it('should validate static methods exist on runtime nerdamer function', () => {
// Get all static methods from TypeScript namespace
const nerdamerNamespace = sourceFile.getModules().find(m => m.getName() === 'nerdamer');
// Get export declarations and manually filter to find import equals declarations
const exportDeclarations = nerdamerNamespace?.getExportedDeclarations();
const importDeclarations: tsMorph.ImportEqualsDeclaration[] = [];
if (exportDeclarations) {
for (const [_key, declarations] of exportDeclarations) {
for (const decl of declarations) {
if (decl.getKind?.() === tsMorph.SyntaxKind.ImportEqualsDeclaration) {
importDeclarations.push(decl as unknown as tsMorph.ImportEqualsDeclaration);
}
}
}
}
// Extract method names from import declarations
const staticMethods = importDeclarations.map((decl: tsMorph.ImportEqualsDeclaration) => decl.getName());
// Test that at least some of these exist on the runtime
const existingStaticMethods = staticMethods.filter(
(method: string) => typeof nerdamer[method] !== 'undefined'
);
console.log(
`Static methods in TS: ${staticMethods.length}, Existing in runtime: ${existingStaticMethods.length}`
);
// Should have a good percentage of the TypeScript static methods
expect(existingStaticMethods.length).toBeGreaterThan(staticMethods.length * 0.5);
// Test some essential ones specifically
const essentialMethods = ['expand', 'factor', 'simplify', 'solve', 'diff', 'integrate', 'cos', 'sin'];
for (const method of essentialMethods) {
if (staticMethods.includes(method)) {
expect(nerdamer[method]).toBeDefined();
expect(typeof nerdamer[method]).toBe('function');
}
}
});
});
describe('Interface Consistency', () => {
it('should have consistent return types for arithmetic operations', () => {
const nerdamerExpression = sourceFile.getInterface('NerdamerExpression');
const arithmeticMethods = ['add', 'subtract', 'multiply', 'divide', 'pow'];
for (const methodName of arithmeticMethods) {
const method = getMethodLikeMember(nerdamerExpression, methodName);
const returnType = method?.getReturnTypeNode()?.getText();
expect(returnType).toBe('NerdamerExpression');
}
});
it('should have consistent parameter types for arithmetic operations', () => {
const nerdamerExpression = sourceFile.getInterface('NerdamerExpression');
const arithmeticMethods = ['add', 'subtract', 'multiply', 'divide', 'pow'];
for (const methodName of arithmeticMethods) {
const method = getMethodLikeMember(nerdamerExpression, methodName);
const parameters = method?.getParameters();
expect(parameters?.length).toBe(1);
const paramType = parameters?.[0]?.getTypeNode()?.getText();
expect(paramType).toBe('ExpressionParam');
}
});
it('should have consistent boolean return types for comparison operations', () => {
const nerdamerExpression = sourceFile.getInterface('NerdamerExpression');
const comparisonMethods = ['eq', 'lt', 'gt', 'lte', 'gte'];
for (const methodName of comparisonMethods) {
const method = getMethodLikeMember(nerdamerExpression, methodName);
const returnType = method?.getReturnTypeNode()?.getText();
expect(returnType).toBe('boolean');
}
});
});
describe('Runtime to TypeScript Coverage Analysis', () => {
it('should analyze runtime expression prototype vs TypeScript interface coverage', () => {
const expr = nerdamerRuntime('x + 1');
// Get all methods including inherited ones by walking the prototype chain
const getAllMethods = (obj: unknown): string[] => {
const methods = new Set<string>();
let current = obj;
while (current && current !== Object.prototype) {
Object.getOwnPropertyNames(current).forEach(name => {
if (typeof (obj as Record<string, unknown>)[name] === 'function') {
methods.add(name);
}
});
current = Object.getPrototypeOf(current);
}
return Array.from(methods).sort();
};
const runtimeMethods = getAllMethods(expr);
// Get TypeScript methods from both NerdamerExpression and CoreExpressionBase
const nerdamerExpression = sourceFile.getInterface('NerdamerExpression');
const coreExpressionBase = sourceFile.getInterface('CoreExpressionBase');
const nerdamerMethods = getMethodLikeMembers(nerdamerExpression);
const coreMethods = getMethodLikeMembers(coreExpressionBase);
const allTsMethods = [...new Set([...nerdamerMethods, ...coreMethods])].sort();
const missingInTs = runtimeMethods.filter(method => !allTsMethods.includes(method));
const missingInRuntime = allTsMethods.filter((method: string) => !runtimeMethods.includes(method));
console.log('\n=== Expression Method Coverage Analysis ===');
console.log(`Runtime methods: ${runtimeMethods.length}`);
console.log(`TypeScript methods: ${allTsMethods.length}`);
console.log(`Missing in TypeScript (first 10):`, missingInTs.slice(0, 10));
console.log(`Missing in Runtime (first 10):`, missingInRuntime.slice(0, 10));
// Calculate coverage
const coverage = (allTsMethods.length - missingInRuntime.length) / allTsMethods.length;
console.log(`TypeScript to Runtime coverage: ${Math.round(coverage * 100)}%`);
// Expect high coverage since we now include inherited methods
expect(coverage).toBeGreaterThan(0.8); // At least 80% coverage
});
it('should analyze static function coverage', () => {
const runtimeStaticMethods = Object.getOwnPropertyNames(nerdamerRuntime)
.filter((name: string) => typeof nerdamer[name] === 'function')
.sort();
const nerdamerNamespace = sourceFile.getModules().find(m => m.getName() === 'nerdamer');
// Get export declarations and manually filter to find import equals declarations
const exportDeclarations = nerdamerNamespace?.getExportedDeclarations();
const importEqualsDeclarations: tsMorph.ImportEqualsDeclaration[] = [];
if (exportDeclarations) {
for (const [_key, declarations] of exportDeclarations) {
for (const decl of declarations) {
if (decl.getKind?.() === tsMorph.SyntaxKind.ImportEqualsDeclaration) {
importEqualsDeclarations.push(decl as unknown as tsMorph.ImportEqualsDeclaration);
}
}
}
}
const tsStaticMethods = importEqualsDeclarations
.map((decl: tsMorph.ImportEqualsDeclaration) => decl.getName())
// Filter out TypeScript-only constructs like 'NerdamerCore'
.filter((name: string) => name !== 'NerdamerCore')
.sort();
const missingInTs = runtimeStaticMethods.filter(method => !tsStaticMethods.includes(method));
const missingInRuntime = tsStaticMethods.filter((method: string) => !runtimeStaticMethods.includes(method));
console.log('\n=== Static Method Coverage Analysis ===');
console.log(`Runtime static methods: ${runtimeStaticMethods.length}`);
console.log(`TypeScript static methods: ${tsStaticMethods.length}`);
console.log(`Missing in TypeScript (first 10):`, missingInTs.slice(0, 10));
console.log(`Missing in Runtime (first 10):`, missingInRuntime.slice(0, 10));
// Calculate coverage
const coverage = (tsStaticMethods.length - missingInRuntime.length) / tsStaticMethods.length;
console.log(`TypeScript to Runtime static coverage: ${Math.round(coverage * 100)}%`);
// Expect very high coverage since we exclude TypeScript-only constructs
expect(coverage).toBeGreaterThan(0.95); // At least 95% coverage for static methods
});
});
describe('Completeness Check', () => {
it('should have a reasonable number of interfaces defined', () => {
const allInterfaces = sourceFile.getInterfaces();
const primeNamespace = sourceFile.getModules().find(m => m.getName() === 'nerdamerPrime');
const nerdamerCoreInterfaces =
primeNamespace
?.getModules()
.find(m => m.getName() === 'NerdamerCore')
?.getInterfaces() || [];
const totalInterfaces = allInterfaces.length + nerdamerCoreInterfaces.length;
// Should have a substantial number of interfaces for a comprehensive math library
expect(totalInterfaces).toBeGreaterThan(20);
});
it('should have proper namespace organization', () => {
// Main namespace should exist
const nerdamerNamespace = sourceFile.getModules().find(m => m.getName() === 'nerdamer');
expect(nerdamerNamespace).toBeDefined();
// NerdamerPrime namespace should exist
const primeNamespace = sourceFile.getModules().find(m => m.getName() === 'nerdamerPrime');
expect(primeNamespace).toBeDefined();
// NerdamerCore namespace should exist inside nerdamerPrime
const coreNamespace = primeNamespace?.getModules().find(m => m.getName() === 'NerdamerCore');
expect(coreNamespace).toBeDefined();
});
});
});