namespace Bit.Seeder.Data.Distributions; /// /// Provides deterministic, percentage-based item selection for test data generation. /// Replaces duplicated distribution logic in GetRealisticStatus, GetFolderCountForUser, etc. /// /// The type of values in the distribution. public sealed class Distribution { private readonly (T Value, double Percentage)[] _buckets; /// /// Creates a distribution from percentage buckets. /// /// Value-percentage pairs that must sum to 1.0 (within 0.001 tolerance). /// Thrown when percentages don't sum to 1.0. public Distribution(params (T Value, double Percentage)[] buckets) { var total = buckets.Sum(b => b.Percentage); if (Math.Abs(total - 1.0) > 0.001) { throw new ArgumentException($"Percentages must sum to 1.0, got {total}"); } _buckets = buckets; } /// /// Selects a value deterministically based on index position within a total count. /// Remainder items go to buckets with the largest fractional parts, /// not unconditionally to the last bucket. /// /// Zero-based index of the item. /// Total number of items being distributed. /// The value assigned to this index position. public T Select(int index, int total) { var cumulative = 0; foreach (var (value, count) in GetCounts(total)) { cumulative += count; if (index < cumulative) { return value; } } return _buckets[^1].Value; } /// /// Returns all values with their calculated counts for a given total. /// Each bucket gets its truncated share, then the deficit is distributed one-at-a-time /// to buckets with the largest fractional remainders. /// Zero-weight buckets always receive exactly zero items. /// /// Total number of items to distribute. /// Sequence of value-count pairs. public IEnumerable<(T Value, int Count)> GetCounts(int total) { var counts = new int[_buckets.Length]; var remainders = new double[_buckets.Length]; var allocated = 0; for (var i = 0; i < _buckets.Length; i++) { var exact = total * _buckets[i].Percentage; counts[i] = (int)exact; remainders[i] = exact - counts[i]; allocated += counts[i]; } var deficit = total - allocated; for (var d = 0; d < deficit; d++) { var bestIdx = 0; for (var i = 1; i < remainders.Length; i++) { if (remainders[i] > remainders[bestIdx]) { bestIdx = i; } } counts[bestIdx]++; remainders[bestIdx] = -1.0; } for (var i = 0; i < _buckets.Length; i++) { yield return (_buckets[i].Value, counts[i]); } } }