Skip to content

Instantly share code, notes, and snippets.

@in-async
Last active September 10, 2021 07:53
Show Gist options
  • Save in-async/44cf29c7017147b31057c4b622e4ba5f to your computer and use it in GitHub Desktop.
Save in-async/44cf29c7017147b31057c4b622e4ba5f to your computer and use it in GitHub Desktop.
汎用按分器 in .NET
/// Usage
void Main() {
var weights = new decimal[] { 4, 6, 7.3m };
Prorate(total: 1014, weights: weights, ProrationRounding.SpecifiedOrder).Dump(nameof(ProrationRounding.SpecifiedOrder));
Prorate(total: 1014, weights: weights, ProrationRounding.DecimalPartDescending).Dump(nameof(ProrationRounding.DecimalPartDescending));
ProrateExact(total: 1014, weights: weights).Dump(nameof(ProrateExact));
}
/// <summary>
/// 整数按分で生じた端数の処理方法を表します。
/// </summary>
public enum ProrationRounding {
/// <summary>
/// 指定された重み配列の順に端数を割り振ります。
/// </summary>
/// <remarks>低コスト</remarks>
SpecifiedOrder,
/// <summary>
/// 小数部の大きい順に端数を割り振ります。
/// </summary>
/// <remarks>高コスト</remarks>
DecimalPartDescending,
}
public static IEnumerable<int> Prorate(
int total
, IEnumerable<decimal> weights
, ProrationRounding rounding
) {
if (weights is null) { throw new ArgumentNullException(nameof(weights)); }
List<(int integerPart, decimal decimalPart)> proratedList = new();
int remaineds = total;
foreach (decimal prorated in ProrateExact(total, weights)) {
int integerPart = decimal.ToInt32(prorated); // NOTE: https://docs.microsoft.com/en-us/dotnet/api/system.decimal.toint32?view=net-5.0#remarks
remaineds -= integerPart;
proratedList.Add((
integerPart,
decimalPart: prorated % 1
));
}
if (remaineds is 0) { return proratedList.Select(t => t.integerPart); }
switch (rounding) {
case ProrationRounding.SpecifiedOrder:
return proratedList.Select(t => {
if (remaineds is 0) { return t.integerPart; }
if (t.decimalPart is 0) { return t.integerPart; }
remaineds--;
return t.integerPart + 1;
});
case ProrationRounding.DecimalPartDescending:
return proratedList
.Select((parts, i) => (parts, i))
.OrderByDescending(t => t.parts.decimalPart)
.Select(t => {
if (remaineds is 0) { return (prorated: t.parts.integerPart, t.i); }
remaineds--;
return (prorated: t.parts.integerPart + 1, t.i);
})
.OrderBy(t => t.i)
.Select(t => t.prorated)
;
default:
throw new ArgumentException(nameof(rounding));
}
}
public static IEnumerable<decimal> ProrateExact(
decimal total
, IEnumerable<decimal> weights
) {
if (weights is null) { throw new ArgumentNullException(nameof(weights)); }
switch (weights) {
case IReadOnlyCollection<decimal>:
case ICollection<decimal>:
break;
default:
weights = weights.ToArray();
break;
}
decimal totalWeight = weights.Sum();
return weights.Select(weight => total * weight / totalWeight);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment