Bläddra i källkod

стратегии расчёта сдачи работают!!!

kpmy 5 år sedan
förälder
incheckning
fe6c82d622

+ 10 - 3
src/main/java/in/ocsf/bee/freigeld/core/model/BankAccount.java

@@ -1,7 +1,10 @@
 package in.ocsf.bee.freigeld.core.model;
 
+import org.jetbrains.annotations.NotNull;
+
 import java.util.Collection;
 import java.util.UUID;
+import java.util.stream.Collectors;
 
 public interface BankAccount {
 
@@ -9,9 +12,13 @@ public interface BankAccount {
 
     Long getOverall();
 
-    Collection<Coin> extractMoreOrExact(Long amount);
+    Collection<Coin> getCoins();
+
+    Coin extractOne(@NotNull UUID coinId);
 
-    Coin extractOne(UUID coinId);
+    default Collection<Coin> extract(@NotNull Collection<UUID> coinIds) {
+        return coinIds.stream().map(this::extractOne).collect(Collectors.toList());
+    }
 
-    void accept(Collection<Coin> coins);
+    void accept(@NotNull Collection<Coin> coins);
 }

+ 8 - 1
src/main/java/in/ocsf/bee/freigeld/core/model/CoinValue.java

@@ -10,7 +10,10 @@ public enum CoinValue {
     full(100, one.amount * 100),
     bi(200, one.amount * 200),
     mega(500, one.amount * 500),
-    internal(1000, one.amount * 1000);
+    internal(1000, one.amount * 1000),
+    myinternal(2000, one.amount * 2000),
+    sointernal(5000, one.amount * 5000),
+    biginternal(10000, one.amount * 10000);
 
     public static final Long MAX_ERA = one.amount;
 
@@ -33,4 +36,8 @@ public enum CoinValue {
     public long getDelta() {
         return amount / MAX_ERA;
     }
+
+    public long getCurrent(Integer era) {
+        return amount - (era * getDelta());
+    }
 }

+ 11 - 4
src/main/java/in/ocsf/bee/freigeld/core/model/Emitter.java

@@ -6,6 +6,7 @@ import java.util.Collection;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 public interface Emitter {
 
@@ -13,17 +14,23 @@ public interface Emitter {
         emit(count, CoinValue.one);
     }
 
-    void emit(long count, CoinValue value);
+    default Collection<UUID> emit(@NotNull Collection<Coin> futureCoins) {
+        return futureCoins.stream().peek(c -> {
+            if (c.getId() != null) throw new RuntimeException("only future coins");
+        }).map(this::emitOne).collect(Collectors.toList());
+    }
+
+    void emit(long count, @NotNull CoinValue value);
 
-    UUID emitOne(Coin coin);
+    UUID emitOne(@NotNull Coin coin);
 
     default void free(@NotNull Collection<Coin> coins) {
         coins.forEach(this::free);
     }
 
-    void free(Coin coin);
+    void free(@NotNull Coin coin);
 
-    void listen(Consumer<EmitterEvent> fn);
+    void listen(@NotNull Consumer<EmitterEvent> fn);
 
     CompletableFuture<Object> calc();
 

+ 279 - 89
src/main/kotlin/in/ocsf/bee/freigeld/core/demo/DemoInMem.kt

@@ -1,7 +1,6 @@
 package `in`.ocsf.bee.freigeld.core.demo
 
 import `in`.ocsf.bee.freigeld.core.model.*
-import `in`.ocsf.bee.freigeld.utils.ZBase32Utils
 import com.oblac.nomen.Nomen
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.GlobalScope
@@ -62,15 +61,16 @@ class DemoInMem {
         bank.exchange(dir.second, dir.first, x)
     }
 
-    @Scheduled(initialDelay = 10 * 1000L, fixedDelay = 5 * 1000L)
+    @Scheduled(initialDelay = 5 * 1000L, fixedDelay = 5 * 1000L)
     fun tick() {
         doRand0()
         log.info("tick")
     }
 
-    //@Scheduled(initialDelay = 60 * 1000L, fixedDelay = 60 * 1000L)
+    @Scheduled(initialDelay = 60 * 1000L, fixedDelay = 60 * 1000L)
     fun calc() {
         emitter.calc()
+        log.info("calc")
     }
 }
 
@@ -142,63 +142,90 @@ class GlobalBank : Bank {
     suspend fun pollLater(e: BankEvent): Boolean {
         return when (e) {
             is ExchangeStartEvent -> {
-                if (e.from.overall < e.amount) {
-                    globalQueue.offerLast(ExchangeFailedEvent(UUID.randomUUID(), e, "not enough overall"))
-                } else {
-                    val coinsMoreOrExact = e.from.extractMoreOrExact(e.amount)
-                    val coinsExact = coinsMoreOrExact.filter { it.id != null }
-                    val coinsCashback = coinsMoreOrExact.filter { it.id == null }
-                    val ok = if (coinsCashback.isNotEmpty()) {
-                        if (e.to.id == selfAccount.id && e.from.id == emitter.id) {
-                            true
-                        } else if (e.from.id == selfAccount.id) {
-                            false
+                CoinUtils.makeSomeStrategy(CoinStrategy(e.amount, if (e.from.id != emitter.id) e.from.coins else null, if (e.from.id != selfAccount.id) e.to.coins else null)).let { st ->
+                    if (st.res!!.isOk()) {
+                        st
+                    } else if (st.res == CoinStrategyType.error) {
+                        globalQueue.offerLast(ExchangeFailedEvent(UUID.randomUUID(), e, "strategy error"))
+                        null
+                    } else {
+                        TODO()
+                    }
+                }?.let { st ->
+                    log.info("${selfAccount} do ${st.res?.name} transaction")
+                    var fromAmount = e.from.overall
+                    var toAmount = e.to.overall
+
+                    val leftExtracted = if (st.leftExtract != null) {
+                        val old = e.from.extract(st.leftExtract?.map { it.id }!!)
+                        emitter.accept(old)
+                        old
+                    } else {
+                        listOf()
+                    }
+
+                    if (st.leftEmit != null) {
+                        val newIds = emitter.emit(st.leftEmit!!)
+                        val newCoins = emitter.extract(newIds)
+                        e.from.accept(newCoins)
+                        if (e.from.id != selfAccount.id) {
+                            emitter.free(newCoins)
                         } else {
-                            false
+                            //do nothing
                         }
+                    }
+
+                    val rightExtracted = if (st.rightExtract != null) {
+                        val old = e.to.extract(st.rightExtract?.map { it.id }!!)
+                        emitter.accept(old)
+                        old
                     } else {
-                        true
+                        listOf()
                     }
-                    if (ok) {
-                        e.to.accept(coinsExact)
-                        if (e.to.id != selfAccount.id) emitter.free(coinsExact)
-                        globalQueue.offerLast(ExchangeSuccessEvent(UUID.randomUUID(), e))
-                    } else if (e.from.id == selfAccount.id) {
-                        if (e.retry)
-                            throw IllegalArgumentException("already handled event retry, failed")
-                        e.retry = true
-                        e.from.accept(coinsExact)
-                        if (coinsCashback.isNotEmpty()) {
-                            val invCoinsCashback = coinsCashback.map { it as DemoCashbackCoin }.map { it.invert() }
-                            log.info("${e.from} get cashback from emitter ${invCoinsCashback.map { it.current }.joinToString(", ")}")
-                            invCoinsCashback.forEach {
-                                val coinId = emitter.emitOne(it)
-                                val coin = emitter.extractOne(coinId)
-                                e.from.accept(listOf(coin))
-                            }
+
+                    if (st.rightEmit != null) {
+                        val newIds = emitter.emit(st.rightEmit!!)
+                        if (e.from.id == emitter.id) {
+                            fromAmount = e.from.overall
                         }
-                    } else {
-                        val fromFake: BankAccount = DemoSelfAccount(UUID.randomUUID())
-                        fromFake.accept(coinsExact)
-                        log.info("${fromFake} hold ${fromFake.overall} from ${e.from}")
-                        val invCoins = coinsCashback.map { it as DemoCashbackCoin }.map { it.invert() }.map {
-                            val coinId = emitter.emitOne(it)
-                            emitter.extractOne(coinId)
+                        val newCoins = emitter.extract(newIds)
+                        e.to.accept(newCoins)
+                        if (e.to.id != selfAccount.id) {
+                            emitter.free(newCoins)
+                        } else {
+                            //do nothing
                         }
-                        val backAmount = fromFake.overall - e.amount
-                        fromFake.accept(invCoins)
-                        val extraCoins = fromFake.extractMoreOrExact(e.amount)
-                        val backCoinsCashback = fromFake.extractMoreOrExact(backAmount)
-                        val backInvCoins = backCoinsCashback.filter { it.id == null }.map { it as DemoCashbackCoin }.map { it.invert() }.map {
-                            val coinId = emitter.emitOne(it)
-                            emitter.extractOne(coinId)
+                    }
+
+                    if (st.rightCashback != null) {
+                        val someCoins = emitter.extract(st.rightCashback?.map { it.id }!!)
+                        e.to.accept(someCoins)
+                        if (e.to.id != selfAccount.id) {
+                            emitter.free(someCoins)
+                        } else {
+                            //do nothing
                         }
-                        e.from.accept(extraCoins + backInvCoins)
-                        selfAccount.accept(backCoinsCashback.filter { it.id != null })
-                        ok
                     }
-                    ok
-                }
+
+                    if (st.leftCashback != null) {
+                        val someCoins = emitter.extract(st.leftCashback?.map { it.id }!!)
+                        e.from.accept(someCoins)
+                        if (e.from.id != selfAccount.id) {
+                            emitter.free(someCoins)
+                        } else {
+                            //do nothing
+                        }
+                    }
+
+                    val creditOk = (fromAmount - e.amount == e.from.overall)
+                    val debitOk = (toAmount + e.amount == e.to.overall)
+                    if (debitOk && creditOk) {
+                        true
+                    } else {
+                        TODO()
+                        false
+                    }
+                } ?: true
             }
             is ExchangeSuccessEvent -> {
                 globalFutureMap.get(e.parentEvent.id)?.let {
@@ -299,19 +326,46 @@ class DemoInMemEmitter : GlobalEmitter {
 
     val globalId = UUID.fromString("a671cdca-782a-4caf-8aad-056f6b62d822")
     val coinMap = mutableMapOf<UUID, Coin>()
+    val coinValueIndex = mutableMapOf<Pair<CoinValue, Int>, MutableSet<UUID>>()
     val coinExtractedSet = mutableSetOf<UUID>()
     val coinFreeSet = mutableSetOf<UUID>()
     val listeners = mutableListOf<Consumer<EmitterEvent>>()
     var deferred: CompletableDeferred<Any>? = null
 
+    private val log = LoggerFactory.getLogger(javaClass)
+
+    private fun computeIfAbsent(future: Coin? = null, fn: (UUID) -> DemoCoin): Coin {
+        var ret: Coin? = if (future != null) {
+            coinValueIndex.get(future.value to future.era)?.filterNot { coinExtractedSet.contains(it) }?.firstOrNull()?.let { coinMap.get(it) }
+        } else {
+            null
+        }
+        return if (ret == null) {
+            val coin = coinMap.computeIfAbsent(UUID.randomUUID(), fn)
+            coinValueIndex.computeIfAbsent(coin.value to coin.era) { c -> mutableSetOf() }.add(coin.id)
+            coin
+        } else {
+            log.info("emitter reuse coin ${CoinUtils.sum(listOf(ret))}")
+            ret
+        }
+    }
+
     override fun emit(count: Long, value: CoinValue) {
-        (0 until count).forEach { coinMap.computeIfAbsent(UUID.randomUUID()) { id -> DemoCoin(id, value) } }
+        (0 until count).forEach {
+            computeIfAbsent { id ->
+                val coin = DemoCoin(id, value)
+                coin
+            }
+        }
+        log.info("emitter emit ${count} of ${value}")
     }
 
     override fun accept(nullCoins: MutableCollection<Coin>) {
         val nullIds = nullCoins.map { it.id }
         coinFreeSet.removeAll(nullIds)
         coinExtractedSet.removeAll(nullIds)
+        nullCoins.forEach { coinValueIndex.computeIfAbsent(it.value to it.era) { c -> mutableSetOf() }.add(it.id) }
+        log.info("emitter redeem ${nullCoins.size} coins, ${CoinUtils.sum(nullCoins)}")
     }
 
     override fun getId(): UUID {
@@ -321,19 +375,25 @@ class DemoInMemEmitter : GlobalEmitter {
     override fun extractOne(coinId: UUID): Coin {
         val coin = coinMap.get(coinId)!!
         coinExtractedSet.add(coin.id)
+        coinValueIndex.computeIfAbsent(coin.value to coin.era) { c -> mutableSetOf() }.remove(coin.id)
+        log.info("emitter extract ${CoinUtils.sum(listOf(coin))}, ${CoinUtils.sumString(listOf(coin))}")
         return coin
     }
 
-    override fun extractMoreOrExact(amount: Long): MutableCollection<Coin> {
+    /*override fun extractMoreOrExact(amount: Long): MutableCollection<Coin> {
         val indoorCoins = coinMap.values.filter { !coinExtractedSet.contains(it.id) }
         val ret = CoinUtils.mergeCoins(indoorCoins, amount)
         val realCoins = ret.filter { it.id != null }
         realCoins.forEach { coinExtractedSet.add(it.id) }
         return ret.toMutableList()
-    }
+    }*/
 
     override fun getOverall(): Long {
-        return coinMap.values.filter { !coinExtractedSet.contains(it.id) }.map { it.current }.sum()
+        return coinMap.values.filterNot { coinExtractedSet.contains(it.id) }.map { it.current }.sum()
+    }
+
+    override fun getCoins(): MutableCollection<Coin>? {
+        return null
     }
 
     override fun listen(fn: Consumer<EmitterEvent>) {
@@ -343,6 +403,7 @@ class DemoInMemEmitter : GlobalEmitter {
     override fun free(coin: Coin) {
         if (coinMap.containsKey(coin.id) && coinExtractedSet.contains(coin.id)) {
             coinFreeSet.add(coin.id)
+            log.info("emitter free ${CoinUtils.sum(listOf(coin))}, ${CoinUtils.sumString(listOf(coin))}")
         } else {
             throw IllegalArgumentException("coin is not free")
         }
@@ -370,9 +431,11 @@ class DemoInMemEmitter : GlobalEmitter {
     }
 
     override fun emitOne(coin: Coin): UUID {
-        val new = DemoCoin(UUID.randomUUID(), coin.value, coin.era)
-        coinMap.put(new.id, new)
-        return new.id
+        return computeIfAbsent(coin) { id ->
+            val new = DemoCoin(id, coin.value, coin.era)
+            log.info("emitter emit coin ${CoinUtils.sum(listOf(new))}")
+            new
+        }.id
     }
 }
 
@@ -389,7 +452,7 @@ class DemoCoin(val coinId: UUID, val initialValue: CoinValue, var _era: Int = 0)
     }
 
     override fun toString(): String {
-        return "${initialValue.name} jd (${current})"
+        return "${initialValue.name} jd${_era} (${current})"
     }
 }
 
@@ -419,7 +482,7 @@ class DemoCashbackCoin(val _value: CoinValue, val cashback: Long, val _era: Int)
         return "${value} jdc${era} (${current})"
     }
 
-    fun invert(): DemoFutureCoin {
+    /*fun invert(): DemoFutureCoin {
         var newEra = maxEraValue - _era
         if (newEra == maxEraValue) newEra = 0
         var newCurrent = value.amount + cashback
@@ -428,7 +491,7 @@ class DemoCashbackCoin(val _value: CoinValue, val cashback: Long, val _era: Int)
         if (ret.current != newCurrent)
             throw IllegalArgumentException("wrong inverted coin")
         return ret
-    }
+    }*/
 }
 
 class DemoFutureCoin(val _value: CoinValue, val _era: Int) : Coin {
@@ -472,7 +535,13 @@ open class DemoAccount(val accountId: UUID, personId: UUID) : BankAccount {
     override fun extractOne(coinId: UUID): Coin {
         return if (internalCoins.containsKey(coinId)) {
             val coin = internalCoins.remove(coinId)!!
-            log.info("${this} credit coin ${coin.current}")
+            if (coin.current == 0L) {
+                _overall = null
+                log.info("${this} leave coin ${coin.current}")
+            } else {
+                decOverall(coin.current)
+                log.info("${this} credit coin ${coin.current}")
+            }
             coin
         } else {
             throw IllegalArgumentException("no such coin")
@@ -483,12 +552,17 @@ open class DemoAccount(val accountId: UUID, personId: UUID) : BankAccount {
         return if (_overall != null) {
             _overall
         } else {
-            _overall = internalCoins.values.map { it.current }.sum()
+            _overall = internalCoins.values.map { it.current }.onEach { if (it == 0L) throw RuntimeException("illegal value 0") }.sum()
             _overall
         } ?: 0L
     }
 
+    override fun getCoins(): MutableCollection<Coin> {
+        return internalCoins.values
+    }
+
     private fun decOverall(amount: Long) {
+        if (amount == 0L) throw IllegalArgumentException("should not be 0")
         if (_overall != null) {
             _overall = _overall!! - amount
         } else {
@@ -497,6 +571,7 @@ open class DemoAccount(val accountId: UUID, personId: UUID) : BankAccount {
     }
 
     private fun incOverall(amount: Long) {
+        if (amount == 0L) throw IllegalArgumentException("should not be 0")
         if (_overall != null) {
             _overall = _overall!! + amount
         } else {
@@ -518,29 +593,38 @@ open class DemoAccount(val accountId: UUID, personId: UUID) : BankAccount {
         log.info("${this} debit coins ${CoinUtils.sumString(coins)}")
     }
 
-    override fun extractMoreOrExact(amount: Long): MutableCollection<Coin> {
-
-        val coins = CoinUtils.mergeCoins(internalCoins.values, amount).toMutableList()
-        coins.forEach {
-            internalCoins.remove(it.id)
-            decOverall(it.current)
-        }
-        log.info("${this} credit coins ${CoinUtils.sumString(coins)}")
-        return coins
-    }
 
     override fun toString(): String {
-        return ZBase32Utils.encode(accountId.hashCode().toString(16))
+        return accountId.hashCode().toString(16)
     }
 }
 
+
+enum class CoinStrategyType {
+    error, L2R_RL, L2R_R, B2R_E, L2R, E2B, L2R_E;
+
+    fun isOk(): Boolean = this != error
+}
+
+class CoinStrategy(val credit: Long, val leftCoins: Collection<Coin>? = null, val rightCoins: Collection<Coin>? = null) {
+    var res: CoinStrategyType? = null
+    var parent: CoinStrategy? = null
+    var leftExtract: Collection<Coin>? = null
+    var rightExtract: Collection<Coin>? = null
+    var leftCashback: Collection<Coin>? = null
+    var rightCashback: Collection<Coin>? = null
+    var leftEmit: Collection<Coin>? = null
+    var rightEmit: Collection<Coin>? = null
+}
+
 class CoinUtils {
 
     companion object {
 
-        val avaiues = CoinValue.values().flatMap { value -> (0..0/*(maxEraValue - 1)*/).map { era -> value to era to (value.amount - (era * value.delta)) } }.map { it.second to it.first }.toMap()
+        val avaiues = CoinValue.values().flatMap { value -> (0..maxEraValue - 1).map { era -> value to era to (value.getCurrent(era)) } }.map { it.second to it.first }.groupBy { it.first }.map { it.key to it.value.map { it.second } }.toMap()
+        val akeis = avaiues.keys.sortedDescending()
 
-        fun makeSomeFake(cashback: Long): Collection<Coin> {
+        private fun makeSomeFake(cashback: Long): Collection<Coin> {
             val ret = mutableListOf<Coin>()
             var totalCashback = cashback
             do {
@@ -556,7 +640,7 @@ class CoinUtils {
             return ret
         }
 
-        fun mergeCoins(coins: Collection<Coin>, amount: Long): Collection<Coin> {
+        private fun splitCoins(coins: Collection<Coin>, amount: Long): Collection<Coin> {
             val ret = mutableListOf<Coin>()
             val coinMap = coins.groupBy { it.current }.map { it.key to it.value.map { it.id }.toMutableList() }.toMap().toMutableMap()
             val usedCoins = mutableSetOf<UUID>()
@@ -611,23 +695,129 @@ class CoinUtils {
             return if (neg != 0L) "${pos + neg}(${pos}${neg})" else "$pos"
         }
 
+        fun makeSomeFuture(x: Long): List<Coin> {
+            return makeSomeVariants(x).first().flatMap { n -> (0 until n.value).map { DemoFutureCoin(n.key.first, n.key.second) } }
+        }
 
-        fun makeSomeVariants(x: Long): List<Map<Pair<CoinValue, Int>, Int>> {
-            val baseValues = avaiues.keys
-            val variants: MutableSet<Set<Long>> = mutableSetOf()
-            var calcFn: ((excl: Set<Long>) -> Unit)? = null
+        fun makeSomeStrategy(st: CoinStrategy): CoinStrategy {
+            return if (st.credit > 0) {
+                if (st.leftCoins != null) {
+                    if (st.credit <= sum(st.leftCoins)) {
+                        val creditCoins = splitCoins(st.leftCoins, st.credit)
+                        val creditCoinsReal = creditCoins.filter { it.id != null }
+                        val creditCoinsCashback = creditCoins.filter { it.id == null }
+                        if (creditCoinsCashback.isNotEmpty()) {
+                            val creditCashback = sum(creditCoinsReal) - st.credit
+                            if (st.rightCoins != null && creditCashback <= sum(st.rightCoins)) {
+                                val debitCoins = splitCoins(st.rightCoins, creditCashback)
+                                val debitCoinsReal = debitCoins.filter { it.id != null }
+                                val debitCoinsCashback = debitCoins.filter { it.id == null }
+                                if (debitCoinsCashback.isNotEmpty()) {
+                                    val debitCashback = sum(debitCoinsReal) - creditCashback
+                                    val replaceStrategy: CoinStrategy? = if (st.parent == null) {
+                                        val debitNoCashbackStrategy = CoinStrategy(st.credit + debitCashback, st.leftCoins, st.rightCoins)
+                                        debitNoCashbackStrategy.parent = st
+                                        makeSomeStrategy(debitNoCashbackStrategy)
+                                        if (setOf(CoinStrategyType.L2R_R, CoinStrategyType.L2R).contains(debitNoCashbackStrategy.res)) {
+                                            null //TODO exchange with extra, L3R_R
+                                        } else {
+                                            null
+                                        }
+                                    } else {
+                                        null
+                                    }
+                                    if (replaceStrategy != null) {
+                                        replaceStrategy
+                                    } else {
+                                        st.leftExtract = creditCoinsReal
+                                        st.leftEmit = makeSomeFuture(creditCashback)
+                                        st.rightExtract = debitCoinsReal
+                                        st.rightEmit = makeSomeFuture(debitCashback)
+                                        st.rightCashback = st.leftExtract
+                                        st.res = CoinStrategyType.L2R_RL
+                                        st
+                                    }
+                                } else {
+                                    st.leftExtract = creditCoinsReal
+                                    st.rightExtract = debitCoinsReal
+                                    st.leftCashback = st.rightExtract
+                                    st.rightCashback = st.leftExtract
+                                    st.res = CoinStrategyType.L2R_R
+                                    st
+                                }
+                            } else if (st.rightCoins == null) {
+                                st.leftExtract = creditCoinsReal
+                                st.leftEmit = makeSomeFuture(creditCashback)
+                                st.rightEmit = makeSomeFuture(st.credit)
+                                st.res = CoinStrategyType.B2R_E
+                                st
+                            } else {
+                                st.leftExtract = creditCoinsReal
+                                st.leftEmit = makeSomeFuture(creditCashback)
+                                st.rightEmit = makeSomeFuture(st.credit)
+                                st.res = CoinStrategyType.L2R_E
+                                st
+                            }
+                        } else {
+                            st.leftExtract = creditCoins
+                            st.rightCashback = creditCoins
+                            st.res = CoinStrategyType.L2R
+                            st
+                        }
+                    } else {
+                        st.res = CoinStrategyType.error
+                        st
+                    }
+                } else {
+                    st.rightEmit = makeSomeFuture(st.credit)
+                    st.res = CoinStrategyType.E2B
+                    st
+                }
+            } else {
+                st.res = CoinStrategyType.error
+                st
+            }
+        }
 
-            calcFn = { skip ->
-                val checkValues = baseValues - skip
-                if (checkValues.isNotEmpty()) variants.add(checkValues)
-                checkValues.forEach { x -> calcFn!!(skip + setOf(x)) }
+        fun makeSomeVariants(x: Long, skip: Set<Long>? = null): List<Map<Pair<CoinValue, Int>, Long>> {
+            val thisAkeis = if (skip != null && skip.isNotEmpty()) {
+                akeis.filterNot { skip.contains(it) }
+            } else {
+                akeis
             }
+            if (!thisAkeis.contains(1)) throw IllegalArgumentException("cannot split without 1")
+            val rkeys = mutableListOf<Pair<Long, Long>>()
+            var z = x
+            do {
+                val next = thisAkeis.first { z >= it }
+                val n = z / next
+                rkeys.add(next to n)
+                z %= (n * next)
+            } while (z != 0L)
+
+            val ret = mutableListOf<Map<Pair<CoinValue, Int>, Long>>()
+            val rmap = rkeys.map { it.first to avaiues.get(it.first)?.toSet()!! }
+
+            var calcFn: ((Int, List<Pair<CoinValue, Int>>) -> Unit)? = null
+
+            calcFn = { depth, keys ->
+                if (depth < rmap.size) {
+                    rmap[depth].second.forEach { k ->
+                        calcFn!!(depth + 1, keys + listOf(k))
+                    }
+                } else if (depth == rmap.size) {
+                    calcFn!!(depth + 1, keys)
+                } else {
+                    ret.add(keys.mapIndexed { i, k -> k to rkeys[i].second }.toMap())
+                }
+            }
+
+            calcFn(0, listOf())
 
             return if (x > 0L) {
-                calcFn(setOf())
-                return listOf()
+                ret
             } else {
-                return listOf()
+                listOf()
             }
         }
     }

+ 17 - 4
src/test/kotlin/`in`/ocsf/bee/freigeld/core/demo/CoinUtilsTest.kt

@@ -1,7 +1,9 @@
 package `in`.ocsf.bee.freigeld.core.demo
 
 import `in`.ocsf.bee.freigeld.core.model.CoinValue
+import org.junit.jupiter.api.RepeatedTest
 import org.junit.jupiter.api.Test
+import org.slf4j.LoggerFactory
 import java.util.*
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
@@ -16,6 +18,8 @@ class CoinUtilsTest {
 
     val toAccount = DemoAccount(toId, toId)
 
+    private val log = LoggerFactory.getLogger(javaClass)
+
     init {
         fromAccount.accept(mutableListOf(DemoCoin(UUID.randomUUID(), CoinValue.one)))
     }
@@ -25,11 +29,20 @@ class CoinUtilsTest {
         assertEquals(fromAccount.overall, CoinValue.one.amount)
     }
 
-    @Test
+    @RepeatedTest(5)
     fun `split one to million`() {
-        (1L..1_000_000L).forEach { x ->
-            val vars = CoinUtils.makeSomeVariants(x)
-            assertFalse(vars.isEmpty())
+        (1..100).map { Math.round(Math.random() * 100_000) }.forEach { x ->
+            val skipValuesValues = CoinUtils.akeis.chunked(10)
+            val skipValuesIndex = Math.round((skipValuesValues.size - 1) * Math.random()).toInt()
+            val skipValues = skipValuesValues[skipValuesIndex].toMutableSet()
+            if (skipValues.contains(1)) skipValues.remove(1)
+            val varx = CoinUtils.makeSomeVariants(x, skip = skipValues)
+            varx.map { v ->
+                log.info("${x} = " + v.map { "${it.key.first.getCurrent(it.key.second)}[${it.key.first.amount}c${it.key.second}] * ${it.value}" }.joinToString(" + "))
+                assertEquals(x, v.map { it.key.first.getCurrent(it.key.second) * it.value }.sum())
+                assertFalse(v.isEmpty())
+            }
+            assertFalse(varx.isEmpty())
         }
     }