0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Kotlin】合計値を丸めた結果を変えずに、リストの各要素を丸める

Last updated at Posted at 2024-11-23
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()
}

/以上

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?