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. /// Items 0 to (total * percentage1 - 1) get value1, and so on. /// /// Zero-based index of the item. /// Total number of items being distributed. For best accuracy, use totals >= 100. /// The value assigned to this index position. public T Select(int index, int total) { var cumulative = 0; foreach (var (value, percentage) in _buckets) { cumulative += (int)(total * percentage); if (index < cumulative) { return value; } } return _buckets[^1].Value; } /// /// Returns all values with their calculated counts for a given total. /// The last bucket receives any remainder from rounding. /// /// Total number of items to distribute. /// Sequence of value-count pairs. public IEnumerable<(T Value, int Count)> GetCounts(int total) { var remaining = total; for (var i = 0; i < _buckets.Length - 1; i++) { var count = (int)(total * _buckets[i].Percentage); yield return (_buckets[i].Value, count); remaining -= count; } yield return (_buckets[^1].Value, remaining); } }