Имеется одномерный массив типа decimal.
Задача: Перебрать все возможные соотношения значений массива с целью поиска максимально релевантного. Функция определения релевантности есть. Единственное, что приходит в голову - создавать и рандомно менять ("мутировать") "генетический код" соотношения.
Пример: необходимо x[1] + x[5] * x[7] / x[3] представлять в виде 001A005C007D003, где трехзначные числа - номер элемента массива, а символы A,B,C,D - соответственно +,-,* и /. Это позволит отбрасывать наименее релевантные соотношения и запускать в "мутацию" более релевантные.
Вопрос: понятно, что сформировать строку исполняемого кода из такого "генетического кода" (простите за тавтологию) нетрудно, но мы получим просто переменную типа string
, и как ее потом выполнить прямо в программе? И возможно ли это?
Хей, давно что-то никто не писал парсеров. Давайте-ка разомнёмся и напишем.
Итак, для начала структура данных, которая описывает части выражения. Это стандартная реализация discriminated union:
class Node { }
class Index : Node { public int Value { get; set; } }
class BinaryOperation : Node
{
public Node Left { get; set; }
public Node Right { get; set; }
public int Precedence { get; set; }
public string Name { get; set; }
}
Теперь, лексер. Поскольку я ленивый, то для токенов не буду создавать отдельную структуру данных, а просто использую Node
и его варианты. Лексер у нас получится очень простой:
// вспомогательная таблица операций и их приоритетов
Dictionary<char, (string, int)> Operations = new Dictionary<char, (string, int)>()
{
['A'] = ("+", 1),
['B'] = ("-", 1),
['C'] = ("*", 2),
['D'] = ("/", 2)
};
// сам лексер
IEnumerable<Node> Tokenize(string s)
{
int i = 0;
while (i < s.Length)
{
switch (s[i])
{
// это операция? создаём узел операции
case char c when Operations.ContainsKey(c):
(var name, var prec) = Operations[c];
yield return new BinaryOperation() { Precedence = prec, Name = name };
i++;
break;
// это число, создаём узел индекса
case char c when char.IsDigit(c):
if (i + 2 >= s.Length || !int.TryParse(s.Substring(i, 3), out var num))
throw new FormatException();
yield return new Index() { Value = num };
i += 3;
break;
default:
throw new FormatException();
}
}
}
С лексером всё.
Дальше, парсер. Для построения синтаксического дерева из арифметического выражения воспользуемся классическим алгоритмом сортировочной станции.
Node Scan(string s)
{
Stack<Node> output = new Stack<Node>();
Stack<BinaryOperation> operations = new Stack<BinaryOperation>();
void Shift()
{
var op = operations.Pop();
op.Right = output.Pop();
op.Left = output.Pop();
output.Push(op);
}
foreach (var token in Tokenize(s))
{
switch (token)
{
case Index i:
output.Push(i);
break;
case BinaryOperation bin:
while (operations.Count > 0 && operations.Peek().Precedence >= bin.Precedence)
Shift();
operations.Push(bin);
break;
}
}
while (operations.Count > 0)
Shift();
return output.Single();
}
Отлично, теперь бекэнд нашего компилятора. Скомпилируем Node
в LINQ Expression, чтобы потом скомпилировать в нормальную функцию. Здесь тоже никаких проблем. Единственная хитрость — нам нужен «общий» параметр p
.
using System.Linq;
using System.Linq.Expressions;
Expression<Func<int[], int>> Build(Node n)
{
var p = Expression.Parameter(typeof(int[]), "arr");
return BuildRec(n);
Expression<Func<int[], int>> BuildRec(Node node)
{
switch (node)
{
case Index idx:
return Expression.Lambda<Func<int[], int>>(
Expression.ArrayAccess(p, Expression.Constant(idx.Value)), p);
case BinaryOperation bin:
var l = BuildRec(bin.Left);
var r = BuildRec(bin.Right);
var op = GetOp(bin.Name);
return Expression.Lambda<Func<int[], int>>(op(l.Body, r.Body), p);
default:
throw new ArgumentException();
}
}
Func<Expression, Expression, BinaryExpression> GetOp(string name)
{
switch (name)
{
case "+": return Expression.Add;
case "-": return Expression.Subtract;
case "*": return Expression.Multiply;
case "/": return Expression.Divide;
default: throw new ArgumentException();
}
}
}
Готово, проверяем!
// входная строка
var s = "001A005C007D003";
// получаем функцию
var node = Scan(s);
var func = Build(node).Compile();
// данные для функции
var array = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var result = func(array);
Получаем результат 12, как и ожидалось.
Проверку ошибок добавьте по вкусу.
Если вы хотите в самом сложном случае использовать базовые арифметические операции, то нет смысла городить код сложными конструкциями.
Представим, что у вас есть массив чисел с плавающей точкой и выражение типа
x[1] + x[5] * x[7] / x[3]
Для начала можно заменить все переменные значениями из массива
string SetValues(string expression, double[] array)
{
var result = expression;
var regex = new Regex("x\\[(:?[0-9]+)\\]", RegexOptions.Compiled);
foreach (Match match in regex.Matches(expression))
{
var ind = int.Parse(match.Groups[1].Value);
result = result.Replace(match.Value, array[ind]
.ToString(CultureInfo.InvariantCulture));
}
return result;
}
Получим что то вроде
9.04579045206578 + 3.56569983231169 * 5.23985354939469 / 9.11708742339028
Теперь посчитаем выражение (старый добрый трюк с DataTable
)
double Eval(string input)
{
return Convert.ToDouble(new DataTable().Compute(input, null));
}
В итоге пользоваться можно этой штукой как то так:
void Main()
{
// Генерим массив со случайными числами
var random = new Random();
var data = Enumerable.Range(0, 10).Select(x=>random.NextDouble()*10).ToArray();
// Наше выражение
var expression = "x[1] + x[5] * x[7] / x[3]";
// результат, который ожидаем (для проверки)
var realResult = data[1] + data[5] * data[7] / data[3];
// Тут заменяем переменные значениями
var expressionToEvaluate = SetValues(expression, data);
// Тут наш вычисленный результат
var result = Eval(expressionToEvaluate);
// выводим все в консоль
Console.WriteLine($"real:{realResult}");
Console.WriteLine($"calculated:{result}");
Console.WriteLine($"from {expressionToEvaluate}");
}
Получим в консоли
real: 11,0951011644409
calculated: 11,0951011644409
from 9.04579045206578 + 3.56569983231169 * 5.23985354939469 / 9.11708742339028
Подробнее, что можно себе позволить использовать в выражении, описано тут https://msdn.microsoft.com/ru-ru/library/system.data.datacolumn.expression(v=vs.110).aspx
Оборудование для ресторана: новинки профессиональной кухонной техники
Частный дом престарелых в Киеве: комфорт, забота и профессиональный уход
Доброго времени суток друзьяЯ в данный момент изучаю C#, но в дальнейшем планирую заниматься 2Д играми на Unity3D
Здравствуйте! Расскажу я немного о себе и про мою ситуацию: Я бекенд разработчикПишу на PHP и Node
Здрасвуйте, пишу бота на VK API который выдает аккаунты, и мне нужно записывать в какое время он выдал аккаунт, записывает он в формате G