根据开始/结束事件将活动流中的事件分组

时间:2019-12-05 14:01:04

标签: c# system.reactive reactive

我观察到一个反应性流Observable<Event>,当前它直接发出事件。我希望根据BEGIN / END事件将这些事件流在内部小组的支持下进行分组。

输入流

我有一系列类似的事件:

Event(type = Data, groupId = 1)
Event(type = BeginGroup, groupId = 2)   // outer group begins
Event(type = Data, groupId = 2)
Event(type = BeginGroup, groupId = 3)   // inner group begins
Event(type = Data, groupId = 3)
Event(type = EndGroup, groupId = 3)     // inner group ends
Event(type = EndGroup, groupId = 2)     // outer group ends
Event(type = Data, groupId = 4)
Event(type = Data, groupId = 5)

编辑-其他前提条件:

我在示例数据中添加了ID,但是通常我不需要ID。该流将注意满足以下条件:

  • 每个BeginGroup事件都会在某个时间之后跟随相应的EndGroup事件
  • 组中的
  • 事件(在我的示例中,具有相同的组构想)将始终位于流内的begin / end事件中,因此可以保证顺序(如上述示例)

期望的输出流

因此,我确保每个事件都属于其上的组,或者如果它不是真实组的一部分,则具有唯一的ID。我想将上述9个事件流归为以下4个事件流:

Event(type = Data, groupId = 1)
GroupEvent(groupId = 2, data = <LIST of Events and/or sub groups>) with following data:
    data = [
        Event(type = BeginGroup, groupId = 2)
        Event(type = Data, groupId = 2)
        GroupEvent(groupId = 3, data = <LIST of Events and/or sub groups>) with following data:
            data = [
                Event(type = BeginGroup, groupId = 3)
                Event(type = Data, groupId = 3)
                Event(type = EndGroup, groupId = 3)
            ]
        Event(type = EndGroup, groupId = 2)
    ]
Event(type = Data, groupId = 4)
Event(type = Data, groupId = 5)

我想要的是-逻辑

我想在发生BeginGroup类型的事件后立即开始分组,直到发生正确的EndGroup事件为止,并将这两个事件之间的所有事件(包括最终嵌套的组事件)分组。开始/结束组事件之外的元素仅作为单个事件传递。

这是我到目前为止尝试过的

我在问这个问题之前尝试了一些东西,但是我来自Java,甚至在那儿我很少使用window / buffer运算符,并且对它们的使用经验很少。我看到C#中有一个类似GroupByUntil的运算符,所以我尝试使用它,但是在我的示例中它从未发出任何东西。

var eventObservable: Observable<Event> = ...

// 1) make the observable hot so it can be resued inside the groupbyuntil operator
eventObservable = observable.Publish().RefCount();
var res = eventObservable
    .GroupByUntil(
        e => e.GroupId, // selector for groups => the group id can be used here
        grp => eventObservable.Where(e => e.GroupId != grp.Key) // stop a group as soon as the group id changes
    )
    .SelectMany(data => data.ToList()) // flatten the observable
    .Select(data => {
        // Convert the list of Events to GroupEvent if it contains more than 1 event
        var list = data.ToList();
        if (list.Count == 1)
            return list[0];
        return new GroupEvent(list);
    })

这种方法行不通,它根本不会发出任何东西(尽管eventObservable确实正确地发出了它的物品)。此外,它缺少嵌套组的支持(从理论上讲是否可行)。

有人可以向我解释如何解决我的问题吗?

1 个答案:

答案 0 :(得分:2)

首先进行代码转储,然后进行解释。

我写的数据类:

public enum EventType
{
    Data,
    BeginGroup,
    EndGroup,
    Group
}

public class Event<T>
{
    public Event(int id, EventType type, T data)
    {
        this.Id = id;
        this.Type = type;
        this.Data = data;
    }

    public int Id { get; set; }
    public EventType Type { get; set; }
    public T Data { get; set;}
}

public class GroupEvent<T> : Event<T> {
    public GroupEvent(int id, IEnumerable<Event<T>> events)
        : base(id, EventType.Group, default(T))
    {
        this.ChildData = events;
    }

    public IEnumerable<Event<T>> ChildData { get; set; }
}

这是逻辑扩展方法(使用Nuget包System.Collections.Immutable):

public static class X
{
    public static IObservable<Event<T>> GroupEvents<T>(this IObservable<Event<T>> source)
    {
        return source
            .Scan((groupId: 1, stack: ImmutableStack<ImmutableList<Event<T>>>.Empty, output: (Event<T>)null), (state, inEvent) =>
            {
                if(inEvent.Type == EventType.Data)
                {
                    if (state.stack.IsEmpty)
                        return (state.groupId + 1, state.stack, new Event<T>(state.groupId, EventType.Data, inEvent.Data));
                    else
                    {
                        var newEvent = new Event<T>(state.stack.Peek()[0].Id, EventType.Data, inEvent.Data);
                        var newList = state.stack.Peek().Add(newEvent);
                        var newStack = state.stack.Pop().Push(newList);
                        return (state.groupId, newStack, null);
                    }
                }

                if(inEvent.Type == EventType.BeginGroup)
                {
                    var newEvent = new Event<T>(state.groupId, EventType.BeginGroup, inEvent.Data);
                    return (state.groupId + 1, state.stack.Push(ImmutableList<Event<T>>.Empty.Add(newEvent)), null);
                }

                if (inEvent.Type == EventType.EndGroup)
                {
                    var newEvent = new Event<T>(state.stack.Peek()[0].Id, EventType.EndGroup, inEvent.Data);
                    var newList = state.stack.Peek().Add(newEvent);
                    var newStack = state.stack.Pop();
                    var toEmit = new GroupEvent<T>(newList[0].Id, newList);
                    if(newStack.IsEmpty)
                        return (state.groupId, newStack, toEmit);
                    else
                    {
                        var parentList = newStack.Peek().Add(toEmit);
                        newStack = newStack.Pop().Push(parentList);
                        return (state.groupId, newStack, null);
                    }
                }

                throw new NotImplementedException();
            })
            .Where(t => t.output != null)
            .Select(t => t.output);
    }
}

此处为跑步者代码:

var s = new Subject<Event<int>>();
var o = s.GroupEvents();

s.OnNext(new Event<int>(-1, EventType.Data, 1));
s.OnNext(new Event<int>(-1, EventType.BeginGroup, 2));
s.OnNext(new Event<int>(-1, EventType.Data, 3));
s.OnNext(new Event<int>(-1, EventType.BeginGroup, 4));
s.OnNext(new Event<int>(-1, EventType.Data, 5));
s.OnNext(new Event<int>(-1, EventType.EndGroup, 6));
s.OnNext(new Event<int>(-1, EventType.EndGroup, 7));
s.OnNext(new Event<int>(-1, EventType.Data, 8));
s.OnNext(new Event<int>(-1, EventType.Data, 9));

结果看起来像您对问题的预期。


说明:

我采用了状态机方法,这通常意味着使用.Scan方法。我们这里的状态是运行中的groupId计数,以及一堆消息列表。堆栈的顶部代表我们当前要向其添加消息的组。由于Scan不允许您在状态和输出之间进行区分,因此,第三个状态值是输出变量。

我正在使用不可变的集合,因为它们在Rx上表现最好。如果使用可变集合,则性能可以提高,但是您必须注意多重订阅的含义。

至于使用内置运算符(GroupByBufferWindowJoin等),我认为这些都不起作用以及所需的树状递归结构。如果您的结构较平坦,则Window可能有用,但是需要一些工作。