Использование LINQ для поиска в байтовом массиве всех подмассивов, которые запускаются / останавливаются с определенным байтом - PullRequest
11 голосов
/ 06 января 2011

Я имею дело с приложением COM-порта, и у нас есть определенная структура пакетов переменной длины, с которой я общаюсь с микроконтроллером. Пакет имеет разделители для начального и конечного байтов. Проблема в том, что иногда буфер чтения может содержать посторонние символы. Кажется, что я всегда получу весь пакет, только немного болтовни до / после фактических данных. Поэтому у меня есть буфер, к которому я добавляю данные всякий раз, когда новые данные поступают из COM-порта. Каков наилучший способ поиска в этом буфере любых возможных вхождений моего пакета? Например:

Скажите, что мой разделитель пакетов 0xFF, и у меня есть массив как таковой

{ 0x00, 0xFF, 0x02, 0xDA, 0xFF, 0x55, 0xFF, 0x04 }

Как создать функцию / LINQ-statment, которая возвращает все подмассивы, начинающиеся и заканчивающиеся разделителем (почти как скользящий коррелятор с подстановочными знаками)?

Образец будет возвращать следующие 3 массива:

{0xFF, 0x02, 0xDA, 0xFF}, {0xFF, 0x55, 0xFF}, and
{0xFF, 0x02, 0xDA, 0xFF, 0x55, 0xFF}

Ответы [ 6 ]

16 голосов
/ 06 января 2011

Хотя ответ Тристана технически верен, он делает множество копий исходного массива одновременно. Если начальный массив большой и имеет несколько разделителей, он быстро увеличивается. Такой подход позволяет избежать значительного потребления памяти, используя только исходный массив и массив для текущего оцениваемого сегмента.

public static List<ArraySegment<byte>> GetSubArrays(this byte[] array, byte delimeter)
{
    if (array == null) throw new ArgumentNullException("array");

    List<ArraySegment<byte>> retval = new List<ArraySegment<byte>>();

    for (int i = 0; i < array.Length; i++)
    {
        if (array[i] == delimeter)
        {
            for (int j = i + 1; j < array.Length; j++)
            {
                if (array[j] == delimeter)
                {
                    retval.Add(new ArraySegment<byte>(array, i + 1, j - i - 1));
                }
            }
        }
    }

    return retval;
}

Может использоваться как таковой:

static void Main(string[] args)
{
    byte[] arr = new byte[] { 0x00, 0xFF, 0x02, 0xDA, 0xFF, 0x55, 0xFF, 0x04 };
    List<ArraySegment<byte>> retval = GetSubArrays(arr, 0xFF);

    // this also works (looks like LINQ):
    //List<ArraySegment<byte>> retval = arr.GetSubArrays(0xFF);

    byte[] buffer = new byte[retval.Select(x => x.Count).Max()];
    foreach (var x in retval)
    {
        Buffer.BlockCopy(x.Array, x.Offset, buffer, 0, x.Count);
        Console.WriteLine(String.Join(", ", buffer.Take(x.Count).Select(b => b.ToString("X2")).ToArray()));
    }


    Console.ReadLine();
}
5 голосов
/ 06 января 2011

Вот как вы можете сделать это, используя LINQ ...

int[] list = new int[] { 0x00, 0xFF, 0x02, 0xDA, 0xFF, 0x55, 0xFF, 0x04 };
int MAXLENGTH = 10;

var windows = list.Select((element, i) => list.Skip(i).Take(MAXLENGTH));
var matched = windows.Where(w => w.First() == 0xFF);
var allcombinations = matched.SelectMany(m => Enumerable.Range(1, m.Count())
          .Select(i => m.Take(i)).Where(x => x.Count() > 2 && x.Last() == 0xFF));

Или используя индексы:

int length = list.Count();
var indexes = Enumerable.Range(0, length)
              .SelectMany(i => Enumerable.Range(3, Math.Min(length-i, MAXLENGTH))
              .Select(count => new {i, count}));
var results = indexes.Select(index => list.Skip(index.i).Take(index.count))
              .Where(x => x.First() == 0xFF && x.Last() == 0xFF);
2 голосов
/ 06 января 2011

Если вы действительно хотите использовать LINQ, это должно работать довольно быстро (даже если не так быстро, как старый добрый цикл for):

public static IEnumerable<T[]> GetPackets<T>(this IList<T> buffer, T delimiter)
{
    // gets delimiters' indexes
    var delimiterIdxs = Enumerable.Range(0, buffer.Count())
                                  .Where(i => buffer[i].Equals(delimiter))
                                  .ToArray();

    // creates a list of delimiters' indexes pair (startIdx,endIdx)
    var dlmtrIndexesPairs = delimiterIdxs.Take(delimiterIdxs.Count() - 1)
                                         .SelectMany(
                                                     (startIdx, idx) => 
                                                     delimiterIdxs.Skip(idx + 1)
                                                                  .Select(endIdx => new { startIdx, endIdx })
                                                    );
    // creates array of packets
    var packets = dlmtrIndexesPairs.Select(p => buffer.Skip(p.startIdx)
                                                      .Take(p.endIdx - p.startIdx + 1)
                                                      .ToArray())
                                   .ToArray();

    return packets;
}
0 голосов
/ 06 января 2011

Хотя структура разделителя кажется немного расплывчатой, я бы не стал использовать linq и делал бы что-то похожее на приведенное ниже (обширные тесты не выполнялись). Он вернет все подмножества (байтов , окруженные разделителем), без включения разделителя (в любом случае, это данность, зачем его включать?). Он также не возвращает объединение результатов, но его всегда можно собрать вручную.

public IEnumerable<byte[]> GetArrays(byte[] data, byte delimiter)
{
    List<byte[]> arrays = new List<byte[]>();
    int start = 0;
    while (start >= 0 && (start = Array.IndexOf<byte>(data, delimiter, start)) >= 0)
    {
        start++;
        if (start >= data.Length - 1)
        {
            break;
        }

        int end = Array.IndexOf<byte>(data, delimiter, start);
        if (end < 0)
        {
            break;
        }

        byte[] sub = new byte[end - start];
        Array.Copy(data, start, sub, 0, end - start);
        arrays.Add(sub);
        start = end;
    }

    return arrays;
}
0 голосов
/ 06 января 2011

Вы можете сделать это с помощью агрегатора Linq, но это гораздо менее просто, чем другие решения, предложенные здесь, также пришлось добавить специальный случай для покрытия расширения уже заполненных массивов, как вы предложили выше.

byte[] myArray = new byte[] { 0x00, 0xFF, 0x02, 0xDA, 0xFF, 0x55, 0xFF, 0x04 };
var arrayList = myArray.Aggregate(
                new { completedLists = new List<List<byte>>(), 
                      activeList = new List<byte>() },
                (seed, s) =>
                {
                    if (s == 0xFF)
                    {
                        if (seed.activeList.Count == 0)
                        {
                            seed.activeList.Add(s);
                        }
                        else
                        {
                            seed.activeList.Add(s);
                            var combinedLists = new List<List<byte>>();

                            foreach (var l in seed.completedLists)
                            {
                                var combinedList = new List<byte>(l);
                                combinedList.AddRange(seed.activeList.Skip(1));
                                combinedLists.Add(combinedList);
                            }
                            seed.completedLists.AddRange(combinedLists);
                            seed.completedLists.Add(new List<byte>(seed.activeList));
                            seed.activeList.Clear();
                            seed.activeList.Add(s);
                        }
                    }
                    else
                    {
                        if (seed.activeList.Count > 0)
                            seed.activeList.Add(s);
                    }
                    return seed;
                }).completedLists;
0 голосов
/ 06 января 2011

Я бы не стал делать это с linq, поэтому вот обычный метод, который возвращает тот же вывод, что и вы.

public List<byte[]> GetSubArrays(byte[] array, byte delimeter)
{
  if (array == null) throw new ArgumentNullException("array");

  List<byte[]> subArrays = new List<byte[]>();

  for (int i = 0; i < array.Length; i++)
  {
    if (array[i] == delimeter && i != array.Length - 1)
    {
      List<byte> subList = new List<byte>() { delimeter };

      for (int j = i+1; j < array.Length; j++)
      {
        subList.Add(array[j]);
        if (array[j] == delimeter)
        {
          subArrays.Add(subList.ToArray());
        }
      }
    }
  }

  return subArrays;
}

Если это должно быть лямбда-выражение на месте, просто изменитепервая строка к (byte[] array, byte delimeter) => (без модификаторов метода и имени) и называется так.

...