Almeno una volta nella vita programmativa ci si è posti di fornite all'esigenza di fare anche un piccolo parse di una stringa. Beh questa volta è toccato a me. Ho dovuto creare un parser per espressioni matematiche che a run-time "parsasse" le espressioni e le eseguisse inserendo al posto di alcuni marcatori dei valori passati da programma.
Da prima ho provato a cercare su San Google e ho trovato su Santissimo CodeProject e ho trovato una soluzione che poteva andare ma che non era di mio gradimento, infatti anche se molto performante e molto vicina a quello che realmente è un parser, è molto difficile da "smanettare": infatti fa largo uso di lunghe e qualche volta non del tutto comprensibili Espressioni Regolari. Analizzando il codice ho scoperto che questo parser ti permetteva di inserire funzioni matematiche e di richiamare una serie di funzioni del .NET (Sin, Cos, ecc... la lista completa è su the code project), pecca però per la mia utilità di una sostituzioni di variabili. Provando a mettere insieme qualcosa di concreto ne ho tirato fuori poco o niente.
Mi sono chiesto allora: ci sarà di meglio? Beh se devo dirla tutta di meglio non è (anche perchè il meglio in questo settore è soggettivo rispetto al contesto) ma cmq è qualcosa che si avvicinava molto a quello che interessava a me.Cercando sempre sui theCodeProject ho trovato un secondo parser per espressioni che però era legato alla sintassi C#. Infatti quest'ultimo prende l'espressione matematica scritta in C# e la esegue creando a run-time un assembly in memoria che esegue l'espressione. Le pecche di questo parser sono ovvie! Nessuna protezione! Io posso girare il codice che mi interessa senza sessuna limitazione, il programma, ne perde in sicurezza. Inoltre, neanche questo parser
permette di inserire delle variabili e quindi bisogna dargli in pasta una espressione bella e formattata.
Ne è venuto fuori questo:
MATH PARSERIl parser che da ora in poi chiameremo MathParser esegue espressioni matematiche tra le più comuni. Le espressioni possono essere nella prima versione la 1.0.0b solo di tipo algebrico. Le variabili possono essere definite inserendo il nome delle variabili racchiuso tra parentesi quadre (e.g. [valore1]), le funzioni trigonometrie e le più normali funzioni algebriche supportate sono:
addizione +, sottrazione -, moltiplicazione *, divisione /,% modulo, assoluto abs(espressione), arcocoseno acos(espressione), arcoseno asin(espressione), numero intero int(espressione), arrotondamento a n cifre decimali round(espressione,n), radice quadrata sqrt(espressione).
Possono essere inserite le costanti matematiche pi-greco e e (numero di nepero) attraverso l'inserimento delle stringe @pi ed @e rispettivamente. I numeri decimali devono essere divisi dalla loro parte intera tramite la virgola. L'ordine delle partentesi è dettato dal loro ordine nelle espressioni e sono tutte di tipo ( e ).
Ci sono alcuni caratteri che non è possibile inserire quali: . punto, " virgolette, { } parentesi graffe, ? punto interrogativo, : due punti, & e commerciale, | pipe, = uguale, < > minore e maggiore, per ragioni di sicurezza.
Le pecche di questo sistema almeno nella sua prima versione beta sono: non c'è l'elevamento a potenza, non ha un elevatissimo controllo sul codice dell'espressione, è ancora abbastanza legato al linguaggio di programmazione, non ha tutte le funzioni matematiche di base, quando si fanno delle divisioni tra numeri interi, se si vuole fare una divisione reale bisogna inserire i numeri comprensivi di parte decimale anche se zero (e.g. 4/3 deve essere 4.0/3.0), ma rispetto alla sua versione, ... chiamiamola alfa e da cui eredita la maggior parte di questi bug, è abbastanza avanzato e sicuro.
Passiamo ora a implementare il sistema. Per prima cosa definiamo le classi di servizio che ci servono:
Una classe di eccezione che sarà utilizzata dal parser per lanciare le eccezioni:
namespace MathParser {
public class MathParserException : Exception {
public MathParserException() {}
public MathParserException(String message) : base(message) {}
public MathParserException(String message, Exception innerException) : base(message, innerException) {}
}
}
niente di complicato, abbiamo semplicemente ereditato dalla classe Exception e ricostruito i costruttori che ci interessano.
La seconda classe di servizio è quella che poi verrà ereditata dalla classe dell'espressione:
namespace MathParser {
public abstract class BaseClass : IDisposable {
public virtual double eval() {
return 0.0;
}
#region IDisposable Membri di
public void Dispose() {}
#endregion
}
}
anche qui niente di complicato definiamo una classe che è IDisposable (da implementare) e un metodo virtuale double eval() che ci ritorna 0.0. Questo metodo verrà poi ridefinito quando la classe viene ereditata e viene sostituito dall'espressione.
Bene abbiamo fatto la base del nostro parser, passiamo ora a implementare la nostra ultima classe, che sarà quella principale del parser che creerà l'assembly ed eseguirà l'espressione.
Definiamo qualche using tra cui gli using base System e il conosciuto System.Collection.Generic per le collezioni. I restanti due servono per la creazione dell'assembly e sfruttano il compilatore integrato nel Framework .NET 2.0.
using System.CodeDom.Compiler;
using System.Collections.Generic;
using Microsoft.CSharp;
e definiamo la classe:
namespace MathParser {
public class MathExpressionParser : IDisposable {
dichiariamo poi alcuni membri per la classe:
#region Membri
//Lista di funzioni di sistema
private static readonly Dictionary functionList = new Dictionary();
//Lista di errori di compilazione
private readonly List listError = new List();
//Espressione da parsare
private String expression = "";
//Oggetto che eseguirà l'espressione di tipo BaseClass precedentemente definito
private BaseClass myobj = null;
//Variabile che indica se l'espressione è stata passata già al parser o no
private bool parsed = false;
//Collezione di valori per le variabili
private Dictionary valori = new Dictionary();
#endregion
Inseriamo un costruttore statico che verrà chiamato solo la prima volta che la classe viene istanziata:
static MathExpressionParser() {
functionList.Add("abs", "Math.Abs");
functionList.Add("acos", "Math.Acos");
functionList.Add("asin", "Math.Asin");
functionList.Add("int", "Math.Floor");
functionList.Add("round", "Math.Round");
functionList.Add("sqrt", "Math.Sqrt");
functionList.Add("@pi", "Math.PI");
functionList.Add("@e", "Math.E");
//Aggiungere qui altre funzioni di sistema
}
#endregion
questo costruttore contiene la lista delle funzioni di sistema che possono essere inserite nell'espressione, come si nota ogni espressione verrà poi trasformata nel suo rispettivo corrispondente in C#. Questo fa capire anche com'è semplice inserire delle funzioni personalizzate all'interno del parser in una nuova features, o completare semplicemente la lista.
Creiamo qualche proprietà che aiuta chi utilizza il Parser:
// Lista degli errori dopo il Parse
public List ListError {
get { return listError; }
}
// Restituisce o imposta la lista dei valori
public Dictionary Valori {
get { return valori; }
set {
listError.Clear();
parsed = false;
if(valori == null) valori = new Dictionary();
else valori = value;
}
}
// Restituisce se l'espressione è stata passata al parser
public bool Parsed {
get { return parsed; }
}
// Restituisce o imposta la stringa di espressione
public string Expression {
get { return expression; }
set {
listError.Clear();
parsed = false;
expression = value;
}
}
#endregion
Come possiamo vedere le uniche due proprietà settabili sono Expression e Valori, che a loro volta azzerano il parse se settate ponendo parsed=false. Le restanti proprietà sono opportunamente commentate.
Vengono i metodi privati. Questi sono particolarmente importanti poichè permettono di trasformare l'espressione passata al Parser in una espressione completamente comprensibile al C# (ovviamente se scritta correttamente).
#region Metodi privati
// Sostituisce le funzioni con quelle di sistema
Espressione in cui sostituire i valori della function di Sistema
// Ritorna la stringa filtrata
private static string sobstituteSystemFunction(String expr) {
Dictionary.Enumerator c = functionList.GetEnumerator();
while (c.MoveNext()) {
KeyValuePair kvp = c.Current;
expr = expr.Replace(kvp.Key.ToLower(), kvp.Value.Replace(",", "."));
}
return expr;
}
//
// Sostituisce i marcatori con i valori
//
//
Espressione in cui sostituire i marcatori con i valori
// Ritorna la stringa filtrata
private string sobstitute(String expr) {
Dictionary.Enumerator c = Valori.GetEnumerator();
while (c.MoveNext()) {
KeyValuePair kvp = c.Current;
expr = expr.Replace("[" + kvp.Key.ToLower() + "]", kvp.Value.ToString().Replace(",", "."));
}
return expr;
}
//
// Filtro anti hack. Filtra tutti i caratteri o le stringhe che potrebbero essere usate come codice non desiderato
//
//
Espressione da filtrare
// Espressione filtata
private static string filter(String expr) {
if(expr.Contains(".")) throw new MathParserException("I numeri decimali devono essere separati con la virgola (,)");
if(expr.Contains("{") || expr.Contains("}")) throw new MathParserException("Non sono ammesse parentesi {, solo parentesi tonde per priorità alle espressioni e quadre per identificare i marcatori");
if(expr.Contains("=") || expr.Contains("&gt;") || expr.Contains("&gt;") || expr.Contains("!") || expr.Contains("\"") || expr.Contains("'") || expr.Contains("?") || expr.Contains(":") || expr.Contains(";") || expr.Contains("&") || expr.Contains("|")) throw new MathParserException("Sono stati inseriti caratteri non ammessi");
if(expr.Contains("^")) throw new MathParserException("Impossibile effettuare l'elevamento a potenza in questa versione");
expr = expr.ToLower();
expr = expr.Replace(" ", "");
expr = expr.Replace("\r\n", "");
expr = expr.Replace("\n\r", "");
expr = expr.Replace("\n", "");
expr = expr.Replace("\r", "");
return expr;
}
#endregion
#region IDisposable Membri di
public void Dispose() {
if(myobj != null) myobj.Dispose();
}
#endregion
Per quanti riguarda i metodi pubblici ce ne sono 2 e sono alla base del Parse, bisogna usarli in coppia per eseguire una espressione.
#region Metodi
/// <summary>
/// Esegue il parse dell'espressione con l'espressione e la lista valori contenuta in memoria
/// </summary>
/// <exception cref="MathParserException"></exception>
public void Parse() {
Parse(Expression, Valori);
}
/// <summary>
/// Esegue il parse dell'espressione
/// </summary>
/// <param name="expr">Espressione da eseguire</param>
/// <param name="listaValori">Lista parametri</param>
/// <exception cref="MathParserException"></exception>
public void Parse(string expr, Dictionary<string, double> listaValori) {
try {
parsed = false;
listError.Clear();
valori.Clear();
if(expr == null || expr == "") throw new MathParserException("Nessuna espressione impostata");
if(listaValori != null) foreach (KeyValuePair<string, double> kvp in listaValori) Valori.Add(kvp.Key, kvp.Value);
expression = expr;
//Settaggio della stringa
expr = filter(expr);
expr = sobstitute(expr);
expr = sobstituteSystemFunction(expr);
expr = expr.Replace(",", ".");
//Creazione dell'assembly di esecuzione
CSharpCodeProvider cp = new CSharpCodeProvider();
ICodeCompiler ic = cp.CreateCompiler();
CompilerParameters cpar = new CompilerParameters();
cpar.GenerateInMemory = true;
cpar.GenerateExecutable = false;
cpar.ReferencedAssemblies.Add("system.dll");
cpar.ReferencedAssemblies.Add("MathParser.dll");
string src = "using System;" +
"class myclass:MathParser.BaseClass" +
"{" +
"public myclass(){}" +
"public override double eval()" +
"{" +
"return " + expr + ";" +
"}" +
"}";
CompilerResults cr = ic.CompileAssemblyFromSource(cpar, src);
foreach (CompilerError ce in cr.Errors) {
listError.Add(ce.ErrorText);
}
if(cr.Errors.Count == 0 && cr.CompiledAssembly != null) {
Type ObjType = cr.CompiledAssembly.GetType("myclass");
try {
if(ObjType != null) myobj = (BaseClass) Activator.CreateInstance(ObjType);
} catch {
throw new MathParserException("Errore parse nell'espressione");
}
parsed = true;
} else parsed = false;
} catch (MathParserException mpe) {
parsed = false;
throw mpe;
} catch {
parsed = false;
throw new MathParserException("Errore parse nell'espressione");
}
}
/// <summary>
/// Valuta l'espessione inserita
/// </summary>
/// <returns>Ritorna il valore dell'espressione inserita</returns>
/// <exception cref="MathParserException"></exception>
public double Evaluate() {
if(Parsed)
if(myobj != null)
try {
return myobj.eval();
} catch {
throw new MathParserException("Errore nell'esecuzione dell'espressione, controllare tipi e valori");
}
else throw new MathParserException("Impossibile valutare l'espressione");
else throw new MathParserException("Impossibile valutare l'espressione se prima non si esegue il Parse dell'oggetto");
}
#endregion
Viene poi il momento della creazione dell'assembly: si crea il compilatore e con la proprietà GenerateInMemory viene detto al compilatore di creare l'assembly in memoria e non fisicamente, di modo che poi verrà eliminato:
CSharpCodeProvider cp = new CSharpCodeProvider();
ICodeCompiler ic = cp.CreateCompiler();
CompilerParameters cpar = new CompilerParameters();
//Comunica che l'assembly viene inserito in memoria
cpar.GenerateInMemory = true;
//Comunica che l'assembly è una dll
cpar.GenerateExecutable = false;
//Aggiunge i riferimenti alla nuova classe
cpar.ReferencedAssemblies.Add("system.dll");
cpar.ReferencedAssemblies.Add("MathParser.dll");
//Crea il codice sorgente della classe
string src = "using System;" +
"class myclass:MathParser.BaseClass" +
"{" +
"public myclass(){}" +
"public override double eval()" +
"{" +
"return " + expr + ";" +
"}" +
"}";
//Compila la classe
CompilerResults cr = ic.CompileAssemblyFromSource(cpar, src);
if(cr.Errors.Count == 0 && cr.CompiledAssembly != null) {
Type ObjType = cr.CompiledAssembly.GetType("myclass");
try {
if(ObjType != null) myobj = (BaseClass) Activator.CreateInstance(ObjType);
} catch {
throw new MathParserException("Errore parse nell'espressione");
}
parsed = true;
} else parsed = false;
Per provare il Parser in un programmino potete usare questo codice:
do {
c = Console.ReadLine();
MathExpressionParser p = new MathExpressionParser();
Dictionary valori = new Dictionary();
valori.Add("numero", 3.5);
valori.Add("desi", 12);
valori.Add("system", 32.254123);
try {
p.Parse(c, valori);
if(p.Parsed) Console.WriteLine("Risultato: " + p.Evaluate());
else {
foreach(String s in p.ListError) {
Console.WriteLine("Errore di no parse: " + s);
}
}
}catch(Exception ex) {
Console.WriteLine(ex.Message);
}
} while (c != "");
Espressioni del tipo sono ad esempio:
[numero]*3+4/[desy]
In questo modo si avrà un parser semplice ed efficace da usare nelle vostre applicazioni.
ALLEGATI
- Assembly MathParser
- Codice sorgente
LINKS
- Parser alfa
BUGS
I bugs finora riscontrati sono stati citati nell'intestazione.
COMMENTI
Una serie di commenti sono disponibili sul link della versione alfa del Parser
--------------------------------------------------------------------------------------------
1 commento:
da Turbo Pascal a Visual C
http://www.fraspe.it/effedix/effedix.htm
un parser matematico che consente di creare funzioni di una variabile e di inserirle all'interno di una qualsiasi espressione matematica anche complessa
Posta un commento