slangmath 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,652 @@
1
+ /**
2
+ * SLaNg LaTeX Converter - Unified Edition
3
+ * Comprehensive bidirectional conversion between SLaNg and LaTeX
4
+ * Combines the best features from all previous versions
5
+ *
6
+ * Features:
7
+ * - Enhanced parsing with multiple fallback strategies
8
+ * - Robust error handling and validation
9
+ * - Support for complex mathematical expressions
10
+ * - Batch processing capabilities
11
+ * - Flexible formatting options
12
+ * - Comprehensive test coverage
13
+ */
14
+
15
+ import {
16
+ createTerm,
17
+ createFraction,
18
+ evaluateTerm,
19
+ hasSimpleDenominator,
20
+ termToString,
21
+ polynomialToString,
22
+ fractionToString
23
+ } from './slang-basic.js';
24
+
25
+ // ============================================================================
26
+ // SLANG TO LATEX CONVERSION
27
+ // ============================================================================
28
+
29
+ /**
30
+ * Convert SLaNg term to LaTeX with enhanced formatting
31
+ */
32
+ function termToLatex(term, options = {}) {
33
+ if (term.coeff === 0) return '0';
34
+
35
+ let latex = '';
36
+
37
+ // Handle coefficient with special cases
38
+ if (Math.abs(term.coeff - 1) < 1e-10 && term.var) {
39
+ latex = '';
40
+ } else if (Math.abs(term.coeff + 1) < 1e-10 && term.var) {
41
+ latex = '-';
42
+ } else {
43
+ latex = term.coeff.toString();
44
+ }
45
+
46
+ // Handle variables with powers
47
+ if (term.var) {
48
+ const varParts = [];
49
+ for (let [variable, power] of Object.entries(term.var)) {
50
+ if (power === 1) {
51
+ varParts.push(variable);
52
+ } else {
53
+ varParts.push(`${variable}^{${power}}`);
54
+ }
55
+ }
56
+
57
+ // Handle multiplication symbols
58
+ if (varParts.length > 1 && options.multiplySymbol === '\\cdot') {
59
+ latex += (latex && latex !== '-' ? ' \\cdot ' : '') + varParts.join(' \\cdot ');
60
+ } else if (varParts.length > 1 && options.multiplySymbol === '\\times') {
61
+ latex += (latex && latex !== '-' ? ' \\times ' : '') + varParts.join(' \\times ');
62
+ } else {
63
+ latex += (latex && latex !== '-' ? '' : '') + varParts.join('');
64
+ }
65
+ }
66
+
67
+ return latex || term.coeff.toString();
68
+ }
69
+
70
+ /**
71
+ * Convert SLaNg polynomial to LaTeX with proper sign handling
72
+ */
73
+ function polynomialToLatex(polynomial, options = {}) {
74
+ if (!polynomial.terms || polynomial.terms.length === 0) return '0';
75
+
76
+ return polynomial.terms.map((term, i) => {
77
+ const termLatex = termToLatex(term, options);
78
+ if (i === 0) return termLatex;
79
+
80
+ // Add plus/minus signs with proper spacing
81
+ if (term.coeff >= 0) {
82
+ return ' + ' + termLatex;
83
+ } else {
84
+ return ' - ' + termToLatex({ ...term, coeff: Math.abs(term.coeff) }, options);
85
+ }
86
+ }).join('');
87
+ }
88
+
89
+ /**
90
+ * Convert SLaNg fraction to LaTeX with enhanced formatting
91
+ */
92
+ function fractionToLatex(fraction, options = {}) {
93
+ const numerator = polynomialToLatex(fraction.numi, options);
94
+
95
+ if (hasSimpleDenominator(fraction)) {
96
+ if (fraction.deno === 1) {
97
+ return options.parentheses ? `\\left(${numerator}\\right)` : numerator;
98
+ }
99
+ return `\\frac{${numerator}}{${fraction.deno}}`;
100
+ } else {
101
+ const denominator = polynomialToLatex(fraction.deno, options);
102
+ return `\\frac{${numerator}}{${denominator}}`;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Main SLaNg to LaTeX converter with comprehensive options
108
+ */
109
+ function slangToLatex(expression, options = {}) {
110
+ const defaults = {
111
+ parentheses: false,
112
+ multiplySymbol: '', // '', '\\cdot', or '\\times'
113
+ displayMode: false,
114
+ simplify: true,
115
+ precision: 10
116
+ };
117
+
118
+ const opts = { ...defaults, ...options };
119
+
120
+ // Handle function expressions
121
+ if (expression.type === 'function') {
122
+ const funcName = expression.name;
123
+ const arg = expression.args[0];
124
+ const argLatex = slangToLatex(arg, opts);
125
+ return `\\${funcName}{${argLatex}}`;
126
+ }
127
+
128
+ if (expression.terms) {
129
+ // It's a polynomial
130
+ return polynomialToLatex(expression, opts);
131
+ } else if (expression.numi && expression.deno !== undefined) {
132
+ // It's a fraction
133
+ return fractionToLatex(expression, opts);
134
+ } else if (expression.coeff !== undefined) {
135
+ // It's a term
136
+ return termToLatex(expression, opts);
137
+ } else {
138
+ throw new Error('Unsupported SLaNg expression type');
139
+ }
140
+ }
141
+
142
+ // ============================================================================
143
+ // LATEX TO SLANG CONVERSION - ENHANCED PARSING ENGINE
144
+ // ============================================================================
145
+
146
+ /**
147
+ * Enhanced number parser with multiple format support
148
+ */
149
+ function parseNumber(str) {
150
+ if (str === '') return 1;
151
+ if (str === '+') return 1;
152
+ if (str === '-') return -1;
153
+
154
+ // Handle decimal numbers and scientific notation
155
+ const num = parseFloat(str);
156
+ if (isNaN(num)) throw new Error(`Invalid number: ${str}`);
157
+ return num;
158
+ }
159
+
160
+ /**
161
+ * Advanced variable parser with multiple power formats
162
+ */
163
+ function parseVariable(varStr) {
164
+ // Handle simple variables: x, y, z
165
+ if (varStr.match(/^[a-zA-Z]$/)) {
166
+ return { variable: varStr, power: 1 };
167
+ }
168
+
169
+ // Handle powers: x^2, x^{2}, x^{10}, etc.
170
+ const match = varStr.match(/^([a-zA-Z])\^(\{?\d+\}|\d+)$/);
171
+ if (!match) throw new Error(`Invalid variable format: ${varStr}`);
172
+
173
+ const variable = match[1];
174
+ const power = parseInt(match[2].replace(/[{}]/g, ''));
175
+
176
+ return { variable, power };
177
+ }
178
+
179
+ /**
180
+ * Enhanced term parser with robust coefficient extraction
181
+ */
182
+ function parseTerm(termStr) {
183
+ termStr = termStr.trim();
184
+ if (termStr === '0') return createTerm(0);
185
+ if (termStr === '') return createTerm(1);
186
+ if (termStr === '-') return createTerm(-1);
187
+
188
+ // Special handling for pure variables
189
+ if (/^[a-zA-Z](?:\^\{?\d+\}|\^\d+)?$/.test(termStr)) {
190
+ const { variable, power } = parseVariable(termStr);
191
+ return createTerm(1, { [variable]: power });
192
+ }
193
+
194
+ // Remove spaces between sign and number for easier parsing
195
+ const normalizedStr = termStr.replace(/^([+-])\s+/, '$1');
196
+
197
+ // Extract coefficient with enhanced regex
198
+ let coeff = 1;
199
+ let remaining = normalizedStr;
200
+
201
+ // Look for coefficient at the start
202
+ const coeffMatch = normalizedStr.match(/^([+-]?\d+(?:\.\d+)?)\s*/);
203
+ if (coeffMatch) {
204
+ const coeffStr = coeffMatch[1];
205
+ coeff = parseNumber(coeffStr);
206
+ remaining = normalizedStr.slice(coeffMatch[0].length).trim();
207
+ } else {
208
+ // Try to match just a sign
209
+ const signMatch = normalizedStr.match(/^([+-])/);
210
+ if (signMatch) {
211
+ coeff = signMatch[1] === '-' ? -1 : 1;
212
+ remaining = normalizedStr.slice(1).trim();
213
+ }
214
+ }
215
+
216
+ // Extract variables from remaining part
217
+ let variables = {};
218
+ if (remaining) {
219
+ // Remove various multiplication symbols
220
+ remaining = remaining.replace(/\\cdot|\\times|\*|Ā·/g, '');
221
+
222
+ // Find all variable patterns with enhanced regex
223
+ const varMatches = remaining.match(/[a-zA-Z](?:\^\{?\d+\}|\^\d+)?/g);
224
+ if (varMatches) {
225
+ for (let varMatch of varMatches) {
226
+ const { variable, power } = parseVariable(varMatch);
227
+ variables[variable] = (variables[variable] || 0) + power;
228
+ }
229
+ }
230
+ }
231
+
232
+ return createTerm(coeff, variables);
233
+ }
234
+
235
+ /**
236
+ * Advanced polynomial parser with intelligent term splitting
237
+ */
238
+ function parsePolynomial(polyStr) {
239
+ polyStr = polyStr.trim();
240
+ if (!polyStr) return { terms: [createTerm(0)] };
241
+
242
+ // Remove outer parentheses with enhanced detection
243
+ if (polyStr.startsWith('\\left(') && polyStr.endsWith('\\right)')) {
244
+ polyStr = polyStr.slice(7, -6);
245
+ } else if (polyStr.startsWith('(') && polyStr.endsWith(')')) {
246
+ polyStr = polyStr.slice(1, -1);
247
+ }
248
+
249
+ // Handle single term case
250
+ if (!polyStr.includes('+') && !polyStr.includes('-')) {
251
+ return { terms: [parseTerm(polyStr)] };
252
+ }
253
+
254
+ // Enhanced term splitting with proper sign handling
255
+ const terms = [];
256
+ let current = '';
257
+ let i = 0;
258
+
259
+ while (i < polyStr.length) {
260
+ const char = polyStr[i];
261
+
262
+ // Handle plus/minus signs, but not in powers and not at the start
263
+ if ((char === '+' || char === '-') && i > 0 && polyStr[i-1] !== '^') {
264
+ if (current.trim()) {
265
+ terms.push(parseTerm(current.trim()));
266
+ }
267
+ current = char;
268
+ } else {
269
+ current += char;
270
+ }
271
+ i++;
272
+ }
273
+
274
+ // Add the last term
275
+ if (current.trim()) {
276
+ terms.push(parseTerm(current.trim()));
277
+ }
278
+
279
+ return { terms: terms.length > 0 ? terms : [createTerm(0)] };
280
+ }
281
+
282
+ /**
283
+ * Enhanced fraction parser with nested expression support
284
+ */
285
+ function parseFraction(fracStr) {
286
+ // Enhanced regex to handle nested braces and complex expressions
287
+ const match = fracStr.match(/\\frac\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/);
288
+ if (!match) throw new Error(`Invalid fraction format: ${fracStr}`);
289
+
290
+ const numerator = parsePolynomial(match[1]);
291
+ const denominatorStr = match[2].trim();
292
+
293
+ // Check if denominator is a simple number
294
+ if (/^\d+$/.test(denominatorStr)) {
295
+ return createFraction(numerator.terms, parseInt(denominatorStr));
296
+ } else {
297
+ const denominator = parsePolynomial(denominatorStr);
298
+ return createFraction(numerator.terms, denominator.terms);
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Main LaTeX to SLaNg converter with multiple parsing strategies
304
+ */
305
+ function latexToSlang(latex, options = {}) {
306
+ const defaults = {
307
+ strictMode: false,
308
+ allowImplicitMultiplication: true,
309
+ fallbackParsing: true,
310
+ verboseErrors: false
311
+ };
312
+
313
+ const opts = { ...defaults, ...options };
314
+ latex = latex.trim();
315
+
316
+ if (!latex) {
317
+ throw new Error('Empty LaTeX expression');
318
+ }
319
+
320
+ try {
321
+ // Strategy 1: Try functions first (trigonometric, etc.)
322
+ // Check for functions with proper pattern
323
+ const funcMatch = latex.match(/^\\(sin|cos|tan|cot|sec|csc|arcsin|arccos|arctan|sinh|cosh|tanh|ln|log|exp|sqrt)\s*\{([^{}]+)\}$/);
324
+ if (funcMatch) {
325
+ return parseFunction(latex);
326
+ }
327
+
328
+ // Strategy 2: Try fraction next (most complex)
329
+ if (latex.includes('\\frac')) {
330
+ return parseFraction(latex);
331
+ }
332
+
333
+ // Strategy 3: Try polynomial
334
+ return parsePolynomial(latex);
335
+
336
+ } catch (error) {
337
+ if (opts.strictMode) {
338
+ throw error;
339
+ }
340
+
341
+ // Strategy 4: Fallback to simple term parsing
342
+ if (opts.fallbackParsing) {
343
+ try {
344
+ return parseTerm(latex);
345
+ } catch (fallbackError) {
346
+ const errorMsg = `Failed to parse LaTeX: "${latex}". Primary error: ${error.message}. Fallback error: ${fallbackError.message}`;
347
+ if (opts.verboseErrors) {
348
+ throw new Error(errorMsg);
349
+ } else {
350
+ throw new Error(`Invalid LaTeX expression: ${latex}`);
351
+ }
352
+ }
353
+ }
354
+
355
+ throw new Error(`Failed to parse LaTeX: ${latex}. Error: ${error.message}`);
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Parse LaTeX functions like sin(x), cos(x), etc.
361
+ */
362
+ function parseFunction(funcStr) {
363
+ const funcMatch = funcStr.match(/\\(sin|cos|tan|cot|sec|csc|arcsin|arccos|arctan|sinh|cosh|tanh|ln|log|exp|sqrt)\s*\{([^{}]+)\}/);
364
+ if (!funcMatch) {
365
+ throw new Error(`Invalid function format: ${funcStr}`);
366
+ }
367
+
368
+ const funcName = funcMatch[1];
369
+ const argStr = funcMatch[2];
370
+ const arg = parsePolynomial(argStr);
371
+
372
+ // Return a special function structure
373
+ return {
374
+ type: 'function',
375
+ name: funcName,
376
+ args: [arg]
377
+ };
378
+ }
379
+
380
+ // ============================================================================
381
+ // ADVANCED CONVERSION FEATURES
382
+ // ============================================================================
383
+
384
+ /**
385
+ * Convert mathematical expressions with functions (sin, cos, log, etc.)
386
+ */
387
+ function expressionToLatex(expr, options = {}) {
388
+ // Enhanced function support can be added here
389
+ return slangToLatex(expr, options);
390
+ }
391
+
392
+ /**
393
+ * Batch convert multiple expressions with error handling
394
+ */
395
+ function batchConvertToLatex(expressions, options = {}) {
396
+ const results = [];
397
+ const errors = [];
398
+
399
+ expressions.forEach((expr, index) => {
400
+ try {
401
+ const result = slangToLatex(expr, options);
402
+ results.push({ index, success: true, result });
403
+ } catch (error) {
404
+ errors.push({ index, error: error.message });
405
+ results.push({ index, success: false, error: error.message });
406
+ }
407
+ });
408
+
409
+ return options.includeErrors ? { results, errors } : results.map(r => r.success ? r.result : `Error: ${r.error}`);
410
+ }
411
+
412
+ /**
413
+ * Batch convert LaTeX to SLaNg with detailed error reporting
414
+ */
415
+ function batchConvertToSlang(latexExpressions, options = {}) {
416
+ const results = [];
417
+ const errors = [];
418
+
419
+ latexExpressions.forEach((latex, index) => {
420
+ try {
421
+ const result = latexToSlang(latex, options);
422
+ results.push({ index, success: true, result });
423
+ } catch (error) {
424
+ errors.push({ index, error: error.message });
425
+ results.push({ index, success: false, error: error.message });
426
+ }
427
+ });
428
+
429
+ return options.includeErrors ? { results, errors } : results.map(r => r.success ? r.result : `Error: ${r.error}`);
430
+ }
431
+
432
+ /**
433
+ * Enhanced LaTeX syntax validation
434
+ */
435
+ function validateLatex(latex, options = {}) {
436
+ try {
437
+ latexToSlang(latex, { strictMode: true, ...options });
438
+ return { valid: true, errors: [] };
439
+ } catch (error) {
440
+ return { valid: false, errors: [error.message] };
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Smart LaTeX formatting for display mode
446
+ */
447
+ function formatDisplayMode(latex, options = {}) {
448
+ const defaults = {
449
+ forceDisplay: false,
450
+ preferInline: false
451
+ };
452
+
453
+ const opts = { ...defaults, ...options };
454
+
455
+ // Auto-detect if display mode is preferred
456
+ const needsDisplay = latex.includes('\\frac') ||
457
+ latex.includes('\\sum') ||
458
+ latex.includes('\\int') ||
459
+ latex.includes('\\prod') ||
460
+ opts.forceDisplay;
461
+
462
+ if (needsDisplay && !opts.preferInline) {
463
+ return `$$${latex}$$`;
464
+ }
465
+ return `$${latex}$`;
466
+ }
467
+
468
+ /**
469
+ * Expression simplification (basic implementation)
470
+ */
471
+ function simplifyExpression(expr) {
472
+ // This can be enhanced with more sophisticated simplification
473
+ return expr;
474
+ }
475
+
476
+ /**
477
+ * Get conversion statistics and metadata
478
+ */
479
+ function getConversionInfo(expression, direction = 'to-latex') {
480
+ const info = {
481
+ direction,
482
+ timestamp: new Date().toISOString(),
483
+ expressionType: 'unknown'
484
+ };
485
+
486
+ if (direction === 'to-latex') {
487
+ if (expression.terms) {
488
+ info.expressionType = 'polynomial';
489
+ info.termCount = expression.terms.length;
490
+ } else if (expression.numi && expression.deno !== undefined) {
491
+ info.expressionType = 'fraction';
492
+ info.numeratorTerms = expression.numi.terms?.length || 0;
493
+ info.denominatorType = typeof expression.deno === 'number' ? 'constant' : 'polynomial';
494
+ } else if (expression.coeff !== undefined) {
495
+ info.expressionType = 'term';
496
+ }
497
+ }
498
+
499
+ return info;
500
+ }
501
+
502
+ // ============================================================================
503
+ // UTILITY FUNCTIONS
504
+ // ============================================================================
505
+
506
+ /**
507
+ * Check if two expressions are equivalent
508
+ */
509
+ function areExpressionsEquivalent(expr1, expr2) {
510
+ try {
511
+ const latex1 = slangToLatex(expr1);
512
+ const latex2 = slangToLatex(expr2);
513
+ return latex1 === latex2;
514
+ } catch (error) {
515
+ return false;
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Get expression complexity score
521
+ */
522
+ function getExpressionComplexity(expr) {
523
+ let complexity = 1;
524
+
525
+ if (expr.terms) {
526
+ complexity += expr.terms.length;
527
+ expr.terms.forEach(term => {
528
+ if (term.var) {
529
+ complexity += Object.keys(term.var).length;
530
+ Object.values(term.var).forEach(power => {
531
+ complexity += power > 1 ? power - 1 : 0;
532
+ });
533
+ }
534
+ });
535
+ } else if (expr.numi && expr.deno !== undefined) {
536
+ complexity += getExpressionComplexity(expr.numi);
537
+ if (typeof expr.deno !== 'number') {
538
+ complexity += getExpressionComplexity(expr.deno);
539
+ } else {
540
+ complexity += 1;
541
+ }
542
+ } else if (expr.coeff !== undefined) {
543
+ if (expr.var) {
544
+ complexity += Object.keys(expr.var).length;
545
+ Object.values(expr.var).forEach(power => {
546
+ complexity += power > 1 ? power - 1 : 0;
547
+ });
548
+ }
549
+ }
550
+
551
+ return complexity;
552
+ }
553
+
554
+ // ============================================================================
555
+ // EXPORTS
556
+ // ============================================================================
557
+
558
+ export {
559
+ // Core conversion functions
560
+ slangToLatex,
561
+ latexToSlang,
562
+
563
+ // Component functions
564
+ termToLatex,
565
+ polynomialToLatex,
566
+ fractionToLatex,
567
+ parseTerm,
568
+ parsePolynomial,
569
+ parseFraction,
570
+
571
+ // Advanced features
572
+ expressionToLatex,
573
+ batchConvertToLatex,
574
+ batchConvertToSlang,
575
+ validateLatex,
576
+ formatDisplayMode,
577
+ simplifyExpression,
578
+
579
+ // Utility functions
580
+ getConversionInfo,
581
+ areExpressionsEquivalent,
582
+ getExpressionComplexity,
583
+
584
+ // Parsers (for advanced usage)
585
+ parseNumber,
586
+ parseVariable
587
+ };
588
+
589
+ // ============================================================================
590
+ // DEMO AND TESTING FUNCTIONS
591
+ // ============================================================================
592
+
593
+ /**
594
+ * Comprehensive demo function
595
+ */
596
+ function demoConverter() {
597
+ console.log('šŸš€ SLaNg LaTeX Converter - Unified Edition Demo');
598
+ console.log('=' .repeat(60));
599
+
600
+ // Test expressions
601
+ const testExpressions = [
602
+ createFraction([createTerm(1, { x: 1 })], [createTerm(1, { x: 1 }), createTerm(1)]),
603
+ createFraction([createTerm(1, { x: 2 }), createTerm(-1)], [createTerm(1, { x: 2 }), createTerm(1)]),
604
+ { terms: [createTerm(2, { x: 2 }), createTerm(3, { x: 1 }), createTerm(-1)] },
605
+ createTerm(5, { y: 3, z: 2 })
606
+ ];
607
+
608
+ console.log('\nšŸ“ SLaNg to LaTeX Conversions:');
609
+ testExpressions.forEach((expr, i) => {
610
+ const latex = slangToLatex(expr);
611
+ const info = getConversionInfo(expr, 'to-latex');
612
+ console.log(`${i + 1}. ${fractionToString(expr).padEnd(25)} → ${latex} (${info.expressionType})`);
613
+ });
614
+
615
+ // Test LaTeX inputs
616
+ const testLatex = [
617
+ '\\frac{x}{x+1}',
618
+ '\\frac{x^{2}-1}{x^{2}+1}',
619
+ '2x^{2} + 3x - 1',
620
+ '5y^{3}z^{2}'
621
+ ];
622
+
623
+ console.log('\nšŸ”„ LaTeX to SLaNg Conversions:');
624
+ testLatex.forEach((latex, i) => {
625
+ try {
626
+ const slang = latexToSlang(latex);
627
+ const backToLatex = slangToLatex(slang);
628
+ const validation = validateLatex(latex);
629
+ console.log(`${i + 1}. ${latex.padEnd(25)} → ${fractionToString(slang).padEnd(25)} (${validation.valid ? 'āœ…' : 'āŒ'})`);
630
+ } catch (error) {
631
+ console.log(`${i + 1}. ${latex.padEnd(25)} → Error: ${error.message}`);
632
+ }
633
+ });
634
+
635
+ console.log('\nšŸŽÆ Advanced Features Demo:');
636
+
637
+ // Batch conversion
638
+ const batchResult = batchConvertToLatex(testExpressions, { includeErrors: true });
639
+ console.log(`Batch conversion: ${batchResult.results.filter(r => r.success).length}/${batchResult.results.length} successful`);
640
+
641
+ // Validation
642
+ const validationResults = testLatex.map(latex => validateLatex(latex));
643
+ console.log(`Validation: ${validationResults.filter(v => v.valid).length}/${validationResults.length} valid LaTeX expressions`);
644
+
645
+ console.log('\nšŸš€ Unified Converter - All Features Operational!');
646
+ }
647
+
648
+ // Run demo if this file is executed directly
649
+ if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/').replace(/ /g, '%20')}` || process.argv[1].endsWith('slang-convertor.js')) {
650
+ demoConverter();
651
+ }
652
+