κρμγ 5 роки тому
батько
коміт
315e3c5c00

+ 6 - 0
src/main/java/in/ocsf/bee/freigeld/core/model/BankAccount.java

@@ -1,11 +1,17 @@
 package in.ocsf.bee.freigeld.core.model;
 
 import java.util.Collection;
+import java.util.UUID;
 
 public interface BankAccount {
+
+    UUID getId();
+
     Long getOverall();
 
     Collection<Coin> extractMoreOrExact(Long amount);
 
+    Coin extractOne(UUID coinId);
+
     void accept(Collection<Coin> coins);
 }

+ 5 - 1
src/main/java/in/ocsf/bee/freigeld/core/model/Coin.java

@@ -7,5 +7,9 @@ public interface Coin {
 
     CoinValue getValue();
 
-    Long getCurrent();
+    default Long getCurrent() {
+        return getValue().getAmount() - ((getEra() - 1) * getValue().getDelta());
+    }
+
+    Integer getEra();
 }

+ 23 - 10
src/main/java/in/ocsf/bee/freigeld/core/model/CoinValue.java

@@ -1,23 +1,36 @@
 package in.ocsf.bee.freigeld.core.model;
 
 public enum CoinValue {
-    one(364),
-    three(one.amount * 3),
-    five(one.amount * 5),
-    ten(one.amount * 10),
-    quarter(one.amount * 25),
-    half(one.amount * 50),
-    full(one.amount * 100),
-    mega(one.amount * 500),
-    internal(one.amount * 1000);
+    one(1, 364),
+    three(3, one.amount * 3),
+    five(5, one.amount * 5),
+    ten(10, one.amount * 10),
+    quarter(25, one.amount * 25),
+    half(50, one.amount * 50),
+    full(100, one.amount * 100),
+    bi(200, one.amount * 200),
+    mega(500, one.amount * 500),
+    internal(1000, one.amount * 1000);
 
+    public static final Long MAX_ERA = one.amount;
+
+    private final int quantity;
     private final long amount;
 
-    CoinValue(long amount) {
+    CoinValue(int quantity, long amount) {
+        this.quantity = quantity;
         this.amount = amount;
     }
 
+    public int getQuantity() {
+        return quantity;
+    }
+
     public long getAmount() {
         return amount;
     }
+
+    public long getDelta() {
+        return amount / MAX_ERA;
+    }
 }

+ 3 - 0
src/main/java/in/ocsf/bee/freigeld/core/model/Emitter.java

@@ -1,6 +1,7 @@
 package in.ocsf.bee.freigeld.core.model;
 
 import java.util.Collection;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.Consumer;
 
@@ -12,6 +13,8 @@ public interface Emitter {
 
     void emit(long count, CoinValue value);
 
+    UUID emitOne(Coin coin);
+
     default void free(Collection<Coin> coins) {
         coins.forEach(this::free);
     }

+ 221 - 58
src/main/kotlin/in/ocsf/bee/freigeld/core/demo/DemoInMem.kt

@@ -12,7 +12,6 @@ import org.springframework.scheduling.annotation.Scheduled
 import org.springframework.stereotype.Service
 import java.util.*
 import java.util.concurrent.CompletableFuture
-import java.util.concurrent.LinkedBlockingQueue
 import java.util.function.Consumer
 import javax.annotation.PostConstruct
 import kotlin.math.min
@@ -33,13 +32,16 @@ class DemoInMem {
 
     @PostConstruct
     fun init() {
-        emitter.emit(1000, CoinValue.one)
-        emitter.emit(500, CoinValue.three)
-        emitter.emit(200, CoinValue.five)
-        emitter.emit(100, CoinValue.ten)
-        emitter.emit(50, CoinValue.quarter)
-        emitter.emit(10, CoinValue.half)
-        emitter.emit(5, CoinValue.full)
+        emitter.emit(10000, CoinValue.one)
+        emitter.emit(5000, CoinValue.three)
+        emitter.emit(2000, CoinValue.five)
+        emitter.emit(1000, CoinValue.ten)
+        emitter.emit(500, CoinValue.quarter)
+        emitter.emit(100, CoinValue.half)
+        emitter.emit(50, CoinValue.full)
+        emitter.emit(25, CoinValue.bi)
+        emitter.emit(10, CoinValue.mega)
+
         bank.exchange(bank.getSelfAccount(), emitter, 1_300_000)
         (0 until 100).forEach { personMap.computeIfAbsent(UUID.randomUUID()) { id -> DemoHuman(id, Nomen.randomName()) } }
         personMap.keys.map { bank.addAccount(it) }.forEach {
@@ -72,7 +74,7 @@ class GlobalBank : Bank {
 
     val accountMap = mutableMapOf<UUID, DemoAccount>()
     val personToAccountTable = mutableSetOf<Pair<UUID, UUID>>()
-    val globalQueue: Queue<BankEvent> = LinkedBlockingQueue()
+    val globalQueue: Deque<BankEvent> = ArrayDeque()
     val globalFutureMap = mutableMapOf<UUID, CompletableDeferred<Any>>()
 
     private lateinit var selfId: UUID
@@ -121,22 +123,44 @@ class GlobalBank : Bank {
             def.invokeOnCompletion { t: Throwable? -> if (t == null) ret.complete(null) else ret.completeExceptionally(t) }
             def
         }
-        globalQueue.offer(e)
+        globalQueue.offerLast(e)
         return ret
     }
 
-    suspend fun pollLater(e: BankEvent) {
-        when (e) {
+    suspend fun pollLater(e: BankEvent): Boolean {
+        return when (e) {
             is ExchangeStartEvent -> {
                 if (e.from.overall < e.amount) {
-                    globalQueue.offer(ExchangeFailedEvent(UUID.randomUUID(), e, "not enough overall"))
+                    globalQueue.offerLast(ExchangeFailedEvent(UUID.randomUUID(), e, "not enough overall"))
                 } else {
                     val coinsMoreOrExact = e.from.extractMoreOrExact(e.amount)
-                    val coinsExactAndCashback = coinsMoreOrExact
-                    e.to.accept(coinsExactAndCashback)
-                    //e.from.accept(coinsExactAndCashback.second)
-                    emitter.free(coinsExactAndCashback)
-                    globalQueue.offer(ExchangeSuccessEvent(UUID.randomUUID(), e))
+                    val coinsExact = coinsMoreOrExact.filter { it.id != null }
+                    val coinsCashback = coinsMoreOrExact.filter { it.id == null }
+                    val retry = if (coinsCashback.isNotEmpty()) {
+                        if (e.to.id == selfAccount.id && e.from.id == emitter.id) {
+                            false
+                        } else if (e.from.id == selfAccount.id) {
+                            true
+                        } else {
+                            TODO()
+                        }
+                    } else {
+                        false
+                    }
+                    if (!retry) {
+                        e.to.accept(coinsExact)
+                        emitter.free(coinsExact)
+                        globalQueue.offerLast(ExchangeSuccessEvent(UUID.randomUUID(), e))
+                        true
+                    } else {
+                        e.from.accept(coinsExact)
+                        coinsCashback.map { it as DemoCashbackCoin }.map { it.invert() }.forEach {
+                            val coinId = emitter.emitOne(it)
+                            val coin = emitter.extractOne(coinId)
+                            e.from.accept(listOf(coin))
+                        }
+                        false
+                    }
                 }
             }
             is ExchangeSuccessEvent -> {
@@ -144,22 +168,25 @@ class GlobalBank : Bank {
                     globalFutureMap.remove(e.parentEvent.id)
                     it.complete(true)
                 }
+                true
             }
             is ExchangeFailedEvent -> {
                 globalFutureMap.get(e.parentEvent.id)?.let {
                     globalFutureMap.remove(e.parentEvent.id)
                     it.completeExceptionally(ExchangeFailedException(e, e.reason))
                 }
+                true
             }
             else -> throw IllegalArgumentException("wrong event type ${e.javaClass.name}")
         }
     }
 
-    fun poll(e: BankEvent) = runBlocking {
+    fun poll(e: BankEvent): Boolean = runBlocking {
         try {
             pollLater(e)
         } catch (t: Throwable) {
             log.error("error in event", t)
+            true
         }
     }
 
@@ -176,8 +203,10 @@ class GlobalBank : Bank {
     fun tick() {
         var pollCount = 0
         while (globalQueue.isNotEmpty() && pollCount < maxPollCount) {
-            val e = globalQueue.poll()
-            poll(e)
+            val e = globalQueue.element()
+            if (poll(e)) {
+                globalQueue.remove(e)
+            }
             pollCount++
             log.info("poll")
         }
@@ -202,6 +231,7 @@ class ExchangeStartEvent(eventId: UUID, val to: BankAccount, val from: BankAccou
 @Service
 class DemoInMemEmitter : GlobalEmitter {
 
+    val globalId = UUID.fromString("a671cdca-782a-4caf-8aad-056f6b62d822")
     val coinMap = mutableMapOf<UUID, Coin>()
     val coinExtractedSet = mutableSetOf<UUID>()
     val coinFreeSet = mutableSetOf<UUID>()
@@ -216,10 +246,21 @@ class DemoInMemEmitter : GlobalEmitter {
         TODO("not implemented")
     }
 
+    override fun getId(): UUID {
+        return globalId
+    }
+
+    override fun extractOne(coinId: UUID): Coin {
+        val coin = coinMap.get(coinId)!!
+        coinExtractedSet.add(coin.id)
+        return coin
+    }
+
     override fun extractMoreOrExact(amount: Long): MutableCollection<Coin> {
         val indoorCoins = coinMap.values.filter { !coinExtractedSet.contains(it.id) }
         val ret = CoinUtils.mergeCoins(indoorCoins, amount)
-        ret.forEach { coinExtractedSet.add(it.id) }
+        val realCoins = ret.filter { it.id != null }
+        realCoins.forEach { coinExtractedSet.add(it.id) }
         return ret.toMutableList()
     }
 
@@ -240,7 +281,7 @@ class DemoInMemEmitter : GlobalEmitter {
     }
 
     private suspend fun calcLater() {
-        coinFreeSet.map { coinMap[it] }.filterNotNull().map { it as DemoCoin }.forEach { it.era = min(maxEraValue, it.era + 1) }
+        coinFreeSet.map { coinMap[it] }.filterNotNull().map { it as DemoCoin }.forEach { it._era = min(maxEraValue, it._era + 1) }
     }
 
     override fun calc(): CompletableFuture<Any> {
@@ -259,21 +300,30 @@ class DemoInMemEmitter : GlobalEmitter {
         }
         return ret
     }
-}
 
-val maxEraValue = 364
+    override fun emitOne(coin: Coin): UUID {
+        val new = DemoCoin(UUID.randomUUID(), coin.value, coin.era)
+        coinMap.put(new.id, new)
+        return new.id
+    }
+}
 
-class DemoCoin(val coinId: UUID, val initialValue: CoinValue) : Coin {
+val maxEraValue: Int = CoinValue.MAX_ERA.toInt()
 
-    var era = 1
-    var delta: Long = initialValue.amount / maxEraValue
+class DemoCoin(val coinId: UUID, val initialValue: CoinValue, var _era: Int = 1) : Coin {
 
     override fun getId(): UUID = coinId
 
     override fun getValue(): CoinValue = initialValue
 
+    /*
     override fun getCurrent(): Long {
-        return initialValue.amount - ((era - 1) * delta)
+        return initialValue.amount - ((_era - 1) * initialValue.delta)
+    }
+    */
+
+    override fun getEra(): Int {
+        return _era
     }
 
     override fun toString(): String {
@@ -281,6 +331,59 @@ class DemoCoin(val coinId: UUID, val initialValue: CoinValue) : Coin {
     }
 }
 
+class DemoCashbackCoin(val _value: CoinValue, val cashback: Long, val _era: Int) : Coin {
+
+    init {
+        if (cashback >= 0) throw IllegalArgumentException("cashback must be negative")
+    }
+
+    override fun getEra(): Int {
+        return _era
+    }
+
+    override fun getId(): UUID? {
+        return null
+    }
+
+    override fun getValue(): CoinValue {
+        return _value
+    }
+
+    override fun getCurrent(): Long {
+        return cashback
+    }
+
+    override fun toString(): String {
+        return "${value} jdc${era} (${current})"
+    }
+
+    fun invert(): DemoFutureCoin {
+        val ret = DemoFutureCoin(_value, Math.max(1, maxEraValue - _era + 1))
+        if (ret.current != value.amount + cashback)
+            throw IllegalArgumentException("wrong inverted coin")
+        return ret
+    }
+}
+
+class DemoFutureCoin(val _value: CoinValue, val _era: Int) : Coin {
+
+    override fun getId(): UUID? {
+        return null
+    }
+
+    override fun getValue(): CoinValue {
+        return _value
+    }
+
+    override fun getEra(): Int {
+        return _era
+    }
+
+    override fun toString(): String {
+        return "${value} jdf${era} (${current})"
+    }
+}
+
 class DemoHuman(val selfId: UUID, name: String) : NaturalPerson {
 
     override fun getId(): UUID = selfId
@@ -289,11 +392,24 @@ class DemoHuman(val selfId: UUID, name: String) : NaturalPerson {
 
 class DemoSelfAccount(val selfId: UUID) : DemoAccount(selfId, selfId)
 
-open class DemoAccount(val id: UUID, personId: UUID) : BankAccount {
+open class DemoAccount(val accountId: UUID, personId: UUID) : BankAccount {
 
     val internalCoins = mutableMapOf<UUID, Coin>()
     var _overall: Long? = null
 
+    override fun getId(): UUID {
+        return accountId
+    }
+
+    override fun extractOne(coinId: UUID): Coin {
+        return if (internalCoins.containsKey(coinId)) {
+            val coin = internalCoins.remove(coinId)!!
+            coin
+        } else {
+            throw IllegalArgumentException("no such coin")
+        }
+    }
+
     override fun getOverall(): Long {
         return if (_overall != null) {
             _overall
@@ -331,6 +447,7 @@ open class DemoAccount(val id: UUID, personId: UUID) : BankAccount {
     }
 
     override fun extractMoreOrExact(amount: Long): MutableCollection<Coin> {
+
         val coins = CoinUtils.mergeCoins(internalCoins.values, amount).toMutableList()
         coins.forEach {
             internalCoins.remove(it.id)
@@ -345,39 +462,85 @@ class CoinUtils {
 
     companion object {
 
+        fun makeSomeFake(cashback: Long): Collection<Coin> {
+            val ret = mutableListOf<Coin>()
+            var totalCashback = cashback
+            do {
+                val value = CoinValue.values().filter { it.amount >= Math.abs(totalCashback) }.sortedBy { it.amount }.first()
+                val valueByEra = (1..maxEraValue).map { era -> era to (value.amount - ((era - 1) * value.delta)) }.filter { it.second <= Math.abs(totalCashback) }.first()
+                ret.add(DemoCashbackCoin(value, -valueByEra.second, valueByEra.first))
+                totalCashback += valueByEra.second
+            } while (totalCashback < 0)
+            return ret
+        }
+
         fun mergeCoins(coins: Collection<Coin>, amount: Long): Collection<Coin> {
-            var totalAmount = 0L
-            val sortedCoins = coins.sortedBy { it.current }
-            val position = sortedCoins.binarySearch { it.current.compareTo(amount) }
             val ret = mutableListOf<Coin>()
-            if (position == 0) {
-                TODO()
-            } else if (position > 0) {
-                val coinL = sortedCoins.get(position)
-                val coinR = sortedCoins.get(position - 1)
-                if (coinL.current == amount) {
-                    ret.add(coinL)
-                } else if (coinR.current == amount) {
-                    ret.add(coinR)
-                } else {
-                    TODO()
-                }
-            } else if (position < 0) {
-                val actualPosition = -(position + 1)
-                if (actualPosition == 0) {
-                    TODO()
-                } else if (actualPosition < sortedCoins.size) {
-                    TODO()
+            val coinMap = coins.groupBy { it.current }.map { it.key to it.value.map { it.id }.toMutableList() }.toMap().toMutableMap()
+            val usedCoins = mutableSetOf<UUID>()
+            var totalAmount = 0L
+            do {
+                val diffAmount = amount - totalAmount
+                val leastCoinMap = coinMap.map { it.key to it.value.subtract(usedCoins).toList() }.filter { it.second.isNotEmpty() }.toMap()
+                val loeCoinMap = leastCoinMap.filter { it.key <= diffAmount }.map { it.key to it.value }.sortedByDescending { it.first }
+                if (loeCoinMap.isNotEmpty()) {
+                    val biggestLoe = loeCoinMap.first().first
+                    totalAmount += biggestLoe
+                    val coinId = loeCoinMap.first().second.first()
+                    usedCoins.add(coinId)
                 } else {
-                    var idx = sortedCoins.size - 1
-                    do {
-                        val coin = sortedCoins.get(idx)
-                        totalAmount = totalAmount + coin.current
-                        ret.add(coin)
-                        idx--
-                    } while (totalAmount < amount)
+                    val smallestGt = leastCoinMap.keys.sorted().firstOrNull()
+                    if (smallestGt != null) {
+                        val cashbackAmount = diffAmount - smallestGt
+                        totalAmount += smallestGt
+                        val coinId = leastCoinMap.get(smallestGt)?.first()!!
+                        usedCoins.add(coinId)
+                        ret.addAll(makeSomeFake(cashbackAmount))
+                    } else {
+                        TODO()
+                    }
                 }
+            } while (totalAmount < amount)
+            ret.addAll(coins.filter { usedCoins.contains(it.id) })
+            val realAmount = ret.filter { it.id != null }.map { it.current }.sum()
+            if (totalAmount != realAmount) {
+                TODO()
+            }
+            val fakeAmount = ret.map { it.current }.sum()
+            if (fakeAmount != amount) {
+                TODO()
             }
+            /* val sortedCoins = coins.sortedBy { it.current }
+             val position = sortedCoins.binarySearch { it.current.compareTo(amount) }
+             val ret = mutableListOf<Coin>()
+             if (position == 0) {
+                 TODO()
+             } else if (position > 0) {
+                 val coinL = sortedCoins.get(position)
+                 val coinR = sortedCoins.get(position - 1)
+                 if (coinL.current == amount) {
+                     ret.add(coinL)
+                 } else if (coinR.current == amount) {
+                     ret.add(coinR)
+                 } else {
+                     TODO()
+                 }
+             } else if (position < 0) {
+                 val actualPosition = -(position + 1)
+                 if (actualPosition == 0) {
+                     TODO()
+                 } else if (actualPosition < sortedCoins.size) {
+                     TODO()
+                 } else {
+                     var idx = sortedCoins.size - 1
+                     do {
+                         val coin = sortedCoins.get(idx)
+                         totalAmount = totalAmount + coin.current
+                         ret.add(coin)
+                         idx--
+                     } while (totalAmount < amount)
+                 }
+             } */
             return ret
         }
     }