Tide Gu
Tide Gu

Reputation: 835

C# Find continuous values in List quickly

I am working on a project that plotting the mouse tracking. The MouseInfo class is defined like:

public class MouseInfo {
    public readonly long TimeStamp;
    public readonly int PosX;
    public readonly int PosY;
    public int ButtonsDownFlag;
}

I need to find a way to extract the mouse positions from a List<MouseInfo> which ButtonsDownFlag has at least 2 continuous 1s and group them together, so that I can distinguish clicks and draggings, which will then being used for plotting.

The current way I am doing is to iterate through the list, and add the found values one by one to other lists, which is very slow, expensive and the code looks messy. I wonder if there is any more elegant way to do it? Will Linq help?

For example, I have the recording of below:

(t1, x1, y1, 0)
(t2, x2, y2, 1)
(t3, x3, y3, 1)
(t4, x4, y4, 0)
(t5, x5, y5, 1)
(t6, x6, y6, 0)
(t7, x7, y7, 1)
(t8, x8, y8, 1)
(t9, x9, y9, 1)
(ta, xa, ya, 0)
(tb, xb, yb, 2) <- Yes, ButtonDownFlag can be 2 for RightClicks or even 3 for both buttons are down
(tc, xc, yc, 0)
(td, xd, yd, 2)
(te, xe, ye, 2)

I want two Lists (or similiar presentation) which are

((t2, x2, y2), (t2, x3, y3), (t7, x7, y7), (t7, x8, y8), (t7, x9, y9))

and

((x5, y5, 1), (xb, yb, 2), (xd, yd, 2), (xe, ye, 2))

Note:

  1. In the first list, I need TimeStamp in the subsequence elements being altered to the first element's TimeStamp, so that I can group in later plotting.
  2. In the second list, I don't care TimeStamp but I do care the ButtonDownFlag
  3. I don't mind if ButtonDownFlag exists in the first list, nor TimeStamp exists in the second list.
  4. Continuous "Right Clicks" are treated as separate "Right Clicks" rather than "Right dragging".

Upvotes: 1

Views: 683

Answers (3)

Tide Gu
Tide Gu

Reputation: 835

Just trying to share some knowledge - I found GroupAdjacent solved my problem very well (along with some tweeks for the plotting in a later stage).

The performance is surely not the best (compare to for loop) but I feel the code is more elegant!

Reference: https://blogs.msdn.microsoft.com/ericwhite/2008/04/20/the-groupadjacent-extension-method/

public static class LocalExtensions {
    public static IEnumerable<IGrouping<TKey, TSource>> GroupAdjacent<TSource, TKey>(
        this IEnumerable<TSource> source,
        Func<TSource, TKey> keySelector) {
        TKey last = default(TKey);
        bool haveLast = false;
        List<TSource> list = new List<TSource>();
        foreach (TSource s in source) {
            TKey k = keySelector(s);
            if (haveLast) {
                if (!k.Equals(last)) {
                    yield return new GroupOfAdjacent<TSource, TKey>(list, last);
                    list = new List<TSource>();
                    list.Add(s);
                    last = k;
                } else {
                    list.Add(s);
                    last = k;
                }
            } else {
                list.Add(s);
                last = k;
                haveLast = true;
            }
        }
        if (haveLast)
            yield return new GroupOfAdjacent<TSource, TKey>(list, last);
    }
}

class GroupOfAdjacent<TSource, TKey> : IEnumerable<TSource>, IGrouping<TKey, TSource> {
    public TKey Key { get; set; }
    private List<TSource> GroupList { get; set; }
    IEnumerator IEnumerable.GetEnumerator() {
        return ((IEnumerable<TSource>)this).GetEnumerator();
    }
    IEnumerator<TSource> IEnumerable<TSource>.GetEnumerator() {
        foreach (TSource s in GroupList)
            yield return s;
    }
    public GroupOfAdjacent(List<TSource> source, TKey key) {
        GroupList = source;
        Key = key;
    }
}

And my working code for testing:

private class MouseInfo {
    public readonly long TimeStamp;
    public readonly int PosX;
    public readonly int PosY;
    public int ButtonsDownFlag;
    public MouseInfo(long t, int x, int y, int flag) {
        TimeStamp = t;
        PosX = x;
        PosY = y;
        ButtonsDownFlag = flag;
    }
    public override string ToString() {
        return $"({TimeStamp:D2}: {PosX:D3}, {PosY:D4}, {ButtonsDownFlag})";
    }
}

public Program() {
    List<MouseInfo> mi = new List<MouseInfo>(14);
    mi.Add(new MouseInfo(1, 10, 100, 0));
    mi.Add(new MouseInfo(2, 20, 200, 1));
    mi.Add(new MouseInfo(3, 30, 300, 1));
    mi.Add(new MouseInfo(4, 40, 400, 0));
    mi.Add(new MouseInfo(5, 50, 500, 1));
    mi.Add(new MouseInfo(6, 60, 600, 0));
    mi.Add(new MouseInfo(7, 70, 700, 1));
    mi.Add(new MouseInfo(8, 80, 800, 1));
    mi.Add(new MouseInfo(9, 90, 900, 1));
    mi.Add(new MouseInfo(10, 100, 1000, 0));
    mi.Add(new MouseInfo(11, 110, 1100, 2));
    mi.Add(new MouseInfo(12, 120, 1200, 0));
    mi.Add(new MouseInfo(13, 130, 1300, 2));
    mi.Add(new MouseInfo(14, 140, 1400, 2));

    var groups = mi.GroupAdjacent(x => x.ButtonsDownFlag);

    List<List<MouseInfo>> drags = groups.Where(x => x.Key == 1 && x.Count() > 1).Select(x => x.ToList()).ToList();
    foreach (var d in drags) 
        foreach (var item in d)
            Console.Write($"{item} ");
    Console.WriteLine();

    List<List<MouseInfo>> clicks = groups.Where(x => x.Key > 1 || (x.Key == 1 && x.Count() == 1)).Select(x => x.ToList()).ToList();
    foreach (var d in clicks) {
        foreach (var item in d)
            Console.Write($"{item} ");
        Console.WriteLine();
    }
}

[MTAThread]
static void Main(string[] args) {
    var x = new Program();
    Console.ReadLine();
    return;
}

Upvotes: 0

NetMage
NetMage

Reputation: 26927

I don't think this is too easy to follow, but it is similar to how you might handle this in APL (I used Excel to work it out). I also won't promise how fast this is - generally foreach is faster than LINQ, even if only by a small amount.

Using extension methods to implement APL's scan and compress operators and to append/prepend to IEnumerables:

public static IEnumerable<TResult> Scan<T, TResult>(this IEnumerable<T> src, TResult seed, Func<TResult, T, TResult> combine) {
    foreach (var s in src) {
        seed = combine(seed, s);
        yield return seed;
    }
}
public static IEnumerable<T> Compress<T>(this IEnumerable<bool> bv, IEnumerable<T> src) {
    var srce = src.GetEnumerator();
    foreach (var b in bv) {
        srce.MoveNext();
        if (b)
            yield return srce.Current;
    }
}
public static IEnumerable<T> Prepend<T>(this IEnumerable<T> rest, params T[] first) => first.Concat(rest);
public static IEnumerable<T> Append<T>(this IEnumerable<T> rest, params T[] last) => rest.Concat(last);

You can filter the list to groups of drags and what's not in a drag:

// create a terminal MouseInfo for scanning along the moves
var mterm = new MouseInfo { t = 0, x = 0, y = 0, b = 4 };
// find the drags for button 1 except the first row
var bRestOfDrag1s = moves.Append(mterm).Zip(moves.Prepend(mterm), (dm, em) => dm.b == 1 && dm.b == em.b).ToList();
// number the drags by finding the drag beginnings
var iLastDragNums = bRestOfDrag1s.Zip(bRestOfDrag1s.Skip(1), (fm, gm) => (!fm && gm)).Scan(0, (a, hm) => hm ? a + 1 : a).ToList();
// find the drags
var bInDrag1s = bRestOfDrag1s.Zip(bRestOfDrag1s.Skip(1), (fm, gm) => (fm || gm));
// number each drag row by its drag number
var dnmiDrags = bInDrag1s.Compress(Enumerable.Range(0, moves.Count)).Select(idx => new { DragNum = iLastDragNums[idx], mi = moves[idx] });
// group by drag number and smear first timestamp along drags
var drags = dnmiDrags.GroupBy(dnmi => dnmi.DragNum)
                     .Select(dnmig => dnmig.Select(dnmi => dnmi.mi).Select(mi => new MouseInfo { t = dnmig.First().mi.t, x = mi.x, y = mi.y, b = mi.b }).ToList()).ToList();

var clicks = bInDrag1s.Select(b => !b).Compress(moves).Where(mi => mi.b != 0).ToList();

When done, drags contains a List<List<MouseInfo>> where each sub-list is a drag. You can use SelectMany instead of the last (outside) Select to get just a flat List<MouseInfo> instead. clicks will contain a List<MouseInfo> with just the clicks.

Note that I abbreviated the MouseInfo field names.

BTW, using a for loop is considerably faster:

var inDrag = false;
var drags = new List<MouseInfo>();
var clicks = new List<MouseInfo>();
var beginTime = 0L;
for (var i = 0; i < moves.Count; ++i) {
    var curMove = moves[i];
    var wasDrag = inDrag;
    inDrag = curMove.b == 1 && (inDrag || (i + 1 < moves.Count ? moves[i + 1].b == 1 : false));
    if (inDrag) {
        if (wasDrag)
            drags.Add(new MouseInfo { t = beginTime, x = curMove.x, y = curMove.y, b = curMove.b });
        else {
            drags.Add(curMove);
            beginTime = curMove.t;
        }
    }
    else {
        if (curMove.b != 0)
            clicks.Add(curMove);
    }
}

Upvotes: 1

Paul Marshall
Paul Marshall

Reputation: 643

There is a means by which you can use LINQ to do this which will produce one list for all events which are part of a drag sequence and a separate list for individual click events.

            List<MouseInfo> mainList = new List<MouseInfo>();

            //populate mainList with some data...

            List<MouseInfo> dragList = mainList.Where
                (
                // check if the left click is pressed
                    x => x.ButtonsDownFlag == 1

                    //then check if either the previous or the following elements are also clicked
                    &&
                    (
                        //if this isnt the first element in the list, check the previous one
                        (mainList.IndexOf(x) != 0 ? mainList[mainList.IndexOf(x) - 1].ButtonsDownFlag == 1 : false)

                        //if this isnt the last element in the list, check the next one
                        || (mainList.IndexOf(x) != (mainList.Count - 1) ? mainList[mainList.IndexOf(x) + 1].ButtonsDownFlag == 1 : false)
                    )
                ).ToList();

            List<MouseInfo> clickList = mainList.Where
                (
                    // check if the left/right or both click is pressed
                    x => (x.ButtonsDownFlag == 1 || x.ButtonsDownFlag == 2 || x.ButtonsDownFlag == 3)

                    //then make sure that neither of the previous or following elements are also clicked
                    &&
                    (mainList.IndexOf(x) != 0 ? mainList[mainList.IndexOf(x) - 1].ButtonsDownFlag != 1 : true)

                    &&                 
                    (mainList.IndexOf(x) != (mainList.Count - 1) ? mainList[mainList.IndexOf(x) + 1].ButtonsDownFlag != 1 : true)

                ).ToList();

This approach does have the limitation of not "labelling" each sequence of drags with the same timestamp.

An alternative would be to do this logic at point of data capture. When each data point is captured, if it has a "ButtonDown" value, check the previous data point. If that data point is also a "ButtonDown" add them both (or however many you end up with in the sequence) to your "dragList", otherwise add it to the "clickList". For this option I would also be tempted to add some logic to separate out your different drag sequences. You have done this by changing the time stamp of the subsequent points, I would instead be tempted to create your "dragList" as a dictionary instead. With each sequences of drags put into a different distinct key.

Upvotes: 1

Related Questions