using System; using System.Collections.Generic; using System.Linq; using TabletTester2025.Models; namespace TabletTester2025.Services { public readonly record struct HardnessStatistics( double Average, double AverageDeviation, double RsdPercent, double Maximum, double Minimum, int Count, bool IsPass); public static class TestCalculationService { public static HardnessStatistics CalculateHardness( IReadOnlyCollection values, double internalMin, double internalMax, int requiredCount) { var validValues = values .Where(value => double.IsFinite(value) && value > 0) .ToList(); if (validValues.Count == 0) return new HardnessStatistics(0, 0, 0, 0, 0, 0, false); double average = validValues.Average(); double averageDeviation = validValues.Average(value => Math.Abs(value - average)); double standardDeviation = CalculateSampleStandardDeviation(validValues, average); double rsd = average == 0 ? 0 : standardDeviation / average * 100; bool countMet = validValues.Count >= Math.Max(1, requiredCount); bool inRange = validValues.All(value => value >= internalMin && value <= internalMax); return new HardnessStatistics( average, averageDeviation, rsd, validValues.Max(), validValues.Min(), validValues.Count, countMet && inRange); } public static double CalculateFriabilityLossPercent(double weightBefore, double weightAfter) { if (!double.IsFinite(weightBefore) || !double.IsFinite(weightAfter) || weightBefore <= 0) throw new InvalidOperationException("脆碎前重量必须大于0"); if (weightAfter < 0) throw new InvalidOperationException("脆碎后重量数据异常"); if (weightAfter > weightBefore) throw new InvalidOperationException("脆碎后重量不能大于初始重量"); return (weightBefore - weightAfter) / weightBefore * 100; } public static bool TryGetDissolutionRateAt30Min( IReadOnlyList times, IReadOnlyList values, out double rate) { rate = 0; if (times.Count == 0 || times.Count != values.Count) return false; var points = times .Zip(values, (time, value) => new { Time = time, Value = value }) .Where(point => double.IsFinite(point.Time) && double.IsFinite(point.Value) && point.Time >= 0 && point.Value >= 0 && point.Value <= 150) .OrderBy(point => point.Time) .ToList(); if (points.Count == 0) return false; var exact = points.FirstOrDefault(point => Math.Abs(point.Time - 30) < 0.0001); if (exact != null) { rate = exact.Value; return true; } var before = points.LastOrDefault(point => point.Time < 30); var after = points.FirstOrDefault(point => point.Time > 30); if (before == null || after == null || after.Time <= before.Time) return false; double fraction = (30 - before.Time) / (after.Time - before.Time); rate = before.Value + (after.Value - before.Value) * fraction; return double.IsFinite(rate); } public static double CalculateRSquared(IReadOnlyList timeMinutes, IReadOnlyList concentration) { if (timeMinutes.Count < 2 || timeMinutes.Count != concentration.Count) return 0; int n = timeMinutes.Count; double sumX = timeMinutes.Sum(); double sumY = concentration.Sum(); double sumXY = timeMinutes.Zip(concentration, (x, y) => x * y).Sum(); double sumX2 = timeMinutes.Select(x => x * x).Sum(); double sumY2 = concentration.Select(y => y * y).Sum(); double numerator = n * sumXY - sumX * sumY; double denominator = Math.Sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY)); if (denominator <= 0 || double.IsNaN(denominator)) return 0; double r = numerator / denominator; double result = r * r; return double.IsFinite(result) ? result : 0; } public static bool ResolveCurrentTestQualified( TestType currentTest, bool hardnessPass, bool friabilityPass, bool disintegrationPass, bool dissolutionPass) { return currentTest switch { TestType.Hardness => hardnessPass, TestType.Friability => friabilityPass, TestType.Disintegration => disintegrationPass, TestType.Dissolution => dissolutionPass, _ => false }; } private static double CalculateSampleStandardDeviation(IReadOnlyList values, double average) { if (values.Count < 2) return 0; double sum = values.Sum(value => Math.Pow(value - average, 2)); return Math.Sqrt(sum / (values.Count - 1)); } } }