import kotlin.math.absoluteValue
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.round
/**
* 合計値を「偶数に丸め」た値を保ったまま、各要素の値を丸めた、新しいリストを返す。
*
* レシーバーであるリストの各要素の値を丸めた新しいリストを返す。
* レシーバーであるリストの全要素の合計値を「偶数に丸め」た値と、返値であるリストの全要素の合計値は、一致する。
*
* 「偶数に丸める」とは次のような処理である。
* - 端数が 0.5 より小さければ切り下げる。
* - 端数が 0.5 なら偶数側に丸める。(0.5 加えた値が偶数なら 0.5 加える。0.5 減じた値が偶数なら 0.5 減じる。)
* - 端数が 0.5 より大きければ切り上げる。
*
* 各要素については、端数を「0 から合計値への方向に丸め」た場合の変化量が小さいものを優先して「0 から合計値への方向に丸め」、
* 残りを「合計値から 0 への方向に丸め」る。
* 合計値が 0 の場合は、合計値が正であると考える。
* レシーバーに同じ値の要素が複数含まれていたとき、それぞれを丸めた結果が同じ値になるとは限らない。
*
* 例えばレシーバーが [0.8, 0.1] の場合、全要素の合計値は 0.9、合計値を偶数に丸めると 1.0。
* 各要素の端数を 0 から合計値 0.9 への方向(正の方向)に丸める(つまり切り上げる)と [1.0, 1.0]。
* その変化量は [0.2, 0.9]。
* そのため、変化量が小さいインデックス 0 の要素を切り上げ、変化量が大きいインデックス 1 の要素を切り下げて、
* 返値は [1.0, 0.0] となる。
* この合計値は 1.0 で、レシーバーの合計値を偶数に丸めた値と一致する。
*
* 元のリストの全要素の合計値が有限でない(すなわち [Double.NaN], [Double.POSITIVE_INFINITY], [Double.NEGATIVE_INFINITY] のいずれかである)場合は、
* 各要素を偶数に丸める。
* この場合も合計値を偶数に丸めた値は保たれる。
*/
fun Iterable<Double>.roundedElementsToPreserveSum(): List<Double> {
val numberList = this.toList()
// 合計値を偶数に丸めた値
val roundedSum = round(numberList.sum())
// 合計値が有限でない場合
if (roundedSum.isFinite().not()) {
// 全ての要素を偶数に丸めたリストを返す。
return numberList.map { round(it) }
}
// roundUp: 端数を 0 から合計値への方向に丸める関数
// roundDown: 端数を合計値から 0 への方向に丸める関数
val (roundUp, roundDown) =
if (roundedSum >= 0) {
::ceil to ::floor
} else {
::floor to ::ceil
}
// 各要素を roundDown した値の合計値
val sumOfRoundedDown = numberList.sumOf { roundDown(it) }
// 「合計値を偶数に丸めた値」と「各要素を roundDown した値の合計値」の差。
// 端数がある要素について、この数の要素を roundUp し、残りの要素を roundDown すれば、
// 合計値を丸めた値が変わらないように各要素を丸められる。
val roundingUpCount = (roundedSum - sumOfRoundedDown).absoluteValue.toInt()
return numberList
.asSequence()
// 後で元の順番に並べ直せるようにするため、インデックス付きにしておく。
.withIndex()
// roundDown した場合の変化量が大きい(roundUp したときの変化量が小さいが 0 ではない)順にソートする。
.sortedByDescending { (_, number) ->
(number - roundDown(number)).absoluteValue
}
// roundDown した場合の変化量が大きい順に roundingUpCount 個を roundUp し、残りを roundDown する。
.let { sorted ->
val roundedUp = sorted.take(roundingUpCount).map { it.copy(value = roundUp(it.value)) }
val roundedDown = sorted.drop(roundingUpCount).map { it.copy(value = roundDown(it.value)) }
roundedUp + roundedDown
}
// 元の順番に並べ直す。
.sortedBy { (index, _) -> index }
// インデックスを除く。
.map { (_, roundedNumber) -> roundedNumber }
.toList()
}
/以上