Skip to content

Commit a38dfb2

Browse files
authored
Merge pull request #30 from /issues/29
Fix #29: Convert monetary amounts
2 parents deab162 + 3f0644e commit a38dfb2

File tree

28 files changed

+669
-0
lines changed

28 files changed

+669
-0
lines changed

currencies/README.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
= Currencies
2+
3+
This feature extracts monetary amounts from the messages and converts them to well-known currencies, like EUR or USD.

currencies/build.gradle.kts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
plugins {
2+
kotlin("jvm")
3+
kotlin("plugin.serialization")
4+
}
5+
6+
dependencies {
7+
implementation(platform(libs.ktor.bom))
8+
9+
api(project.projects.core)
10+
api(libs.tgbotapi.core)
11+
implementation(libs.tgbotapi.extensions.api)
12+
implementation(libs.log4j.api)
13+
implementation(libs.ktor.client.apache)
14+
implementation(libs.ktor.client.serialization)
15+
16+
testImplementation(libs.junit.jupiter.api)
17+
testImplementation(libs.junit.jupiter.params)
18+
testImplementation(libs.mockk)
19+
testRuntimeOnly(libs.junit.jupiter.engine)
20+
testRuntimeOnly(libs.log4j.core)
21+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package by.jprof.telegram.bot.currencies
2+
3+
import by.jprof.telegram.bot.core.UpdateProcessor
4+
import by.jprof.telegram.bot.currencies.parser.MonetaryAmountParsingPipeline
5+
import by.jprof.telegram.bot.currencies.rates.ExchangeRateClient
6+
import dev.inmo.tgbotapi.bot.RequestsExecutor
7+
import dev.inmo.tgbotapi.extensions.api.send.reply
8+
import dev.inmo.tgbotapi.types.ParseMode.MarkdownV2ParseMode
9+
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
10+
import dev.inmo.tgbotapi.types.message.content.TextContent
11+
import dev.inmo.tgbotapi.types.update.CallbackQueryUpdate
12+
import dev.inmo.tgbotapi.types.update.MessageUpdate
13+
import dev.inmo.tgbotapi.types.update.abstracts.Update
14+
import dev.inmo.tgbotapi.utils.extensions.escapeMarkdownV2Common
15+
import org.apache.logging.log4j.LogManager
16+
17+
class CurrenciesUpdateProcessor(
18+
private val monetaryAmountParsingPipeline: MonetaryAmountParsingPipeline,
19+
private val exchangeRateClient: ExchangeRateClient,
20+
private val targetCurrencies: List<String> = listOf("EUR", "USD"),
21+
private val bot: RequestsExecutor,
22+
) : UpdateProcessor {
23+
companion object {
24+
private val logger = LogManager.getLogger(CurrenciesUpdateProcessor::class.java)!!
25+
}
26+
27+
override suspend fun process(update: Update) {
28+
val update = update as? MessageUpdate ?: return
29+
val message = update.data as? ContentMessage<*> ?: return
30+
val content = message.content as? TextContent ?: return
31+
32+
val monetaryAmounts = monetaryAmountParsingPipeline.parse(content.text)
33+
34+
logger.debug("Parsed monetary amounts: {}", monetaryAmounts)
35+
36+
val conversions = targetCurrencies.flatMap { to ->
37+
monetaryAmounts.mapNotNull {
38+
exchangeRateClient.convert(it.amount, it.currency, to)
39+
}
40+
}.groupBy { it.query.amount to it.query.from }
41+
42+
val reply = conversions
43+
.map { (query, conversions) ->
44+
val (amount, from) = query
45+
val results = conversions
46+
.sortedBy { it.query.to }
47+
.joinToString(", ") { "%.2f %s".format(it.result, it.query.to).escapeMarkdownV2Common() }
48+
49+
"**${"%.0f %s".format(amount, from).escapeMarkdownV2Common()}**: %s".format(results)
50+
}
51+
.joinToString("\n")
52+
53+
if (reply.isNotEmpty()) {
54+
bot.reply(to = message, text = reply, parseMode = MarkdownV2ParseMode)
55+
}
56+
}
57+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package by.jprof.telegram.bot.currencies.model
2+
3+
data class MonetaryAmount(
4+
val amount: Double,
5+
val currency: String,
6+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package by.jprof.telegram.bot.currencies.parser
2+
3+
4+
class CAD : MonetaryAmountParserBase() {
5+
override val currency: String = "CAD"
6+
7+
override val currencyRegex: String = "(CAD|CA$|Can$|C$)"
8+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package by.jprof.telegram.bot.currencies.parser
2+
3+
class GBP : MonetaryAmountParserBase() {
4+
override val currency: String = "GBP"
5+
6+
override val currencyRegex: String = "(GBP|£|POUND|ФУНТ|ФУНТОВ)"
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package by.jprof.telegram.bot.currencies.parser
2+
3+
class GEL : MonetaryAmountParserBase() {
4+
override val currency: String = "GEL"
5+
6+
override val currencyRegex: String = "(GEL|₾|ლ|ЛАРИ)"
7+
}
8+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package by.jprof.telegram.bot.currencies.parser
2+
3+
class HRK : MonetaryAmountParserBase() {
4+
override val currency: String = "HRK"
5+
6+
override val currencyRegex: String = "(HRK|KN|KUNA|KUN|КУН)"
7+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package by.jprof.telegram.bot.currencies.parser
2+
3+
import by.jprof.telegram.bot.currencies.model.MonetaryAmount
4+
5+
typealias MonetaryAmountParser = (String) -> Set<MonetaryAmount>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package by.jprof.telegram.bot.currencies.parser
2+
3+
import by.jprof.telegram.bot.currencies.model.MonetaryAmount
4+
5+
abstract class MonetaryAmountParserBase : MonetaryAmountParser {
6+
companion object {
7+
private const val RANGE = "(-|–|—|―|‒|\\.\\.|\\.\\.\\.|…)"
8+
}
9+
10+
override fun invoke(message: String): Set<MonetaryAmount> {
11+
return (
12+
listOf(r1, r2)
13+
.flatMap { it.findAll(message) }
14+
.mapNotNull {
15+
val rawAmount = it.groups["amount"]?.value ?: return@mapNotNull null
16+
val isK = it.groups["K"] != null
17+
val isM = it.groups["M"] != null
18+
val amount = rawAmount.toDouble().run { if (isK) this * 1000 else if (isM) this * 1000000 else this }
19+
20+
MonetaryAmount(amount, currency)
21+
} + listOf(r3, r4)
22+
.flatMap { it.findAll(message) }
23+
.mapNotNull {
24+
val rawAmount1 = it.groups["amount1"]?.value ?: return@mapNotNull null
25+
val rawAmount2 = it.groups["amount2"]?.value ?: return@mapNotNull null
26+
val isK = it.groups["K1"] != null || it.groups["K2"] != null
27+
val isM = it.groups["M1"] != null || it.groups["M2"] != null
28+
29+
val amount1 = rawAmount1.toDouble().run { if (isK) this * 1000 else if (isM) this * 1000000 else this }
30+
val amount2 = rawAmount2.toDouble().run { if (isK) this * 1000 else if (isM) this * 1000000 else this }
31+
32+
listOf(MonetaryAmount(amount1, currency), MonetaryAmount(amount2, currency))
33+
}.flatten()
34+
).toSet()
35+
}
36+
37+
protected abstract val currency: String
38+
39+
protected abstract val currencyRegex: String
40+
41+
private fun amount(index: Int? = null): String = "(?<amount${index ?: ""}>(\\d+\\.\\d+)|(\\d+))( *(?<K${index ?: ""}>[КK])|(?<M${index ?: ""}>[МM]))?"
42+
43+
private val r1 get() = "${amount()} *$currencyRegex".toRegex()
44+
45+
private val r2 get() = "$currencyRegex *${amount()}".toRegex()
46+
47+
private val r3 get() = "${amount(1)} *$RANGE *${amount(2)} *$currencyRegex".toRegex()
48+
49+
private val r4 get() = "$currencyRegex *${amount(1)} *$RANGE *${amount(2)}".toRegex()
50+
51+
private fun String.toRegex() = this.toRegex(RegexOption.IGNORE_CASE)
52+
}

0 commit comments

Comments
 (0)