TypeEffectivenessMultiplierCodec.kt

package io.github.lishangbu.avalon.dataset.service.impl

import java.math.BigDecimal

/**
 * 属性相克倍率的定点编解码器。
 *
 * 数据库存储的不是浮点数,而是放大 100 倍后的整数百分比:
 * - 100 表示 1.00x
 * - 50 表示 0.50x
 * - 200 表示 2.00x
 *
 * 这样设计有两个目的:
 * 1. 避免 0.5、1.5 这类值在浮点存取和比较时产生二进制误差。
 * 2. 让数据库层、仓储层和业务层都围绕同一套定点规则运转。
 *
 * 双属性计算同样保持在这套定点规则内完成。
 * 假设两个已编码倍率分别为 a、b,它们的真实值是 a/100 和 b/100。
 * 为了让乘法结果继续保持“放大 100 倍后的整数”这一编码约定,需要执行:
 *
 *     ((a / 100) * (b / 100)) * 100 = a * b / 100
 *
 * 例如:
 * - 50(0.50x) * 50(0.50x) / 100 = 25
 * - 25 再解码回真实倍率后就是 0.25x
 */
internal object TypeEffectivenessMultiplierCodec {
    internal const val ONE_X_PERCENT: Int = 100
    private val STORAGE_DIVISOR: BigDecimal = BigDecimal.valueOf(ONE_X_PERCENT.toLong())
    private val ALLOWED_ENTRY_MULTIPLIERS: List<BigDecimal> =
        listOf(
            BigDecimal.ZERO,
            BigDecimal("0.5"),
            BigDecimal.ONE,
            BigDecimal("2"),
        )

    /**
     * 将 API 层的自然倍率编码为数据库使用的定点整数。
     *
     * 这里保留 `BigDecimal` 作为 API 类型,是为了继续向上层暴露自然倍率语义;
     * 但一旦进入持久化边界,必须立即转换为整数百分比,避免后续链路回到浮点。
     */
    internal fun encodeEntryMultiplier(multiplier: BigDecimal?): Int? {
        if (multiplier == null) {
            return null
        }
        require(ALLOWED_ENTRY_MULTIPLIERS.any { it.compareTo(multiplier) == 0 }) {
            "multiplier must be one of ${allowedEntryMultiplierLabels()} or null"
        }
        return multiplier.multiply(STORAGE_DIVISOR).intValueExact()
    }

    /**
     * 将数据库中的定点整数解码为 API 层返回的自然倍率。
     *
     * 该方法只在对外输出时调用,保证数据库内部始终只流转整数定点值。
     */
    internal fun decode(storedPercent: Int?): BigDecimal? = storedPercent?.let { BigDecimal.valueOf(it.toLong()).divide(STORAGE_DIVISOR) }

    /**
     * 在不离开定点表示的前提下完成倍率乘法。
     *
     * `leftPercent` 和 `rightPercent` 都是“放大 100 倍后的整数”,
     * 所以乘积需要再除以 100 才能回到同一缩放单位。
     */
    internal fun multiplyStoredPercents(
        leftPercent: Int,
        rightPercent: Int,
    ): Int {
        val scaledProduct = leftPercent.toLong() * rightPercent.toLong()
        check(scaledProduct % ONE_X_PERCENT.toLong() == 0L) {
            "Stored multiplier product $scaledProduct cannot be represented with scale $ONE_X_PERCENT"
        }
        return (scaledProduct / ONE_X_PERCENT).toInt()
    }

    private fun allowedEntryMultiplierLabels(): String = ALLOWED_ENTRY_MULTIPLIERS.joinToString(prefix = "[", postfix = "]") { it.stripTrailingZeros().toPlainString() }
}