Parsing a mathematical expression - c#

Given a string containing a mathematical expression, given a set of functions/commands and given a set of assigned variables, are there tools that .NET provides to quickly build a parser?
I would like to build a simple parser that analyzes an expression and breaks it into its simplest components, for example:
d*(abs(a-b)+sqrt(c))
becomes
f = abs(a-b) and g = sqrt(c)
e = f + g
d*e

Do you want to build a parser or just have the solution presented?
Either way, check out nCalc. If you just need to solve it, grab the binaries. If you need to see how they parse out the expression tree, grab the source.

I've heard good things about the Grammatica parser generator. ANTLR is also widely used (especially in Java).
I assume you know how to define a BNF grammar and have learned about or built parsers in the past.

Check out veparser as well. Here is a sample code that shows how you can build an expression evaluator( the code parses the expression and directly calculates the output ). This sample can be modified to store the evaluation tree instead of running it.
using System;
using VeParser;
public class MathEvaluator : CharParser
{
protected override Parser GetRootParser()
{
Func<double, double, double> productFunc = (value1, value2) => value1 * value2;
Func<double, double, double> divideFunc = (value1, value2) => value1 / value2;
Func<double, double, double> sumFunc = (value1, value2) => value1 + value2;
Func<double, double, double> subtractFunc = (value1, value2) => value1 - value2;
Func<double, double> negativeFunc = value => -value;
Func<double, double> posititveFunc = value => value;
var dot = token('.');
var op = token('(');
var cp = token(')');
var sumOp = create(sumFunc, token('+'));
var subtractOp = create(subtractFunc, token('-'));
var positiveOp = create(posititveFunc, token('+'));
var negativeOp = create(negativeFunc, token('-'));
var productOp = create(productFunc, token('*'));
var divideOp = create(divideFunc, token('/'));
// Numbers
var deciamlPlaceValue = 1M;
var decimalDot = run(() => { deciamlPlaceValue = 1; }, dot);
var digit = consume((n, d) => n * 10 + char.GetNumericValue(d), keep(Digit));
var decimalDigit = consume((n, d) => { deciamlPlaceValue = deciamlPlaceValue * 10; return (double)((decimal)n + ((decimal)char.GetNumericValue(d)) / deciamlPlaceValue); }, keep(Digit));
var number = any(
/* float */ create(0, seq(zeroOrMore(digit), decimalDot, oneOrMore(decimalDigit))),
/* int */ create(0, oneOrMore(digit))
);
var expression = createReference();
var simpleExpression = createReference();
// Unary
var unaryOp = any(positiveOp, negativeOp);
var unaryExpression = update(d => d.action(d.value),
createNew(seq(set("action", unaryOp), set("value", expression))));
// Binary
var binaryOp = any(sumOp, subtractOp, productOp, divideOp);
var binaryExpressinoTree = update(x => x.value1, createNew(
seq(
set("value1", simpleExpression),
zeroOrMore(
update(d => { var r = base.CreateDynamicObject(); r.value1 = d.action(d.value1, d.value2); return r; },
seq(
set("action", binaryOp),
set("value2", simpleExpression))))
)));
var privilegedExpressoin = seq(op, expression, cp);
setReference(simpleExpression, any(privilegedExpressoin, unaryExpression, number));
setReference(expression, any(binaryExpressinoTree, simpleExpression));
return seq(expression, endOfFile());
}
public static object Eval(string expression)
{
MathEvaluator me = new MathEvaluator();
var result = me.Parse(expression.ToCharArray());
return result;
}
}

Another Aproach
class Program
{
static void Main(string[] args)
{
var a = 1;
var b = 2;
Console.WriteLine(FN_ParseSnippet($"{a} + {b} * 2"));
Console.ReadKey();
}
public static object FN_ParseSnippet(string snippet)
{
object ret = null;
var usingList = new List<string>();
usingList.Add("System");
usingList.Add("System.Collections.Generic");
usingList.Add("System.Text");
usingList.Add("Microsoft.CSharp");
//Create method
CodeMemberMethod pMethod = new CodeMemberMethod();
pMethod.Name = "Execute";
pMethod.Attributes = MemberAttributes.Public;
pMethod.ReturnType = new CodeTypeReference(typeof(object));
pMethod.Statements.Add(new CodeSnippetExpression(" return " + snippet));
//Create Class
CodeTypeDeclaration pClass = new System.CodeDom.CodeTypeDeclaration("Compilator");
pClass.Attributes = MemberAttributes.Public;
pClass.Members.Add(pMethod);
//Create Namespace
CodeNamespace pNamespace = new CodeNamespace("MyNamespace");
pNamespace.Types.Add(pClass);
foreach (string sUsing in usingList)
pNamespace.Imports.Add(new CodeNamespaceImport(sUsing));
//Create compile unit
CodeCompileUnit pUnit = new CodeCompileUnit();
pUnit.Namespaces.Add(pNamespace);
CompilerParameters param = new CompilerParameters();
param.GenerateInMemory = true;
List<AssemblyName> pReferencedAssemblys = new List<AssemblyName>();
pReferencedAssemblys = Assembly.GetExecutingAssembly().GetReferencedAssemblies().ToList();
pReferencedAssemblys.Add(Assembly.GetExecutingAssembly().GetName());
pReferencedAssemblys.Add(Assembly.GetCallingAssembly().GetName());
foreach (AssemblyName asmName in pReferencedAssemblys)
{
Assembly asm = Assembly.Load(asmName);
param.ReferencedAssemblies.Add(asm.Location);
}
//Compile
CompilerResults pResults = (new CSharpCodeProvider()).CreateCompiler().CompileAssemblyFromDom(param, pUnit);
if (pResults.Errors != null && pResults.Errors.Count > 0)
{
//foreach (CompilerError pError in pResults.Errors)
// MessageBox.Show(pError.ToString());
}
var instance = pResults.CompiledAssembly.CreateInstance("MyNamespace.Compilator");
ret = instance.GetType().InvokeMember("Execute", BindingFlags.InvokeMethod, null, instance, null);
return ret;
}
}

Related

Unable to cast object of type 'System.Linq.Expressions.ConstantExpression' to type 'Microsoft.EntityFrameworkCore.Query.QueryRootExpression'

This is the function i am trying to write unit test for. On of the methods happens to use FromSqlRaw() to fetch the data from SQL Server via a stored procedure.
public async Task<IQueryable<LoadDetail>> GlobalSearchMobileByLocation(IQueryable<LoadDetail> loadDetails, List<LocationCoordinates> origin, List<LocationCoordinates> destination, int originRadius, int destinationRadius)
{
// Getting Cities within Radius
Stopwatch getCitiesStopWatch = new Stopwatch();
getCitiesStopWatch.Start();
var rawsql = new StringBuilder();
rawsql.Append(Constant.SqlBaseQueryLoadDetail);
if (origin != null && originRadius != 0)
{
rawsql.Append(" ( ");
foreach (var locationCoordinates in origin)
{
rawsql.Append(BuildSearchMobileQuery(locationCoordinates, originRadius, Constant.OriginLatitudeField, Constant.OriginLongitudeField));
}
rawsql = new StringBuilder(rawsql.ToString().Substring(0, rawsql.ToString().LastIndexOf("or") - 1));
rawsql.Append(") and ");
}
if (destination != null && destinationRadius != 0)
{
rawsql.Append(" ( ");
foreach (var locationCoordinates in destination)
{
rawsql.Append(BuildSearchMobileQuery(locationCoordinates, destinationRadius, Constant.DestinationLatitudeField, Constant.DestinationLongitudeField));
}
rawsql = new StringBuilder(rawsql.ToString().Substring(0, rawsql.ToString().LastIndexOf("or") - 1));
rawsql.Append(") and ");
}
rawsql.Append(Constant.TrueStatementSql);
var query = rawsql.ToString();
try
{
loadDetails = _readContext.LoadDetails.FromSqlRaw(rawsql.ToString()).AsNoTracking().AsQueryable();
}
catch (Exception ex )
{
throw;
}
getCitiesStopWatch.Stop();
_logger.LogInformation("Time taken to get cities based on radius(ms) : " + getCitiesStopWatch.ElapsedMilliseconds.ToString());
return await Task.FromResult(loadDetails);
}
I wrote this x unit test case for the function but it is throwing error.
public void Should_return_LoadWithinTheDHRadius_OriginIsPassedOnly()
{
// Arrange
var fixture = new Fixture();
var loadDetail = Enumerable.Empty<LoadDetail>().AsQueryable();
var loadDetailList = fixture.Create<LoadDetail>();
loadDetailList.PickupDateTime = DateTime.Now.Date;
loadDetailList.DeliveryDateTime = DateTime.Now.Date;
loadDetailList.OriginZipCode = Constant.Zip;
loadDetailList.DestinationZipCode = Constant.Zip;
loadDetailList.LoadId = Guid.Parse("8563c24c-c402-4960-b172-d379543f0096");
loadDetailList.OriginCityName = Constant.OriginCityName;
loadDetailList.OriginStateName = Constant.OriginStateName;
loadDetailList.DestinationCityName = Constant.DestinationCityName;
loadDetailList.DestinationStateName = Constant.DestinationStateName;
loadDetailList.OriginLongitude = 34.45666;
loadDetailList.OriginLatitude = 34.45666;
loadDetailList.DestinationLongitude = 34.45666;
loadDetailList.DestinationLatitude = 34.45666;
loadDetailList.SourceLoadId = 123456;
var loadDetailMock = new List<LoadDetail>() { loadDetailList }.AsQueryable();
//this.readContext<IQueryable<LoadDetails>>.Setup(x => x.LoadDetails).Returns(() => loadDetailMock.Object);
//readContext.Setup(x => x.LoadDetails(It.IsAny<string>(), It.IsAny<object[]>())).Returns(tableContent);
// this.readContext.Setup(x => x.LoadDetails)..Returns(() => loadDetailMock.Object);
var dbset = new Mock<DbSet<LoadDetail>>();
dbset.As<IQueryable<LoadDetail>>().Setup(x => x.Provider).Returns(loadDetailMock.Provider);
dbset.As<IQueryable<LoadDetail>>().Setup(x => x.Expression).Returns(loadDetailMock.Expression);
dbset.As<IQueryable<LoadDetail>>().Setup(x => x.ElementType).Returns(loadDetailMock.ElementType);
dbset.As<IQueryable<LoadDetail>>().Setup(x => x.GetEnumerator()).Returns(loadDetailMock.GetEnumerator());
this.readContext.Setup(x => x.)
var originCoordinate = new List<LocationCoordinates>() { new LocationCoordinates() { Radius = 75, Latitude = "34.45666", Longitude = "34.45666" } };
var destinationCoordinate = new List<LocationCoordinates>() { new LocationCoordinates() { Radius = 75, Latitude = "34.45666", Longitude = "34.45666" } };
var searchByRadius = GetSearchByRadiusObject();
// Act
var result = searchByRadius.GlobalSearchMobileByLocation(loadDetail, originCoordinate, destinationCoordinate, 75, 75);
// Assert to be written
}
The exception it throwing :
Unable to cast object of type 'System.Linq.Expressions.ConstantExpression' to type 'Microsoft.EntityFrameworkCore.Query.QueryRootExpression'.
At line loadDetails = _readContext.LoadDetails.FromSqlRaw(rawsql.ToString()).AsNoTracking().AsQueryable();
Please help me out.
Stack Trace :
at System.Runtime.CompilerServices.CastHelpers.ChkCast_Helper(Void* toTypeHnd, Object obj)
at Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.GenerateFromSqlQueryRoot(IQueryable source, String sql, Object[] arguments, String memberName)
at Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.FromSqlRaw[TEntity](DbSet`1 source, String sql, Object[] parameters)
at NTG.FH.LoadBoard.Service.Common.SearchByRadius.<GlobalSearchMobileByLocation>d__4.MoveNext() in C:\loadboard\src\NTG.FH.LoadBoard.Service\Common\SearchByRadius.cs:line 125

C# Convert string to if condition

Is it possible to convert string
"value > 5 && value <= 10"
to if statement?
if (value > 5 && value <= 10)
{
//do something
}
I have conditions stored as strings in DB, so it must be a dynamic conversion
Instead you can treat it as a javascript line and can get this done using Windows Script Engines, provided value is a real value instead of variable name.
if(ScriptEngine.Eval("jscript", "value > 5 && value <= 10"))
{
//Your logic
}
Or if its a variable then you can build a JS function like below to accomplish this:
using (ScriptEngine engine = new ScriptEngine("jscript"))
{
string JSfunction = "MyFunc(value){return " + "value > 5 && value <= 10" + "}";
ParsedScript parsed = engine.Parse(JSfunction);
if(parsed.CallMethod("MyFunc", 3))
{
// Your Logic
}
}
Use Compute method of System.Data.DataTable:
static class ExpressionHelper
{
private static readonly DataTable dt = new DataTable();
private static readonly Dictionary<string, string> expressionCache = new Dictionary<string, string>();
private static readonly Dictionary<string, object> resultCache = new Dictionary<string, object>();
// to be amended with necessary transforms
private static readonly (string old, string #new)[] tokens = new[] { ("&&", "AND"), ("||", "OR") };
public static T Compute<T>(this string expression, params (string name, object value)[] arguments) =>
(T)Convert.ChangeType(expression.Transform().GetResult(arguments), typeof(T));
private static object GetResult(this string expression, params (string name, object value)[] arguments)
{
foreach (var arg in arguments)
expression = expression.Replace(arg.name, arg.value.ToString());
if (resultCache.TryGetValue(expression, out var result))
return result;
return resultCache[expression] = dt.Compute(expression, string.Empty);
}
private static string Transform(this string expression)
{
if (expressionCache.TryGetValue(expression, out var result))
return result;
result = expression;
foreach (var t in tokens)
result = result.Replace(t.old, t.#new);
return expressionCache[expression] = result;
}
}
Usage
var expr = "value > 5 && value <= 10";
var a1 = expr.Compute<bool>(("value", 5)); // false
var a2 = expr.Compute<bool>(("value", 7)); // true
var a3 = expr.Compute<bool>(("value", 11)); // false
You could use Linq.Expression to build up the expression tree, the one you provided:
"value > 5 && value <= 10"
var val = Expression.Parameter(typeof(int), "x");
var body = Expression.And(
Expression.MakeBinary(ExpressionType.GreaterThan, val, Expression.Constant(5)),
Expression.MakeBinary(ExpressionType.LessThanOrEqual, val, Expression.Constant(10)));
var lambda = Expression.Lambda<Func<int, bool>>(exp, val);
bool b = lambda.Compile().Invoke(6); //true
bool b = lambda.Compile().Invoke(11); //false
This is just an example to get some idea, still you need a smart way to parse and build the tree.
I am afraid that you will have to create the simple parser for that.
You can try use something like FParsec. This is an F# parser library. I am not aware of such code in C#

Find and remove parameter declaration inside Expression.Block

I know how to replace a parameter with ExpressionVisitor but I was wondering if there's a way to remove a parameter from a Expression.Block.
Ideally I should crawl the entire Expression tree and remove the parameter every time it is declared inside a Block.
Any idea how to do that with ExpressionVisitor?
A simple class to remove local variables from a BlockExpression and replace them with whatever you want.
public class BlockVariableRemover : ExpressionVisitor
{
private readonly Dictionary<Expression, Expression> replaces = new Dictionary<Expression, Expression>();
public readonly Func<ParameterExpression, int, Expression> Replacer;
public BlockVariableRemover(Func<ParameterExpression, int, Expression> replacer)
{
Replacer = replacer;
}
protected override Expression VisitBlock(BlockExpression node)
{
var removed = new List<Expression>();
var variables = node.Variables.ToList();
for (int i = 0; i < variables.Count; i++)
{
var variable = variables[i];
var to = Replacer(variable, i);
if (to != variable)
{
removed.Add(variable);
replaces.Add(variable, to);
variables.RemoveAt(i);
i--;
}
}
if (removed.Count == 0)
{
return base.VisitBlock(node);
}
var expressions = node.Expressions.ToArray();
for (int i = 0; i < expressions.Length; i++)
{
expressions[i] = Visit(expressions[i]);
}
foreach (var rem in removed)
{
replaces.Remove(rem);
}
return Expression.Block(variables, expressions);
}
public override Expression Visit(Expression node)
{
Expression to;
if (node != null && replaces.TryGetValue(node, out to))
{
return base.Visit(to);
}
return base.Visit(node);
}
}
Use it like:
Expression<Func<int, int>> exp;
{
var var1 = Expression.Variable(typeof(int), "var1");
var var2 = Expression.Variable(typeof(long), "var2");
var par1 = Expression.Parameter(typeof(int), "par1");
var block = Expression.Block(new[] { var1, var2 }, Expression.Increment(var1));
exp = Expression.Lambda<Func<int, int>>(block, par1);
// Test
var compiled = exp.Compile();
Console.WriteLine(compiled(10));
}
// Begin replace
{
var par1 = exp.Parameters[0];
var block2 = new BlockVariableRemover(
// ix is the index of the variable,
// return x if you don't want to modify,
// return whatever you want (even Expression.Empty()) to do
// a replace
(x, ix) => ix == 0 && x.Type == typeof(int) ? par1 : x)
.Visit(exp.Body);
// Final result
var exp2 = Expression.Lambda<Func<int, int>>(block2, par1);
// Test
var compiled = exp2.Compile();
Console.WriteLine(compiled(10));
}

Pythagorean theorem and Autocad objects

I've been trying to use that Math.Sqrt method but I'm not getting very far. Are the ###'s maybe screwing me up? I'm getting unable to convert errors when trying to call the length/elevation variables using that method. Should I approach this from a different direction?
Full disclosure: newbie.
public partial class Form1 : Form
{
AcDb.ObjectId id = AcDb.ObjectId.Null;
public Form1()
{
InitializeComponent();
}
public void GetVertices()
{
AcAp.Document doc = AcAp.Application.DocumentManager.MdiActiveDocument;
AcDb.Database db = doc.Database;
AcEd.Editor ed = doc.Editor;
using (var doclock = doc.LockDocument())
{
using (var trans = db.TransactionManager.StartTransaction())
{
var options = new AcEd.PromptEntityOptions("\nSelect a Line:");
options.SetRejectMessage("That is not Line" + "\n");
options.AddAllowedClass(typeof(AcDb.Line), true);
var result = ed.GetEntity(options);
if (result.Status != AcEd.PromptStatus.OK)
return;
this.id = result.ObjectId;
var line = (AcDb.Line)trans.GetObject(this.id, AcDb.OpenMode.ForRead);
var vertexClass = AcTrx.RXClass.GetClass(typeof(AcDb.Line));
var length = line.Length.ToString("#.##");
tbLength.Text = length;
var northing = line.EndPoint.Y.ToString("#.##");
tbNorthing.Text = northing;
var easting = line.EndPoint.X.ToString("#.##");
tbEasting.Text = easting;
var elevation = line.EndPoint.Z.ToString("#.##");
tbElevation.Text = elevation;
var endpoint = line.EndPoint.ToString();
tbEndpoint.Text = endpoint;
var slope = Math.Sqrt(elevation/length) //something something something
}
}
}
private void toolStripButton1_Click(object sender, EventArgs e)
{
this.Hide();
GetVertices();
this.Show();
}
}
elevation and length are strings. You cannot perform math on strings!
Try
double slope = Math.Sqrt(line.EndPoint.Z / line.Length);
Or change your code to:
double length = line.Length
tbLength.Text = length.ToString("#.##");
double northing = line.EndPoint.Y;
tbNorthing.Text = northing.ToString("#.##");
double easting = line.EndPoint.X;
tbEasting.Text = easting.ToString("#.##");
double elevation = line.EndPoint.Z;
tbElevation.Text = elevation.ToString("#.##");
double endpoint = line.EndPoint;
tbEndpoint.Text = endpoint.ToString();
double slope = Math.Sqrt(elevation/length);
And also I suggest you to use var only when it represents a real simplification and the type can easily be inferred or if the exact type is not of great interest as for LINQ query results with complicated IGouping<whatever> constructs possibly involving anonymous types.
var x = new Dictionary<string, List<Entity<Person>>>();
.... is a simplyfication and yet the type is visible as you can read it on the same line;
var i = 15;
is not a simplyfication over
int i = 15;
I would never use var as substitution for simple types like int, string, double etc.

Lambda to SQL Translation

So I am having fun with myself and C#, by creating a nice Data Access Layer.
I have the following method that translates a simple expression to a SQL where clause, but it only works with the following
var people = DataAccessLayer.SelectAllPeople(x => x.Name == "Donald");
//Do some changes to the list
people[0].Surname = "Jansen"
var m = p.BuildUpdateQuerry(people[0], x => x.PersonID == 1);
I get the following result
UPDATE People SET Name='Donald',Surname='Jansen' WHERE (PersonID = 1)
But now If I do the following
var m = p.BuildUpdateQuerry(people[0], x => x.PersonID == people[0].PersonID);
I get the following Result
UPDATE People SET Name='Donald',Surname='Jansen' WHERE (PersonID = value(ReflectionExampleByDonaldJansen.Program+<>c__DisplayClass0).people.get_Item(0).PersonID)
My method I am using to Convert the Lambda to String is
public static string GetWhereClause<T>(Expression<Func<T, bool>> expression)
{
var name = expression.Parameters[0].ToString();
var body = expression.Body.ToString().Replace("\"", "'");
body = body.Replace("OrElse", "OR");
body = body.Replace("AndAlso", "AND");
body = body.Replace("==", "=");
body = body.Replace("!=", "<>");
body = body.Replace(string.Format("{0}.", name), "");
return body;
}
So far this is very basic and real fun to do, but I have no Idea how to overcome this XDXD, any Suggestions or Codes ?
I managed to solve it myself, hehehe here is what I did, not finnished yet but maybe someone else might find it usefull
public static string GetWhereClause<T>(Expression<Func<T, bool>> expression)
{
return GetValueAsString(expression.Body);
}
public static string GetValueAsString(Expression expression)
{
var value = "";
var equalty = "";
var left = GetLeftNode(expression);
var right = GetRightNode(expression);
if (expression.NodeType == ExpressionType.Equal)
{
equalty = "=";
}
if (expression.NodeType == ExpressionType.AndAlso)
{
equalty = "AND";
}
if (expression.NodeType == ExpressionType.OrElse)
{
equalty = "OR";
}
if (expression.NodeType == ExpressionType.NotEqual)
{
equalty = "<>";
}
if (left is MemberExpression)
{
var leftMem = left as MemberExpression;
value = string.Format("({0}{1}'{2}')", leftMem.Member.Name, equalty, "{0}");
}
if (right is ConstantExpression)
{
var rightConst = right as ConstantExpression;
value = string.Format(value, rightConst.Value);
}
if (right is MemberExpression)
{
var rightMem = right as MemberExpression;
var rightConst = rightMem.Expression as ConstantExpression;
var member = rightMem.Member.DeclaringType;
var type = rightMem.Member.MemberType;
var val = member.GetField(rightMem.Member.Name).GetValue(rightConst.Value);
value = string.Format(value, val);
}
if (value == "")
{
var leftVal = GetValueAsString(left);
var rigthVal = GetValueAsString(right);
value = string.Format("({0} {1} {2})", leftVal, equalty, rigthVal);
}
return value;
}
private static Expression GetLeftNode(Expression expression)
{
dynamic exp = expression;
return ((Expression)exp.Left);
}
private static Expression GetRightNode(Expression expression)
{
dynamic exp = expression;
return ((Expression)exp.Right);
}
You can use the inbuilt extension methods of System.Linq to convert lambda expressions to SQL.
See the code below...
var Enames = emp.Employees.Select ( e => e.EmployeeName );
Console.WriteLine ( Enames );
The output which I got..
SELECT
[Extent1].[EmployeeName] AS [EmployeeName]
FROM [dbo].[Employee] AS [Extent1]
I had the same problem and started to solve it some time ago. Take a look on LambdaSql.
For now it contains basic scenarios for select clause and where filters. Setting fields, where, group by, having, order by, joins, nested queries are already supported. Insert, Update and Delete are going to be supported later.
Example:
var qry = new SqlSelect
(
new SqlSelect<Person>()
.AddFields(p => p.Id, p => p.Name)
.Where(SqlFilter<Person>.From(p => p.Name).EqualTo("Sergey"))
, new SqlAlias("inner")
).AddFields<Person>(p => p.Name);
Console.WriteLine(qry.ParametricSql);
Console.WriteLine("---");
Console.WriteLine(string.Join("; ", qry.Parameters
.Select(p => $"Name = {p.ParameterName}, Value = {p.Value}")));
Output:
SELECT
inner.Name
FROM
(
SELECT
pe.Id, pe.Name
FROM
Person pe
WHERE
pe.Name = #w0
) AS inner
---
Name = #w0, Value = Sergey
See more here https://github.com/Serg046/LambdaSql

Categories