From bf8abdaefbd801e604a4fa023f448ece812bc97d Mon Sep 17 00:00:00 2001 From: Johannes Mols Date: Wed, 8 Mar 2023 12:43:06 +0100 Subject: [PATCH] Added functions to convert objects and attributes based on name --- OCEL.CSharp.Tests/UtilityTests.cs | 88 +++++++++++++++++++++++++++++- OCEL.CSharp/Types.cs | 21 +++++++ OCEL.Tests/UtilityTests.fs | 91 ++++++++++++++++++++++++++++++- OCEL/Types.fs | 60 ++++++++++++++++++++ 4 files changed, 258 insertions(+), 2 deletions(-) diff --git a/OCEL.CSharp.Tests/UtilityTests.cs b/OCEL.CSharp.Tests/UtilityTests.cs index d4e7a71..66f40a5 100644 --- a/OCEL.CSharp.Tests/UtilityTests.cs +++ b/OCEL.CSharp.Tests/UtilityTests.cs @@ -69,7 +69,6 @@ public void CanCorrectlyMergeTwoLogs() public void CanCorrectlyRemoveDuplicateObjects() { var timestamp = DateTimeOffset.Now; - var log = new OcelLog( new Dictionary(), new Dictionary @@ -99,5 +98,92 @@ public void CanCorrectlyRemoveDuplicateObjects() Assert.True(log.MergeDuplicateObjects() == correctLog); } + + [Fact] + public void CanCorrectlyConvertObjectsToAttributes() + { + var timestamp = DateTimeOffset.Now; + var log = new OcelLog( + new Dictionary(), + new Dictionary + { + { "e1", new OcelEvent("Activity 1", timestamp, new List { "o1", "o2" }, new Dictionary()) }, + { "e2", new OcelEvent("Activity 2", timestamp, new List { "o2" }, new Dictionary()) } + }, + new Dictionary + { + { "o1", new OcelObject("Type 1", new Dictionary { { "prop1", new OcelString("some string") }, { "prop2", new OcelInteger(123) } })}, + { "o2", new OcelObject("Type 2", new Dictionary { { "prop1", new OcelBoolean(false) } })}, + } + ); + + var correctLog = new OcelLog( + new Dictionary(), + new Dictionary + { + { "e1", new OcelEvent("Activity 1", timestamp, new List(), + new Dictionary + { + { "Type 1", new OcelMap(new Dictionary { + { "prop1", new OcelString("some string") }, + { "prop2", new OcelInteger(123) } + })}, + { "Type 2", new OcelBoolean(false) } + }) + }, + { "e2", new OcelEvent("Activity 2", timestamp, new List(), new Dictionary + { + { "Type 2", new OcelBoolean(false) } + })} + }, + new Dictionary() + ); + + Assert.True(log.ConvertObjectsToAttributes(new List { "Type 1", "Type 2" }) == correctLog); + } + + [Fact] + public void CanCorrectlyConvertAttributesToObjects() + { + var timestamp = DateTimeOffset.Now; + var log = new OcelLog( + new Dictionary(), + new Dictionary + { + { "e1", new OcelEvent("Activity 1", timestamp, new List(), + new Dictionary + { + { "Type 1", new OcelMap(new Dictionary { + { "prop1", new OcelString("some string") }, + { "prop2", new OcelInteger(123) } + })}, + { "Type 2", new OcelBoolean(false) } + }) + }, + { "e2", new OcelEvent("Activity 2", timestamp, new List(), new Dictionary + { + { "Type 2", new OcelBoolean(false) } + })} + }, + new Dictionary() + ); + + var result = log.ConvertAttributesToObjects(new List { "Type 1", "Type 2" }); + Assert.All(result.Events, e => + { + switch (e.Key) + { + case "e1": + Assert.True(e.Value.OMap.Count() == 2 && e.Value.VMap.Count == 0); + return; + case "e2": + Assert.True(e.Value.OMap.Count() == 1 && e.Value.VMap.Count == 0); + return; + default: + throw new Exception("Unknown event"); + } + }); + Assert.True(result.Objects.Count == 2); + } } } diff --git a/OCEL.CSharp/Types.cs b/OCEL.CSharp/Types.cs index ed4f4cf..015eec4 100644 --- a/OCEL.CSharp/Types.cs +++ b/OCEL.CSharp/Types.cs @@ -4,6 +4,7 @@ using OCEL; using OCEL.Types; using System.Linq; +using Microsoft.FSharp.Collections; namespace OCEL.CSharp { @@ -89,6 +90,26 @@ public OcelLog MergeDuplicateObjects() return new OcelLog(this.ToFSharpOcelLog().MergeDuplicateObjects()); } + /// + /// Convert a list of object types to attributes by moving objects to all events that reference them. + /// + /// The object types that should be converted. + /// An updated OCEL log with objects moved to events. + public OcelLog ConvertObjectsToAttributes(IEnumerable objectTypes) + { + return new OcelLog(this.ToFSharpOcelLog().ConvertObjectsToAttributes(ListModule.OfSeq(objectTypes))); + } + + /// + /// Convert a list of attributes to objects by moving attributes to new objects and add references to the events. + /// + /// + /// + public OcelLog ConvertAttributesToObjects(IEnumerable attributes) + { + return new OcelLog(this.ToFSharpOcelLog().ConvertAttributesToObjects(ListModule.OfSeq(attributes))); + } + private static IEnumerable ExtractKeysFromValue(OcelValue value) { switch (value) diff --git a/OCEL.Tests/UtilityTests.fs b/OCEL.Tests/UtilityTests.fs index 65cfd7a..4245ce7 100644 --- a/OCEL.Tests/UtilityTests.fs +++ b/OCEL.Tests/UtilityTests.fs @@ -126,4 +126,93 @@ type UtilityTests(output: ITestOutputHelper) = ] |> Map.ofList } - log.MergeDuplicateObjects() = correctLog |> Assert.True \ No newline at end of file + log.MergeDuplicateObjects() = correctLog |> Assert.True + + [] + let ``Can correctly convert objects to attributes`` () = + let timestamp = DateTimeOffset.Now + let log = { + GlobalAttributes = Map.empty + Events = [ + "e1", { + Activity = "Activity 1" + Timestamp = timestamp + OMap = ["o1"; "o2"] + VMap = Map.empty + } + "e2", { + Activity = "Activity 2" + Timestamp = timestamp + OMap = ["o2"] + VMap = Map.empty + } + ] |> Map.ofList + Objects = [ + "o1", { + Type = "Type 1" + OvMap = ["prop1", OcelString "some string"; "prop2", OcelInteger 123] |> Map.ofList + } + "o2", { + Type = "Type 2" + OvMap = ["prop1", OcelBoolean false] |> Map.ofList + } + ] |> Map.ofList + } + + let correctLog = { + GlobalAttributes = Map.empty + Events = [ + "e1", { + Activity = "Activity 1" + Timestamp = timestamp + OMap = [] + VMap = [ + "Type 1", OcelMap(["prop1", OcelString "some string"; "prop2", OcelInteger 123] |> Map.ofList) + "Type 2", OcelBoolean false + ] |> Map.ofList + } + "e2", { + Activity = "Activity 2" + Timestamp = timestamp + OMap = [] + VMap = ["Type 2", OcelBoolean false] |> Map.ofList + } + ] |> Map.ofList + Objects = Map.empty + } + + log.ConvertObjectsToAttributes ["Type 1"; "Type 2"] = correctLog |> Assert.True + + [] + let ``Can correctly convert attributes to objects`` () = + let timestamp = DateTimeOffset.Now + let log = { + GlobalAttributes = Map.empty + Events = [ + "e1", { + Activity = "Activity 1" + Timestamp = timestamp + OMap = [] + VMap = [ + "Type 1", OcelMap(["prop1", OcelString "some string"; "prop2", OcelInteger 123] |> Map.ofList) + "Type 2", OcelBoolean false + ] |> Map.ofList + } + "e2", { + Activity = "Activity 2" + Timestamp = timestamp + OMap = [] + VMap = ["Type 2", OcelBoolean false] |> Map.ofList + } + ] |> Map.ofList + Objects = Map.empty + } + + // Resulting log will have randomly assigned object ID's, so direct equality won't work like in previous tests + let result = log.ConvertAttributesToObjects ["Type 1"; "Type 2"] + result.Events |> Map.iter (fun k e -> + match k with + | "e1" -> (e.OMap.Length = 2 && e.VMap.IsEmpty) |> Assert.True + | "e2" -> (e.OMap.Length = 1 && e.VMap.IsEmpty) |> Assert.True + | _ -> failwith "Unknown event") + result.Objects.Count = 2 |> Assert.True diff --git a/OCEL/Types.fs b/OCEL/Types.fs index 19dbe97..533fc4d 100644 --- a/OCEL/Types.fs +++ b/OCEL/Types.fs @@ -142,6 +142,66 @@ type OcelLog with Objects = updatedObjs } + /// Convert a list of object types to attributes by moving objects to all events that reference them. + member this.ConvertObjectsToAttributes objectTypes = + + /// Add an object as attributes to all events that reference the object ID + let addObjectToEvents objId (obj: OcelObject) (log: OcelLog) = + { log with + Events = + log.Events + |> Map.map (fun _ e -> + match e.OMap |> List.tryFindIndex (fun id -> id = objId) with + | None -> e + | Some i -> + let attr = // If object only has one attribute, we don't need to use a map for it + match obj.OvMap |> Map.toList with + | [_, o] -> o + | _ -> OcelMap(obj.OvMap) + { e with + OMap = e.OMap |> List.removeAt i + VMap = e.VMap |> Map.add obj.Type attr + } + ) + } + + /// Remove an object with some ID from the log + let removeObject objId log = + { log with Objects = log.Objects |> Map.remove objId } + + /// Convert all instances of an object type to attributes in all referencing events + let convertObjectToAttributes objType (log: OcelLog) = + let objs = log.Objects |> Map.filter (fun _ o -> o.Type = objType) |> Map.toList + (log, objs) ||> List.fold (fun log (id, obj) -> log |> addObjectToEvents id obj |> removeObject id) + + (this, objectTypes) ||> List.fold (fun log objType -> convertObjectToAttributes objType log) + + /// Convert a list of attributes to objects by moving attributes to new objects and add references to the events. + member this.ConvertAttributesToObjects attributes = + + /// Move an attribute from an event to the object map, removing the attribute from itself and adding a reference to the object instead + let moveAttrFromEventToObject (id, event: OcelEvent) attr (log: OcelLog) = + let objId = Guid.NewGuid().ToString() + let newObj = { + Type = attr + OvMap = + match event.VMap[attr] with + | OcelMap m -> m + | _ -> ["value", event.VMap[attr]] |> Map.ofList + } + + { log with + Events = log.Events |> Map.change id (fun _ -> Some { event with VMap = event.VMap |> Map.remove attr; OMap = objId :: event.OMap }) + Objects = log.Objects |> Map.add objId newObj + } + + /// Find all events that have the attribute, and add them to the log as objects + let convertAttributesToObjects attr (log: OcelLog) = + let events = log.Events |> Map.filter (fun _ e -> e.VMap.ContainsKey attr) |> Map.toList + (log, events) ||> List.fold (fun log event -> moveAttrFromEventToObject event attr log) + + (this, attributes) ||> List.fold (fun log attr -> convertAttributesToObjects attr log) |> fun log -> log.MergeDuplicateObjects() + /// An empty log static member Empty = {