private static double ToTimeUnits(Time value, TargetTimeUnit target)
        {
            double time = value.ConvertTo(target.Unit);

            if (!double.IsNaN(target.Factor))
            {
                time /= target.Factor;
            }

            return(time);
        }
        private static Time FromTimeUnits(double value, TargetTimeUnit target)
        {
            double time = Time.ConvertFrom(value, target.Unit);

            if (!double.IsNaN(target.Factor))
            {
                time *= target.Factor;
            }

            return(time);
        }
            public static bool TryParse(string value, out TargetTimeUnit targetTimeUnit)
            {
                if (Enum.TryParse(value, out TimeUnit timeUnit))
                {
                    targetTimeUnit = new TargetTimeUnit
                    {
                        Unit = timeUnit
                    };

                    return(true);
                }

                switch (value?.ToLowerInvariant())
                {
                case "milliseconds":
                    targetTimeUnit = new TargetTimeUnit
                    {
                        Unit   = TimeUnit.Seconds,
                        Factor = SI.Milli
                    };

                    return(true);

                case "microseconds":
                    targetTimeUnit = new TargetTimeUnit
                    {
                        Unit   = TimeUnit.Seconds,
                        Factor = SI.Micro
                    };

                    return(true);

                case "nanoseconds":
                    targetTimeUnit = new TargetTimeUnit
                    {
                        Unit   = TimeUnit.Seconds,
                        Factor = SI.Nano
                    };

                    return(true);
                }

                targetTimeUnit = null;
                return(false);
            }
        // Design philosophy: whenever possible this function should delay source enumeration since source data sets could be very large.
        private static IEnumerable <DataSourceValue> ExecuteSeriesFunctionOverSource(IEnumerable <DataSourceValue> source, SeriesFunction seriesFunction, string[] parameters, bool isSliceOperation = false)
        {
            DataSourceValue[] values;
            DataSourceValue   result     = new DataSourceValue();
            double            lastValue  = double.NaN;
            double            lastTime   = 0.0D;
            string            lastTarget = null;

            IEnumerable <double> trackedValues = source.Select(dataValue =>
            {
                lastTime   = dataValue.Time;
                lastTarget = dataValue.Target;
                return(dataValue.Value);
            });

            double         baseTime, timeStep, value, low, high;
            bool           normalizeTime, lowInclusive, highInclusive;
            int            count;
            TargetTimeUnit timeUnit;
            AngleUnit      angleUnit;

            switch (seriesFunction)
            {
            case SeriesFunction.Minimum:
                DataSourceValue minValue = new DataSourceValue {
                    Value = double.MaxValue
                };

                foreach (DataSourceValue dataValue in source)
                {
                    if (dataValue.Value <= minValue.Value)
                    {
                        minValue = dataValue;
                    }
                }

                if (minValue.Time > 0.0D)
                {
                    yield return(minValue);
                }

                break;

            case SeriesFunction.Maximum:
                DataSourceValue maxValue = new DataSourceValue {
                    Value = double.MinValue
                };

                foreach (DataSourceValue dataValue in source)
                {
                    if (dataValue.Value >= maxValue.Value)
                    {
                        maxValue = dataValue;
                    }
                }

                if (maxValue.Time > 0.0D)
                {
                    yield return(maxValue);
                }

                break;

            case SeriesFunction.Average:
                result.Value  = trackedValues.Average();
                result.Time   = lastTime;
                result.Target = lastTarget;
                yield return(result);

                break;

            case SeriesFunction.Total:
                result.Value  = trackedValues.Sum();
                result.Time   = lastTime;
                result.Target = lastTarget;
                yield return(result);

                break;

            case SeriesFunction.Range:
                DataSourceValue rangeMin = new DataSourceValue {
                    Value = double.MaxValue
                };
                DataSourceValue rangeMax = new DataSourceValue {
                    Value = double.MinValue
                };

                foreach (DataSourceValue dataValue in source)
                {
                    if (dataValue.Value <= rangeMin.Value)
                    {
                        rangeMin = dataValue;
                    }

                    if (dataValue.Value >= rangeMax.Value)
                    {
                        rangeMax = dataValue;
                    }
                }

                if (rangeMin.Time > 0.0D && rangeMax.Time > 0.0D)
                {
                    result       = rangeMax;
                    result.Value = rangeMax.Value - rangeMin.Value;
                    yield return(result);
                }
                break;

            case SeriesFunction.Count:
                result.Value  = trackedValues.Count();
                result.Time   = lastTime;
                result.Target = lastTarget;
                yield return(result);

                break;

            case SeriesFunction.Distinct:
                foreach (DataSourceValue dataValue in source.DistinctBy(dataValue => dataValue.Value))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.AbsoluteValue:
                foreach (DataSourceValue dataValue in source.Select(dataValue => new DataSourceValue {
                    Value = Math.Abs(dataValue.Value), Time = dataValue.Time, Target = dataValue.Target
                }))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.Add:
                value = ParseFloat(parameters[0], source, false, isSliceOperation);

                foreach (DataSourceValue dataValue in source.Select(dataValue => new DataSourceValue {
                    Value = dataValue.Value + value, Time = dataValue.Time, Target = dataValue.Target
                }))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.Subtract:
                value = ParseFloat(parameters[0], source, false, isSliceOperation);

                foreach (DataSourceValue dataValue in source.Select(dataValue => new DataSourceValue {
                    Value = dataValue.Value - value, Time = dataValue.Time, Target = dataValue.Target
                }))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.Multiply:
                value = ParseFloat(parameters[0], source, false, isSliceOperation);

                foreach (DataSourceValue dataValue in source.Select(dataValue => new DataSourceValue {
                    Value = dataValue.Value * value, Time = dataValue.Time, Target = dataValue.Target
                }))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.Divide:
                value = ParseFloat(parameters[0], source, false, isSliceOperation);

                foreach (DataSourceValue dataValue in source.Select(dataValue => new DataSourceValue {
                    Value = dataValue.Value / value, Time = dataValue.Time, Target = dataValue.Target
                }))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.Round:
                count = parameters.Length == 0 ? 0 : ParseInt(parameters[0]);

                foreach (DataSourceValue dataValue in source.Select(dataValue => new DataSourceValue {
                    Value = Math.Round(dataValue.Value, count), Time = dataValue.Time, Target = dataValue.Target
                }))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.Floor:
                foreach (DataSourceValue dataValue in source.Select(dataValue => new DataSourceValue {
                    Value = Math.Floor(dataValue.Value), Time = dataValue.Time, Target = dataValue.Target
                }))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.Ceiling:
                foreach (DataSourceValue dataValue in source.Select(dataValue => new DataSourceValue {
                    Value = Math.Ceiling(dataValue.Value), Time = dataValue.Time, Target = dataValue.Target
                }))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.Truncate:
                foreach (DataSourceValue dataValue in source.Select(dataValue => new DataSourceValue {
                    Value = Math.Truncate(dataValue.Value), Time = dataValue.Time, Target = dataValue.Target
                }))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.StandardDeviation:
                result.Value  = trackedValues.StandardDeviation(parameters.Length > 0 && parameters[0].Trim().ParseBoolean());
                result.Time   = lastTime;
                result.Target = lastTarget;
                yield return(result);

                break;

            case SeriesFunction.Median:
                values = source.Median();

                if (values.Length == 0)     //-V3080
                {
                    yield break;
                }

                result = values.Last();

                if (values.Length > 1)
                {
                    result.Value = values.Select(dataValue => dataValue.Value).Average();
                }

                yield return(result);

                break;

            case SeriesFunction.Mode:
                values = source.ToArray();
                yield return(values.MajorityBy(values.Last(), dataValue => dataValue.Value, false));

                break;

            case SeriesFunction.Top:
                values = source.ToArray();

                if (values.Length == 0)
                {
                    yield break;
                }

                count = ParseCount(parameters[0], values, isSliceOperation);

                if (count > values.Length)
                {
                    count = values.Length;
                }

                normalizeTime = parameters.Length == 1 || parameters[1].Trim().ParseBoolean();
                baseTime      = values[0].Time;
                timeStep      = (values[values.Length - 1].Time - baseTime) / (count - 1).NotZero(1);
                Array.Sort(values, (a, b) => a.Value < b.Value ? -1 : (a.Value > b.Value ? 1 : 0));

                foreach (DataSourceValue dataValue in values.Take(count).Select((dataValue, i) => new DataSourceValue {
                    Value = dataValue.Value, Time = normalizeTime ? baseTime + i * timeStep : dataValue.Time, Target = dataValue.Target
                }))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.Bottom:
                values = source.ToArray();

                if (values.Length == 0)
                {
                    yield break;
                }

                count = ParseCount(parameters[0], values, isSliceOperation);

                if (count > values.Length)
                {
                    count = values.Length;
                }

                normalizeTime = parameters.Length == 1 || parameters[1].Trim().ParseBoolean();
                baseTime      = values[0].Time;
                timeStep      = (values[values.Length - 1].Time - baseTime) / (count - 1).NotZero(1);
                Array.Sort(values, (a, b) => a.Value > b.Value ? -1 : (a.Value < b.Value ? 1 : 0));

                foreach (DataSourceValue dataValue in values.Take(count).Select((dataValue, i) => new DataSourceValue {
                    Value = dataValue.Value, Time = normalizeTime ? baseTime + i * timeStep : dataValue.Time, Target = dataValue.Target
                }))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.Random:
                values = source.ToArray();

                if (values.Length == 0)
                {
                    yield break;
                }

                count = ParseCount(parameters[0], values, isSliceOperation);

                if (count > values.Length)
                {
                    count = values.Length;
                }

                normalizeTime = parameters.Length == 1 || parameters[1].Trim().ParseBoolean();
                baseTime      = values[0].Time;
                timeStep      = (values[values.Length - 1].Time - baseTime) / (count - 1).NotZero(1);
                List <int> indexes = new List <int>(Enumerable.Range(0, values.Length));
                indexes.Scramble();

                foreach (DataSourceValue dataValue in indexes.Take(count).Select((index, i) => new DataSourceValue {
                    Value = values[index].Value, Time = normalizeTime ? baseTime + i * timeStep : values[index].Time, Target = values[index].Target
                }))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.First:
                values = source.ToArray();

                if (values.Length == 0)
                {
                    yield break;
                }

                count = parameters.Length == 0 ? 1 : ParseCount(parameters[0], values, isSliceOperation);

                if (count > values.Length)
                {
                    count = values.Length;
                }

                for (int i = 0; i < count; i++)
                {
                    yield return(values[i]);
                }

                break;

            case SeriesFunction.Last:
                values = source.ToArray();

                if (values.Length == 0)
                {
                    yield break;
                }

                count = parameters.Length == 0 ? 1 : ParseCount(parameters[0], values, isSliceOperation);

                if (count > values.Length)
                {
                    count = values.Length;
                }

                for (int i = 0; i < count; i++)
                {
                    yield return(values[values.Length - 1 - i]);
                }

                break;

            case SeriesFunction.Percentile:
                double percent = ParsePercentage(parameters[0]);
                values = source.ToArray();

                if (values.Length == 0)
                {
                    yield break;
                }

                Array.Sort(values, (a, b) => a.Value < b.Value ? -1 : (a.Value > b.Value ? 1 : 0));
                count = values.Length;

                if (percent == 0.0D)
                {
                    yield return(values.First());
                }
                else if (percent == 100.0D)
                {
                    yield return(values.Last());
                }
                else
                {
                    double          n     = (count - 1) * (percent / 100.0D) + 1.0D;
                    int             k     = (int)n;
                    DataSourceValue kData = values[k];
                    double          d     = n - k;
                    double          k0    = values[k - 1].Value;
                    double          k1    = kData.Value;

                    result.Value  = k0 + d * (k1 - k0);
                    result.Time   = kData.Time;
                    result.Target = kData.Target;
                    yield return(result);
                }
                break;

            case SeriesFunction.Difference:
                foreach (DataSourceValue dataValue in source)
                {
                    if (lastTime > 0.0D)
                    {
                        yield return new DataSourceValue {
                                   Value = dataValue.Value - lastValue, Time = dataValue.Time, Target = lastTarget
                        }
                    }
                    ;

                    lastValue  = dataValue.Value;
                    lastTime   = dataValue.Time;
                    lastTarget = dataValue.Target;
                }
                break;

            case SeriesFunction.TimeDifference:
                if (parameters.Length == 0 || !TargetTimeUnit.TryParse(parameters[0], out timeUnit))
                {
                    timeUnit = new TargetTimeUnit {
                        Unit = TimeUnit.Seconds
                    }
                }
                ;

                foreach (DataSourceValue dataValue in source)
                {
                    if (lastTime > 0.0D)
                    {
                        yield return new DataSourceValue {
                                   Value = ToTimeUnits((dataValue.Time - lastTime) * SI.Milli, timeUnit), Time = dataValue.Time, Target = lastTarget
                        }
                    }
                    ;

                    lastTime   = dataValue.Time;
                    lastTarget = dataValue.Target;
                }
                break;

            case SeriesFunction.Derivative:
                if (parameters.Length == 0 || !TargetTimeUnit.TryParse(parameters[0], out timeUnit))
                {
                    timeUnit = new TargetTimeUnit {
                        Unit = TimeUnit.Seconds
                    }
                }
                ;

                foreach (DataSourceValue dataValue in source)
                {
                    if (lastTime > 0.0D)
                    {
                        yield return new DataSourceValue {
                                   Value = (dataValue.Value - lastValue) / ToTimeUnits((dataValue.Time - lastTime) * SI.Milli, timeUnit), Time = dataValue.Time, Target = lastTarget
                        }
                    }
                    ;

                    lastValue  = dataValue.Value;
                    lastTime   = dataValue.Time;
                    lastTarget = dataValue.Target;
                }
                break;

            case SeriesFunction.TimeIntegration:
                if (parameters.Length == 0 || !TargetTimeUnit.TryParse(parameters[0], out timeUnit))
                {
                    timeUnit = new TargetTimeUnit {
                        Unit = TimeUnit.Hours
                    }
                }
                ;

                result.Value = 0.0D;

                foreach (DataSourceValue dataValue in source)
                {
                    if (lastTime > 0.0D)
                    {
                        result.Value += dataValue.Value * ToTimeUnits((dataValue.Time - lastTime) * SI.Milli, timeUnit);
                    }

                    lastTime   = dataValue.Time;
                    lastTarget = dataValue.Target;
                }

                if (lastTime > 0.0D)
                {
                    result.Time   = lastTime;
                    result.Target = lastTarget;
                    yield return(result);
                }
                break;

            case SeriesFunction.Interval:
                if (parameters.Length == 1 || !TargetTimeUnit.TryParse(parameters[1], out timeUnit))
                {
                    timeUnit = new TargetTimeUnit {
                        Unit = TimeUnit.Seconds
                    }
                }
                ;

                value = FromTimeUnits(ParseFloat(parameters[0], source, true, isSliceOperation), timeUnit) / SI.Milli;

                foreach (DataSourceValue dataValue in source)
                {
                    if (lastTime > 0.0D)
                    {
                        if (dataValue.Time - lastTime > value)
                        {
                            lastTime = dataValue.Time;
                            yield return(dataValue);
                        }
                    }
                    else
                    {
                        lastTime = dataValue.Time;
                        yield return(dataValue);
                    }
                }
                break;

            case SeriesFunction.IncludeRange:
                low           = ParseFloat(parameters[0], source, false, isSliceOperation);
                high          = ParseFloat(parameters[1], source, false, isSliceOperation);
                lowInclusive  = parameters.Length > 2 && parameters[2].Trim().ParseBoolean();
                highInclusive = parameters.Length > 3 ? parameters[3].Trim().ParseBoolean() : lowInclusive;

                foreach (DataSourceValue dataValue in source.Where(dataValue => (lowInclusive ? dataValue.Value >= low : dataValue.Value > low) && (highInclusive ? dataValue.Value <= high : dataValue.Value < high)))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.ExcludeRange:
                low           = ParseFloat(parameters[0], source, false, isSliceOperation);
                high          = ParseFloat(parameters[1], source, false, isSliceOperation);
                lowInclusive  = parameters.Length > 2 && parameters[2].Trim().ParseBoolean();
                highInclusive = parameters.Length > 3 ? parameters[3].Trim().ParseBoolean() : lowInclusive;

                foreach (DataSourceValue dataValue in source.Where(dataValue => (lowInclusive ? dataValue.Value <= low : dataValue.Value < low) || (highInclusive ? dataValue.Value >= high : dataValue.Value > high)))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.FilterNaN:
                bool alsoFilterInifinity = parameters.Length == 0 || parameters[0].Trim().ParseBoolean();

                foreach (DataSourceValue dataValue in source.Where(dataValue => !(double.IsNaN(dataValue.Value) || alsoFilterInifinity && double.IsInfinity(dataValue.Value))))     //-V3130
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.UnwrapAngle:
                if (parameters.Length == 0 || !Enum.TryParse(parameters[0], true, out angleUnit))
                {
                    angleUnit = AngleUnit.Degrees;
                }

                values = source.ToArray();

                foreach (DataSourceValue dataValue in Angle.Unwrap(values.Select(dataValue => Angle.ConvertFrom(dataValue.Value, angleUnit))).Select((angle, index) => new DataSourceValue {
                    Value = angle.ConvertTo(angleUnit), Time = values[index].Time, Target = values[index].Target
                }))
                {
                    yield return(dataValue);
                }

                break;

            case SeriesFunction.WrapAngle:
                if (parameters.Length == 0 || !Enum.TryParse(parameters[0], true, out angleUnit))
                {
                    angleUnit = AngleUnit.Degrees;
                }

                foreach (DataSourceValue dataValue in source.Select(dataValue => new DataSourceValue {
                    Value = Angle.ConvertFrom(dataValue.Value, angleUnit).ToRange(-Math.PI, false).ConvertTo(angleUnit), Time = dataValue.Time, Target = dataValue.Target
                }))
                {
                    yield return(dataValue);
                }

                break;
            }
        }