예제 #1
0
        private static ProcessingRulEngStore OperationSxProcessing(this ProcessingRulEngStore newState, RulEngStore previousState,
                                                                   List <Guid> ruleResultIds, List <Operation> operationprescriptionsToProcessList,
                                                                   List <EntMatch> acceptableDestinations)
        {
            // Get all of the sources from the previous state
            var acceptableSourceIds = new List <Guid>();
            //foreach (var opPresProc in operationprescriptionsToProcessList)
            //{
            //    var opPresOperands = opPresProc.Operands;

            //    var matchFound = false;
            //    foreach (var opo in opPresOperands)
            //    {
            //        if (!acceptableDestinations
            //            .Any(ad => ad.EntType == opo.EntType && (ad.EntityId == opo.EntityId || opo.EntityId == Guid.Empty)))
            //        {
            //            continue;
            //        }

            //        matchFound = true;
            //        break;
            //    }

            //    if (!matchFound)
            //    {
            //        continue;
            //    }

            //    acceptableSourceIds.AddRange(opPresOperands.SelectMany(oo => oo.SourceValueIds));
            //}

            //var acceptableSources = previousState.Values
            //    .Where(v => acceptableSourceIds.Contains(v.EntityId))
            //    .ToList();

            var e = new Engine();

            foreach (var ruleResultIdToProcess in ruleResultIds)
            {
                // Get all of the operations relevant to the Rule
                var relevantOps = operationprescriptionsToProcessList
                                  .Where(o => o.RuleResultId == ruleResultIdToProcess)
                                  .ToList();

                if (!relevantOps.Any())
                {
                    // TODO: confirm if we should be doing this if there was nothing relevant to process
                    //newState.RuleResults.RemoveWhere(r => r.RuleResultId == ruleResultIdToProcess);
                    continue;
                }

                // Process the acceptable
                foreach (var relevantOp in relevantOps)
                {
                    // Note: A Search operation does not specify an output destination as it does not 'know' in advance how many results will be found
                    // However, it may specify a reduced set of Ids to search through.
                    // These will be provided in the SourceValueIds field of each relevantOp.Operand

                    //        var firstEnt = new EntMatch
                    //        {
                    //            EntityId = relevantOp.Operands[0].EntityId,
                    //            EntType = relevantOp.Operands[0].EntType
                    //        };

                    //        if (!acceptableDestinations.Any(ad =>
                    //            ad.EntType == firstEnt.EntType && ad.EntityId == firstEnt.EntityId))
                    //        {
                    //            continue;
                    //        }

                    //        var destEntsToProcess = relevantOp.Operands
                    //            .Select(de => new
                    //            {
                    //                de.EntityId,
                    //                EntType = Convert.ToInt32(de.EntType),
                    //                sourceValues = de.SourceValueIds
                    //                    .Select(sv => JObject.Parse($"{{\"Id\":\"{sv}\",\"Value\":{acceptableSources.FirstOrDefault(a => a.EntityId == sv)?.Detail.ToString(Formatting.None)}}}"))
                    //                    .ToArray()
                    //            })
                    //            .ToList();

                    switch (relevantOp.Operands[0].SourceEntType)
                    {
                    case EntityType.Rule:
                        e.SetValue("source", JsonConvert.SerializeObject(previousState.Rules.ToArray()));
                        break;

                    case EntityType.RuleResult:
                        e.SetValue("source", JsonConvert.SerializeObject(previousState.RuleResults.ToArray()));
                        break;

                    case EntityType.Operation:
                        e.SetValue("source", JsonConvert.SerializeObject(previousState.Operations.ToArray()));
                        break;

                    case EntityType.Request:
                        e.SetValue("source", JsonConvert.SerializeObject(previousState.Requests.ToArray()));
                        break;

                    case EntityType.Value:
                        e.SetValue("source", JsonConvert.SerializeObject(previousState.Values.ToArray()));
                        break;
                    }

                    // The result must be a list of guids for the entities that met the search criteria
                    var result = e
                                 .Execute(relevantOp.OperationTemplate)
                                 .GetCompletionValue();

                    var sourceGuids = result.ToString()
                                      .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                                      .Select(Guid.Parse)
                                      .ToList();

                    for (var ix = 0; ix < sourceGuids.Count; ix++)
                    {
                        var sourceEnt = new TypeKey
                        {
                            EntityId = sourceGuids[ix],
                            EntType  = relevantOp.Operands[0].SourceEntType,
                            EntTags  = relevantOp.EntTags
                        } as IEntity;
                        switch (relevantOp.Operands[0].EntType)
                        {
                        case EntityType.Rule:
                            // Create/Update a rule using destEnt.EntityId and result
                            var rl = newState.FromSearchOperationAddUpdateExistsRule(sourceEnt, relevantOp.EntTags, GuidHelpers.NewTimeUuid());
                            var rr = new RuleResult(rl);
                            newState.RuleResults.Add(rr);
                            break;

                        case EntityType.Operation:
                            // Create/Update an Operation using destEnt.EntityId and result
                            var op = newState.FromSearchOperationAddUpdateOperation(sourceEnt, relevantOp.EntTags, OperationType.Delete, "", GuidHelpers.NewTimeUuid());
                            //var rr = new RuleResult(rl);
                            //newState.RuleResults.Add(rr);

                            //newState.FromOperationResultAddUpdateOperation(result, destEnt.EntityId);
                            break;
                            //case EntityType.Request:
                            //    // Create/Update a Request using destEnt.EntityId and result
                            //    newState.FromOperationResultAddUpdateRequest(result, destEnt.EntityId);
                            //    break;
                            //case EntityType.Value:
                            //    // Create/Update a Value using destEnt.EntityId and result
                            //    newState.FromOperationResultAddUpdateValue(result, destEnt.EntityId);
                            //    break;
                            //            }
                        }

                        //var values = new List<string>();
                        //for (var i = 0; i < a.GetLength(); i++)
                        //{
                        //    values.Add(a.Get(i.ToString()).AsString());
                        //}
                        //return values;

                        Console.WriteLine(JsonConvert.SerializeObject(result));



                        //        foreach (var destEnt in destEntsToProcess)
                        //        {
                        //            var sourceVals = destEnt.sourceValues;
                        //            var isSubstOk = true;

                        //            foreach (Match match in regexToken.Matches(jTempl))
                        //            {
                        //                var token = match.Groups["Token"].Value;
                        //                var indexOk = int.TryParse(match.Groups["Index"].Value, out var index);

                        //                if (!indexOk)
                        //                {
                        //                    break;
                        //                }

                        //                if (sourceVals.Length < index)
                        //                {
                        //                    isSubstOk = false;
                        //                    break;
                        //                }

                        //                jCode = jCode.Replace(token, sourceVals[index]["Value"].ToString(Formatting.None));
                        //            }

                        //            if (!isSubstOk)
                        //            {
                        //                Console.WriteLine(jCode);
                        //                continue;
                        //            }

                        //            JToken result = null;
                        //            if (jCode.StartsWith("{"))
                        //            {
                        //                result = JObject.FromObject(e.Execute(jCode).GetCompletionValue().ToObject());
                        //            }
                        //            if (jCode.StartsWith("["))
                        //            {
                        //                result = JArray.FromObject(e.Execute(jCode).GetCompletionValue().ToObject());
                        //            }
                        //            //Console.WriteLine(result);
                        //            switch ((EntityType)destEnt.EntType)
                        //            {
                        //                case EntityType.Rule:
                        //                    // Create/Update a rule using destEnt.EntityId and result
                        //                    newState.FromOperationResultAddUpdateRule(result, destEnt.EntityId);
                        //                    break;
                        //                case EntityType.Operation:
                        //                    // Create/Update an Operation using destEnt.EntityId and result
                        //                    newState.FromOperationResultAddUpdateOperation(result, destEnt.EntityId);
                        //                    break;
                        //                case EntityType.Request:
                        //                    // Create/Update a Request using destEnt.EntityId and result
                        //                    newState.FromOperationResultAddUpdateRequest(result, destEnt.EntityId);
                        //                    break;
                        //                case EntityType.Value:
                        //                    // Create/Update a Value using destEnt.EntityId and result
                        //                    newState.FromOperationResultAddUpdateValue(result, destEnt.EntityId);
                        //                    break;
                        //            }

                        //            // Mark the operation as Executed
                        //            var actionDate = DateTime.UtcNow;

                        //            // Mark this Rule as executed
                        //            relevantOp.LastExecuted = actionDate;
                        //        }
                    }

                    // newState.RuleResults.RemoveWhere(r => r.RuleResultId == ruleResultIdToProcess);
                }
            }

            return(newState);
        }
예제 #2
0
        private static (Operation operation, OperationMxProcessing operationPrescription) BuildTheGeoJsonOutput(RuleResult collectRuleResult, List <Value> values, Operation buildGeoJsonOperation = null)
        {
            var cityCount = values.Count;

            // Build the Javascript template for creating the entire GeoJSON Value
            var valueBody = "{\"type\":\"FeatureCollection\",\"features\":[";

            for (var ix = 0; ix < cityCount; ix++)
            {
                if (ix > 0)
                {
                    valueBody += ",";
                }
                valueBody += $"${{{ix}}}";
            }
            valueBody += "]}";
            var valueTemplate = $"{{JSON.parse('{valueBody}')}}";

            // Add an Operation to reference the collect Rule and merge all of the results into one GeoJSON
            if (buildGeoJsonOperation == null)
            {
                var opKey = values.OperandKey(EntityType.Value);

                buildGeoJsonOperation = collectRuleResult.CreateUpdateOperation(new[] { opKey }, GuidHelpers.NewTimeUuid(), valueTemplate);
            }
            else
            {
                var opKey = values.OperandKey(EntityType.Value, buildGeoJsonOperation.Operands[0].EntityId);

                buildGeoJsonOperation =
                    buildGeoJsonOperation.RecreateUpdateOperation(collectRuleResult, new[] { opKey }, valueTemplate);
            }

            var buildGeoJsonPrescription = buildGeoJsonOperation.AddUpdate();

            return(buildGeoJsonOperation, buildGeoJsonPrescription);
        }
예제 #3
0
        private static (List <Operation> operations, List <OperationMxProcessing> operationPrescriptions) BuildTheCityDistances(RuleResult cityRuleResults, List <Value> values)
        {
            var operations             = new List <Operation>();
            var operationPrescriptions = new List <OperationMxProcessing>();

            // Build the Javascript template for calculating the length of each connecting GeoJSON line
            // Concept - Id of this city, then formula to calculate each distance and output the result as a sorted list.
            const string cityATempl   = "{{'cityAId':'{0}','destinations':[";
            const string lonTempl     = "JSON.parse('${{{0}}}')['geometry']['coordinates'][0]";
            const string latTempl     = "JSON.parse('${{{0}}}')['geometry']['coordinates'][1]";
            const string getDistTempl = "{{'cityBId':'{0}','distance':Math.pow(Math.pow({1} - {2}, 2) + Math.pow({3} - {4}, 2), 0.5),'usage':'not set'}}";

            for (var ix = 0; ix < values.Count; ix++)
            {
                var jTemplate = new StringBuilder();
                jTemplate.AppendLine("[");

                jTemplate.AppendFormat(cityATempl, values[ix].EntityId);
                jTemplate.AppendLine();
                var cityALonTempl = string.Format(lonTempl, ix);
                var cityALatTempl = string.Format(latTempl, ix);

                var needsComma = false;
                for (var jx = 0; jx < values.Count; jx++)
                {
                    if (ix == jx)
                    {
                        continue;
                    }

                    var cityBLonTempl     = string.Format(lonTempl, jx);
                    var cityBLatTempl     = string.Format(latTempl, jx);
                    var cityAtoBDistTempl = string.Format(getDistTempl, values[jx].EntityId, cityALonTempl, cityBLonTempl, cityALatTempl, cityBLatTempl);

                    if (needsComma)
                    {
                        jTemplate.AppendLine(",");
                    }
                    jTemplate.Append(cityAtoBDistTempl);
                    if (!needsComma)
                    {
                        needsComma = true;
                    }
                }
                jTemplate.AppendLine();
                jTemplate.AppendLine("].sort(function(a, b) {return a.distance - b.distance;})");
                // Adding a slice at the end (for the 10 shortest paths) reduce the overall file size but did not seem to reduce processing time
                // jTemplate.AppendLine("].sort(function(a, b) {return a.distance - b.distance;}).slice(0, 10)");
                jTemplate.AppendLine("}");
                jTemplate.AppendLine("]");

                var jTempl = jTemplate.ToString();

                // Although the source values are always the same we need a new OperandKey each time
                // for each new Value to be generated
                var opKeys = new[] { values.OperandKey(EntityType.Value) };

                // Add an Operation to reference the collect Rule and merge all of the results into one GeoJSON
                var buildCityDistancesOperation    = cityRuleResults.CreateUpdateOperation(opKeys, GuidHelpers.NewTimeUuid(), jTempl);
                var buildCityDistancesPrescription = buildCityDistancesOperation.AddUpdate();

                operations.Add(buildCityDistancesOperation);
                operationPrescriptions.Add(buildCityDistancesPrescription);
            }

            return(operations, operationPrescriptions);
        }
예제 #4
0
        private static (List <Operation> operations, List <OperationMxProcessing> operationPrescriptions) BuildTheCityRoads(
            List <Rule> cityDistRules, List <Value> values)
        {
            var operations             = new List <Operation>();
            var operationPrescriptions = new List <OperationMxProcessing>();

            foreach (var cityDistRule in cityDistRules)
            {
                var cityDistRuleResultId  = cityDistRule.ReferenceValues.RuleResultId;
                var cityDistSourceEntType = cityDistRule.ReferenceValues.EntityIds[0];
                var cityDistValue         = values.FirstOrDefault(c => c.ValueId == cityDistSourceEntType.EntityId);

                if (cityDistValue == null)
                {
                    continue;
                }

                var cityAId   = Guid.Parse((string)cityDistValue.Detail[0]["cityAId"]);
                var nextCityB = (JObject)((JArray)cityDistValue.Detail[0]["destinations"])
                                .FirstOrDefault(d => (string)d["usage"] == "not set");
                // TODO: Should not be setting usage here - this needs to be its own operation
                nextCityB["usage"] = "accepted";
                var nextCityBId = Guid.Parse((string)nextCityB["cityBId"]);

                var roadId = cityAId.Merge(nextCityBId);

                var lineGeo =
                    $"{{JSON.parse(JSON.stringify({{\"type\":\"Feature\",\"properties\":{{\"roadId\":\"{roadId}\",\"cityAId\":\"{cityAId}\",\"cityBId\":\"{nextCityBId}\"}},\"geometry\":{{\"type\":\"LineString\",\"coordinates\":[JSON.parse('${{0}}')[\"geometry\"][\"coordinates\"],JSON.parse('${{1}}')[\"geometry\"][\"coordinates\"]]}}}}))}}";
                var opKeys = new[] { cityAId, nextCityBId }.OperandKey(EntityType.Value);

                // Add an Operation to reference the collect Rule and merge all of the results into one GeoJSON
                var buildCityRoadOperation = cityDistRuleResultId.CreateUpdateOperation(new[] { opKeys }, GuidHelpers.NewTimeUuid(), lineGeo);
                var buildCityRoadPrescription = buildCityRoadOperation.AddUpdate();

                operations.Add(buildCityRoadOperation);
                operationPrescriptions.Add(buildCityRoadPrescription);
            }

            return(operations, operationPrescriptions);
        }
예제 #5
0
        public static void Main()
        {
            Console.WriteLine("Hello Salesman!");

            // Travelling Salesman - Setup
            const int cityCount = 50;

            Console.WriteLine($"Start Setup for {cityCount} cities : {DateTime.UtcNow.ToString("yyyy-MMM-dd HH:mm:ss.ff")}");

            (var rules, var ruleResults, var values, var rulePrescriptions) = BuildTheCities(cityCount);

            // Build a Collection Rule, Result and Prescription for all of the above rules
            (var collectRule, var collectRuleResult, var collectRulePrescription) = ruleResults.And();

            // Add the Collection Rule, Result and Prescription
            rules.Add(collectRule);
            ruleResults.Add(collectRuleResult);
            rulePrescriptions.Add(collectRulePrescription);

            var operations             = new List <Operation>();
            var operationPrescriptions = new List <OperationMxProcessing>();

            var cityValues = values.Where(c => c.Detail["properties"]["cityNo"] != null).ToList();

            (var pointOperations, var pointOperationPrescriptions) = BuildTheGeoJsonOutput(collectRuleResult, cityValues);
            operations.Add(pointOperations);
            operationPrescriptions.Add(pointOperationPrescriptions);

            (var distOperations, var distOperationPrescriptions) = BuildTheCityDistances(collectRuleResult, cityValues);
            operations.AddRange(distOperations);
            operationPrescriptions.AddRange(distOperationPrescriptions);

            // Build the Rule Engine Store ready for processing
            var startingStore = new RulEngStore
            {
                Rules       = rules.ToImmutableHashSet(),
                RuleResults = ruleResults.ToImmutableHashSet(),
                Operations  = operations.ToImmutableHashSet(),
                Values      = values.ToImmutableHashSet()
            };

            RvStore = new Store <RulEngStore>(StoreReducers.ReduceStore, startingStore);
            File.WriteAllText("storeStart.json", RvStore.GetState().ToString());

            // Commence Processing
            RulEngStore changes;

            RvStore.Subscribe(state => changes = state);

            var pass = 0;

            TikTok(pass++, rulePrescriptions, operationPrescriptions);

            Console.WriteLine($"Add more to Store for {cityCount} cities : {DateTime.UtcNow.ToString("yyyy-MMM-dd HH:mm:ss.ff")}");
            // Build the exists rules for the values resulting from the distance operations
            (var distRules, var distRuleResults, var distRulePrescriptions) =
                distOperations.SelectMany(dp => dp.Operands).Exists(false);

            // Build the operations to convert the distance results to roads
            var storeValues = RvStore.GetState().Values.ToList();

            (var roadOperations, var roadOperationPrescriptions) = BuildTheCityRoads(distRules, storeValues);

            // Add the new Entities to the Store ready for processing
            RvStore.AddUpdate(distRules, distRuleResults, roadOperations, null);
            TikTok(pass++, distRulePrescriptions, roadOperationPrescriptions);

            Console.WriteLine($"Add more to Store for {cityCount} cities : {DateTime.UtcNow.ToString("yyyy-MMM-dd HH:mm:ss.ff")}");
            // Build the exists rules for the values resulting from the 'road' operations
            (var roadExistsRules, var roadExistsRuleResults, var roadExistsRulePrescriptions) =
                roadOperations.SelectMany(rp => rp.Operands).Exists(false);
            // TODO: Build operations to mark the roads between CityA and CityB (and vice versa) as 'accepted'
            // Build a Collection Rule, Result and Prescription for all of the 'road' rules
            (var collectRoadRule, var collectRoadRuleResult, var collectRoadRulePrescription) = roadExistsRuleResults.And();
            roadExistsRules.Add(collectRoadRule);
            roadExistsRuleResults.Add(collectRoadRuleResult);
            roadExistsRulePrescriptions.Add(collectRoadRulePrescription);

            // Join the new Roads together in one map
            var roadValues = RvStore.GetState().Values
                             .Where(c => c.Detail != null && c.Detail.Type == JTokenType.Object && c.Detail["properties"]?["roadId"] != null)
                             .ToList();

            (var lineOperation, var lineOperationPrescription) = BuildTheGeoJsonOutput(collectRoadRuleResult, roadValues);

            // Add the new Entities to the Store ready for processing
            RvStore
            .AddUpdate(roadExistsRules, roadExistsRuleResults, new[] { lineOperation }.ToList(), null);

            TikTok(pass++, roadExistsRulePrescriptions, new[] { lineOperationPrescription });

            // Searching or finding entities by arbitrary requirements is not currently supported
            // It could potentially be implemented with:
            // a new Rule (e.g. AnyMatch) that triggers
            // a special Operation (Match -> Generates Exists Rules as simple triggers)
            // regular Operation(s) to process the matching entities

            // So search for duplicate roads (A->B and B->A) by code external to the RuleEngine store
            //(var dupRoadExistsRules, var dupRoadExistsRuleResults, var dupRoadDeleteOperations,
            //        var dupRoadRulePrescriptions, var dupRoadOpPrescriptions) =
            //    DeleteTheDuplicateRoads(roadValues, roadExistsRules);

            //RvStore.AddUpdate(dupRoadExistsRules, dupRoadExistsRuleResults, dupRoadDeleteOperations, null);

            //TikTok(pass++, dupRoadRulePrescriptions, dupRoadOpPrescriptions);

            // Refresh the list of roads
            roadValues = RvStore.GetState().Values
                         .Where(c => c.Detail != null && c.Detail.Type == JTokenType.Object && c.Detail["properties"]?["roadId"] != null)
                         .ToList();
            roadExistsRules = RvStore.GetState().Rules
                              .Where(c => c.RuleType == RuleType.Exists &&
                                     roadValues.Select(v => v.ValueId).Contains(c.ReferenceValues.EntityIds[0].EntityId))
                              .ToList();
            roadExistsRuleResults = RvStore.GetState().RuleResults
                                    .Where(s => roadExistsRules.Select(c => c.RuleId).Contains(s.RuleId))
                                    .ToList();

            // recreate the prescriptions for road rules
            roadExistsRulePrescriptions = new List <IRuleProcessing>();
            foreach (var rv in roadExistsRules)
            {
                roadExistsRulePrescriptions.Add(rv.Exists());
            }
            // Rebuild the Collection Rule, Result and Prescription for all of the 'road' rules
            (collectRoadRule, collectRoadRuleResult, collectRoadRulePrescription) = roadExistsRuleResults.And(collectRoadRule, collectRoadRuleResult);
            roadExistsRules.Add(collectRoadRule);
            roadExistsRuleResults.Add(collectRoadRuleResult);
            roadExistsRulePrescriptions.Add(collectRoadRulePrescription);

            // Join the new Roads together in one map
            (lineOperation, lineOperationPrescription) = BuildTheGeoJsonOutput(collectRoadRuleResult, roadValues, lineOperation);

            // Add the new Entities to the Store ready for processing
            RvStore
            .AddUpdate(roadExistsRules, roadExistsRuleResults, new[] { lineOperation }.ToList(), null);

            TikTok(pass++, roadExistsRulePrescriptions, new[] { lineOperationPrescription });

            // Find the Duplicate roads to be deleted
            // First create a Search Operation to generate Exists Rules and RuleResults
            var opKeyDups = new OperandKey
            {
                EntityId = GuidHelpers.NewTimeUuid(),
                EntTags  = new List <string> {
                    "Duplicates"
                },
                EntType       = EntityType.Rule,
                SourceEntType = EntityType.Value
            };

            // The source data is always presented as a serialised JSON string
            var searchTemplate = "JSON.parse(source)"
                                 // Filter for values with a roadId property
                                 + ".filter(function(s){return s.Detail&&s.Detail.properties&&s.Detail.properties.roadId})"
                                 // Map the results to just the values Id and the roadId (a hash)
                                 + ".map(function(s){return {vId:s.ValueId,rId:s.Detail.properties.roadId};})"
                                 // Reduce to an array grouping by the roadId, the first valueId with that roadId will also be in the structure
                                 + ".reduce(function(a,c){var ix=0;"
                                 + "for(;ix<a.length;ix++){if(a[ix].el.rId===c.rId)break;}"
                                 + "if(ix<a.length){a[ix].t++;}else{a.push({el:c,t:1});}"
                                 + "return a;},[])"
                                 // Filter for roadIds that occur more than once
                                 + ".filter(function(s){return s.t>1})"
                                 // Get the valueId
                                 + ".map(function(s){return s.el.vId})"
                                 // Sort (makes it easier to follow what's going on)
                                 + ".sort(function(a,b){if(a<b)return -1;return(a>b)?1:0;})";

            var opRoadSearch             = collectRuleResult.SearchOperation(new[] { opKeyDups }, GuidHelpers.NewTimeUuid(), searchTemplate);
            var opRoadSearchPrescription = opRoadSearch.Search();

            RvStore.AddUpdate(null, null, opRoadSearch, null);

            TikTok(pass++, null, new[] { opRoadSearchPrescription });

            // Generate Prescriptions for all of the Rules and execute them
            var searchForDupPrescriptions = RvStore.GetState()
                                            .Rules
                                            .Where(r => r.EntTags != null && r.EntTags[0] == "Duplicates")
                                            .Select(r => r.Exists())
                                            .ToList();

            TikTok(pass++, searchForDupPrescriptions, null);

            // Grab one of the Rule Results from above that was successful and use it to trigger the next operation
            var dupRuleResults    = searchForDupPrescriptions.Select(fdp => fdp.Entities.RuleResultId).ToList();
            var dupRoadRuleResult = RvStore.GetState()
                                    .RuleResults
                                    .FirstOrDefault(r => r.EntTags != null && r.EntTags[0] == "Duplicates" && dupRuleResults.Contains(r.RuleResultId) && r.Detail);

            var opKeyDels = new OperandKey
            {
                EntityId = GuidHelpers.NewTimeUuid(),
                EntTags  = new List <string> {
                    "Duplicates"
                },
                EntType       = EntityType.Operation,
                SourceEntType = EntityType.RuleResult
            };

            // The source data is always presented as a serialised JSON string
            var searchDupsTemplate = "JSON.parse(source)"
                                     // Filter for Rule Results with a Duplicates tag
                                     + ".filter(function(s){return s.EntTags&&s.EntTags[0]==='Duplicates'})"
                                     // Map the results to just the ruleresult Id
                                     + ".map(function(s){return s.RuleResultId;})"
                                     // Sort (makes it easier to follow what's going on)
                                     + ".sort(function(a,b){if(a<b)return -1;return(a>b)?1:0;})";

            var opDelRoadSearch = dupRoadRuleResult.SearchOperation(new[] { opKeyDels }, GuidHelpers.NewTimeUuid(), searchDupsTemplate);

            opDelRoadSearch.OperationType = OperationType.Delete;
            var opDelRoadSearchPrescription = opDelRoadSearch.Search();

            RvStore.AddUpdate(null, null, opDelRoadSearch, null);

            TikTok(pass++, null, new[] { opDelRoadSearchPrescription });


            // We'll start by adding all the shortest ones as the first set of 'actual' roads
            // A minimum of two * (cityCount - 1) roads will be required
            ////var roadSet = values
            ////    .Where(r => r.Detail["properties"]["cityAId"] != null && (r.Detail["properties"]["usage"] == null || (string)r.Detail["properties"]["usage"] == "Not Set"))
            ////    .OrderBy(r => (double)r.Detail["properties"]["distance"]);
            ////var cityIds = new List<(Guid cityId, int Count)>();
            ////foreach (var road in roadSet)
            ////{
            ////    var roadGeoJson = (JObject)road.Detail;
            ////    var cityAId = (Guid)roadGeoJson["properties"]["cityAId"];
            ////    var cityBId = (Guid)roadGeoJson["properties"]["cityBId"];

            ////    // Test whether either city at the end of this road already have two roads
            ////    var cityHasRoads = cityIds.Where(ci => ci.cityId == cityAId || ci.cityId == cityBId).ToList();

            ////    var citiesFullyConnected = cityHasRoads.Count(chr => chr.Count >= 2);

            ////    Console.WriteLine($"{cityIds.Count} - {citiesFullyConnected}");

            ////    switch (citiesFullyConnected)
            ////    {
            ////        case 0:
            ////            // Road connects two cities and neither has a full set of connecting roads
            ////            // Do connection
            ////            roadGeoJson["properties"]["usage"] = "Accepted";
            ////            try
            ////            {
            ////                var a = cityIds.First(chr => chr.cityId == cityAId);
            ////                cityIds.Remove(a);
            ////                a.Count++;
            ////                cityIds.Add(a);
            ////            }
            ////            catch
            ////            {
            ////                cityIds.Add((cityAId, 1));
            ////            }
            ////            try
            ////            {
            ////                var b = cityIds.First(chr => chr.cityId == cityBId);
            ////                cityIds.Remove(b);
            ////                b.Count++;
            ////                cityIds.Add(b);
            ////            }
            ////            catch
            ////            {
            ////                cityIds.Add((cityBId, 1));
            ////            }
            ////            break;
            ////        case 1:
            ////            // Road connecting one fully connected city
            ////            if (cityHasRoads.Count == 1)
            ////            {
            ////                // And only one city - so create an empty connection record for the other
            ////                var ci = cityHasRoads.All(chr => chr.cityId == cityAId) ? (cityBId, 0) : (cityAId, 0);
            ////                cityIds.Add(ci);
            ////            }
            ////            break;

            ////        case 2:
            ////        default:
            ////            // Road connecting two already full connected cities
            ////            break;
            ////    }

            ////    if (cityIds.Count >= 10)
            ////    {
            ////        break;
            ////    }
            ////}

            ////var acceptedRoads = values.Where(v =>
            ////    v.Detail["properties"]["cityAId"] != null &&
            ////    v.Detail["properties"]["usage"] != null && (string)v.Detail["properties"]["usage"] == "Accepted")
            ////    .ToList();
            ////var salesmansJourney = new StringBuilder();
            ////salesmansJourney.Append("{\"type\":\"FeatureCollection\",\"features\":[");
            ////salesmansJourney.Append(string.Join(',', acceptedRoads.Select(v => v.Detail.ToString())));
            ////salesmansJourney.Append("]}");
            ////var jny = JObject.Parse(salesmansJourney.ToString());
            ////File.WriteAllText("Routes00.json", jny.ToString());

            //var citiesWithNoRoadsCount = cityIds.Count(ci => ci.Count == 0);

            // For each city with no connections
            // Determine its two closest connected neighbours and reject that road
            // and accept the two roads to and from this city to those two closest neighbours
            ////foreach (var ci in cityIds.Where(ci => ci.Count == 0))
            ////{
            ////    Console.WriteLine($"{ci.cityId}");

            ////    var closestNeighboursWithRoads = values.Where(v =>
            ////        v.Detail["properties"]["cityAId"] != null &&
            ////        ((Guid)v.Detail["properties"]["cityAId"] == ci.cityId || (Guid)v.Detail["properties"]["cityBId"] == ci.cityId) &&
            ////        v.Detail["properties"]["usage"] != null && (string)v.Detail["properties"]["usage"] == "Accepted")
            ////        .OrderBy(r => (double)r.Detail["properties"]["distance"])
            ////        .ToList();

            ////    var closestNeighbourGuids = closestNeighboursWithRoads.SelectMany(ar => new[]
            ////        {(Guid) ar.Detail["properties"]["cityAId"], (Guid) ar.Detail["properties"]["cityBId"]})
            ////        .GroupBy(cg => cg)
            ////        .Select(cg => cg.Key)
            ////        .ToList();

            ////    foreach (var cng in closestNeighbourGuids)
            ////    {
            ////        var notCng = closestNeighbourGuids.Where(cg => cng != cg).ToList();

            ////        var cnwr = closestNeighboursWithRoads.Where(v =>
            ////        v.Detail["properties"]["cityAId"] != null &&
            ////        ((Guid)v.Detail["properties"]["cityAId"] == ci.cityId || (Guid)v.Detail["properties"]["cityBId"] == ci.cityId) &&
            ////        (notCng.Contains((Guid)v.Detail["properties"]["cityAId"]) || notCng.Contains((Guid)v.Detail["properties"]["cityBId"])))
            ////        .ToList();

            ////        if (!cnwr.Any())
            ////        {
            ////            continue;
            ////        }
            ////        // Road to Reject
            ////        var r2R = cnwr.First();
            ////        values.First(r => r.EntityId == r2R.EntityId).Detail["properties"]["usage"] = "Rejected";

            ////        // Rejected road cities
            ////        var cnwor = new[] { (Guid)r2R.Detail["properties"]["cityAId"], (Guid)r2R.Detail["properties"]["cityBId"] };

            ////        // Roads to Accept
            ////        var r2A = closestNeighboursWithRoads.Where(v =>
            ////            v.Detail["properties"]["cityAId"] != null &&
            ////            ((Guid)v.Detail["properties"]["cityAId"] == ci.cityId || (Guid)v.Detail["properties"]["cityBId"] == ci.cityId) &&
            ////            (cnwor.Contains((Guid)v.Detail["properties"]["cityAId"]) || cnwor.Contains((Guid)v.Detail["properties"]["cityBId"])));
            ////        foreach (var rd in r2A)
            ////        {
            ////            values.First(r => r.EntityId == rd.EntityId).Detail["properties"]["usage"] = "Accepted";
            ////        }
            ////        break;
            ////    }
            ////}

            // Now we'll ensure that every city has at least two roads connecting it
            // First step is to group all of the cities and get a count for the number of roads to each one
            //var citiesWithRoads = acceptedRoads
            //    .SelectMany(ar => new[]
            //        {(Guid) ar.Detail["properties"]["cityAId"], (Guid) ar.Detail["properties"]["cityBId"]})
            //    .GroupBy(cg => cg)
            //    .Select(cg => new { cityId = cg.Key, Count = cg.Count() })
            //    .ToList();
            //citiesWithRoadsCount = citiesWithRoads.Count;

            ////        acceptedRoads = values.Where(v =>
            ////v.Detail["properties"]["cityAId"] != null &&
            ////v.Detail["properties"]["usage"] != null && (string)v.Detail["properties"]["usage"] == "Accepted")
            ////.ToList();
            ////        salesmansJourney = new StringBuilder();
            ////        salesmansJourney.Append("{\"type\":\"FeatureCollection\",\"features\":[");
            ////        salesmansJourney.Append(string.Join(',', acceptedRoads.Select(v => v.Detail.ToString())));
            ////        salesmansJourney.Append("]}");
            ////        jny = JObject.Parse(salesmansJourney.ToString());
            ////        File.WriteAllText("Routes01.json", jny.ToString());

            // Then there's a need to check for any cities with no roads at all connected to them (a possibility)
            // and add these to the same list with a count of zero for each one.
            //if (citiesWithRoadsCount < cityCount)
            //{
            //    var citiesWithNoRoads = values.Where(c =>
            //            c.Detail["properties"]["cityNo"] != null &&
            //            !citiesWithRoads.Select(cr => cr.cityId).Contains(c.EntityId))
            //        .Select(cn => new { cityId = cn.EntityId, Count = 0 })
            //        .ToList();

            //    citiesWithNoRoadsCount = citiesWithNoRoads.Count;

            //    citiesWithRoads.AddRange(citiesWithNoRoads);
            //}

            //do
            //{
            //    if (pass > 10)
            //    {
            //        break;
            //    }

            //    // Take this list and add the two closest roads for each city with less than two roads
            //    // and output the result.
            //    foreach (var cwr in citiesWithRoads.Where(cwr => cwr.Count < 2))
            //    {
            //        roadSet = values.Where(v =>
            //                v.Detail["properties"]["cityAId"] != null &&
            //                v.Detail["properties"]["usage"] != null &&
            //                (string) v.Detail["properties"]["usage"] == "Not Set" &&
            //                ((Guid) v.Detail["properties"]["cityAId"] == cwr.cityId ||
            //                 (Guid) v.Detail["properties"]["cityBId"] == cwr.cityId))
            //            .OrderBy(r => (double) r.Detail["properties"]["distance"])
            //            .Take(2 - cwr.Count);

            //        foreach (var road in roadSet)
            //        {
            //            var roadGeoJson = (JObject) road.Detail;

            //            roadGeoJson["properties"]["usage"] = "Accepted";
            //        }
            //    }

            //    acceptedRoads = values.Where(v =>
            //            v.Detail["properties"]["cityAId"] != null &&
            //            v.Detail["properties"]["usage"] != null &&
            //            (string) v.Detail["properties"]["usage"] == "Accepted")
            //        .ToList();
            //    salesmansJourney = new StringBuilder();
            //    salesmansJourney.Append("{\"type\":\"FeatureCollection\",\"features\":[");
            //    salesmansJourney.Append(string.Join(',', acceptedRoads.Select(v => v.Detail.ToString())));
            //    salesmansJourney.Append("]}");
            //    jny = JObject.Parse(salesmansJourney.ToString());
            //    File.WriteAllText($"routes{pass++}.json", jny.ToString());

            //    // Identify cities with too many roads
            //    var citiesWithTooManyRoads = acceptedRoads
            //        .SelectMany(ar => new[]
            //            {(Guid) ar.Detail["properties"]["cityAId"], (Guid) ar.Detail["properties"]["cityBId"]})
            //        .GroupBy(cg => cg)
            //        .Select(cg => new {cityId = cg.Key, Count = cg.Count()})
            //        .Where(cwr => cwr.Count > 2)
            //        .ToList();

            //    foreach (var cwr in citiesWithTooManyRoads)
            //    {
            //        var road = values.Where(v =>
            //                v.Detail["properties"]["cityAId"] != null &&
            //                v.Detail["properties"]["usage"] != null &&
            //                (string) v.Detail["properties"]["usage"] == "Accepted" &&
            //                ((Guid) v.Detail["properties"]["cityAId"] == cwr.cityId ||
            //                 (Guid) v.Detail["properties"]["cityBId"] == cwr.cityId))
            //            .OrderByDescending(r => (double) r.Detail["properties"]["distance"])
            //            .First();
            //            //.Take(cwr.Count - 2);

            //        //foreach (var road in roadSet)
            //        //{
            //            Guid otherCityId;
            //            otherCityId = (Guid) road.Detail["properties"]["cityAId"] == cwr.cityId
            //                ? (Guid) road.Detail["properties"]["cityBId"]
            //                : (Guid) road.Detail["properties"]["cityAId"];
            //            var otherCityHasTooManyRoads = values.Count(v =>
            //                                               v.Detail["properties"]["cityAId"] != null &&
            //                                               v.Detail["properties"]["usage"] != null &&
            //                                               (string) v.Detail["properties"]["usage"] == "Accepted" &&
            //                                               ((Guid) v.Detail["properties"]["cityAId"] == otherCityId ||
            //                                                (Guid) v.Detail["properties"]["cityBId"] == otherCityId)) >
            //                                           2;

            //            if (!otherCityHasTooManyRoads)
            //            {
            //                continue;
            //            }

            //            var roadGeoJson = (JObject) road.Detail;

            //            roadGeoJson["properties"]["usage"] = "Rejected";
            //        //}
            //    }

            //    acceptedRoads = values.Where(v =>
            //            v.Detail["properties"]["cityAId"] != null &&
            //            v.Detail["properties"]["usage"] != null &&
            //            (string) v.Detail["properties"]["usage"] == "Accepted")
            //        .ToList();
            //    salesmansJourney = new StringBuilder();
            //    salesmansJourney.Append("{\"type\":\"FeatureCollection\",\"features\":[");
            //    salesmansJourney.Append(string.Join(',', acceptedRoads.Select(v => v.Detail.ToString())));
            //    salesmansJourney.Append("]}");
            //    jny = JObject.Parse(salesmansJourney.ToString());
            //    File.WriteAllText($"routes{pass++}.json", jny.ToString());

            //    citiesWithRoads = acceptedRoads
            //        .SelectMany(ar => new[]
            //            {(Guid) ar.Detail["properties"]["cityAId"], (Guid) ar.Detail["properties"]["cityBId"]})
            //        .GroupBy(cg => cg)
            //        .Select(cg => new {cityId = cg.Key, Count = cg.Count()})
            //        .ToList();
            //    citiesWithRoadsCount = citiesWithRoads.Count(cwr => cwr.Count >= 2);

            //    // Then there's a need to check for any cities with no roads at all connected to them (a possibility)
            //    // and add these to the same list with a count of zero for each one.
            //    if (citiesWithRoadsCount < cityCount)
            //    {
            //        var citiesWithNoRoads = values.Where(c =>
            //                c.Detail["properties"]["cityNo"] != null &&
            //                !citiesWithRoads.Select(cr => cr.cityId).Contains(c.EntityId))
            //            .Select(cn => new {cityId = cn.EntityId, Count = 0})
            //            .ToList();

            //        citiesWithNoRoadsCount = citiesWithNoRoads.Count;
            //        citiesWithRoads.AddRange(citiesWithNoRoads);
            //    }
            //} while (citiesWithRoadsCount != cityCount || citiesWithNoRoadsCount != 0);
            //values.Add(new Value(jny));

            //var startingStore = new RulEngStore
            //{
            //    Values = values.ToImmutableHashSet()
            //};

            //RvStore = new Store<RulEngStore>(null, startingStore);



            //var requestJObj = JToken.Parse("{\"Q\": \"How can we help?\", \"AA\":[\"New Claim\", \"Existing Claim\"]}");
            //var requestObj = new Value(12);

            //(rule, ruleResult, rulePrescription) = requestObj.Exists();

            //rules.Add(rule);
            //ruleResults.Add(ruleResult);
            //rulePrescriptions.Add(rulePrescription);

            //(operation, operationPrescription) = ruleResult.Create<Value>(requestObj);

            //operations.Add(operation);
            //operationPrescriptions.Add(operationPrescription);
            //values.Add(requestObj);

            //requestObj = new Value(13);
            //(ruleResult, rulePrescription) = requestObj.Exists(rule);
            //ruleResults.Add(ruleResult);

            //(operation, operationPrescription) = ruleResult.Create<Value>(requestObj);

            //operations.Add(operation);
            //operationPrescriptions.Add(operationPrescription);
            //values.Add(requestObj);



            //var valIds = new List<Guid> {values[0].ValueId, values[1].ValueId};
            //(operation, value, operationPrescription) = ruleResult.Create<Value>(new [] { values[0].ValueId }.ToList());
            //operations.Add(operation);
            //values.Add(value);
            //operationPrescriptions.Add(operationPrescription);
            //(operation, value, operationPrescription) = ruleResult.Create<Value>(new[] { values[1].ValueId }.ToList());
            //operations.Add(operation);
            //values.Add(value);
            //operationPrescriptions.Add(operationPrescription);

            ////var procAllRules = new ProcessAllRulesPrescription();
            ////var procAllOperations = new ProcessAllOperationsPrescription();

            //RvStore = new Store<RulEngStore>(StoreReducers.ReduceStore);

            //RulEngStore changes;
            //RvStore.Subscribe(state => changes = state);

            //var act = RvStore.Dispatch(rulePrescription);
            //foreach (var prescription in operationPrescriptions)
            //{
            //    act = RvStore.Dispatch(prescription);
            //}

            //File.WriteAllText("storeBefore.json", RvStore.GetState().ToString());
            //act = rvStore.Dispatch(procAllRules);
            // File.WriteAllText("storeMiddle.json", rvStore.GetState().ToString());
            //act = rvStore.Dispatch(procAllOperations);
            //File.WriteAllText("storeAfter.json", RvStore.GetState().ToString());
            pass++;
        }