private IEnumerable <DataSourceValueGroup> ExecuteSeriesFunction(Target sourceTarget, Tuple <SeriesFunction, string, GroupOperation> parsedFunction, DateTime startTime, DateTime stopTime, string interval, bool decimate, bool dropEmptySeries, CancellationToken cancellationToken)
        {
            SeriesFunction seriesFunction = parsedFunction.Item1;
            string         expression     = parsedFunction.Item2;
            GroupOperation groupOperation = parsedFunction.Item3;

            // Parse out function parameters and target expression
            Tuple <string[], string> expressionParameters = TargetCache <Tuple <string[], string> > .GetOrAdd(expression, () =>
            {
                List <string> parsedParameters = new List <string>();

                // Extract any required function parameters
                int requiredParameters = s_requiredParameters[seriesFunction]; // Safe: no lock needed since content doesn't change

                // Any slice operation adds one required parameter for time tolerance
                if (groupOperation == GroupOperation.Slice)
                {
                    requiredParameters++;
                }

                if (requiredParameters > 0)
                {
                    int index = 0;

                    for (int i = 0; i < requiredParameters && index > -1; i++)
                    {
                        index = expression.IndexOf(',', index + 1);
                    }

                    if (index > -1)
                    {
                        parsedParameters.AddRange(expression.Substring(0, index).Split(','));
                    }

                    if (parsedParameters.Count == requiredParameters)
                    {
                        expression = expression.Substring(index + 1).Trim();
                    }
                    else
                    {
                        throw new FormatException($"Expected {requiredParameters + 1} parameters, received {parsedParameters.Count + 1} in: {(groupOperation == GroupOperation.None ? "" : groupOperation.ToString())}{seriesFunction}({expression})");
                    }
                }

                // Extract any provided optional function parameters
                int optionalParameters = s_optionalParameters[seriesFunction]; // Safe: no lock needed since content doesn't change

                bool hasSubExpression(string target) => target.StartsWith("FILTER", StringComparison.OrdinalIgnoreCase) || target.Contains("(");

                if (optionalParameters > 0)
                {
                    int index = expression.IndexOf(',');
                    int lastIndex;

                    if (index > -1 && !hasSubExpression(expression.Substring(0, index)))
                    {
                        lastIndex = index;

                        for (int i = 1; i < optionalParameters && index > -1; i++)
                        {
                            index = expression.IndexOf(',', index + 1);

                            if (index > -1 && hasSubExpression(expression.Substring(lastIndex + 1, index - lastIndex - 1).Trim()))
                            {
                                index = lastIndex;
                                break;
                            }

                            lastIndex = index;
                        }

                        if (index > -1)
                        {
                            parsedParameters.AddRange(expression.Substring(0, index).Split(','));
                            expression = expression.Substring(index + 1).Trim();
                        }
                    }
                }

                return(new Tuple <string[], string>(parsedParameters.ToArray(), expression));
            });

            string[] parameters      = expressionParameters.Item1;
            string   queryExpression = expressionParameters.Item2; // Final function parameter is always target expression

            // When accurate calculation results are requested, query data source at full resolution
            if (seriesFunction == SeriesFunction.Interval && ParseFloat(parameters[0]) == 0.0D)
            {
                decimate = false;
            }

            // Query function expression to get series data
            IEnumerable <DataSourceValueGroup> dataset = QueryTarget(sourceTarget, queryExpression, startTime, stopTime, interval, decimate, dropEmptySeries, cancellationToken);

            // Handle label function as a special edge case - group operations on label are ignored
            if (seriesFunction == SeriesFunction.Label)
            {
                // Derive labels
                string label = parameters[0];

                if (label.StartsWith("\"") || label.StartsWith("'"))
                {
                    label = label.Substring(1, label.Length - 2);
                }

                DataSourceValueGroup[] valueGroups = dataset.ToArray();
                string[] seriesLabels = new string[valueGroups.Length];

                for (int i = 0; i < valueGroups.Length; i++)
                {
                    string target = valueGroups[i].RootTarget;

                    seriesLabels[i] = TargetCache <string> .GetOrAdd($"{label}@{target}", () =>
                    {
                        string table, derivedLabel;
                        string[] components = label.Split('.');

                        if (components.Length == 2)
                        {
                            table        = components[0].Trim();
                            derivedLabel = components[1].Trim();
                        }
                        else
                        {
                            table        = "ActiveMeasurements";
                            derivedLabel = label;
                        }

                        DataRow record = target.MetadataRecordFromTag(Metadata, table);

                        if (record != null && derivedLabel.IndexOf('{') >= 0)
                        {
                            foreach (string fieldName in record.Table.Columns.Cast <DataColumn>().Select(column => column.ColumnName))
                            {
                                derivedLabel = derivedLabel.ReplaceCaseInsensitive($"{{{fieldName}}}", record[fieldName].ToString());
                            }
                        }

                        // ReSharper disable once AccessToModifiedClosure
                        if (derivedLabel.Equals(label, StringComparison.Ordinal))
                        {
                            derivedLabel = $"{label}{(valueGroups.Length > 1 ? $" {i + 1}" : "")}";
                        }

                        return(derivedLabel);
                    });
                }

                // Verify that all series labels are unique
                if (seriesLabels.Length > 1)
                {
                    HashSet <string> uniqueLabelSet = new HashSet <string>(StringComparer.OrdinalIgnoreCase);

                    for (int i = 0; i < seriesLabels.Length; i++)
                    {
                        while (uniqueLabelSet.Contains(seriesLabels[i]))
                        {
                            seriesLabels[i] = $"{seriesLabels[i]}\u00A0"; // Suffixing with non-breaking space for label uniqueness
                        }
                        uniqueLabelSet.Add(seriesLabels[i]);
                    }
                }

                for (int i = 0; i < valueGroups.Length; i++)
                {
                    yield return(new DataSourceValueGroup
                    {
                        Target = seriesLabels[i],
                        RootTarget = valueGroups[i].RootTarget,
                        SourceTarget = sourceTarget,
                        Source = valueGroups[i].Source,
                        DropEmptySeries = dropEmptySeries
                    });
                }
            }
            else
            {
                switch (groupOperation)
                {
                case GroupOperation.Set:
                {
                    // Flatten all series into a single enumerable
                    DataSourceValueGroup valueGroup = new DataSourceValueGroup
                    {
                        Target          = $"Set{seriesFunction}({string.Join(", ", parameters)}{(parameters.Length > 0 ? ", " : "")}{queryExpression})",
                        RootTarget      = queryExpression,
                        SourceTarget    = sourceTarget,
                        Source          = ExecuteSeriesFunctionOverSource(dataset.AsParallel().WithCancellation(cancellationToken).SelectMany(source => source.Source), seriesFunction, parameters),
                        DropEmptySeries = dropEmptySeries
                    };

                    // Handle edge-case set operations - for these functions there is data in the target series as well
                    if (seriesFunction == SeriesFunction.Minimum || seriesFunction == SeriesFunction.Maximum || seriesFunction == SeriesFunction.Median)
                    {
                        DataSourceValue dataValue = valueGroup.Source.First();
                        valueGroup.Target     = $"Set{seriesFunction} = {dataValue.Target}";
                        valueGroup.RootTarget = dataValue.Target;
                    }

                    yield return(valueGroup);

                    break;
                }

                case GroupOperation.Slice:
                {
                    TimeSliceScanner scanner = new TimeSliceScanner(dataset, ParseFloat(parameters[0]) / SI.Milli);
                    parameters = parameters.Skip(1).ToArray();

                    foreach (DataSourceValueGroup valueGroup in ExecuteSeriesFunctionOverTimeSlices(scanner, seriesFunction, parameters, cancellationToken))
                    {
                        yield return(new DataSourceValueGroup
                            {
                                Target = $"Slice{seriesFunction}({string.Join(", ", parameters)}{(parameters.Length > 0 ? ", " : "")}{valueGroup.Target})",
                                RootTarget = valueGroup.RootTarget ?? valueGroup.Target,
                                SourceTarget = sourceTarget,
                                Source = valueGroup.Source,
                                DropEmptySeries = dropEmptySeries
                            });
                    }

                    break;
                }

                default:
                {
                    foreach (DataSourceValueGroup valueGroup in dataset)
                    {
                        yield return(new DataSourceValueGroup
                            {
                                Target = $"{seriesFunction}({string.Join(", ", parameters)}{(parameters.Length > 0 ? ", " : "")}{valueGroup.Target})",
                                RootTarget = valueGroup.RootTarget ?? valueGroup.Target,
                                SourceTarget = sourceTarget,
                                Source = ExecuteSeriesFunctionOverSource(valueGroup.Source, seriesFunction, parameters),
                                DropEmptySeries = dropEmptySeries
                            });
                    }

                    break;
                }
                }
            }
        }
        // Execute series function over a set of points from each series at the same time-slice
        private static IEnumerable <DataSourceValueGroup> ExecuteSeriesFunctionOverTimeSlices(TimeSliceScanner scanner, SeriesFunction seriesFunction, string[] parameters, CancellationToken cancellationToken)
        {
            IEnumerable <DataSourceValue> readSliceValues()
            {
                while (!scanner.DataReadComplete && !cancellationToken.IsCancellationRequested)
                {
                    foreach (DataSourceValue dataValue in ExecuteSeriesFunctionOverSource(scanner.ReadNextTimeSlice(), seriesFunction, parameters, true))
                    {
                        yield return(dataValue);
                    }
                }
            }

            foreach (IGrouping <string, DataSourceValue> valueGroup in readSliceValues().GroupBy(dataValue => dataValue.Target))
            {
                yield return(new DataSourceValueGroup
                {
                    Target = valueGroup.Key,
                    RootTarget = valueGroup.Key,
                    Source = valueGroup
                });
            }
        }