/// <summary> /// Construct new <see cref="VotePartition"/> /// </summary> /// <param name="voteLine">The vote line that the partition is composed of.</param> /// <exception cref="ArgumentNullException"/> public VotePartition(VoteLine voteLine, VoteType voteType) { VoteType = voteType; AddLine(voteLine); }
/// <summary> /// Take the initial value of a post, and substitute plans and /// proxy vote lines as appropriate. /// </summary> /// <param name="post">The original post.</param> /// <returns>Returns a list of VoteLines to use for the purpose of processing the vote.</returns> public static List <VotePartition> GetVotePartitions(Post post, IQuest quest) { if (post == null) { throw new ArgumentNullException(nameof(post)); } if (quest == null) { throw new ArgumentNullException(nameof(quest)); } List <VotePartition> results = new List <VotePartition>(); VotePartition partition = null; VoteLine parent = null; // Move through the vote lines by index so we can modify the // index pointer directly when doing comparisons. var voteLineArray = post.Vote.VoteLines.ToArray(); int index = 0; while (index < voteLineArray.Length) { // Current line. VoteLine line = voteLineArray[index]; // First check if the line is a proxy vote for a plan or a user Match m = startsWithPlan.Match(line.ComparableContent); // "Plan XYZ" if (m.Success) { string planname = m.Groups["planname"].Value; // Plan has priority, in case the plan is named after the user. if (VotingRecords.Instance.HasPlanName(planname)) { // Get the collection of variants of this plan name. var plans = VotingRecords.Instance.GetPlans(planname); Plan plan = null; // Make sure we don't go over the limit of the remaining post vote lines. int remainingLines = voteLineArray.Length - index - 1; if (remainingLines == 0) { // If there are no remaining lines in the current post, we // just take the provided variant 0. plan = plans.First(p => p.Identity.Number == 0); } else { // OK, there are lines that may potentially be copied from the plan. // Limit the plans we're looking at to those that can fit in our remaining line space. var limitedPlans = plans.Where(p => p.Content.VoteLines.Count <= remainingLines).ToList(); if (limitedPlans.Any()) { foreach (var p in limitedPlans) { // In each of the plans that can fit in the remaining vote space, // compare them with an equal-sized segment of the vote. // If it matches, that's a copy of the plan, and we can skip past it. var segment = new ArraySegment <VoteLine>(voteLineArray, index, p.Content.VoteLines.Count); if (segment.SequenceEqual(p.Content.VoteLines)) { plan = p; break; } } // However if no sequence match was found, we only have the naming line to // copy, and we can move on to the next line of the post. if (plan == null) { plan = limitedPlans.First(p => p.Identity.Number == 0); } else { // If we did find the full match, update the index so that we increment past that. index += plan.Content.VoteLines.Count - 1; } } else { // None of the selected plans can fit in the remaining lines of the post. // Thus we don't need to try to match, and just take the named line. plan = plans.First(p => p.Identity.Number == 0); } } // If we found a plan, add its contents to the results and go to move next. // Otherwise, drop out and treat this as a normal line. if (plan != null) { List <VotePartition> planPartitions = VotingRecords.Instance.GetPartitionsForIdentity(plan.Identity); results.AddRange(planPartitions); goto moveNext; } } // Otherwise check if it's a user proxy. Usernames are unlikely to be allowed to be greater // than 20 characters, but giving a fair buffer just in case. Don't check for very long strings // as usernames. Also check global options in case proxy votes are disabled. else if (planname.Length <= 40 && !AdvancedOptions.Instance.DisableProxyVotes) { List <VotePartition> proxyPartitions = GetProxyPartitions(planname, false, post.Identity); // There are no matching identities. Treat as a normal line. if (proxyPartitions == null) { goto normalLine; } // If there are no recorded partitions for this identity, it hasn't been // processed yet, and this is a future reference, or a past vote that itself // has future references and hasn't completed processing yet. if (!proxyPartitions.Any()) { // If we aren't forcing the processing of this post, just bail and return null. // If we *are* forcing it, we have to treat this as a normal line. if (post.ForceProcess) { goto normalLine; } else { VotingRecords.Instance.NoteFutureReference(post); return(null); } } // Add whatever proxy partitions we got to he results and move on. results.AddRange(proxyPartitions); goto moveNext; } } // If it doesn't start with 'plan', also check if it's a (possibly pinned) username proxy. // Usernames are unlikely to be allowed to be greater than 20 characters, but giving // a fair buffer just in case. Don't check for very long strings as usernames. // Also check global options in case proxy votes are disabled. else { // Proxy regex might start with ^ to indicate a pinned name, and have a // username length of no more than 40 characters. m = proxyRegex.Match(line.ComparableContent); if (m.Success && !AdvancedOptions.Instance.DisableProxyVotes) { bool pin = m.Groups["pin"].Success; string username = m.Groups["username"].Value; List <VotePartition> proxyPartitions = GetProxyPartitions(username, pin, post.Identity); // There are no matching identities. Treat as a normal line. if (proxyPartitions == null) { goto normalLine; } // If there are no recorded partitions for this identity, it hasn't been // processed yet, and this is a future reference, or a past vote that itself // has future references and hasn't completed processing yet. if (!proxyPartitions.Any()) { // If we aren't forcing the processing of this post, just bail and return null. // If we *are* forcing it, we have to treat this as a normal line. if (post.ForceProcess) { goto normalLine; } else { VotingRecords.Instance.NoteFutureReference(post); return(null); } } // Add whatever proxy partitions we got to he results and move on. results.AddRange(proxyPartitions); goto moveNext; } // If we have a plan name that doesn't have the 'plan' prefix, redo the logic // of skipping past any copied content. else if (VotingRecords.Instance.HasPlanName(line.ComparableContent)) { string planname = line.ComparableContent; // Get the collection of variants of this plan name. var plans = VotingRecords.Instance.GetPlans(planname); Plan plan = null; // Make sure we don't go over the limit of the remaining post vote lines. int remainingLines = voteLineArray.Length - index - 1; if (remainingLines == 0) { // If there are no remaining lines in the current post, we // just take the provided variant 0. plan = plans.First(p => p.Identity.Number == 0); } else { // OK, there are lines that may potentially be copied from the plan. // Limit the plans we're looking at to those that can fit in our remaining line space. var limitedPlans = plans.Where(p => p.Content.VoteLines.Count <= remainingLines).ToList(); if (limitedPlans.Any()) { foreach (var p in limitedPlans) { // In each of the plans that can fit in the remaining vote space, // compare them with an equal-sized segment of the vote. // If it matches, that's a copy of the plan, and we can skip past it. var segment = new ArraySegment <VoteLine>(voteLineArray, index, p.Content.VoteLines.Count); if (segment.SequenceEqual(p.Content.VoteLines)) { plan = p; break; } } // However if no sequence match was found, we only have the naming line to // copy, and we can move on to the next line of the post. if (plan == null) { plan = limitedPlans.First(p => p.Identity.Number == 0); } else { // If we did find the full match, update the index so that we increment past that. index += plan.Content.VoteLines.Count - 1; } } else { // None of the selected plans can fit in the remaining lines of the post. // Thus we don't need to try to match, and just take the named line. plan = plans.First(p => p.Identity.Number == 0); } } // If we found a plan, add its contents to the results and go to move next. // Otherwise, drop out and treat this as a normal line. if (plan != null) { List <VotePartition> planPartitions = VotingRecords.Instance.GetPartitionsForIdentity(plan.Identity); results.AddRange(planPartitions); goto moveNext; } } } // Not a plan proxy, and not a user proxy. Handle normal partitioning. normalLine: if (partition == null) { partition = new VotePartition(); } if (parent == null) { parent = line; } switch (quest.PartitionMode) { case PartitionMode.None: partition.AddLine(line); break; case PartitionMode.ByLine: if (partition.VoteLines.Count > 0) { results.Add(partition); } partition = new VotePartition(line, VoteType.Vote); break; case PartitionMode.ByLineTask: if (partition.VoteLines.Count > 0) { results.Add(partition); } if (line.Prefix.Length == 0) { parent = line; partition = new VotePartition(line, VoteType.Vote); } else if (parent.Task != string.Empty && line.Task == string.Empty) { var mLine = line.Modify(task: parent.Task); partition = new VotePartition(mLine, VoteType.Vote); } else { partition = new VotePartition(line, VoteType.Vote); } break; case PartitionMode.ByBlock: case PartitionMode.ByBlockAll: if (line.Prefix.Length == 0) { if (partition.VoteLines.Count > 0) { results.Add(partition); } partition = new VotePartition(line, VoteType.Vote); } else { partition.AddLine(line); } break; default: break; } // Exit point for next enumerator line. moveNext: index++; } if (partition != null && partition.VoteLines.Count > 0) { results.Add(partition); } return(results); }