@@ -10,6 +10,7 @@ import { globalUnits } from "../utils/globalUnits.js";
1010import { createVarStore } from "../variables/store.js" ;
1111import { createFunctionRegistry } from "../function/registry.js" ;
1212import { internalFunctions } from "../function/internal.js" ;
13+ import { isDenseMatrixWrapper , serializeExprifyValue , wrapDenseMatrix } from "../utils/matrix.js" ;
1314
1415import { buildAST } from "../parser/astBuild.js" ;
1516
@@ -42,6 +43,18 @@ const formatComplex = (value) => {
4243 return `${ real } ${ sign } ${ imagPart } ` ;
4344} ;
4445
46+ const formatScalar = ( value ) => {
47+ if ( typeof value !== "number" ) {
48+ return String ( value ) ;
49+ }
50+
51+ if ( Number . isInteger ( value ) ) {
52+ return String ( value ) ;
53+ }
54+
55+ return Number ( value . toFixed ( 14 ) ) . toString ( ) ;
56+ } ;
57+
4558const formatResult = ( value ) => {
4659 if ( isComplex ( value ) ) {
4760 return formatComplex ( value ) ;
@@ -51,12 +64,20 @@ const formatResult = (value) => {
5164 return `${ value . value } ${ value . unit } ` ;
5265 }
5366
67+ if ( isDenseMatrixWrapper ( value ) ) {
68+ return serializeExprifyValue ( value ) ;
69+ }
70+
5471 if ( isMatrix ( value ) ) {
55- return value . map ( ( row ) => row . join ( "\t" ) ) . join ( "\n" ) ;
72+ return value . map ( ( row ) => row . map ( formatScalar ) . join ( "\t" ) ) . join ( "\n" ) ;
5673 }
5774
5875 if ( Array . isArray ( value ) ) {
59- return value . join ( "\n" ) ;
76+ return JSON . stringify ( value ) ;
77+ }
78+
79+ if ( value && typeof value === "object" ) {
80+ return serializeExprifyValue ( value ) ;
6081 }
6182
6283 return value ;
@@ -70,6 +91,214 @@ class exprify {
7091 this . functions = createFunctionRegistry ( internalFunctions ) ;
7192 this . variables = createVarStore ( ) ;
7293 this . _cache = new Map ( ) ;
94+ this . variables . set ( "pi" , Math . PI ) ;
95+ this . variables . set ( "e" , Math . E ) ;
96+ this . addFunction ( "parse" , ( expression ) => {
97+ if ( typeof expression !== "string" ) {
98+ throw new Error ( "parse() expects an expression string" ) ;
99+ }
100+ return expression ;
101+ } ) ;
102+ this . addFunction ( "leafCount" , ( value ) => {
103+ const countLeafTokens = ( expression ) => {
104+ const strippedKeys = expression . replace ( / ( ^ | [ { , ] \s * ) [ a - z A - Z _ ] [ a - z A - Z 0 - 9 _ ] * \s * : / g, "$1" ) ;
105+ const matches = strippedKeys . match ( / \d + ( \. \d + ) ? ( e [ + - ] ? \d + ) ? n ? | [ a - z A - Z _ ] [ a - z A - Z 0 - 9 _ ] * / gi) ;
106+ return matches ? matches . length : 0 ;
107+ } ;
108+
109+ let ast = value ;
110+ if ( typeof value === "string" ) {
111+ try {
112+ ast = this . parse ( value ) . ast ;
113+ } catch {
114+ return countLeafTokens ( value ) ;
115+ }
116+ }
117+
118+ const countLeaves = ( node ) => {
119+ if ( ! node || typeof node !== "object" ) return 0 ;
120+
121+ switch ( node . type ) {
122+ case "Literal" :
123+ case "ImaginaryLiteral" :
124+ case "UnitLiteral" :
125+ case "Identifier" :
126+ return 1 ;
127+ default :
128+ return Object . values ( node ) . reduce ( ( sum , child ) => {
129+ if ( Array . isArray ( child ) ) {
130+ return sum + child . reduce ( ( inner , item ) => inner + countLeaves ( item ) , 0 ) ;
131+ }
132+
133+ return sum + countLeaves ( child ) ;
134+ } , 0 ) ;
135+ }
136+ } ;
137+
138+ return countLeaves ( ast ) ;
139+ } ) ;
140+ this . addFunction ( "matrix" , ( value ) => wrapDenseMatrix ( value ) ) ;
141+ this . addFunction ( "sparse" , ( value ) => wrapDenseMatrix ( value ) ) ;
142+ this . addFunction ( "rationalize" , ( expression , withDetails = false ) => {
143+ if ( typeof expression !== "string" ) {
144+ throw new Error ( "rationalize() expects an expression string" ) ;
145+ }
146+
147+ const normalizedExpression = expression
148+ . replace ( / \s + / g, "" )
149+ . replace ( / ( \d ) ( [ a - z A - Z ( ] ) / g, "$1*$2" )
150+ . replace ( / ( [ a - z A - Z ) ] ) ( \d ) / g, "$1*$2" ) ;
151+
152+ const polyKey = ( powers ) => JSON . stringify ( Object . entries ( powers ) . sort ( ( [ a ] , [ b ] ) => a . localeCompare ( b ) ) ) ;
153+ const keyToPowers = ( key ) => Object . fromEntries ( JSON . parse ( key ) ) ;
154+ const makePoly = ( terms = new Map ( ) ) => terms ;
155+ const constPoly = ( value ) => new Map ( [ [ polyKey ( { } ) , value ] ] ) ;
156+ const varPoly = ( name ) => new Map ( [ [ polyKey ( { [ name ] : 1 } ) , 1 ] ] ) ;
157+ const cleanPoly = ( poly ) => new Map ( [ ...poly . entries ( ) ] . filter ( ( [ , coeff ] ) => coeff !== 0 ) ) ;
158+ const addPoly = ( a , b , sign = 1 ) => {
159+ const result = new Map ( a ) ;
160+ for ( const [ key , coeff ] of b . entries ( ) ) {
161+ result . set ( key , ( result . get ( key ) || 0 ) + ( sign * coeff ) ) ;
162+ }
163+ return cleanPoly ( result ) ;
164+ } ;
165+ const multiplyPoly = ( a , b ) => {
166+ const result = new Map ( ) ;
167+ for ( const [ keyA , coeffA ] of a . entries ( ) ) {
168+ const powersA = keyToPowers ( keyA ) ;
169+ for ( const [ keyB , coeffB ] of b . entries ( ) ) {
170+ const powersB = keyToPowers ( keyB ) ;
171+ const merged = { ...powersA } ;
172+ for ( const [ name , power ] of Object . entries ( powersB ) ) {
173+ merged [ name ] = ( merged [ name ] || 0 ) + power ;
174+ }
175+ const key = polyKey ( merged ) ;
176+ result . set ( key , ( result . get ( key ) || 0 ) + ( coeffA * coeffB ) ) ;
177+ }
178+ }
179+ return cleanPoly ( result ) ;
180+ } ;
181+ const powPoly = ( poly , exponent ) => {
182+ let result = constPoly ( 1 ) ;
183+ for ( let index = 0 ; index < exponent ; index ++ ) {
184+ result = multiplyPoly ( result , poly ) ;
185+ }
186+ return result ;
187+ } ;
188+ const rational = ( num , den = constPoly ( 1 ) ) => ( { num, den } ) ;
189+ const addRat = ( a , b , sign = 1 ) => rational (
190+ addPoly (
191+ multiplyPoly ( a . num , b . den ) ,
192+ multiplyPoly ( b . num , a . den ) ,
193+ sign
194+ ) ,
195+ multiplyPoly ( a . den , b . den )
196+ ) ;
197+ const mulRat = ( a , b ) => rational ( multiplyPoly ( a . num , b . num ) , multiplyPoly ( a . den , b . den ) ) ;
198+ const divRat = ( a , b ) => rational ( multiplyPoly ( a . num , b . den ) , multiplyPoly ( a . den , b . num ) ) ;
199+ const negRat = ( value ) => rational ( addPoly ( new Map ( ) , value . num , - 1 ) , value . den ) ;
200+ const astToRat = ( node ) => {
201+ switch ( node . type ) {
202+ case "Literal" :
203+ return rational ( constPoly ( node . value ) ) ;
204+ case "Identifier" :
205+ return rational ( varPoly ( node . name ) ) ;
206+ case "UnaryExpression" :
207+ if ( node . operator === "-" ) return negRat ( astToRat ( node . argument ) ) ;
208+ throw new Error ( "Unsupported unary operator" ) ;
209+ case "BinaryExpression" : {
210+ const left = astToRat ( node . left ) ;
211+ const right = astToRat ( node . right ) ;
212+ switch ( node . operator ) {
213+ case "+" : return addRat ( left , right ) ;
214+ case "-" : return addRat ( left , right , - 1 ) ;
215+ case "*" : return mulRat ( left , right ) ;
216+ case "/" : return divRat ( left , right ) ;
217+ case "^" : {
218+ if ( node . right . type !== "Literal" || ! Number . isInteger ( node . right . value ) || node . right . value < 0 ) {
219+ throw new Error ( "Unsupported exponent" ) ;
220+ }
221+ return rational (
222+ powPoly ( left . num , node . right . value ) ,
223+ powPoly ( left . den , node . right . value )
224+ ) ;
225+ }
226+ default :
227+ throw new Error ( "Unsupported operator in rationalize()" ) ;
228+ }
229+ }
230+ default :
231+ throw new Error ( "Unsupported expression in rationalize()" ) ;
232+ }
233+ } ;
234+ const formatPoly = ( poly ) => {
235+ const entries = [ ...poly . entries ( ) ]
236+ . filter ( ( [ , coeff ] ) => coeff !== 0 )
237+ . sort ( ( [ keyA ] , [ keyB ] ) => {
238+ const powersA = keyToPowers ( keyA ) ;
239+ const powersB = keyToPowers ( keyB ) ;
240+ const firstVarA = Object . keys ( powersA ) . sort ( ) [ 0 ] || "" ;
241+ const firstVarB = Object . keys ( powersB ) . sort ( ) [ 0 ] || "" ;
242+
243+ if ( firstVarA !== firstVarB ) {
244+ return firstVarA . localeCompare ( firstVarB ) ;
245+ }
246+
247+ const degreeA = Object . values ( powersA ) . reduce ( ( sum , value ) => sum + value , 0 ) ;
248+ const degreeB = Object . values ( powersB ) . reduce ( ( sum , value ) => sum + value , 0 ) ;
249+ return degreeB - degreeA ;
250+ } ) ;
251+
252+ if ( ! entries . length ) return "0" ;
253+
254+ return entries . map ( ( [ key , coeff ] , index ) => {
255+ const powers = keyToPowers ( key ) ;
256+ const absCoeff = Math . abs ( coeff ) ;
257+ const variablePart = Object . entries ( powers )
258+ . map ( ( [ name , power ] ) => power === 1 ? name : `${ name } ^ ${ power } ` )
259+ . join ( " * " ) ;
260+ let body = variablePart ;
261+
262+ if ( ! body ) {
263+ body = `${ absCoeff } ` ;
264+ } else if ( absCoeff !== 1 ) {
265+ body = `${ absCoeff } * ${ body } ` ;
266+ }
267+
268+ if ( index === 0 ) {
269+ return coeff < 0 ? `- ${ body } ` . replace ( "- " , "-" ) : body ;
270+ }
271+
272+ return coeff < 0 ? `- ${ body } ` : `+ ${ body } ` ;
273+ } ) . join ( " " ) ;
274+ } ;
275+
276+ const ast = this . parse ( normalizedExpression ) . ast ;
277+ const result = astToRat ( ast ) ;
278+ const numerator = formatPoly ( result . num ) ;
279+ const denominator = formatPoly ( result . den ) ;
280+ const variableSet = new Set ( ) ;
281+
282+ for ( const poly of [ result . num , result . den ] ) {
283+ for ( const key of poly . keys ( ) ) {
284+ for ( const name of Object . keys ( keyToPowers ( key ) ) ) {
285+ variableSet . add ( name ) ;
286+ }
287+ }
288+ }
289+
290+ if ( ! withDetails ) {
291+ return `(${ numerator } ) / (${ denominator } )` ;
292+ }
293+
294+ return {
295+ numerator,
296+ denominator,
297+ coefficients : [ ] ,
298+ variables : [ ...variableSet ] . sort ( ) ,
299+ expression : `(${ numerator } ) / (${ denominator } )`
300+ } ;
301+ } ) ;
73302 }
74303
75304 setVariable ( name , value ) {
0 commit comments